← Блог

Tool calling: как научить модель вызывать ваш код

· 7 минут чтения

Коротко

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

Цикл всегда один и тот же, четыре шага:

Схема из четырех шагов, показывающая обмен сообщениями между приложением и нейросетью.
Модель ничего не выполняет сама — она лишь просит вас запустить нужный код.

  1. Вы описываете доступные функции и отправляете вопрос.
  2. Модель отвечает не текстом, а просьбой о вызове.
  3. Вы выполняете функцию у себя и отправляете результат обратно.
  4. Модель формулирует человеческий ответ.

Понимание, что шаг 3 целиком ваш, снимает половину вопросов о безопасности: модель не получает доступа ни к чему, кроме того, что вы сами ей отдадите.

Tool calling (он же function calling, вызов функций) - режим, в котором модель может попросить выполнить одну из описанных вами функций и получить результат. Ниже - как это устроено на коде, сколько стоит и где ломается.

Шаг 1: описать инструменты

Функции описываются схемой - так же, как формат ответа. Модель видит только это описание, самого кода она не видит никогда.

TOOLS = [{
    "type": "function",
    "function": {
        "name": "get_order_status",
        "description": "Возвращает статус заказа по его номеру. Использовать, когда пользователь спрашивает про конкретный заказ.",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "integer", "description": "Номер заказа, целое число"},
            },
            "required": ["order_id"],
            "additionalProperties": False,
        },
    },
}]

Поле description - не комментарий, а часть промпта. По нему модель решает, вызывать функцию или нет. «Возвращает статус заказа» работает заметно хуже, чем «Использовать, когда пользователь спрашивает про конкретный заказ»: во втором случае описан не результат, а повод.

Это же место, где чинится большинство проблем с выбором. Модель зовёт не ту функцию - правьте описания, а не системный промпт.

Шаг 2: получить просьбу о вызове

messages = [{"role": "user", "content": "Что с моим заказом 4815?"}]

resp = client.chat.completions.create(
    model="claude-sonnet-5",
    messages=messages,
    tools=TOOLS,
)
msg = resp.choices[0].message

Дальше развилка. Если модель решила, что инструмент не нужен, в msg.content лежит обычный текст. Если нужен - msg.tool_calls содержит список вызовов с аргументами в виде строки JSON.

Список - не описка: моделям современных поколений разрешено просить несколько вызовов сразу, и код должен быть к этому готов с самого начала.

Шаг 3: выполнить и вернуть результат

import json

REGISTRY = {"get_order_status": get_order_status}

if msg.tool_calls:
    messages.append(msg)                       # обязательно: просьба модели
    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": "unknown tool"}
        except Exception as e:
            result = {"error": str(e)}          # ошибку тоже возвращаем модели
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result, ensure_ascii=False),
        })

    final = client.chat.completions.create(
        model="claude-sonnet-5", messages=messages, tools=TOOLS,
    )
    print(final.choices[0].message.content)

Три обязательных детали, на которых спотыкаются:

  • Сообщение модели с просьбой добавляется в историю. Пропустите его - и следующий запрос будет непоследовательным: результат вызова придёт без вызова.
  • tool_call_id возвращается ровно тот, что прислали. По нему сопоставляются просьба и ответ.
  • Ошибку исполнения возвращают модели, а не глотают. Получив {"error": "заказ не найден"}, модель скажет пользователю осмысленное. Упав в except молча, вы получите пустой ответ.

Шаг 4: цикл, а не одна итерация

В реальном сценарии модель может запросить вызов снова, получив результат первого: узнала номер клиента из заказа - теперь хочет его контакты.

def run(question, *, max_rounds=5):
    messages = [{"role": "user", "content": question}]
    for _ in range(max_rounds):
        resp = client.chat.completions.create(
            model="claude-sonnet-5", messages=messages, tools=TOOLS,
        )
        msg = resp.choices[0].message
        if not msg.tool_calls:
            return msg.content
        messages.append(msg)
        for call in msg.tool_calls:
            messages.append(execute(call))
    raise RuntimeError("превышено число раундов вызова инструментов")

🔴 max_rounds - обязателен. Без него модель, попавшая в петлю (вызвала, не поняла результат, вызвала снова), крутится, пока не кончатся деньги.

