hubris
Войти
Tool calling: как нейросеть вызывает ваши функции

Tool calling: как нейросеть вызывает ваши функции

7 июля 2026 г. · Команда Hubris · 9 мин чтения

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

Зачем нейросети ваши функции

Модель знает только то, что попало в её обучающие данные. Она не в курсе сегодняшнего курса доллара, погоды за окном и тем более содержимого вашей CRM. Tool calling закрывает этот разрыв: вы описываете, какие функции у вас есть, а модель решает, когда и с какими аргументами их позвать.

Что это даёт на практике:

  • Свежие данные — курсы валют, погода, котировки, статусы доставки;
  • Приватные данные — заказы, остатки на складе, история клиента из вашей БД;
  • Действия — отправить письмо, создать задачу, изменить запись;
  • Точные вычисления — модель плохо считает «в уме», калькулятор-функция считает точно;
  • Агентные сценарии — цепочки вызовов, на которых строятся ассистенты для работы с кодом и автономные агенты.

Важно понимать: модель никогда не исполняет код. Она только формирует заявку на вызов. Исполнение, проверка аргументов и ответственность за результат — на вашей стороне. Это безопасно по построению: без вашего кода ничего не произойдёт.

Схема tools: контракт между моделью и кодом

Голографический чертёж контракта функции между моделью и кодом

Функции описываются в параметре tools запроса к /v1/chat/completions — это массив объектов с JSON Schema. По сути, вы передаёте модели контракт: имя функции, человекочитаемое описание и типизированные параметры.

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Возвращает текущую погоду в указанном городе.",
    "parameters": {
      "type": "object",
      "properties": {
        "city": { "type": "string", "description": "Название города" },
        "units": { "type": "string", "enum": ["celsius", "fahrenheit"] }
      },
      "required": ["city"]
    }
  }
}

Три правила, от которых зависит качество вызовов:

  1. description — это промпт. Модель выбирает функцию и заполняет аргументы, опираясь на описания. «Возвращает погоду» хуже, чем «Возвращает текущую погоду в указанном городе; город — по-русски, в именительном падеже».
  2. required обязателен. Поля вне списка required модель может опустить — ваш код должен иметь значения по умолчанию.
  3. enum сужает свободу. Там, где допустим фиксированный набор значений, перечислите его явно — меньше шансов получить «celsius (по Цельсию)» вместо celsius.

Полная схема запроса и ответа — в справочнике POST /v1/chat/completions.

Цикл вызова: запрос → исполнение → результат

Световой цикл обмена вызовами между моделью и инструментом

Один «раунд» работы с инструментом всегда состоит из пяти шагов:

  1. Вы отправляете запрос с messages и tools.
  2. Модель решает: ответить текстом или попросить вызов. Во втором случае приходит finish_reason: "tool_calls" и массив tool_calls[] в assistant-сообщении — у каждого вызова есть id, имя функции и строка arguments с JSON-аргументами.
  3. Вы исполняете функции у себя.
  4. Отправляете новый запрос: к истории добавляете то самое assistant-сообщение с tool_calls и по одному сообщению role: "tool" на каждый вызов — с результатом в content и обязательным tool_call_id, равным id вызова.
  5. Модель формирует финальный ответ для пользователя.

Модель может попросить несколько функций за один шаг (параллельные вызовы) — тогда в tool_calls[] придёт несколько элементов, и на каждый нужен свой role: "tool" ответ. Если цепочка длинная, шаги 2–4 повторяются, пока модель не ответит текстом. Примеры на чистом curl, поведение tool_choice и разбор стриминга — в документации по вызову инструментов.

Полный пример на Python: погода и курс валют

Используем официальный пакет openai — меняется только base_url. Ключ вида sk-gw-... создаётся в личном кабинете: регистрация на hubris.pw/sign-in по коду из письма, далее раздел «Ключи»; баланс пополняется в разделе «Биллинг» — СБП, карта или счёт для юрлиц.

import json
from openai import OpenAI

client = OpenAI(
    base_url="https://api.hubris.pw/v1",
    api_key="sk-gw-...",  # ваш ключ из раздела «Ключи»
)

def get_weather(city: str, units: str = "celsius") -> dict:
    # Здесь обычно запрос к погодному сервису; для примера — заглушка.
    return {"city": city, "temperature": -3, "conditions": "снег", "units": units}

