Как получать строго структурированный JSON от модели
Просить модель «ответь в формате JSON» в системном промпте работает через раз. Разбираем способ понадёжнее — через tools с жёсткой схемой.
3 мин · · API · разработка
Если единственная защита от сломанного JSON — это фраза «отвечай строго в формате JSON» в системном промпте, рано или поздно модель обернёт ответ в пояснение, добавит текст до или после скобок, или забудет закрывающую кавычку. Есть способ надёжнее — не просить, а структурно ограничить.
Почему просьба в промпте — это не гарантия
{"role": "system", "content": "Отвечай только в формате JSON, без пояснений"}Такой промпт работает в большинстве случаев, но не даёт формальной гарантии. Модель — это генератор текста, и инструкция в системном промпте — это сильная подсказка, а не ограничение на уровне протокола. На длинных или неоднозначных запросах модель может добавить преамбулу вида «Вот запрошенный JSON:», обернуть объект в блок ```json, или сгенерировать невалидный JSON с висячей запятой. Парсер на вашей стороне ломается, и приходится городить регулярки для очистки ответа перед json.loads.
Способ понадёжнее: tools с жёсткой схемой
И /v1/responses, и /v1/chat/completions принимают параметр tools — набор функций с описанием и JSON-схемой параметров. Штатное применение tools — вызов внешних функций моделью, но тот же механизм отлично работает как способ принудительно получить структурированный вывод: вы описываете «функцию», параметры которой и есть нужная вам структура данных, а модель обязана заполнить их по схеме, а не сочинить произвольный текст.
curl https://api.zukko.pro/v1/chat/completions \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-sol",
"messages": [
{"role": "user", "content": "Извлеки из текста имя, возраст и город: Меня зовут Игорь, мне 34, живу в Казани"}
],
"tools": [{
"type": "function",
"function": {
"name": "extract_person",
"description": "Извлекает структурированные данные о человеке из текста",
"parameters": {Ключевой момент — tool_choice, принудительно указывающий модели вызвать именно эту функцию, а не отвечать текстом на своё усмотрение. Ответ приходит не как обычный текст, а как вызов функции с аргументами в формате JSON, строго по объявленной схеме:
{
"choices": [{
"message": {
"tool_calls": [{
"function": {
"name": "extract_person",
"arguments": "{\"name\":\"Игорь\",\"age\":34,\"city\":\"Казань\"}"
}
}]
}
}]
}Поле arguments — это строка с валидным JSON, которую остаётся распарсить напрямую, без очистки от лишнего текста.
То же самое на Python
import json
import requests
def extract_structured(text, api_key):
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"},
},
"required": ["name", "age", "city"],
}
resp = requests.post(Тот же приём через Anthropic-совместимый /v1/messages
Anthropic-стиль запросов к /v1/messages тоже поддерживает tools в том же духе: описываете инструмент со схемой и просите модель обязательно его вызвать. Формат объявления инструмента немного другой (input_schema вместо parameters, вложенных в function), но идея та же — структура гарантируется схемой, а не пожеланием в тексте.
curl https://api.zukko.pro/v1/messages \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Извлеки имя, возраст и город: Меня зовут Игорь, мне 34, живу в Казани"}],
"tools": [{
"name": "extract_person",
"description": "Извлекает структурированные данные о человеке",
"input_schema": {
"type": "object",
"properties": {Что делать, если структура вложенная или список объектов
Схема parameters / input_schema — это обычный JSON Schema, так что вложенные объекты и массивы описываются стандартно:
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"price": {"type": "number"}
},
"required": ["title", "price"]
}
}
},
"required": ["items"]
}Модель заполнит массив items нужным числом элементов исходя из содержания текста, каждый — строго по вложенной схеме.
Валидация всё равно нужна
Схема резко снижает вероятность мусора в ответе, но не отменяет проверку на своей стороне: тип integer в схеме не гарантирует, что число окажется в разумном диапазоне, а обязательное поле не гарантирует, что модель не подставит туда пустую строку при недостатке данных в исходном тексте. Разумно всё равно прогонять результат через валидацию (pydantic, zod или ручные проверки) перед использованием в бизнес-логике.
Итого
Просьба в промпте — это подсказка, а не контракт. Если нужна структура, на которую можно полагаться в коде без защитного парсинга, используйте tools с явной JSON-схемой и tool_choice, принудительно указывающим нужную функцию — это доступно и в OpenAI-совместимом режиме (/v1/chat/completions, /v1/responses), и в Anthropic-совместимом (/v1/messages).
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.