Перейти к содержимому
EN

Вызов инструментов

Вызов инструментов (function calling) даёт модели доступ к вашим данным и действиям: базе заказов, внутреннему API, калькулятору. Модель не отвечает на вопрос сразу — она просит ваш код вызвать функцию, получает результат и продолжает уже с ним. На этом строятся ассистенты, агенты и боты, которые «видят» ваши системы.

Инструменты поддерживаются во всех трёх чат-протоколах: /v1/chat/completions, /v1/messages и /v1/responses. Основной — первый, два других разобраны ниже коротко.

  1. Вы отправляете обычный запрос и рядом — массив tools: описания функций с именем, описанием и JSON Schema аргументов.
  2. Модель понимает, что без инструмента не обойтись, и отвечает не текстом, а вызовом: имя функции и JSON с аргументами. finish_reason такого ответа — "tool_calls".
  3. Ваш код исполняет функцию: ходит в базу, зовёт внешний API, считает — что угодно.
  4. Результат отправляется тем же тредом: вся предыдущая история плюс сообщение с ролью tool.
  5. Модель читает результат и отвечает финальным текстом для пользователя.

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

Инструмент описывается в поле tools:

{
"type": "function",
"function": {
"name": "get_weather",
"description": "Текущая погода в указанном городе",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Название города"}
},
"required": ["city"]
}
}
}

name и description модель читает, чтобы понять, когда инструмент нужен; parameters — JSON Schema аргументов, по ней модель собирает вызов.

Поле tool_choice управляет тем, как модель пользуется инструментами:

Значение Поведение
auto По умолчанию: модель сама решает, звать инструмент или отвечать текстом
none Инструменты не используются
required Модель обязана вызвать хотя бы один инструмент
{"type": "function", "function": {"name": "get_weather"}} Вызвать конкретную функцию

Когда модель решает звать инструмент, ответ приходит без текста — со списком tool_calls и finish_reason: "tool_calls":

{
"choices": [
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_9c2f1e",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"Казань\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}

Частая ошибка: arguments — это строка с JSON, а не объект. Прежде чем работать с аргументами, распарсите её: json.loads(...) в Python, JSON.parse(...) в JavaScript.

Второй раунд — тот же эндпоинт, но в messages теперь лежит:

  1. исходное сообщение пользователя;
  2. полностью тот assistant-ответ, который вернул API, — вместе с его tool_calls (не пересобирайте его вручную, передайте объект как есть);
  3. результат — сообщение с ролью tool:
{
"role": "tool",
"tool_call_id": "call_9c2f1e",
"content": "{\"temp\": 18, \"condition\": \"облачно\"}"
}

tool_call_id обязан совпадать с id из tool_calls. content — строка; для структурированных данных удобно возвращать JSON.

import json
from openai import OpenAI
client = OpenAI(
base_url="https://api.mixen.ai/v1",
api_key="mxn-...",
)
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Текущая погода в указанном городе",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Название города"}
},
"required": ["city"]
}
}
}]
def get_weather(city: str) -> str:
# здесь был бы настоящий вызов вашего бэкенда или внешнего API
return json.dumps(
{"city": city, "temp": 18, "condition": "облачно"},
ensure_ascii=False,
)
messages = [{"role": "user", "content": "Что с погодой в Казани? Гулять или взять зонт?"}]
while True:
resp = client.chat.completions.create(
model="gpt-5.6-luna",
messages=messages,
tools=tools,
)
msg = resp.choices[0].message
messages.append(msg) # assistant-ответ целиком, вместе с tool_calls
if not msg.tool_calls: # finish_reason "stop" — финальный текст
print(msg.content)
break
for call in msg.tool_calls:
args = json.loads(call.function.arguments) # arguments — строка с JSON
result = get_weather(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})

Ответ на этот запрос — JSON с tool_calls, показанный выше.

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

