Tool use в Claude: полный цикл вызова инструментов на Python

Tool use позволяет Claude вызывать функции вашего кода. Разбираем цикл целиком: описание инструментов, параллельные вызовы, ошибки и ограничения.

4 мин · · разработка · агенты

Tool use (вызов инструментов, function calling) позволяет Claude обращаться к функциям вашего кода: модель решает, какую функцию вызвать и с какими аргументами, вы выполняете её и возвращаете результат, модель продолжает работу. На этом строится любой агент — от калькулятора до бота с доступом к базе данных. Ниже — полный рабочий пример tool use в Claude на Python через Anthropic SDK и Zukko, сверенный с документацией Anthropic.

Как устроен цикл tool use

  1. Вы передаёте в запросе список инструментов: имя, описание и JSON Schema аргументов.
  2. Если Claude решает вызвать инструмент, ответ приходит с stop_reason: "tool_use" и одним или несколькими блоками tool_use.
  3. Вы добавляете ответ модели в историю целиком (response.content, а не только текст).
  4. Выполняете каждый блок tool_use и отправляете результаты в одном сообщении пользователя — блоками tool_result с tool_use_id соответствующего вызова.
  5. Повторяете запрос. Цикл заканчивается, когда stop_reason перестаёт быть tool_use.

Установка и клиент

bash
pip install anthropic
bash
import json
import anthropic

client = anthropic.Anthropic(
    base_url="https://api.zukko.pro",
    api_key="fetch_ваш_ключ",
)

Адрес указан без /v1: Anthropic SDK добавляет путь сам. Подробнее — в руководстве по Python и справке по Messages.

Функции и их описание

Сначала обычные Python-функции. Калькулятор разбирает выражение через ast, а не через eval: аргументы приходят от модели, а значит, косвенно от пользователя, и выполнять их как код нельзя.

bash
import ast
import operator

def get_weather(location: str, unit: str = "celsius") -> str:
    data = {"London": 15, "Paris": 18, "Tokyo": 22}
    if location not in data:
        raise ValueError(f"Нет данных о погоде для {location}")
    t = data[location]
    return f"{location}: {t}°C" if unit == "celsius" else f"{location}: {round(t * 9 / 5 + 32)}°F"

OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
       ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg}

def _eval(node):

Теперь описание для модели. Описание инструмента — главное, по чему Claude решает, когда его вызывать, поэтому пишите его подробно.

bash
tools = [
    {
        "name": "get_weather",
        "description": "Возвращает текущую температуру в городе. Используй, когда спрашивают о погоде.",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {"type": "string", "description": "Город на английском, например London"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
            },
            "required": ["location"],
        },
    },
    {

Полный цикл

Ключевая деталь: в одном ответе Claude может запросить несколько инструментов сразу (например, погоду в двух городах). Нужно выполнить все вызовы и вернуть все результаты в одном сообщении пользователя. Если обработать только первый блок tool_use, API отклонит следующий запрос: у каждого вызова должен быть свой tool_result.

bash
def run_agent(user_message: str, model: str = "claude-sonnet-5", max_steps: int = 10) -> str:
    messages = [{"role": "user", "content": user_message}]

    for _ in range(max_steps):
        response = client.messages.create(
            model=model,
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason != "tool_use":
            if response.stop_reason == "max_tokens":

Что здесь важно:

  • Ответ модели добавляется целиком. В response.content кроме tool_use могут быть текст и служебные блоки; их нужно вернуть в историю без изменений.
  • Результаты идут первыми. В сообщении с результатами блоки tool_result должны стоять в начале; если хотите добавить текст, ставьте его после всех результатов.
  • Ошибки не теряются. Если функция упала, вернуть нужно tool_result с is_error: True и текстом ошибки. Модель увидит его и сможет исправить аргументы или ответить пользователю.
  • Есть предел шагов. max_steps защищает от бесконечного цикла и неожиданного расхода.

Запуск

bash
print(run_agent("Какая погода в London и Paris? И сколько будет (18 - 15) * 7?"))
print(run_agent("Сколько градусов в Tokyo по Фаренгейту?"))
print(run_agent("Какая погода в Berlin?"))

Третий запрос вернёт ошибку от инструмента (нет данных), и модель сообщит об этом в ответе, вместо того чтобы падать.

Управление выбором инструмента

Параметр tool_choice по умолчанию равен {"type": "auto"} — модель сама решает, вызывать ли инструмент. Можно запретить вызовы ({"type": "none"}) или ограничить их одним за ход: {"type": "auto", "disable_parallel_tool_use": True}.

Принудительный вызов ({"type": "any"} или {"type": "tool", "name": "..."}) поддерживается не всеми моделями. По документации Anthropic, для Claude Opus 5.5 такие значения возвращают ошибку 400. Если с этой моделью нужен гарантированный вызов, используйте auto и явную инструкцию в промпте, например «используй инструмент get_weather».

Асинхронный вариант

bash
import asyncio
from anthropic import AsyncAnthropic

aclient = AsyncAnthropic(base_url="https://api.zukko.pro", api_key="fetch_ваш_ключ")

async def run_agent_async(user_message: str, model: str = "claude-sonnet-5") -> str:
    messages = [{"role": "user", "content": user_message}]
    for _ in range(10):
        response = await aclient.messages.create(
            model=model, max_tokens=4096, tools=tools, messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            return "".join(b.text for b in response.content if b.type == "text")

Типичные ошибки

  • Обработан только первый tool_use. При параллельных вызовах следующий запрос получит ошибку 400. Перебирайте все блоки.
  • В историю добавлен только текст ответа. Добавляйте response.content целиком.
  • Результаты разбиты на несколько сообщений. Все tool_result за один ход — в одном сообщении пользователя.
  • Нет tool_use_id. Без него модель не сопоставит результат с вызовом.
  • Аргументы выполняются как код. Никакого eval, exec и подстановки в shell или SQL без проверки.

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

Частые вопросы

Может ли Claude не вызвать инструмент?

Да. Если инструмент не нужен, модель ответит сразу со stop_reason: "end_turn". Цикл выше это обрабатывает: он возвращает текст, как только stop_reason не равен tool_use.

Может ли Claude вызвать несколько инструментов за один ход?

Да, это поведение по умолчанию. Выполните все вызовы и верните результаты одним сообщением. Ограничить до одного вызова за ход можно через disable_parallel_tool_use.

Есть ли ограничение на размер результата инструмента?

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

Можно ли сделать то же через OpenAI-формат?

Да, но описание инструментов и формат ответа там другие. Этот пример — для Anthropic SDK; про OpenAI-формат смотрите справку по Chat Completions.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.