Назад к блогу

Мультиагентные системы для рекламной платформы: как устроен цикл вызова инструментов

Мультиагентные системы для рекламной платформы: как устроен цикл вызова инструментов

Статья объясняет, как на самом деле работает вызов инструментов в мультиагентных системах для рекламной платформы: почему это не одиночный запрос, а многошаговый цикл обмена сообщениями между моделью и приложением. Разбираются тонкости сопоставления вызовов и результатов, описание функций через JSON-схемы, пространства имён с отложенной загрузкой и компромиссы строгого режима — всё то, что определяет надёжность и предсказуемость агентной архитектуры.

Разбираем механику, на которой держится взаимодействие агента с инструментами: как модель просит вызвать функцию, как приложение возвращает результат и как связать одно с другим. Нетривиальность в том, что вызов инструмента — это не один шаг, а цикл из нескольких обменов с моделью, где каждое сообщение должно быть правильно сопоставлено с конкретным вызовом, а поведение меняется в зависимости от режима выбора инструмента, строгости схемы и способа доставки результата.

Цикл вызова инструмента

Всё начинается с запроса к модели, в который включается список инструментов, доступных для вызова. Модель отвечает вызовом инструмента: в ответе модели есть массив output, и в нём появляется элемент с полем type, равным function_call, а поле call_id служит для последующей передачи результата. Этот элемент содержит имя функции и аргументы в виде закодированного JSON.

Дальше приложение выполняет свой код с аргументами из вызова. Результат оформляется как сообщение function_call_output — обычно это строка, формат которой выбирает само приложение: JSON, код ошибки или обычный текст. После добавления результата в messages они отправляются обратно модели для финального ответа.

Связь между запросом и результатом обеспечивают три поля: call_id сопоставляет результат с конкретным вызовом, id идентифицирует сам элемент function_call, а type со значением function_call помечает элемент как вызов.

Как описываются инструменты

Инструмент-функция описывается JSON-схемой с полями type, name, description, parameters и strict. Поле type всегда равно function. name — имя функции, description — когда и как её использовать, parameters — JSON-схема входных аргументов, strict — включение строгого режима вызова.

Поскольку parameters — это JSON-схема, в ней доступны типы свойств, перечисления, описания, вложенные и рекурсивные объекты. В примере get_weather поле parameters задаёт объект со свойствами location (строка) и units (строка с перечислением ["celsius", "fahrenheit"]), список обязательных полей ["location", "units"] и запрет лишних свойств.

К именам и описаниям предъявляются требования: писать чёткие и подробные имена функций, описания параметров и инструкции; явно описывать назначение функции и каждого параметра (и его формат), а также что представляет вывод. Рекомендуется использовать перечисления и структуру объектов, чтобы не допускать недопустимых состояний: например, toggle_light(on: bool, off: bool) допускает неверные вызовы.

Пространства имён и отложенная загрузка

Пространства имён группируют связанные инструменты по домену — например, crm, billing или shipping. Это помогает организовать похожие инструменты и особенно полезно, когда модель выбирает между инструментами для разных систем или целей.

Для большой экосистемы инструментов можно отложить загрузку части или всех инструментов с помощью tool_search: этот инструмент позволяет модели искать подходящие инструменты, добавлять их в контекст и затем использовать. Отложенная загрузка задаётся полем defer_loading: true у отдельной функции. После загрузки функции вызов обрабатывается как обычно. Поддерживается это только моделями gpt-5.4 и новее.

Строгий режим и его цена

Включение строгого режима меняет требования к схеме. В схеме со strict: true все параметры перечислены в required, а для необязательного по смыслу units тип расширен до ["string", "null"], чтобы значение всё равно передавалось. Схема закрывается через additionalProperties: false — лишние поля в аргументах не допускаются. В схеме без строгого режима обязателен только location, а additionalProperties не задан.

У строгого режима есть ограничения: часть возможностей JSON Schema не поддерживается. Для дообученных моделей схемы проходят дополнительную обработку при первом запросе и затем кэшируются, поэтому изменяющиеся от запроса к запросу схемы могут увеличивать задержки, а кэш не подпадает под zero data retention.

Управление вызовами

Параметр tool_choice определяет, когда и сколько инструментов вызывает модель. По умолчанию модель решает сама. Значение "auto" разрешает вызвать ноль, одну или несколько функций, "required" требует одну или несколько, "none" имитирует поведение, как будто функции вообще не передавались.

Режим allowed_tools отвечает на другой вопрос: не «когда вызывать», а «что вообще доступно для вызова». Он ограничивает вызовы подмножеством инструментов, при этом список передаваемых инструментов не меняется — это позволяет экономить на кэшировании промпта. В этом режиме задаётся объект с типом allowed_tools, полем mode и списком tools с конкретными функциями, а tool_choice продолжает применяться к инструментам, доступным в текущем ходу.

Параллельные вызовы

Модель может вызвать несколько функций за один ход. Это поведение отключается параметром parallel_tool_calls в false, что гарантирует вызов ровно нуля или одной функции. Оркестратор должен взять сообщение из ответа, перебрать все элементы tool_calls, для каждого функционального вызова извлечь имя и аргументы, выполнить функцию и добавить в историю сообщение с ролью tool, тем же tool_call_id и строковым результатом. После этого результаты отправляются обратно модели. Встроенные инструменты не могут входить в пакет параллельных вызовов функций.

Ошибки и пограничные случаи

Если инструмент вернул ошибку, результат передаётся в function_call_output строкой — JSON, код ошибки или обычный текст, — и модель интерпретирует эту строку по своему усмотрению. Для функций без возвращаемого значения (например, send_email) следует вернуть строку, обозначающую успех или неудачу, например "success". Если функция возвращает изображения или файлы, вместо строки можно передать массив объектов изображений или файлов. Каждый результат привязывается к вызову через call_id.

