В документации tool use выглядит просто, а в проде ломается сразу по десятку причин. Ниже о том, как строить схемы и циклы, чтобы до этого не доходило.
Что происходит на уровне протокола
Вы передаёте модели список функций: имя, описание и JSON-схема параметров. Сама функция моделью не вызывается. Она возвращает структурированный блок tool_use с именем и аргументами. Код выполняете вы, у себя, затем отправляете итог обратно как tool_result, и модель продолжает разговор, уже зная результат.
Деталь, которую легко упустить: за один шаг модель может запросить несколько инструментов, если задача позволяет. Ваш код должен уметь выполнить их, при желании параллельно, и вернуть результаты в правильном порядке. Иначе цикл перестанет понимать, какой результат относится к какому вызову.
Как проектировать схему
При выборе инструмента модель опирается только на имя функции и описания параметров. Расплывчатое «работает с данными» приводит к случайным вызовам там, где они не нужны.
Хорошее описание сразу отвечает на три вопроса: что делает функция, когда её вызывать и когда не надо. Пример:
{
"name": "search_orders",
"description": "Ищет заказы клиента по номеру телефона или email. Вызывай только если пользователь явно спрашивает про свой заказ. Не вызывай для общих вопросов о доставке или ценах.",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Телефон или email клиента" },
"status": { "type": "string", "enum": ["any", "pending", "shipped", "delivered"] }
},
"required": ["query"]
}
}
Перечисления вместо свободного текста сокращают долю невалидных вызовов сильнее, чем любая правка промпта. Если параметр принимает три значения, задайте enum из трёх значений, а не абзац прозы.
Многошаговый цикл
Реальная задача редко закрывается одним вызовом. Модель дёргает инструмент, смотрит на результат, решает, нужен ли ещё шаг, и так до итогового текстового ответа. Каркас цикла на Python:
messages = [{"role": "user", "content": user_query}]
while True:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
tool_calls = [b for b in response.content if b.type == "tool_use"]
if not tool_calls:
break
results = [run_tool(call.name, call.input) for call in tool_calls]
messages.append({
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": call.id, "content": result}
for call, result in zip(tool_calls, results)
],
})
Каждый оборот цикла отправляет новый запрос с растущей историей. Считайте токены на каждом шаге: история разбухает быстро, особенно когда инструменты возвращают большие куски текста.
Типичные ошибки в схемах
Слишком много инструментов сразу. Когда на выбор двадцать похожих функций, модель чаще путает, какую вызвать. Объединяйте родственные операции в один инструмент с параметром-режимом, а не заводите десять почти одинаковых.
Описание для человека, а не для модели. «См. документацию API» бесполезно: внешние ссылки модель не открывает, всё нужное должно стоять прямо в тексте описания.
Нет обработки ошибок в tool_result. Если функция упала, верните ошибку как результат с понятным текстом, а не выбрасывайте исключение наружу. Увидев ошибку текстом, модель может отреагировать и попробовать иначе.
Защита от бесконечного цикла
Модель способна застрять и повторять один вызов с чуть изменёнными аргументами, особенно если инструмент вернул пустой или неоднозначный результат. Поставьте жёсткий потолок на число шагов (обычно хватает пяти-десяти) и прерывайте выполнение с понятной ошибкой, когда он достигнут.
Ещё помогает лог: записывайте каждый вызов инструмента вместе с аргументами. Когда цикл начинает крутиться вхолостую, по логу сразу видно, на каком шаге модель пошла по кругу.
Чем отличаются форматы
В codexkey доступны оба формата. У Anthropic это блоки tool_use и tool_result внутри content. У OpenAI это поле tool_calls в ответе ассистента и роль tool в следующем сообщении. Логика одинакова, но форма JSON другая, поэтому при работе напрямую через REST, без SDK, код цикла для двух форматов один в один не переносится.
Итог
Схема должна объяснять модели, что делает функция и когда её вызывать, и лучше с enum вместо свободного текста. Цикл прибавляет токены с каждым шагом, поэтому считайте их и ограничивайте число итераций. Ошибки инструмента возвращайте текстом в tool_result, а не исключением: модель справляется с ними лучше, чем ожидает ваш код.