Что такое инструмент
Инструмент — это функция, которую модель может попросить вызвать. Всё. Никакой особой природы у неё нет: обычная функция вашего кода, у которой есть имя, параметры и результат.
Необычно другое — кто её зовёт. Обычную функцию зовёт программист, написав вызов в коде. Инструмент зовёт модель, выбрав его из списка по описанию, в момент работы, и вы заранее не знаете ни когда это случится, ни с какими параметрами. Отсюда все особенности: инструмент проектируется не для программиста, а для читателя, который видит только описание и решает по нему.
Между просьбой и настоящей функцией стоит обвязка, и она делает три вещи, ни одну из которых пропускать нельзя.
Проверяет, что такой инструмент есть. Модель может попросить несуществующий — обычно потому, что похожий был в её опыте или вы недавно убрали его из набора. Правильный ответ на это — не падение, а вежливый отказ в историю: «инструмента с таким именем нет, доступны такие-то». Модель почти всегда исправляется на следующем шаге.
Проверяет параметры. Модель присылает их в свободной форме, и они бывают неполными, лишними или не того типа: число строкой, путь с опечаткой, поле, которого в схеме нет. Проверять их обязательно, и не ради строгости, а потому что дальше они пойдут в настоящую функцию.
Проверяет права. Тот факт, что инструмент есть в списке, не значит, что его можно вызвать вот сейчас с вот такими параметрами. Запись в файл — да, запись в файл за пределами рабочей папки — нет. Это отдельный слой, и ему посвящён отдельный урок.
Теперь про то, чем инструмент не является. Он не является способом рассказать модели о мире: для этого есть контекст. Заводить инструмент «узнать сегодняшнюю дату» вместо того, чтобы положить дату в системное сообщение, — распространённая ошибка. Она стоит шага: агент потратит оборот круга на то, что можно было сообщить бесплатно.
И он не является способом заставить модель что-то сделать. Наличие инструмента — это возможность, а не обязанность. Если вам нужно, чтобы тесты запускались всегда, это делает обвязка после каждого изменения, а не инструмент, который модель может позвать, а может и не позвать.
Есть и третье «не». Инструмент — не место для бизнес-решений. Соблазн такой: раз уж мы пишем функцию «списать со счёта», давайте прямо в ней проверим, можно ли списывать, и если нельзя — тихо не спишем, а вернём «готово». Так делать нельзя, и причина не в чистоте кода. Модель получит «готово» и пойдёт дальше, считая деньги списанными; отличить эту ситуацию от настоящего успеха она не сможет никогда. Инструмент обязан честно сообщать, что произошло, даже когда «не произошло ничего» — это неудобный ответ.
Общее правило, которое из этого следует и пригодится во всех дальнейших уроках: обвязка вправе решать за модель, но не вправе обманывать её о результате. Не дать инструмент — можно. Отказать в вызове — можно. Повторить вызов молча — можно. Сказать «получилось», когда не получилось, — нельзя ни при каких обстоятельствах, потому что после этого вся дальнейшая работа агента строится на неверном основании, и заметить это будет нечем.
Сколько инструментов давать
Следующий вопрос — сколько инструментов давать и какие. Здесь есть устойчивое заблуждение: кажется, что чем больше возможностей, тем лучше. На деле набор инструментов — это меню выбора, и длинное меню ухудшает выбор.
Почему длинный набор вредит, причин три, и все практические.
Описания занимают окно. Список инструментов передаётся при каждом вызове, целиком, со всеми описаниями и схемами параметров. Тридцать инструментов — это несколько тысяч знаков в каждом вызове, то есть на каждом шаге. Это прямая статья расхода, и она незаметна, потому что не выглядит как сообщение.
Похожие инструменты путаются. Если в наборе есть найти_файл, поиск_по_содержимому и искать, модель будет выбирать между ними, и иногда не то. Это не её слабость: вы бы тоже выбирали, глядя на три названия без контекста.
Лишние возможности зовут. Инструмент, лежащий в наборе, рано или поздно будет вызван, даже если задача его не требует. Если в наборе есть «удалить файл», однажды он будет вызван при задаче «почини расчёт».
И вот здесь пригождается деталь из прошлого урока: список приходит с каждым вызовом, а не объявляется один раз. Значит, его можно менять по ходу работы, и это самый дешёвый способ улучшить выбор.
Приёмы, которые из этого следуют:
Сужать под задачу. Задача на разбор кода не требует инструментов записи. Убрать их — значит убрать и соблазн, и вероятность ошибки, и несколько сотен знаков из каждого вызова.
Открывать по мере надобности. Пока агент не прочитал ни одного файла, инструмент «записать файл» ему не нужен; появится после первого чтения. Такой приём заметно снижает число правок вслепую.
Группировать вместо размножения. Вместо пяти инструментов поиска — один с параметром «где искать». Модели проще выбрать параметр внутри понятного инструмента, чем инструмент среди похожих.
Обратная сторона у последнего приёма тоже есть: инструмент с восемью параметрами, половина которых взаимоисключающие, — это тот же длинный набор, только спрятанный внутрь схемы. Ориентир простой: если описание параметра начинается со слов «если предыдущий параметр равен», инструмент стоит разделить.
Что инструмент возвращает
Половина качества инструмента — в том, что он возвращает. И здесь самая частая ошибка выглядит безобидно: вернуть «то, что напечатала команда».
Проблема в том, что модель — это читатель, который видит только возврат и ничего больше. Она не знает, много это или мало, полное или обрезанное, нормальный это результат или уже беда. Всё, чего нет в тексте возврата, для неё не существует.
Хороший возврат содержит четыре вещи.
Получилось или нет — явно. Не «вывод пуст», а «команда завершилась успешно, вывода нет». Пустой ответ модель истолкует как угодно, и чаще всего неправильно.
Главное из вывода. Из четырёхсот строк сборки значимы обычно три: что упало и где. Остальное — шум, за который вы платите на каждом следующем шаге.
Размер и факт обрезания. Это тонкое место и источник самых обидных ошибок. Если обвязка молча обрезала вывод до первых ста строк, модель считает, что видела всё, и делает выводы по неполной картине. Написать «показаны первые 100 строк из 412» стоит ничего и меняет поведение целиком: модель попросит остальное, если оно нужно.
Что делать дальше — при отказе. Об этом отдельно в следующем уроке, но правило простое: сообщение об ошибке пишется для агента и содержит путь к исправлению.
Отдельно стоит сказать про формат возврата. Соблазн вернуть строгую структуру — скажем, JSON — велик, и иногда он оправдан. Но модель одинаково хорошо читает и текст, а текст занимает меньше места и не ломается от лишней запятой. Практическое правило: структура нужна там, где её будет разбирать код, а не модель. Если возврат читает только модель — пишите его словами.
И про размер. Разумный потолок на один возврат — порядка двух-трёх тысяч знаков. Больше почти всегда означает, что инструмент возвращает сырьё вместо ответа. Исключение — чтение файла, который агент собирается править: тут нужен весь текст, иначе он будет править вслепую. Но и здесь стоит иметь в инструменте параметр «строки с такой-то по такую-то»: файл на две тысячи строк целиком в окне не нужен почти никогда.
Последнее про возвраты — про единообразие. Если два инструмента сообщают об успехе по-разному («OK», «done», пустая строка), модель тратит внимание на расшифровку вместо работы. Договоритесь об одном виде возврата на все инструменты: первая строка — исход, дальше подробности, в конце — размер и отброшенное. Это не эстетика: одинаковая форма означает, что модель узнаёт неудачу с первой строки и не пытается вычитать её из содержания.
Когда инструмент отказал
Инструменты ломаются: команда падает, сеть недоступна, файла нет, прав не хватило. Отказ — это нормальный режим работы, и обходиться с ним надо как с обычным результатом, а не как с исключительной ситуацией.
Первое и главное правило мы уже называли в прошлом уроке: отказ идёт в историю так же, как успех. Обвязка, которая ловит исключение, пишет в лог и молча делает следующий шаг, оставляет модель в уверенности, что вызов удался. Дальше она работает на ложном основании, и всё, что она сделает потом, будет неверно.
Второе правило: различать, чей это отказ. Три вида, и вести себя с ними надо по-разному.
| Чей отказ | Пример | Что делает обвязка |
|---|---|---|
| Модель ошиблась | путь с опечаткой, нет обязательного параметра | вернуть отказ модели, она исправится |
| Среда подвела | сеть отвалилась, сервис ответил 503 | повторить самой, модель не трогать |
| Так устроено | прав нет, файл вне рабочей папки | вернуть отказ и объяснить границу |
Разница между первым и вторым видом важнее, чем кажется. Ошибку модели чинит модель — она видит отказ и на следующем шаге пробует иначе. Сбой среды модель починить не может: сколько ни пробуй, сеть от этого не появится. Если отдать ей сбой среды, она потратит три шага на попытки и, скорее всего, сделает вывод «этот способ не работает» — то есть научится неверному из случайного события.
Третье правило — про идемпотентность, и это слово стоит того, чтобы его знать. Инструмент идемпотентен, если повторный вызов с теми же параметрами не делает ничего нового. Записать файл с тем же содержимым — идемпотентно. Дописать строку в конец файла — нет: два вызова дадут две строки.
Это важно ровно потому, что обвязка повторяет вызовы при сбоях среды. Если инструмент не идемпотентен, повтор после «сеть отвалилась» может сделать действие дважды — притом что первый вызов, возможно, прошёл, а ответ потерялся по дороге. Правило простое: повторять автоматически можно только идемпотентные вызовы. Остальные — только с ведома человека или через ключ, по которому сервис сам отличит повтор.
И последнее. Отказ инструмента — это сигнал не только модели, но и вам. Один и тот же отказ, повторяющийся в трассах разных задач, почти всегда означает, что инструмент спроектирован неудачно: модель систематически зовёт его не так, как вы ожидали. Чинить это правкой инструкции («не забывай указывать путь от корня») — та самая подмена, о которой шла речь в первом уроке. Чинить надо инструмент: сделать параметр необязательным, принять оба формата пути, переименовать так, чтобы назначение было очевидно.