Структурированный JSON и вызов инструментов
Два способа заставить модель вернуть данные, а не текст: режим JSON-вывода, когда ответ целиком является JSON-объектом, и вызов инструментов (tool calls), когда модель сама просит выполнить вашу функцию и получает её результат обратно.
JSON-вывод: response_format
По умолчанию response_format равен text — модель отвечает свободным текстом. Значение json_object включает режим структурированного вывода: {"response_format": {"type": "json_object"}}.
В промпте должно присутствовать слово «json» — иначе запрос завершится ошибкой. Практический приём: описать схему ответа в системном сообщении словами, например «Отвечай json-объектом с полями number, date, total». Слово «json» при этом уже есть в тексте.
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com",
)
response = client.chat.completions.create(
model="deepseek-flash",
messages=[
{
"role": "system",
"content": (
"Ты извлекаешь реквизиты из писем. "
"Отвечай json-объектом с полями number, date, total."
),
},
{"role": "user", "content": "Счёт № 14 от 02.10.2026 на 18 400 руб."},
],
response_format={"type": "json_object"},
# Извлечение полей — задача без рассуждений: thinking можно выключить
extra_body={"thinking": {"type": "disabled"}, "reasoning_effort": "none"},
)
data = json.loads(response.choices[0].message.content)
print(data["number"], data["date"], data["total"])
Ответ приходит строкой, поэтому его нужно разобрать через json.loads и проверить обязательные поля: режим гарантирует корректный JSON, но не гарантирует наличие конкретных ключей. Проверка схемы на стороне приложения остаётся обязательной.
Инструменты: как это работает
Инструменты описываются массивом tools. Поддерживается только тип function: у каждой функции есть имя (уникальное, не длиннее 128 символов), описание и JSON Schema параметров. Модель не вызывает функцию сама — она возвращает имя функции и аргументы, а выполняет их ваш код.
Признак того, что модель просит вызвать инструмент, — finish_reason: "tool_calls" и заполненное поле tool_calls в сообщении ассистента. Полный список значений finish_reason: stop, length, content_filter, tool_calls, insufficient_system_resource, aborted.
Если запрос содержит tools, поле reasoning_content обязательно возвращать назад во всех последующих запросах диалога — иначе HTTP 400. Это относится и к циклу вызова инструментов: каждый следующий запрос должен нести reasoning_content предыдущего ответа ассистента.
В thinking mode значения tool_choice: "required" и выбор конкретной функции возвращают HTTP 400. Рабочие варианты — auto или none. Если нужно обязательное обращение к инструменту, thinking придётся выключить.
| Параметр | Допустимые значения | Комментарий |
|---|---|---|
response_format |
text, json_object |
По умолчанию text. В промпте должно быть слово «json» |
tools |
только type: "function" |
Имена уникальны, до 128 символов; strict по умолчанию false |
tool_choice |
none, auto, required, конкретная функция |
required и конкретная функция недоступны в thinking mode — HTTP 400 |
finish_reason |
stop, length, content_filter, tool_calls, insufficient_system_resource, aborted |
Цикл инструментов продолжается, пока не вернётся значение, отличное от tool_calls |
Нужен API-ключ сегодня
Регистрация по email или Telegram занимает пару минут, оплата идёт в рублях, VPN не нужен. Один ключ работает и с DeepSeek, и с другими моделями.
Рабочий цикл вызова инструментов
Ниже — самодостаточный пример: инструмент один, но структура цикла не меняется при добавлении новых функций. Обратите внимание на три вещи: сообщение ассистента возвращается в историю вместе с reasoning_content и tool_calls, на каждый вызов отвечает сообщение с ролью tool и тем же tool_call_id, а цикл завершается по finish_reason, отличному от tool_calls.
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com",
)
def get_weather(city: str) -> dict:
"""Заглушка вместо реального вызова сервиса погоды."""
return {"city": city, "temp_c": 7, "condition": "облачно"}
AVAILABLE = {"get_weather": get_weather}
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Текущая погода в указанном городе",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Название города, например Казань",
}
},
"required": ["city"],
},
},
}
]
messages = [{"role": "user", "content": "Какая сейчас погода в Казани?"}]
while True:
response = client.chat.completions.create(
model="deepseek-flash",
messages=messages,
tools=TOOLS,
tool_choice="auto",
# thinking включён по умолчанию; мысли reasoning_content
extra_body={"thinking": {"type": "enabled"}, "reasoning_effort": "high"},
)
choice = response.choices[0]
message = choice.message
# Собираем сообщение ассистента вручную, чтобы гарантированно
# вернуть reasoning_content назад: с tools это обязательно.
assistant_message = {
"role": "assistant",
"content": message.content or "",
"reasoning_content": message.reasoning_content or "",
}
if message.tool_calls:
assistant_message["tool_calls"] = [
{
"id": call.id,
"type": "function",
"function": {
"name": call.function.name,
"arguments": call.function.arguments,
},
}
for call in message.tool_calls
]
messages.append(assistant_message)
if choice.finish_reason != "tool_calls":
print(message.content)
break
for call in message.tool_calls:
arguments = json.loads(call.function.arguments)
result = AVAILABLE[call.function.name](**arguments)
messages.append(
{
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
}
)
Аргументы приходят строкой JSON в tool_calls[].function.arguments — их нужно разобрать самостоятельно. Проверяйте имя функции по белому списку и валидируйте аргументы: содержимое формирует модель, а не пользователь, но доверять ему без проверки не стоит.
Strict-схемы в beta
Строгое соответствие аргументов схеме доступно в бета-режиме. Для этого нужно обращаться к другому base URL — https://api.deepseek.com/beta — и явно включать strict в описании функции. Остальные бета-возможности того же адреса: FIM и Chat Prefix Completion (FIM доступен только в non-thinking у обеих моделей).
import os
from openai import OpenAI
# strict-инструменты — бета-функция, поэтому другой base_url
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com/beta",
)
response = client.chat.completions.create(
model="deepseek-flash",
messages=[{"role": "user", "content": "Добавь задачу: позвонить в банк завтра."}],
tools=[
{
"type": "function",
"function": {
"name": "add_task",
"description": "Создаёт задачу в списке дел",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string"},
"due_date": {"type": "string"},
},
"required": ["title", "due_date"],
"additionalProperties": False,
},
},
}
],
tool_choice="auto",
extra_body={"thinking": {"type": "disabled"}, "reasoning_effort": "none"},
)
for call in response.choices[0].message.tool_calls or []:
print(call.function.name, call.function.arguments)
Формат tools, tool_choice и сообщений с ролью tool совпадает с OpenAI, поэтому код цикла переносится на DeepSeek заменой base_url и имени модели. Единственное системное отличие — обязательный возврат reasoning_content, когда в запросе есть tools.
Нужен API-ключ сегодня
Регистрация по email или Telegram занимает пару минут, оплата идёт в рублях, VPN не нужен. Один ключ работает и с DeepSeek, и с другими моделями.
Читайте также
-
Режим рассуждений
Значения
reasoning_effort, полеreasoning_contentи ограничения thinking mode. -
Стриминг и токены
Потоковая выдача tool calls и разбор
usageдля расчёта стоимости. - Ошибки и лимиты Что означают HTTP 400 и 429 и как организовать повторные попытки.
- Примеры на Python Готовые скрипты: чат, извлечение JSON, агент с инструментами.