Циклическая диаграмма, показывающая прерывание бесконечного цикла запросов к модели по достижении лимита.
Ограничение количества раундов — единственный способ защитить бюджет от бесконечных галлюцинаций модели.
Это самый дорогой из известных способов их потратить: каждый раунд - полный запрос со всей выросшей историей.

Сколько это стоит

Главное, чего не ожидают: вызов инструмента удваивает число запросов, а история в каждом следующем запросе длиннее предыдущей.

Считаем один диалог с одним вызовом: 400 токенов вопроса и описаний инструментов, 40 токенов просьбы о вызове, 120 токенов результата, 80 токенов финального ответа.

Столбчатая диаграмма стоимости десяти тысяч диалогов для разных версий моделей Claude.
Вызов инструмента удваивает количество запросов, поэтому использование мощных моделей быстро увеличивает расходы.
Цены - по каталогу на 23.08.2026.

Модель10 000 диалогов
Claude Haiku 4.5$12.21
Claude Sonnet 5$26.42
Claude Opus 4.8$70.20

Без инструментов тот же вопрос стоил бы примерно вдвое дешевле - платите вы за два прохода вместо одного.

Практический вывод: описания инструментов надо держать короткими, потому что они уезжают в каждый запрос цикла. Десять функций с развесистыми описаниями - это несколько сотен токенов накладных расходов на каждом раунде.

Что ломается

Модель не зовёт нужный инструмент. Почти всегда - описание. Опишите повод («когда пользователь спрашивает про…»), а не результат.

Модель зовёт инструмент, когда не надо. Обратная сторона того же. Помогает явная граница в описании: «Не использовать для общих вопросов о доставке».

Аргументы приезжают не того типа. Номер заказа строкой вместо числа. Лечится строгой схемой с additionalProperties: false и валидацией на своей стороне - та же дисциплина, что и с структурированным ответом.

Поддержка зависит от модели. Инструменты держат не все модели каталога, а те, что держат, ведут себя по-разному в мелочах - например, в готовности просить несколько вызовов разом. Проверяйте на своей модели, а не на общем описании формата.

🔴 Про безопасность. Модель не выполняет код - но она формирует аргументы, а текст, который она обрабатывает, мог прийти от пользователя. Функция вроде run_sql(query) превращает вежливую просьбу в чужом письме в команду вашей базе. Инструменты должны быть узкими: get_order_status(order_id), а не «выполни запрос». Права проверяются в вашем коде, до вызова, и никогда не в промпте.

С чего начать

  1. Начните с одной функции, только читающей, без побочных эффектов.
  2. Опишите повод вызова, а не действие - это самая результативная правка.
  3. Поставьте max_rounds до первого запуска, а не после первого счёта.
  4. Возвращайте ошибки модели в виде результата, а не проглатывайте.
  5. Проверяйте права в своём коде. Всё, что даёт запись или удаление, - только после явной проверки, кто спрашивает.

Все 44 модели с параметрами и признаком поддержки инструментов - на витрине. Про строгий формат ответа - отдельный разбор, про то, какой модели поручать какую задачу, - маршрутизация.

FAQ

Что такое function calling простыми словами?

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

Модель выполняет мой код?

Нет. Она видит только описание функции - имя, назначение и схему аргументов. Исполнение полностью на вашей стороне, поэтому и доступ у модели ровно тот, который вы сами решили ей дать.

Почему модель не вызывает нужный инструмент?

Почти всегда дело в поле description: оно попадает в промпт и по нему принимается решение. Описывать нужно повод для вызова («когда пользователь спрашивает про конкретный заказ»), а не результат работы функции.

Насколько дороже работа с инструментами?

Примерно вдвое на один вызов: цикл требует минимум двух запросов вместо одного, и история во втором длиннее. Плюс описания всех инструментов уходят в каждый запрос, поэтому их стоит держать короткими.

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

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

Безопасно ли давать модели доступ к базе?

Только через узкие функции с проверкой прав в вашем коде. Инструмент вида «выполни произвольный запрос» опасен: текст, который обрабатывает модель, мог прийти от пользователя и содержать инструкцию. Права проверяются до вызова и никогда не формулировкой в промпте.