← Claude на русском
Перевод с разбором · для Сони
Адаптировал Claude Opus 4.7 (ИИ) на основе документации Anthropic. Полная версия — в docs/tool-use.html.

Tool use: модель «нажимает кнопки»

Адаптация для Сони · 2026-04-27

Что такое tool use

Tool use (использование инструментов; в OpenAI исторически называлось «function calling») — это механизм, который позволяет языковой модели не просто разговаривать, а выполнять действия. Без tool use бот может только генерировать текст. С tool use он может добавить задачу в БД, сохранить цель, отправить напоминание — потому что у тебя в коде заранее описаны функции, которые модель умеет вызвать.

Anthropic в оригинале формулирует так: «Tool use позволяет модели вызывать функции, которые вы определяете. Модель решает, когда вызвать tool, на основе запроса пользователя и описания tool'а». Концепция у Claude и OpenAI почти одинаковая — различается только формат JSON. Эта адаптация показывает форму OpenAI, потому что у тебя GPT-5.4-mini.

Цикл tool use, шаг за шагом

  1. Ты описываешь в коде, какие функции есть и какие у них параметры.
  2. Ты отправляешь модели сообщение пользователя + список доступных tools.
  3. Модель либо отвечает текстом, либо возвращает «вызов tool'а»: имя функции и аргументы.
  4. Твой код выполняет функцию (записывает в БД, шлёт запрос, что угодно) и получает результат.
  5. Ты отправляешь модели результат tool'а.
  6. Модель на основе результата формулирует финальный ответ пользователю.

Шаги 3–5 могут повториться несколько раз, если модель решит, что нужно вызвать ещё один tool на основе результата предыдущего. Это называется агентским циклом.

Описание tool'ов в OpenAI-формате

Для твоего бота нужны как минимум три tool'а:

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "add_task",
            "description": (
                "Добавить задачу в TODO пользователя. "
                "Вызывай, когда пользователь явно просит запомнить задачу "
                "или поставить напоминание. Не вызывай, если пользователь "
                "просто рассказывает о задаче в прошедшем времени."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "title": {
                        "type": "string",
                        "description": "Краткое название задачи, 1-7 слов"
                    },
                    "due_date": {
                        "type": "string",
                        "description": (
                            "Дата в ISO 8601 (YYYY-MM-DD) или null, "
                            "если пользователь не указал"
                        ),
                        "nullable": True
                    },
                    "priority": {
                        "type": "string",
                        "enum": ["low", "medium", "high"],
                        "description": "Приоритет, по умолчанию medium"
                    }
                },
                "required": ["title"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "save_goal",
            "description": (
                "Сохранить долгосрочную цель пользователя. "
                "Вызывай только после того, как пользователь подтвердил "
                "формулировку цели. Не вызывай при первом упоминании."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "goal": {
                        "type": "string",
                        "description": "Цель одной фразой, до 100 символов"
                    }
                },
                "required": ["goal"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_user_notes",
            "description": (
                "Получить заметки о пользователе из БД. "
                "Вызывай в начале диалога или когда нужен контекст "
                "о предыдущих темах."
            ),
            "parameters": {
                "type": "object",
                "properties": {},
            }
        }
    },
]

Цикл tool use в коде

async def chat_with_tools(
    user_id: int,
    user_text: str,
    history: list[dict],
) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        *history,
        {"role": "user", "content": user_text},
    ]

    while True:
        response = await client.chat.completions.create(
            model="gpt-5.4-mini",
            messages=messages,
            tools=TOOLS,
        )
        msg = response.choices[0].message

        if not msg.tool_calls:
            # Финальный ответ — модель не вызвала tools
            return msg.content

        # Добавляем сообщение модели в историю
        messages.append(msg.model_dump(exclude_none=True))

        # Выполняем все вызовы (могут быть параллельные)
        for call in msg.tool_calls:
            args = json.loads(call.function.arguments)
            result = await execute_tool(
                user_id=user_id,
                name=call.function.name,
                args=args,
            )
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(result, ensure_ascii=False),
            })
        # Цикл — модель увидит результаты и сформулирует ответ