def get_fx_rate(base: str, quote: str) -> dict:
    # Здесь обычно запрос к источнику курсов, например ЦБ РФ.
    return {"pair": f"{base}/{quote}", "rate": 91.0}

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Возвращает текущую погоду в указанном городе.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "Название города"},
                    "units": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_fx_rate",
            "description": "Возвращает курс валютной пары, например USD/RUB.",
            "parameters": {
                "type": "object",
                "properties": {
                    "base": {"type": "string", "description": "Базовая валюта, код ISO"},
                    "quote": {"type": "string", "description": "Котируемая валюта, код ISO"},
                },
                "required": ["base", "quote"],
            },
        },
    },
]

REGISTRY = {"get_weather": get_weather, "get_fx_rate": get_fx_rate}

messages = [
    {"role": "user", "content": "Какая погода в Москве и сколько сейчас стоит доллар в рублях?"}
]

for _ in range(5):  # предохранитель от бесконечного цикла вызовов
    resp = client.chat.completions.create(
        model="anthropic/claude-haiku-4.5",
        messages=messages,
        tools=TOOLS,  # tools передаём на каждом шаге, не только на первом
    )
    msg = resp.choices[0].message

    if not msg.tool_calls:
        print(msg.content)  # финальный ответ модели
        break

    messages.append(msg)  # assistant-сообщение с tool_calls — обязательно в историю

    for call in msg.tool_calls:
        fn = REGISTRY.get(call.function.name)
        try:
            args = json.loads(call.function.arguments)
            result = fn(**args) if fn else {"error": "неизвестная функция"}
        except (json.JSONDecodeError, TypeError) as exc:
            result = {"error": f"некорректные аргументы: {exc}"}

        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result, ensure_ascii=False),
        })

Скрипт обрабатывает оба вызова за один раунд (модель попросит погоду и курс параллельно) и печатает связный ответ. Модель anthropic/claude-haiku-4.5 хорошо подходит для таких задач: быстрая, уверенно работает с инструментами, вход — 106,11 ₽, выход — 530,53 ₽ за миллион токенов (цены актуальны на июнь 2026 года).

Типичные грабли: валидация аргументов и не только

Большинство ошибок при работе с tool calling — на стороне клиентского кода:

  • arguments — это строка, а не объект. Её нужно разбирать через json.loads, и она бывает синтаксически битой. Всегда оборачивайте разбор в try/except и возвращайте модели понятную ошибку — она исправится на следующем шаге.
  • Аргументы приходят от модели, а не от вас. Модель может выдумать поле, перепутать тип или подставить значение вне enum. Проверяйте структуру (например, через Pydantic) до того, как передать аргументы в реальную функцию.
  • Аргументам нельзя доверять как безопасным. Никаких подстановок в SQL-строки, путей к файлам без проверки и тем более eval. Относитесь к ним как к пользовательскому вводу.
  • Потерянный tool_call_id. Каждое сообщение role: "tool" обязано ссылаться на id конкретного вызова — иначе запрос завершится ошибкой.
  • tools забыли передать во втором запросе. Описания инструментов нужны модели на каждом шаге диалога.
  • Нет ограничителя цикла. Модель может звать функции снова и снова; лимит итераций (как range(5) в примере) защищает и от зацикливания, и от незапланированных расходов.
  • Раздутые описания. Описания инструментов входят в prompt_tokens на каждом шаге: десять функций по 200 токенов — это плюс 2000 токенов к каждому запросу. Передавайте только те инструменты, что нужны в текущем сценарии.

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

Какие модели поддерживают tool calling?

Большинство современных: Claude (Haiku, Sonnet, Opus), GPT-4o и новее, Gemini, Llama 3.1+, Mistral. В каталоге моделей на карточке каждой модели указана поддержка параметра tools — проверяйте перед выбором.

Сколько стоит tool calling?

Отдельного тарифа нет: списываются обычные токены за каждый шаг диалога. Описания инструментов и сообщения role: "tool" считаются как часть промпта, сгенерированный tool_calls[] — как ответ. Итоговая цена зависит от модели и объёма диалога.

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

Да, по умолчанию параллельные вызовы включены: в tool_calls[] придёт несколько элементов, на каждый нужен отдельный role: "tool" ответ. Отключаются параметром "parallel_tool_calls": false — тогда модель будет звать функции строго по одной.

Чем tool calling отличается от структурированного вывода?

Tool calling — про действия: модель просит ваш код что-то выполнить и ждёт результат. Структурированный вывод — про формат: модель сразу отвечает JSON-объектом по заданной схеме, без обратного вызова. Если вам нужен только типизированный ответ — берите второй вариант, он проще.

Все модели из статьи доступны в Hubris — единый API, оплата в рублях.