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-4.6",
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. Если функция упала, верните это как результат с понятным текстом ошибки, а не бросайте исключение наружу. Модель умеет реагировать на ошибку и попробовать иначе, если видит её текстом.
Защита от зацикливания
Модель может застрять, повторяя один и тот же вызов с небольшими вариациями аргументов, особенно если инструмент возвращает пустой или неоднозначный результат. Ставьте жёсткий потолок на число шагов цикла, обычно хватает пяти-десяти, и явно прерывайте выполнение с сообщением об ошибке, если потолок достигнут.
Полезно также логировать каждый вызов инструмента с аргументами. Когда цикл зацикливается, лог сразу показывает, на каком шаге модель начала повторяться.
Разница между форматами
Через Claudexia доступны оба формата. У Anthropic это блоки tool_use и tool_result внутри content, у OpenAI это поле tool_calls в ответе ассистента и роль tool в следующем сообщении. Логика та же, но структура JSON различается, поэтому код цикла для двух форматов не переиспользуется один в один, если вы работаете напрямую с REST, а не через SDK.
Коротко
Схема инструмента должна объяснять модели, что делает функция и когда её вызывать, желательно с перечислениями вместо свободного текста. Цикл выполнения растёт по токенам с каждым шагом, считайте их и ставьте потолок на число итераций. Ошибки инструмента возвращайте как текст в tool_result, а не как исключение, модель справляется с ними лучше, чем ваш код думает.