Содержание, 6 разделов
В моём агенте есть счётчик одинаковых вызовов. Появился он не из теории: агент раз за разом дёргал один инструмент с теми же аргументами, получал тот же результат, аккуратно записывал его в переписку и заходил на следующий оборот. Ничего не ломалось. Просто ничего и не заканчивалось. Тогда я впервые всерьёз посмотрел на цикл, который до этого казался самой скучной частью кода.
Выглядит она так: бесконечный while, внутри запрос к модели, после него проверка, попросила ли она вызвать инструменты. Попросила: выполняем, кладём результаты в переписку, идём на новый оборот. Нет: выходим и отдаём ответ. Три шага, и это весь агент. Всё остальное, сотни строк обвязки, отделяет работающую систему от демо, и первая из этих строк это тот самый счётчик: три одинаковых вызова подряд, и модель получает вместо результата ошибку с просьбой сделать что-нибудь другое.
Дальше о том, откуда взялась эта форма и почему она не может быть другой.
Рассуждение без действий
В январе 2022 года Джейсон Вэй с соавторами показали, что модель, которую попросили рассуждать пошагово, резко прибавляет на сложных задачах. Восемь примеров с промежуточными шагами в промпте, и модель на 540 миллиардов параметров ставит рекорд на школьной математике GSM8K, обгоняя дообученную GPT-3 с верификатором. Приём назвали цепочкой рассуждений, chain-of-thought, и на нём до сих пор стоит всё, что модели делают с задачами в несколько шагов.
У чистого рассуждения есть встроенный дефект. Модель рассуждает внутри себя, опираясь только на то, что запомнила при обучении. Ей нечем проверить промежуточный факт. Придумала на третьем шаге неверную посылку, и все следующие шаги будут аккуратно выведены из неё: ответ придёт уверенный, связный и неправильный. Авторы ReAct через несколько месяцев назовут это галлюцинацией и распространением ошибки. Чем длиннее цепочка, тем хуже.
Рассуждению нужен контакт с реальностью, причём по ходу цепочки, пока она ещё строится.
Мысль, действие, наблюдение
В октябре 2022 года Шунью Яо с коллегами предложили ReAct, от reasoning and acting. Модель чередует два вида шагов: пишет мысль о том, что собирается сделать, выполняет действие во внешнем мире, получает наблюдение и на его основе пишет следующую мысль. Рассуждение помогает строить и обновлять план и разбирать исключения. Действие приносит факты, которых у модели не было: из поиска, из базы, из файловой системы. Каждый следующий шаг стоит на проверенном наблюдении, и ошибке труднее накапливаться.
Цифры в статье внушительные: на ALFWorld, бенчмарке бытовых задач, агент справлялся на 34 процентных пункта чаще методов на имитационном и подкрепляющем обучении, на WebShop на 10 пунктов. С одним-двумя примерами в промпте, тогда как конкуренты учились на тысячах демонстраций.
Именно этот цикл и живёт в моём while (true). Его не придумали инженеры ради удобства. Он вырос из наблюдения, что модели нужен внешний контур обратной связи, иначе она уезжает в собственные фантазии.
Как вызов инструмента стал протоколом
Первые агенты вызывали инструменты текстом. Модель писала что-то вроде Action: search[«столица Перу»], обвязка парсила это регулярным выражением, исполняла, подставляла ответ строкой. Работало, пока модель не ставила лишнюю кавычку.
Дальше было два пути. В феврале 2023 года Тимо Шик с соавторами показали Toolformer: модель можно дообучить самой решать, какой API вызвать, когда и с какими аргументами, и обучить самонаблюдением, по нескольким демонстрациям на инструмент. Умение вшивается в веса.
Победил второй путь: вынести умение в протокол. 13 июня 2023 года OpenAI выпустила function calling: разработчик описывает функции схемой, модель возвращает структурированный вызов с именем и аргументами в JSON, исполнение остаётся на стороне приложения. Причина победы прозаическая. Набор инструментов у каждого приложения свой и меняется каждую неделю, веса модели меняются раз в несколько месяцев и без твоего участия. Умение, вшитое в веса, устаревает к моменту релиза, умение читать схему не устаревает. С этого момента цикл перестал быть трюком в промпте и стал контрактом API: описания инструментов уходят в запрос, обратно приходит tool_calls, результат возвращается сообщением с ролью tool и идентификатором вызова.
Мой агент работает поверх любого OpenAI-совместимого API ровно поэтому. Цикл это протокол. Какая модель за ним стоит, протоколу безразлично.
Почему форма именно такая
Три свойства модели задают форму цикла, и ни одно из них не связано со вкусом разработчика.
Модель не имеет состояния. Она не помнит предыдущий вызов. Единственное её состояние это переписка, которую ты отправляешь целиком на каждом шаге. Поэтому результат каждого действия надо физически дописать в историю сообщением, иначе для модели этого просто не было. Отсюда растут все будущие проблемы с длиной контекста.
Действие требует наблюдения. У модели нет ни файловой системы, ни сети, она может только попросить. Значит, между двумя её рассуждениями всегда есть фаза, где работает внешний код. Эту фазу нельзя выкинуть, как ни оптимизируй: именно здесь проходит граница между тем, что модель предсказывает, и тем, что происходит на самом деле.
Решение об остановке принимает модель. Цикл заканчивается, когда в ответе нет вызовов инструментов. Никакой внешний планировщик не знает заранее, сколько оборотов понадобится, два или сорок. Я писал об этом в «Токен подешевел, задача подорожала»: платит человек, число шагов выбирает модель. Здесь та же механика с обратной стороны. Цикл с фиксированной длиной спроектировать невозможно, потому что задача не раскладывается на шаги заранее.
Из этих трёх свойств вырастает вся обвязка. Раз состояние это переписка, а переписка растёт, нужна компакция: у меня она встроена в тот же цикл и срабатывает перед запросом, когда история подходит к пределу окна. Раз останавливается модель, нужна страховка от зацикливания, тот самый счётчик. Раз исполнение вынесено во внешний код, вызовы одного шага можно пускать параллельно: модель просит прочитать пять файлов, и незачем читать их по очереди.
И раз всё это крутится поверх чужого API, всплывают детали, которых нет ни в одной статье. Сообщение ассистента обязано содержать либо текст, либо вызовы инструментов: если модель выдала только рассуждение, а ты записал пустое сообщение, провайдер начнёт отвечать четырёхсотой ошибкой на каждый следующий запрос в этой сессии. Некоторые провайдеры требуют вернуть им их же след рассуждений в истории. Пустой ответ модели надо уметь запросить заново, потому что показывать пользователю «(нет ответа)» нельзя. Все эти правила из того, что цикл живёт в реальном мире.
Чего цикл не решает
Сам по себе цикл не даёт ни надёжности, ни качества. Модель может решить, что задача сделана, когда она не сделана, и цикл послушно выйдет. Если наблюдения не противоречат неверной гипотезе, ошибка спокойно проедет через все обороты: мой зациклившийся агент получал ровно те результаты, которых ждал, поэтому и не видел проблемы. Про стоимость цикл не знает ничего. А закончив задачу, выкидывает весь накопленный опыт вместе с контекстом.
Поверх этого выросли надстройки. Самая известная это Reflexion: агент после неудачной попытки словами формулирует, что пошло не так, кладёт эту рефлексию в эпизодическую память и учитывает на следующей попытке, без дообучения весов. На HumanEval это дало 91% pass@1 против 80% у GPT-4 на тот момент. Формально это тот же цикл с дополнительным шагом «подумай о своей ошибке» и памятью между попытками.
У надстройки есть цена. Каждая рефлексия это ещё один вызов модели, ещё токены в контексте и ещё один шанс, что агент убедительно объяснит себе неправильную вещь. Рефлексию проверяет та же модель, что совершила ошибку, и внешнего наблюдения в этот момент не приходит. Надстройка возвращает в цикл ровно тот дефект чистого рассуждения, ради борьбы с которым цикл и появился.
Что из этого следует
Агентный цикл это самая простая часть агента. Три шага, десяток строк, и форму им задают свойства модели: отсутствие состояния, невозможность действовать напрямую и то, что решение об остановке принимает она сама.
Вся настоящая работа идёт на границах этого цикла: какие инструменты ты дал, что происходит при их отказе, когда сжимается контекст, как ловится зацикливание, что делать с ошибкой провайдера посреди задачи. Об этих границах остальные восемь частей.
Цикл можно написать за вечер. Всё остальное пишется месяцами.