В одном ответе модель может вернуть сразу несколько tool_calls — например, узнать погоду в трёх городах или поискать по нескольким источникам. Клиент исполняет все вызовы (при желании параллельно) и возвращает по одному сообщению role: "tool" на каждый — в том же порядке, в каком пришли вызовы, каждый со своим tool_call_id.

stream: true с инструментами работает, но устроен иначе, чем обычный текст. Текст по-прежнему приходит дельтами, а tool_calls инкрементальными дельтами не приходят: собранный список вызовов отдаётся целиком одним чанком в финальной части потока, а finish_reason: "tool_calls" оказывается в последнем чанке. Кода для склейки аргументов из дельт не потребуется — но и «стриминга вызовов» не будет.

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

В Anthropic-протоколе те же идеи, но поля называются иначе, а аргументы приходят объектом, а не строкой:

  • Описание: tools: [{"name": "...", "description": "...", "input_schema": <JSON Schema>}] — без обёртки function.
  • Ответ: блок {"type": "tool_use", "id": "...", "name": "...", "input": {...}} в массиве content; stop_reason: "tool_use". input — уже объект, парсить его не нужно.
  • Второй раунд: сообщение {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "...", "content": "<результат строкой>"}]} — по одному tool_result на каждый tool_use.
from anthropic import Anthropic
client = Anthropic(base_url="https://api.mixen.ai", api_key="mxn-...")
resp = client.messages.create(
model="anthropic/claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Что с погодой в Казани?"}],
tools=[{
"name": "get_weather",
"description": "Текущая погода в указанном городе",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}],
)
block = next(b for b in resp.content if b.type == "tool_use")
result = get_weather(**block.input) # input — объект, json.loads не нужен
final = client.messages.create(
model="anthropic/claude-sonnet-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Что с погодой в Казани?"},
{"role": "assistant", "content": [block]}, # tool_use-блок как есть
{"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
}]},
],
)
print(final.content[0].text)

Base URL для Anthropic SDK — без суффикса /v1: SDK подставляет путь в запрос сам. Подробнее — Claude Code.

В Responses API описание инструмента плоское — без вложенного function:

  • Описание: tools: [{"type": "function", "name": "...", "description": "...", "parameters": <JSON Schema>}]
  • Ответ: элемент output вида {"type": "function_call", "call_id": "...", "name": "...", "arguments": "<JSON-строка>"} — здесь arguments снова строка.
  • Второй раунд: в input передаётся вся история — сообщения с "type": "message", сам элемент function_call как есть и рядом {"type": "function_call_output", "call_id": "...", "output": "<результат строкой>"}.
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.mixen.ai/v1", api_key="mxn-...")
resp = client.responses.create(
model="gpt-5.6-luna",
input="Что с погодой в Казани?",
tools=[{
"type": "function",
"name": "get_weather",
"description": "Текущая погода в указанном городе",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}],
)
call = next(o for o in resp.output if o.type == "function_call")
result = get_weather(**json.loads(call.arguments)) # arguments — строка с JSON
final = client.responses.create(
model="gpt-5.6-luna",
input=[
{"type": "message", "role": "user", "content": "Что с погодой в Казани?"},
call, # function_call-элемент как есть
{"type": "function_call_output", "call_id": call.call_id, "output": result},
],
)
print(final.output_text)
  • Тарификация обычная — входные и выходные токены по расценкам модели. Описания инструментов входят во вход и тарифицируются как входные токены: в агентных циклах с большим списком тулов это заметно, помогает кэширование контекста.
  • Роль tool без вызова — ошибка. Если прислать сообщение role: "tool", которому не предшествует assistant-ответ с tool_calls, апстрим вернёт 400. Возвращайте результат только в ответ на реальный вызов и тем же тредом.
  • Поддержка зависит от модели. Если модель не умеет инструменты, запрос вернёт 400. Практически все чат-модели в каталоге их поддерживают; нюансы конкретной модели смотрите на её странице.
  • Дальше: Чат и стриминг — параметры основного эндпоинта, Ошибки — коды, лимиты, ретраи.