Содержание, 6 разделов
Описание инструмента edit в моём агенте заканчивается так: «если правка не нашла текст или нашла его дважды, перечитай файл и повтори с точным текстом и большим контекстом. Никогда не сдавайся и не перезаписывай файл целиком через write». Я перечитал это недавно и понял, что так пишут инструкцию сотруднику, который склонен срезать углы. Программам такого не пишут.
Так выглядит инструмент агента изнутри. Половина его это код, который что-то делает с файлами и процессами. Вторая половина это текст, который читает модель и по которому решает, когда этот код звать и как. Вторая половина оказалась сложнее первой, и дальше о ней.
Два текста вместо одной функции
В обычной программе функция это сигнатура и тело. Вызывающая сторона детерминирована: передаст ровно те аргументы, которые ты ждёшь, ровно тогда, когда ты её вызвал. С агентом это не работает. Вызывающая сторона это языковая модель. Она может вызвать инструмент не вовремя, пропустить шаг, выдумать аргумент или пойти обходным путём, о котором ты не думал.
Anthropic в сентябре 2025 года сформулировала это одной фразой: инструменты это новый вид программного обеспечения, контракт между детерминированными системами и недетерминированными агентами. Следствие неочевидное: качество инструмента определяется тем, насколько точно модель понимает, когда и как его звать. В той же статье есть измерение. MCP-серверы для Slack и Asana, написанные людьми, давали 60–70% точности на отложенных тестах, те же серверы с переписанными описаниями: 80–90%. Код не менялся. Менялся текст.
Поэтому описания инструментов у меня постепенно превратились в свод правил. bash объясняет модели, что каждый вызов идёт в свежей оболочке и cd не доживёт до следующей команды, потому что модели раз за разом делали cd src в одном вызове и удивлялись в следующем. read говорит: ты уже держишь содержимое всех прочитанных файлов, не читай тот же путь снова. write предупреждает: не откатывайся на полную перезапись после неудачной правки, потому что файл, переписанный из памяти, воспроизводит устаревшее содержимое. Ни одна из этих строк не появилась из головы. Каждая это след конкретного случая, когда агент делал что-то глупое, и оказалось дешевле объяснить это в описании, чем ловить в коде.
Формально всё это живёт в схеме function calling, той самой, которую OpenAI ввела в июне 2023 года: имя, описание, JSON-схема параметров. Описание там не документация. Это кусок системного промпта, привязанный к конкретному действию.
Почему правка это строка
Самый показательный инструмент это edit. Естественный интерфейс для правки файла это патч: номера строк, что убрать, что добавить. Так работает git, так думает разработчик. Модель так не думает.
Модель плохо считает строки. Она видит файл как текст и помнит его как текст, пронумерованного массива в её памяти нет. После первой же правки нумерация съезжает, и следующий патч ложится не туда. Поэтому edit принимает сам текст: вот кусок, который надо найти, вот на что его заменить. Позиции в этом интерфейсе нет вообще. Единственное требование: кусок должен встречаться в файле ровно один раз, иначе правка отклоняется как неоднозначная.
Дальше начинается компромисс между строгостью и прощением. Модель воспроизводит текст из памяти, и память у неё слегка неточная: лишний пробел, другой отступ, экранированный символ там, где в файле его нет. Отклонять каждый такой случай значит гонять модель по кругу «перечитай и повтори». Принимать что угодно значит однажды заменить не то.
У меня это решено цепочкой из девяти матчеров, от строгого к мягкому: сначала точное совпадение, потом совпадение с обрезанными пробелами по краям строк, потом якорь по первой и последней строке блока с проверкой похожести середины по расстоянию Левенштейна, потом нормализация пробелов, отступов, экранирования. Цепочка останавливается на первом, кто нашёл, и у неё два предохранителя. Если найденный фрагмент непропорционально больше того, что искали, отказ: мягкий матчер мог захватить лишнего. Если совпадений несколько, тоже отказ, пока модель не даст больше контекста.
Даже здесь были баги, которые не придумаешь заранее. Замена всех вхождений через String.replaceAll в JavaScript интерпретирует $& и $1 в строке замены, и код с такими последовательностями молча записывался обратно раскрытым. Правка с другим отступом, чем в файле, вставлялась как есть и ломала форматирование, пока я не добавил выравнивание. Каждый такой случай напоминает, с каким вызывающим инструмент работает на самом деле.
Результат инструмента это контекст
Когда пишешь первый инструмент, легко упустить одну вещь: всё, что он вернул, попадает в переписку и остаётся там до конца сессии. А переписка, как я писал в первой части, единственное состояние модели. Один неосторожный cat на лог в двести мегабайт, и сессия закончилась: контекст забит, полезное вытеснено.
Поэтому каждый инструмент у меня режет свой вывод. bash до 2000 строк или 128 килобайт, что наступит раньше, и вместо продолжения пишет: сузь команду или перенаправь вывод в файл и читай кусками. read так же, с параметрами смещения и лимита, чтобы большой файл можно было пройти окнами. Результаты инструментов из внешних MCP-серверов режутся тем же правилом, что и вывод bash, потому что чужой сервер про мой контекст не знает ничего. Anthropic в той же статье пишет, что Claude Code по умолчанию ограничивает ответ инструмента 25 000 токенами. Порядок тот же.
Отдельная история это файлы, которые не текст. Картинка уходит модели как изображение, если та умеет их смотреть. Но большая картинка при декодировании занимает память пропорционально числу пикселей, и размер файла об этом ничего не говорит: проверка «файл меньше N мегабайт» пропускала опасные случаи, пришлось считать мегапиксели. Именованный канал при попытке открыть его блокирует чтение, пока кто-то не откроет другой конец. Никто не откроет, и агент висит вечно. Теперь такие файлы отклоняются до открытия. Бинарный файл определяется по первым байтам и как текст не отдаётся.
Общий принцип: инструмент отвечает за то, чтобы его ответ не убил разговор.
Ошибка это тоже интерфейс
Когда инструмент падает, у него есть выбор: вернуть трейсбек или объяснить, что делать дальше. Первое естественно для программиста. Второе это единственное, что работает с моделью.
Модель регулярно зовёт инструмент, которого нет: путает имя, берёт название из другого агента. Раньше это заканчивалось ошибкой и повторной попыткой с тем же именем. Теперь ответ выглядит так: «неизвестный инструмент, возможно, вы имели в виду edit; доступные: …; вызовите один из них и не повторяйте это имя». Подсказка считается по расстоянию Левенштейна до реальных имён. Трёх строк текста хватило, чтобы этот класс зацикливаний исчез.
Пустые аргументы это вызов без аргументов, раньше они считались ошибкой разбора JSON. Если процесс убили посреди вызова инструмента, при следующем запуске в переписке останется вызов без ответа, а провайдер требует ответ на каждый вызов и иначе отклоняет всю сессию. Значит, на осиротевшие вызовы надо ответить хотя бы «прервано», иначе сессия окирпичена навсегда. Ошибка файловой системы должна прийти в виде, который модель может использовать: «нет прав на запись в такой-то каталог» вместо голого EACCES.
Anthropic формулирует то же: ответ на ошибку должен содержать конкретные и выполнимые улучшения, без кодов и трассировок. По сути ошибка это ещё одно наблюдение в цикле, и от того, насколько оно информативно, зависит следующий шаг модели.
И здесь самое важное, что я вынес из работы над инструментами. Когда инструмент отказывает по своей вине, завис, вернул невнятную ошибку, сработал через раз, модель не знает, что виноват инструмент. У неё нет способа отличить «я сделала неправильно» от «сломалось у них». И она выбирает первое. Агент перестаёт доверять себе: правильные аргументы начинают меняться на другие, только что прочитанный файл перечитывается заново, простая правка заменяется обходным путём через sed в bash, а к концу сессии агент уже перепроверяет каждый собственный шаг и тратит на это больше, чем на задачу. Один ненадёжный инструмент отравляет поведение агента целиком, далеко за пределами того места, где он сломался.
Отсюда правило, которое я ставлю выше остальных. Инструмент обязан быть предсказуемым, и если он всё-таки падает по своей причине, его ошибка должна прямо говорить модели: это сбой на моей стороне, твой вызов был правильным, повтори его как есть. Богатый набор инструментов, из которых один работает через раз, хуже бедного набора, где каждый работает всегда.
Границы
Последнее и главное: инструмент это единственное место, где модель касается реального мира, а значит, единственное место, где её можно остановить.
Опасные команды вроде rm -rf и sudo bash пропускает только после подтверждения человеком. Без подтверждения модель получает отказ с объяснением и предложением попросить пользователя запустить это самому. Любая запись в файл проходит через тот же механизм. В режиме планирования bash работает только с белым списком read-only программ, ls, cat, grep, git log, а ssh убирается из списка инструментов целиком: на удалённой машине команда с тем же именем может оказаться другой программой, и белый список там ничего не гарантирует.
Параллельно выполняются только инструменты с явным read-only контрактом: поиск, чтение списка файлов, веб-запросы. Всё остальное по порядку, даже если модель попросила несколько вызовов в одном ответе. Иначе две правки в одном ответе гонялись бы за один файл, и результат зависел бы от того, какой промис завершился первым.
И доверие к чужим инструментам. С ноября 2024 года есть протокол MCP, через который к агенту подключается любой сторонний сервер со своими инструментами. Удобно, но у моего агента нет способа узнать, что делает инструмент с чужого сервера. Поэтому любой из них считается потенциально разрушительным и требует подтверждения, у каждого есть тайм-аут, а совпадение имён между двумя серверами это ошибка. Выбирать наугад агент отказывается.
Что из этого следует
Когда я начинал, инструменты казались самой простой частью: обёртки над fs и child_process, час работы. Сейчас на них приходится больше кода, чем на цикл, и почти каждая строка появилась после конкретного инцидента: зациклился, забил контекст, записал не туда, повис на канале.
Инструмент это договор со стороной, которая договоры угадывает вместо того, чтобы читать. Описание это его условия, ограничение вывода это защита обеих сторон, текст ошибки это способ вернуть контрагента в рамки, подтверждения и белые списки это то, что нельзя нарушить даже при желании.