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

- Вы описываете доступные функции и отправляете вопрос.
- Модель отвечает не текстом, а просьбой о вызове.
- Вы выполняете функцию у себя и отправляете результат обратно.
- Модель формулирует человеческий ответ.
Понимание, что шаг 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 токенов финального ответа.

| Модель | 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), а не «выполни запрос». Права проверяются в вашем коде, до вызова, и никогда не в промпте.
С чего начать
- Начните с одной функции, только читающей, без побочных эффектов.
- Опишите повод вызова, а не действие - это самая результативная правка.
- Поставьте
max_roundsдо первого запуска, а не после первого счёта. - Возвращайте ошибки модели в виде результата, а не проглатывайте.
- Проверяйте права в своём коде. Всё, что даёт запись или удаление, - только после явной проверки, кто спрашивает.
Все 44 модели с параметрами и признаком поддержки инструментов - на витрине. Про строгий формат ответа - отдельный разбор, про то, какой модели поручать какую задачу, - маршрутизация.
FAQ
Что такое function calling простыми словами?
Режим, в котором модель может попросить выполнить одну из описанных вами функций. Сама она ничего не выполняет: возвращает имя функции и аргументы, вы вызываете код у себя и отправляете результат обратно, а модель формулирует по нему ответ пользователю.
Модель выполняет мой код?
Нет. Она видит только описание функции - имя, назначение и схему аргументов. Исполнение полностью на вашей стороне, поэтому и доступ у модели ровно тот, который вы сами решили ей дать.
Почему модель не вызывает нужный инструмент?
Почти всегда дело в поле description: оно попадает в промпт и по нему принимается решение. Описывать нужно повод для вызова («когда пользователь спрашивает про конкретный заказ»), а не результат работы функции.
Насколько дороже работа с инструментами?
Примерно вдвое на один вызов: цикл требует минимум двух запросов вместо одного, и история во втором длиннее. Плюс описания всех инструментов уходят в каждый запрос, поэтому их стоит держать короткими.
Можно ли зациклиться на вызовах инструментов?
Да, и это главная статья непредвиденных расходов. Модель может повторно просить вызов, не справившись с результатом предыдущего. Ограничение на число раундов обязательно, ставить его нужно до первого запуска.
Безопасно ли давать модели доступ к базе?
Только через узкие функции с проверкой прав в вашем коде. Инструмент вида «выполни произвольный запрос» опасен: текст, который обрабатывает модель, мог прийти от пользователя и содержать инструкцию. Права проверяются до вызова и никогда не формулировкой в промпте.