async def execute_tool(user_id: int, name: str, args: dict) -> dict:
    if name == "add_task":
        await db.add_task(user_id, args["title"], args.get("due_date"),
                          args.get("priority", "medium"))
        return {"ok": True, "message": "Задача добавлена"}
    if name == "save_goal":
        await db.save_goal(user_id, args["goal"])
        return {"ok": True, "message": "Цель сохранена"}
    if name == "get_user_notes":
        notes = await db.get_user_notes(user_id)
        return {"notes": notes}
    return {"ok": False, "error": f"unknown tool: {name}"}

Этот цикл — каркас. В проде надо добавить: лимит итераций (чтобы модель не зациклилась в tool-вызовах), обработку ошибок tool'ов, логирование, и обновление history после ответа.

Что делает описание tool'а хорошим

Описание — это часть промпта

Модель решает, вызвать tool или нет, по полю description. Это значит: описание — это инструкция, не комментарий. Сравни:

Плохо:  "Добавляет задачу"
Хорошо: "Добавить задачу в TODO пользователя. Вызывай, когда
         пользователь явно просит запомнить задачу или поставить
         напоминание. Не вызывай, если пользователь просто рассказывает
         о задаче в прошедшем времени."

Хорошее описание содержит: что делает, когда вызывать, когда НЕ вызывать. Третья часть особенно важна — без неё модель будет «на всякий случай» вызывать tool там, где этого не нужно.

Параметры лучше типизировать

Если у тебя priority может быть только low / medium / high — пиши enum, не «строка». Иначе получишь "среднее" или "normal", и в коде придётся это разгребать.

"priority": {
    "type": "string",
    "enum": ["low", "medium", "high"],
}

Описывай каждый параметр

Поле description у параметра — тоже инструкция для модели. «Дата в ISO 8601 (YYYY-MM-DD)» точнее, чем просто "type": "string"; модель будет пытаться угадать формат, и не всегда удачно.

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

Современные модели (и Claude, и GPT-5) могут вызвать несколько tools за один шаг. Например: get_user_notes + add_task в одном ответе. В msg.tool_calls придёт массив, и важно обработать все — не первый.

Чтобы выполнять параллельные вызовы действительно параллельно (а не последовательно), используй asyncio.gather:

results = await asyncio.gather(*[
    execute_tool(user_id, c.function.name, json.loads(c.function.arguments))
    for c in msg.tool_calls
])
for call, result in zip(msg.tool_calls, results):
    messages.append({
        "role": "tool",
        "tool_call_id": call.id,
        "content": json.dumps(result, ensure_ascii=False),
    })

Когда модель не уверена

Anthropic в оригинале отмечает: «если в промпте недостаточно информации, чтобы заполнить обязательные параметры, более умные модели спросят уточнение, а менее умные — попробуют угадать». Это применимо и к OpenAI.

Например, у tool'а add_task обязательное поле title, а пользователь написал «поставь напоминание». Без названия задачи. Хорошая модель спросит «о чём напоминание?»; средняя может догадаться (и угадать неправильно).

Чтобы стимулировать уточнения, добавь в системный промпт:

Если для вызова tool'а недостаточно данных — задай пользователю
уточняющий вопрос, не угадывай параметры.

Forced tool use (заставить вызвать)

Иногда нужно гарантированно вызвать конкретный tool. Например, в самом начале диалога — обязательно get_user_notes, чтобы модель не работала вслепую. OpenAI поддерживает это через tool_choice:

response = await client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=messages,
    tools=TOOLS,
    tool_choice={"type": "function", "function": {"name": "get_user_notes"}},
)

Использовать аккуратно: если злоупотребить — модель будет «нажимать кнопки» там, где лучше было разговаривать.

Чего не делать

Тестирование tools

Тесты на tool use — отдельная история. Минимум:

Памятка