Для reasoning-моделей, таких как GPT-5 или o4-mini, любые reasoning items, возвращённые в ответах вместе с вызовами инструментов, должны быть переданы обратно вместе с выводами вызова: без них модель теряет контекст собственных рассуждений, и требование передавать их обратно нарушается. В примерах такие элементы представлены объектами с типом reasoning, пустыми массивами content и summary и уникальным id; рядом в том же массиве идёт объект вызова инструмента.

Про одновременный вызов одного и того же инструмента сказано только для конкретного снимка модели: этот снимок gpt-4.1-nano может иногда включать несколько вызовов одного инструмента при включённых параллельных вызовах, и рекомендуется отключать эту возможность. В общем случае ответ модели может содержать ноль, один или несколько вызовов, поэтому при обработке рекомендуется исходить из того, что вызовов несколько.

Стриминг вызовов

Стриминг вызовов функций работает так же, как стриминг обычных ответов: устанавливается stream в true, и приходят чанки с объектами delta. Но вместо агрегации чанков в единую строку content вы агрегируете чанки в закодированный JSON-объект arguments.

Когда модель вызывает одну или несколько функций, поле tool_calls каждого delta заполняется. Каждый tool_call содержит index (указывает, к какому вызову относится delta), id, function (с name и arguments) и type (всегда function). Многие из этих полей устанавливаются только для первого delta каждого вызова — например, id, function.name и type.

Потоковая передача позволяет показывать пользователю, какая функция вызывается по мере заполнения аргументов, и отображать аргументы в реальном времени.

Ограничение вывода грамматиками

Контекстно-свободная грамматика (CFG) для пользовательских инструментов передаётся через параметр grammar и ограничивает текстовый ввод модели. Поддерживаются две формы синтаксиса: lark и regex. Сэмплирование модели ограничивается с помощью LLGuidance.

У Lark не поддерживаются lookarounds в лексерных regex, ленивые модификаторы, приоритеты терминалов, шаблоны, импорты (кроме встроенного %import common) и %declare. У regex не поддерживаются lookarounds и ленивые модификаторы, используется синтаксис Rust regex crate, а не Python re; паттерн должен быть в одну строку, перенос строки задаётся через \n, и regex передаётся как обычная строка без //.

По сложности грамматику следует ограничивать правилами и паттернами, которые нужны инструменту: API может вернуть ошибку, если грамматика слишком сложна. Менее сложные грамматики работают надёжнее, а сложные часто требуют итераций по определению грамматики, промпту и описанию инструмента.

Почему нельзя дробить свободный текст между терминалами

Лексер сопоставляет терминалы жадно (побеждает самое длинное совпадение) до применения правил CFG, поэтому правила не могут управлять границами терминалов. Если попытаться разделить свободный текст между терминалами, лексер жадно сопоставит куски свободного текста, и контроль над границами будет потерян.

Когда нужен свободный текст между якорями, его следует оформить как один большой regex-терминал, чтобы лексер сопоставил его ровно один раз с нужной структурой. Whitespace следует описывать явно: не полагаться на открытые директивы %ignore, так как неограниченные ignore-директивы могут сделать грамматику слишком сложной и привести модель к выходу из распределения. Вместо этого рекомендуется прописывать явные терминалы везде, где допускается whitespace.

Компромисс между гибкостью и предсказуемостью

Гибкость агента регулируется параметром tool_choice: по умолчанию модель сама решает, вызывать ли инструменты и сколько — это режим Auto, допускающий ноль, один или несколько вызовов. Предсказуемость достигается режимами Required (хотя бы один вызов) и Forced Function (ровно одна конкретная функция). Дополнительно allowed_tools ограничивает набор доступных инструментов подмножеством, не меняя передаваемый список, что позволяет экономить на кэшировании промптов.

Строгий режим включается для схем, сгенерированных в playground, и рекомендуется, но имеет ограничения: часть возможностей JSON Schema не поддерживается, а для дообученных моделей схемы обрабатываются при первом запросе и кэшируются, что при их изменении повышает задержки и исключает zero data retention. Для пользовательских инструментов можно задать грамматику через параметр grammar в формах lark или regex, чтобы ограничить текстовый ввод модели. Баланс выбран так: по умолчанию максимальная гибкость, а предсказуемость включается явно через tool_choice, allowed_tools, строгий режим и грамматики.

Что из этого следует на практике

  • Обрабатывайте ответ как список вызовов. Ответ модели может содержать ноль, один или несколько вызовов, поэтому перебирайте все элементы tool_calls, а не полагайтесь на единственный вызов.
  • Связывайте результат с вызовом через call_id. Без этого модель не сопоставит вывод инструмента с конкретным запросом.
  • Не теряйте reasoning items. Для reasoning-моделей их нужно возвращать обратно вместе с выводами вызова.
  • Держите число доступных инструментов небольшим. Ориентир — менее 20 функций на начало хода; для больших или редко используемых частей набора применяйте tool search.
  • Перекладывайте нагрузку с модели на код. Не заставляйте модель заполнять уже известные аргументы и объединяйте функции, которые всегда вызываются последовательно.
  • Проверяйте инструмент «тестом стажёра»: может ли человек правильно использовать функцию, имея только то, что дано модели.
  • Ограничивайте грамматику только необходимым. Сложные грамматики чаще требуют итераций и могут вызывать ошибку API.
  • Помните про кэш схем. Если схемы меняются от запроса к запросу, задержки растут, а zero data retention не применяется.

Источники

Похожее