awesome-everything EN
↑ Обратно к восхождению

AI / LLM

Вызовы инструментов: цикл round-trip, валидация схемы и защита от зацикливания агента

Суть Вызов инструментов превращает модель в вызывающего функцию, но модель лишь шлёт запрос — выполняешь его ты. Каждый вызов — лишний round trip, аргументы бывают галлюцинацией, а цикл без защиты будет жечь токены вечно.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

Выкатили саппорт-агента. Клиент пишет «отмени мой последний заказ». Модель уверенно выдаёт вызов инструмента: POST /orders/{id}/cancel с id: "ord_9f3c" — id, который она нигде не видела, выдуман так, чтобы выглядеть правдоподобно. Хендлер сработал в стиле eval: доверился аргументам и отправил запрос. Отменён заказ не того клиента. Хуже того, через неделю другой баг заставил модель перевызывать падающий инструмент lookup_order 40 раз за один ход, пока кто-то не убил процесс — $9 токенов на вопрос, у которого не было ответа. У обоих багов один корень: цикл доверился модели.

Цикл round-trip — это контракт, а не функция

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

Каноничная форма — цикл while по полю stop_reason из ответа:

  1. Отправь запрос с массивом tools и сообщением пользователя.
  2. Модель отвечает stop_reason: "tool_use" и одним или несколькими блоками tool_use (у каждого имя инструмента и JSON-объект аргументов).
  3. Выполни каждый инструмент. Оформи результаты как блоки tool_result.
  4. Отправь новый запрос со всей историей плюс эти блоки tool_result.
  5. Повторяй, пока stop_reason всё ещё "tool_use". Выход — на "end_turn" (финальный ответ), "max_tokens", "stop_sequence" или "refusal".

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

Инструменты — это объявления через JSON-схему

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

Схемы не бесплатны. Типичное определение инструмента стоит примерно 500 токенов; десять инструментов — это ~5 000 токенов накладных на каждый запрос в цикле, ведь массив tools пересылается целиком каждый раунд. Агент на 10 инструментов в задаче из 6 шагов платит этот налог в 5 000 токенов шесть раз. Здесь впервые окупается prompt caching — кэширование статичного блока инструментов режет стоимость входа на 40–80% и улучшает time-to-first-token.

tool_choiceПоведениеКогда выбирает сеньор
autoМодель решает: вызвать инструмент или ответить текстомДефолт для агентов; модель сама судит, нужен ли инструмент
anyОбязан вызвать какой-то инструмент, модель выбирает какойКогда текст никогда не валидный ответ (роутер, обязанный диспетчить)
tool (форсированный)Обязан вызвать именно этот названный инструментСтруктурное извлечение: форсируй одну схему ради JSON гарантированной формы
noneЗапретить все инструменты на этот ходЗаставить дать текстовое резюме, когда результаты уже собраны

Параллельные вызовы режут latency — но не всякая цепочка их допускает

Современные модели (класса Claude 4), когда нужно несколько независимых инструментов, выдают несколько блоков tool_use в одном ответе. Ты запускаешь их параллельно и возвращаешь все блоки tool_result вместе. Это схлопывает три последовательных round trip в один — реальный выигрыш по latency, ведь каждый round trip — свежий вызов модели от сотен мс до секунд.

Загвоздка — зависимость. Параллелизм помогает, только когда вызовы независимы (get_weather(NYC) и get_weather(SF)). Цепочка, где второй вызов нуждается в выводе первого (find_user, затем cancel_user_order), по природе последовательна и не параллелится — модель должна увидеть первый результат, прежде чем заполнить аргументы второго инструмента. Можно выставить disable_parallel_tool_use: true, чтобы форсировать один инструмент за ход, когда твой слой исполнения не может безопасно работать конкурентно.

Почему это работает

Серверные инструменты (web search, code execution) крутят свой цикл внутри провайдера и имеют встроенный лимит итераций. Когда они упираются в него посреди задачи, ответ приходит с stop_reason: "pause_turn", а не "end_turn" — ты пересылаешь разговор, чтобы продолжить. У клиентских инструментов такого встроенного лимита нет; защиту пишешь ты сам.

Валидируй аргументы — никогда им не доверяй

Модель выдаёт правдоподобный JSON, а не корректный. Она может выдумать id, изобрести значение enum, которого схема никогда не перечисляла, опустить обязательное поле или передать строку там, где нужно число. Стартовая катастрофа — выдуманный ord_9f3c, скормленный прямо в эндпоинт отмены, — каноничный провал: хендлер обошёлся с выводом модели как с доверенным вводом.

