Четыре поля описания
Описание инструмента — это единственное, по чему модель решает, звать его или нет. Не код, не документация, не ваши намерения: четыре поля, которые приходят к ней в каждом вызове.
Выглядит это примерно так — формат отличается у разных поставщиков в мелочах, а по сути везде одинаков:
{
"name": "search_in_files",
"title": "Поиск по файлам",
"description": "Ищет строку в файлах проекта и возвращает список совпадений
с именем файла и номером строки. Для поиска по имени файла
используйте find_file.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "что искать, обычный текст" },
"folder": { "type": "string", "description": "где искать; по умолчанию весь проект" },
"limit": { "type": "integer", "description": "сколько совпадений вернуть, 1..100" }
},
"required": ["query"]
}
}
Имя — то, чем инструмент зовётся. Оно же первое, что модель видит в списке, и на нём она экономит внимание: беглый взгляд по именам, и только если ничего не подошло — чтение описаний. Отсюда правило, которое звучит наивно и работает: глагол и предмет. Что делает и с чем. read_file, run_tests, parse_csv_report.
Название — строка для человека в интерфейсе. Модель её не читает, и писать туда пояснения бессмысленно. Это поле есть не везде и не обязательно.
Описание — самое важное поле и самое недооценённое. Оно отвечает не на вопрос «что делает инструмент» (это уже сказано именем), а на вопрос «когда его звать и когда не звать». Хорошее описание почти всегда содержит отсылку к соседу: «для поиска по имени файла используйте find_file». Это и есть то, чего модель не может узнать сама.
Схема параметров — что передать и в каком виде. Обычно это JSON Schema: типы, обязательность, описания полей, иногда перечисление допустимых значений. У каждого параметра тоже есть описание, и его тоже читают.
Стоит понимать и обратную сторону: описание — это текст, который влияет на модель, и влияет он всегда, а не только в момент выбора. Фраза «этот инструмент работает медленно, используйте с осторожностью» приведёт к тому, что модель начнёт его избегать даже там, где он нужен, — и вы получите обходные пути вместо прямого вызова. Описание не место для предупреждений себе на будущее и для заметок «надо переписать»: всё, что там написано, будет прочитано и учтено.
Отсюда привычка, которая экономит много времени: читать описание глазами модели. Прочтите его так, будто вы видите этот инструмент впервые, не знаете, как он устроен, и должны за две секунды решить, подходит ли он к задаче. Большинство плохих описаний после такого чтения становятся очевидно плохими — в них написано, как инструмент устроен, а не когда его брать.
Как написать, чтобы звали к месту
Теперь про то, как писать описание, чтобы инструмент звали к месту. Это ремесло, и у него есть приёмы.
Первое предложение — что делает, одной фразой. Не «утилита для работы с файлами», а «читает файл и возвращает его содержимое». Модель читает описания бегло, и первая фраза решает.
Второе — что возвращает. Не в общих словах, а конкретно: «список совпадений с именем файла и номером строки». Знание формата возврата влияет на то, будет ли модель звать инструмент: если непонятно, что придёт, проще не звать.
Третье — когда не звать. Это то, чего почти никто не пишет, и это самая полезная строка описания. «Не подходит для поиска по имени файла — для этого есть find_file». «Не используйте для больших файлов: вернёт первые 200 строк». Каждая такая фраза убирает целый класс неверных вызовов.
Четвёртое — пример, если инструмент нетривиальный. Одна строка примера вызова стоит абзаца объяснений.
Теперь про схему параметров, где ошибок делают больше всего.
Перечисление вместо свободной строки. Если параметр принимает три значения, так и напишите: "enum": ["read", "write", "append"]. Свободная строка приглашает к творчеству, и модель непременно однажды пришлёт "readonly".
Значения по умолчанию — в описании, а не только в коде. Если limit по умолчанию 20, модель должна об этом знать: иначе она будет указывать его каждый раз, тратя знаки, или удивляться, почему вернулось двадцать.
Обязательных параметров — минимум. Каждый обязательный параметр — это возможность ошибиться. Если у инструмента есть разумное поведение без параметра, сделайте его необязательным.
Единицы и форматы — явно. «Время в секундах», «дата в виде ГГГГ-ММ-ДД», «путь от корня проекта». Без этого модель выберет формат сама, и выберет разумно — но не обязательно так же, как вы.
Наконец, приём, который стоит знать: схема — это не только документация, это ещё и проверка. Всё, что вы записали в схему — типы, обязательность, перечисления, границы, — обвязка обязана проверять перед вызовом. Схема, которую никто не проверяет, — это обещание, которое однажды нарушат.
Ошибка, адресованная агенту
Отдельная тема — текст сообщения об ошибке. В обычной разработке его читает человек, и он написан для человека: коротко, с кодом, со ссылкой на документацию. Здесь читатель другой, и это меняет всё.
Сравните два ответа на одну и ту же ситуацию:
ENOENT: no such file or directory, open 'src/cart.js'
Файл src/cart.js не найден. В папке src есть: cart/, checkout.js, index.js.
Возможно, вы имели в виду src/cart/index.js.
Первый — обычная системная ошибка. Модель по ней сделает разумную вещь: попробует другое имя. Наугад. Потратит шаг, может быть два.
Второй — сообщение, адресованное агенту. Оно содержит то, что нужно для исправления: что именно не найдено, что есть рядом, и предположение. Следующий шаг будет верным почти наверняка.
Разница в цене — один-два шага на каждый такой случай, а случаев много. Это одна из самых выгодных правок во всей обвязке: она не требует ни новых инструментов, ни изменений в модели, и окупается сразу.
Дальше — несколько видов ошибок и что в них стоит класть.
| Отказ | Что добавить в сообщение |
|---|---|
| файл не найден | что лежит в той же папке |
| параметр неверного типа | какой тип ожидался и пример правильного значения |
| значение вне допустимых | список допустимых значений |
| прав не хватает | что именно запрещено и что разрешено вместо этого |
| команда упала | три значимые строки вывода, а не весь вывод |
| слишком большой результат | сколько всего и как запросить часть |
Важная оговорка: в сообщение об ошибке не кладут секреты и чужие данные. Соблазн вернуть «подключение не удалось: строка подключения такая-то» велик и понятен — это правда помогло бы. Но всё, что попало в сообщение, попадает в историю, а история идёт в трассу и в следующие вызовы модели. Об этом подробно в уроке про секреты, а правило простое: сообщение об ошибке — это публичный текст.
И последнее. Хорошее сообщение об ошибке помогает не только модели. Оно точно так же помогает человеку, который завтра будет разбирать трассу, — а это, скорее всего, вы. Формулировка «что не так → что есть → что попробовать» одинаково хороша для обоих читателей, и это редкий случай, когда не приходится выбирать.
Есть ещё одна вещь, которая относится к сообщениям и легко пропускается: отказ должен быть отличим от результата. Если инструмент при ошибке возвращает текст, похожий на нормальный ответ, модель его таковым и сочтёт. «Ничего не найдено» и «поиск не выполнен из-за ошибки» — это разные вещи, и первое модель примет как факт о мире («в проекте нет такой строки»), а второе — как повод попробовать снова.
Простой приём, который это закрывает: начинать сообщение об отказе словом «Ошибка» и держать этот вид одинаковым во всех инструментах. Модель узнаёт неудачу с первого слова и не пытается вычитать её из содержания. Тот же приём, что и с единообразием возвратов из прошлого урока, и работает он по той же причине.
Общий протокол вместо своего
Пока инструменты свои и агент один, описания живут прямо в коде. Как только инструментов становится много, а приложений, которым они нужны, больше одного, возникает вопрос: как подключать наборы инструментов к разным программам, не переписывая каждую пару заново.
Ответ отрасли на этот вопрос — общий протокол. Самый распространённый на сегодня называется MCP, Model Context Protocol; это открытый стандарт, и его понимают и чаты, и редакторы кода, и собственные агенты. Устроен он просто.
Хозяин — приложение, которое работает с моделью: ваш агент, редактор, чат. Оно заводит по одному клиенту на каждый сервер инструментов и держит с ним отдельное соединение. Сервер — это программа, которая отдаёт инструменты; она может работать рядом на той же машине или где-то удалённо.
Сервер предлагает три вида вещей, и разницу между ними стоит понимать:
| Что предлагает сервер | Что это | Пример |
|---|---|---|
| Инструменты | действия, которые можно выполнить | выполнить запрос, записать файл |
| Ресурсы | данные, которые можно прочитать | схема базы, содержимое файла |
| Подсказки | заготовки обращений к модели | шаблон разбора отчёта |
Разница между первым и вторым важна при проектировании: инструмент что-то делает, ресурс просто есть. Схему базы правильно отдать ресурсом, а не инструментом «получить схему»: ресурс кладётся в контекст сразу и не стоит оборота круга.
Связь бывает двух видов: по потоку, когда сервер запущен рядом и общается через стандартный ввод-вывод, и по HTTP, когда он удалённый. Первый способ быстрее и не требует сети, второй позволяет одному серверу обслуживать многих. Внутри и там и там обычные сообщения в формате JSON-RPC.
Практический вопрос: стоит ли брать общий протокол или писать своё. Честный ответ — зависит от того, сколько у вас пар «приложение — набор инструментов». Одна пара: своё проще, и протокол добавит слоёв без пользы. Несколько приложений или несколько наборов, которые хочется переиспользовать: протокол окупается, потому что иначе вы напишете тот же протокол, только свой и хуже.
И одна важная оговорка, к которой мы вернёмся в уроке про безопасность. Подключить чужой сервер инструментов — это то же самое, что подключить чужую библиотеку, только хуже: библиотека делает то, что вы вызвали, а сервер инструментов приносит описания, которые читает ваша модель, и эти описания — тоже текст, который на неё влияет. Правила про чужой код здесь действуют полностью, и добавляются свои.