Вызов инструментов
Вызов инструментов (function calling) даёт модели доступ к вашим данным и действиям: базе заказов, внутреннему API, калькулятору. Модель не отвечает на вопрос сразу — она просит ваш код вызвать функцию, получает результат и продолжает уже с ним. На этом строятся ассистенты, агенты и боты, которые «видят» ваши системы.
Инструменты поддерживаются во всех трёх чат-протоколах: /v1/chat/completions, /v1/messages и /v1/responses. Основной — первый, два других разобраны ниже коротко.
Как работает раунд
Заголовок раздела «Как работает раунд»- Вы отправляете обычный запрос и рядом — массив
tools: описания функций с именем, описанием и JSON Schema аргументов. - Модель понимает, что без инструмента не обойтись, и отвечает не текстом, а вызовом: имя функции и JSON с аргументами.
finish_reasonтакого ответа —"tool_calls". - Ваш код исполняет функцию: ходит в базу, зовёт внешний API, считает — что угодно.
- Результат отправляется тем же тредом: вся предыдущая история плюс сообщение с ролью
tool. - Модель читает результат и отвечает финальным текстом для пользователя.
Разделение ответственности здесь жёсткое: модель никогда не исполняет код сама. Она лишь выбирает инструмент и подбирает аргументы — исполнение всегда происходит на вашей стороне, и только вы решаете, что реально выполнить, а что отклонить. Модель не имеет доступа к вашим системам: она видит лишь те инструменты, которые вы описали в запросе, и лишь те результаты, которые сами вернули в диалог.
/v1/chat/completions (OpenAI)
Заголовок раздела «/v1/chat/completions (OpenAI)»Инструмент описывается в поле 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 теперь лежит:
- исходное сообщение пользователя;
- полностью тот assistant-ответ, который вернул API, — вместе с его
tool_calls(не пересобирайте его вручную, передайте объект как есть); - результат — сообщение с ролью
tool:
{ "role": "tool", "tool_call_id": "call_9c2f1e", "content": "{\"temp\": 18, \"condition\": \"облачно\"}"}tool_call_id обязан совпадать с id из tool_calls. content — строка; для структурированных данных удобно возвращать JSON.
Полный пример
Заголовок раздела «Полный пример»import jsonfrom 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, })curl https://api.mixen.ai/v1/chat/completions \ -H "Authorization: Bearer $MIXEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5.6-luna", "messages": [{"role": "user", "content": "Что с погодой в Казани?"}], "tools": [{ "type": "function", "function": { "name": "get_weather", "description": "Текущая погода в указанном городе", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"] } } }] }'Ответ на этот запрос — JSON с tool_calls, показанный выше.
Цикл не обязан заканчиваться одним раундом: если по результату модель решит вызвать ещё один инструмент или другой, цикл продолжится сам — пока не придёт ответ с текстом. Так устроены агентные сценарии.
Параллельные вызовы
Заголовок раздела «Параллельные вызовы»В одном ответе модель может вернуть сразу несколько tool_calls — например, узнать погоду в трёх городах или поискать по нескольким источникам. Клиент исполняет все вызовы (при желании параллельно) и возвращает по одному сообщению role: "tool" на каждый — в том же порядке, в каком пришли вызовы, каждый со своим tool_call_id.
Стриминг
Заголовок раздела «Стриминг»stream: true с инструментами работает, но устроен иначе, чем обычный текст. Текст по-прежнему приходит дельтами, а tool_calls инкрементальными дельтами не приходят: собранный список вызовов отдаётся целиком одним чанком в финальной части потока, а finish_reason: "tool_calls" оказывается в последнем чанке. Кода для склейки аргументов из дельт не потребуется — но и «стриминга вызовов» не будет.
Если промежуточный текст не нужно показывать пользователю вживую, проще вызвать без stream — меньше кода сборки чанков, поведение то же.
/v1/messages (Anthropic-протокол)
Заголовок раздела «/v1/messages (Anthropic-протокол)»В 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.
/v1/responses
Заголовок раздела «/v1/responses»В 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 jsonfrom 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 — строка с JSONfinal = 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. Практически все чат-модели в каталоге их поддерживают; нюансы конкретной модели смотрите на её странице. - Дальше: Чат и стриминг — параметры основного эндпоинта, Ошибки — коды, лимиты, ретраи.