Дисциплина сеньора — жёсткий шлагбаум перед выполнением:

  • Валидируй аргументы по схеме против input_schema инструмента (валидатор вроде Pydantic или jsonschema). Отвергай некорректные формы сразу; никогда не делай eval и не разбирай их вслепую.
  • Авторизуй и проверяй существование упомянутых сущностей. Корректный по форме id всё ещё недоверенный — подтверди, что заказ существует и принадлежит этому вызывающему, прежде чем действовать.
  • При отказе верни tool_result с ошибкой, а не исключение, ломающее цикл. Модель читает ошибку и может исправиться на следующем ходу — этот канал обратной связи и есть весь смысл возврата структурированных ошибок инструмента.

Относись к аргументам инструмента ровно как к любому другому недоверенному вводу пользователя, пересекающему границу доверия, — потому что это именно он и есть.

Защита по max-iteration от зацикленных циклов

Без лимита ходов сбитая с толку модель крутится вечно: зовёт lookup_order, получает ошибку, зовёт снова с теми же аргументами, получает ту же ошибку, и так по кругу. Каждая итерация — полный вызов модели, тарифицирующий весь накапливающийся транскрипт — стоимость и токены растут с каждым шагом. Это реальный инцидент и реальный счёт (один застрявший ход тихо сжёг $9 токенов, пока не вмешался человек).

Две защиты, обе обязательны в проде:

  1. Жёсткий лимит итерацийfor step in range(MAX_STEPS) (часто 8–15). Упёрся в лимит — останови цикл, верни пользователю аккуратный отказ. Никогда не while True.
  2. Детекция повторов — если модель зовёт тот же инструмент с теми же аргументами дважды подряд, это сигнал застревания. Прерви или вброс сообщения, что вызов уже упал, чтобы она перестала повторяться.
Выбери лучший вариант

Цикл агента зовёт реальные мутирующие эндпоинты (отмена заказа, возврат). Как обращаться с аргументами, которые выдаёт модель?

Викторина

Задача агента на 5 шагов использует инструменты на каждом шаге. Сколько это примерно вызовов модели и почему это важно?

Викторина

Модель возвращает tool_use для cancel_order с id 'ord_9f3c', которого в разговоре не было. Какой ход сеньора?

Расставь шаги по порядку

Расставь одну безопасную итерацию клиентского цикла tool-use:

  1. 1 Отправь запрос с массивом tools; прочитай stop_reason из ответа
  2. 2 Если stop_reason это tool_use, извлеки имя и аргументы из каждого блока tool_use
  3. 3 Валидируй аргументы по схеме, потом авторизуй/проверь существование упомянутых сущностей
  4. 4 Выполни валидные вызовы (параллельно, если независимы); оформи результаты как блоки tool_result
  5. 5 Отправь новый запрос со всей историей + блоками tool_result — под лимитом max-iteration
Вспомните перед уходом
  1. 01
    Пройди по тому, почему цикл tool-use без защиты — это и риск корректности, и риск стоимости, и какие две защиты ты добавляешь.
  2. 02
    Почему обязательно валидировать аргументы инструмента и что значит «валидировать» для мутирующего эндпоинта вроде cancel_order?
Итог

Вызов инструментов инвертирует обычный контракт: вызывающий — модель, а твой код — вызываемый. Модель выдаёт структурированный запрос tool_use с именем инструмента и JSON-аргументами; твой код выполняет его и возвращает tool_result, и цикл повторяется, пока stop_reason остаётся "tool_use". Три вещи определяют tool use уровня сеньора. Первое — latency и стоимость: каждый round trip инструмента это совершенно новый вызов модели, пересылающий весь растущий транскрипт плюс массив схем по ~500 токенов на инструмент, так что задача на 5 шагов — это ~6 вызовов модели; prompt caching статичного блока инструментов — стандартное смягчение. Второе — валидация: аргументы модели — недоверенный ввод, который бывает галлюцинацией, поэтому валидируешь по схеме, затем авторизуешь и проверяешь существование сущностей, затем выполняешь, возвращая ошибки как tool_result, чтобы модель исправилась — никогда не eval выдуманный id в мутирующий эндпоинт. Третье — защита: жёсткий лимит итераций плюс детекция повторов, ведь цикл без защиты будет перевызывать падающий инструмент вечно и жечь реальные деньги. Используй tool_choice (auto/any/tool/none), чтобы управлять тем, сработает ли инструмент и какой, и параллельные вызовы, чтобы схлопнуть независимые round trip — но только когда вызовы не зависят друг от друга.

Продолжить восхождение ↑Tool calls: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.