Что происходит за один шаг
Внутри агента нет ничего похожего на разговор. Есть цикл, и каждый его оборот устроен одинаково: собрать историю, позвать модель, разобрать ответ. Разберём этот оборот по частям — от него дальше зависит почти всё, включая счёт за месяц.
Шаг первый: собрать историю. Модель ничего не помнит, поэтому каждый вызов получает всё заново: системную инструкцию, исходное поручение и все предыдущие сообщения — включая просьбы об инструментах и результаты их вызовов. История — это обычный список сообщений, у каждого есть роль: системное, от человека, от модели, от инструмента.
Шаг второй: позвать модель. Вместе с историей ей передаётся список доступных инструментов. Это важная деталь: список приходит с каждым вызовом, а не объявляется один раз в начале. Значит, набор инструментов можно менять по ходу — и на этом строятся полезные приёмы, о которых будет отдельный урок.
Шаг третий: разобрать ответ. Ответ бывает двух видов, и весь цикл держится на том, чтобы их не перепутать. Либо модель просит вызвать инструмент — тогда обвязка его вызывает, кладёт результат в историю и делает следующий оборот. Либо она возвращает обычный текст без просьб — и это её способ сказать «я закончила».
Теперь стоит остановиться на том, что шаг стоит дороже предыдущего. Каждый оборот кладёт в историю минимум два сообщения — просьбу и результат, — и следующий вызов читает всё накопленное. Первый шаг обошёлся в размер поручения; восьмой — в размер поручения плюс семь пар сообщений, среди которых бывают длинные: вывод команды, содержимое файла, простыня ошибок.
Отсюда правило, которое экономит больше всего: в историю кладут не всё, что вернул инструмент, а то, что пригодится. Команда выдала четыреста строк вывода, из которых значимы три — в историю идут три и пометка, что остальное отброшено. Это не хитрость, а обычная инженерная аккуратность; без неё десятишаговая задача упирается в размер окна на седьмом шаге и агент начинает забывать начало собственной работы.
И последнее про устройство шага. Цикл синхронный: пока не вернулся результат инструмента, следующий вызов модели не делается. Значит, медленный инструмент останавливает всё. Час работы агента — это почти всегда не «модель долго думала», а «десять раз ждали команду по сорок секунд».
Про историю стоит добавить ещё одно, потому что это объясняет половину странностей в поведении агента. История — не протокол работы, а единственная реальность модели. Всё, что в ней есть, для модели произошло; чего в ней нет — не происходило, даже если случилось на самом деле. Обвязка сходила в базу, получила отказ, тихо повторила запрос и получила данные — в истории только данные, и модель уверена, что всё прошло гладко. Иногда это ровно то, что нужно. Иногда это причина, по которой агент не догадался, что база нестабильна.
Отсюда привычка, полезная при разборе: перед тем как рассуждать о поведении модели, прочитать историю глазами, как её прочитала она — подряд, без знания того, что было на самом деле. Половина «странных решений» после этого перестаёт быть странными: в истории действительно написано ровно то, из чего такое решение следует.
Правило цикла целиком
Правило цикла в самом простом виде помещается в несколько строк, и полезно увидеть его целиком — дальше мы будем только уточнять эту заготовку.
история = [системное, поручение]
шаг = 0
пока шаг < ПОТОЛОК:
ответ = модель(история, инструменты)
история += ответ
если в ответе нет просьб об инструментах:
вернуть ответ # модель закончила
для каждой просьбы:
результат = вызвать(просьба)
история += сообщение_от_инструмента(результат)
шаг += 1
вернуть ОТКАЗ: упёрлись в потолок
Здесь сразу видно несколько решений, каждое из которых можно принять иначе — и от каждого что-то зависит.
Потолок стоит снаружи цикла, а не внутри. Это не украшение: без него первый же случай, когда модель просит инструмент в ответ на результат инструмента, даёт бесконечный круг. Такое случается не от глупости, а от вполне логичного поведения: команда возвращает непонятную ошибку, модель пробует другой способ, тот тоже не проходит, и так до конца бюджета. Потолок — единственное, что стоит между этим и вашим счётом.
Просьб в одном ответе может быть несколько. Модель вправе попросить сразу три вызова. Обвязка решает, звать их по очереди или разом: разом быстрее, по очереди безопаснее, потому что результат первого может отменить смысл остальных. Общее правило простое: читающие вызовы можно звать разом, изменяющие — по одному.
Ответ модели кладётся в историю до вызова инструментов. Порядок важен: модель должна увидеть, что сама попросила, иначе на следующем шаге она не поймёт, откуда взялся результат.
И отдельно про что считать ответом «закончила». В заготовке это «нет просьб об инструментах». Такое правило простое и работает, но у него есть слабое место: модель может закончить, не сделав задачу, — например, решив, что нужных данных нет. Снаружи это выглядит как успешное завершение. Поэтому в серьёзных системах выход из цикла не отдают модели целиком: рядом с её «готово» ставят проверку. Об этом следующая глава.
Стоит обратить внимание и на то, чего в заготовке нет, — потому что каждое из этих мест позже придётся дописать. В ней нет обрезки истории: когда та перестанет помещаться в окно, цикл просто упадёт. Нет повтора при сбое сети: одна потерянная попытка обрывает всю задачу. Нет учёта расхода: потолок считает шаги, а деньги тратятся неравномерно, и десять дешёвых шагов стоят меньше одного дорогого. Нет различения читающих и изменяющих вызовов, хотя минуту назад мы сказали, что оно важно.
Для заготовки это нормально, и показывать её стоит именно такой. Беда начинается, когда её принимают за готовую систему: она честно работает на демонстрации, где всё идёт хорошо, и разваливается на первой реальной задаче. Разница между демонстрацией и системой — это в основном обработка того, что пошло не так.
Три причины остановиться
Круг может закончиться тремя способами, и путать их нельзя.
Первый: модель перестала просить инструменты. Штатный выход, самый частый. Его слабость мы уже назвали: это её мнение о том, что работа сделана, а не факт.
Второй: сошлась проверка. Обвязка после каждого шага (или после «готово») сама запускает то, что считается доказательством: тесты, сборку, сравнение с эталоном. Выход из цикла происходит, когда проверка прошла. Это надёжнее первого настолько, что стоит считать его основным, а первый — запасным.
Третий: упёрлись в потолок. Шагов, времени или денег. Это не завершение, а отказ, и важно, чтобы система об этом говорила прямо.
Сколько ставить потолок — вопрос без универсального ответа, но подход есть. Посмотрите, за сколько шагов задача решается, когда всё идёт хорошо. Умножьте на два с половиной. Это и будет разумный потолок: он пропускает нормальную работу с запасом на пару неудачных попыток и обрывает вырождение.
Ставить потолок «на всякий случай большим» — распространённая и дорогая ошибка. Потолок в сто шагов при типичных шести не защищает ни от чего: к моменту, когда он сработает, деньги уже потрачены, а полезного сделано столько же, сколько на десятом шаге. Потолок — это не предохранитель от катастрофы, это инструмент раннего обнаружения: он должен срабатывать тогда, когда ещё дёшево.
Полезно завести и второй, более мягкий предел: шаги без продвижения. Если три шага подряд не изменили ни одного файла и не дали нового результата проверки, агент ходит по кругу, даже если формально ещё не исчерпал потолок. Такое условие ловит вырождение раньше и дешевле, чем счётчик шагов, потому что смотрит не на количество, а на результат.
Есть и третья мерка, менее очевидная и очень полезная: повторяющееся действие. Агент второй раз читает тот же файл, третий раз запускает ту же команду с тем же результатом — это почти всегда признак, что он застрял и ходит по кругу в надежде, что в этот раз выйдет иначе. Заметить просто: обвязка помнит, что уже звалось и с какими параметрами. Дальше есть выбор — оборвать круг или сказать модели прямо в истории: «этот вызов уже был и вернул то же самое».
Второй вариант часто лучше первого. Оборвать — значит потерять работу всех шагов; сказать — значит дать шанс выйти из тупика самостоятельно, и модели обычно этого хватает. Но сообщение должно быть фактом, а не упрёком: «вызов чтения того же файла повторяется третий раз, результат не менялся» работает, а «не повторяйся» — нет, потому что не сообщает ничего нового.
Трасса: что видно снаружи
Всё, что мы описали, происходит внутри и снаружи невидимо. Поэтому вместе с циклом пишется трасса — запись того, что было на каждом шаге.
| Шаг | Что попросила модель | Что вернулось | Итог шага |
|---|---|---|---|
| 1 | прочитать orders.js | 218 строк | — |
| 2 | запустить тесты | 3 из 40 упали | — |
| 3 | записать orders.js | записано | изменён 1 файл |
| 4 | запустить тесты | 40 из 40 прошли | проверка сошлась |
| 5 | — | итоговый ответ | выход из круга |
Такая таблица — минимум, и она уже отвечает на большинство вопросов при разборе. Видно, сколько шагов ушло, какие инструменты звались, где произошёл поворот и чем всё кончилось. Дальше в курсе будет отдельный урок про наблюдаемость, где мы добавим к этому размеры, время и расход, — но начинать надо с этого минимума, потому что он пишется за полчаса и окупается в первый же разбор.
Есть одна деталь, которую стоит решить заранее: что делать с содержимым. Полная трасса со всеми сообщениями — это иногда мегабайты на задачу, и в ней бывают секреты и чужие данные. Разумный порядок такой: в трассу идут факты (какой инструмент, с какими параметрами, сколько вернулось, сколько заняло, чем кончилось), а содержимое — по отдельному решению и с ограничением по сроку хранения. Об этом подробно в уроке про секреты.
И последнее про круг, что стоит унести с собой. Цикл — это место, где обвязка перестаёт быть «вызовом модели» и становится системой. У него есть состояние, стоимость, время, условия выхода и режимы отказа. Всё, что мы будем изучать дальше — инструменты, контекст, проверка, бюджеты, — это уточнение одного из мест в тех десяти строках, которые вы уже видели.
Напоследок — про то, как трасса меняет разговор в команде. Без неё обсуждение сбоя состоит из мнений: один говорит «модель слабая», другой «поручение плохое», третий «инструменты кривые», и никто не может ни доказать, ни опровергнуть. С трассой обсуждение занимает три минуты и заканчивается фактом: на четвёртом шаге модель попросила файл, которого нет, потому что имя пришло из вывода на втором шаге, где команда напечатала предупреждение. Дальше понятно, что чинить.
Это и есть главная практическая причина писать трассу с самого начала, и она не техническая. Трасса переводит работу над агентом из области вкуса в область фактов — а без этого перехода улучшать систему невозможно: непонятно, стало ли лучше и от чего именно.