Kvmzen Блог
← Назад к разделу «Технологии на практике»

OpenAI Structured Outputs: как заставить GPT стабильно отдавать JSON по JSON Schema

Технологии на практике ·~12 мин чтения

Написание JSON Schema и конвейера структурированных данных в редакторе кода

Как только вывод модели попадает в базу, форму или следующего Agent, нельзя полагаться на фразу в промпте «верни JSON». OpenAI Structured Outputs через constrained decoding привязывает ответ к вашей JSON Schema: поля не пропадают, enum не выдумывается, типы не прыгают между строкой и числом. Статья для инженеров, которые делают извлечение, классификацию тикетов, скоринг и многошаговые пайплайны: полный путь от JSON Mode к strict: true.

100%
Соответствие Schema на поддерживаемых моделях
2
Точки входа: Chat Completions / Responses
10
Максимальная вложенность объектов

Почему JSON Mode недостаточно стабилен

JSON Mode (type: "json_object") гарантирует только одно: ответ можно прогнать через JSON-парсер. Он не фиксирует имена ключей, обязательные поля, набор enum и числовые типы. В проде чаще всего ломается так: нет severity, вместо 42 приходит "42", или модель изобретает urgent-plus вне enum. Если дальше стоит строгая типизация, пайплайн падает на случайных выборках.

Возможность JSON Mode Structured Outputs
Валидный JSON Да Да
Соблюдение вашей JSON Schema Нет (только промпт) Да (constrained decoding)
Как включать json_object json_schema + strict: true
Типичные модели Ранние GPT-4o / GPT-3.5 и аналоги gpt-4o-2024-08-06, gpt-4o-mini и более новые снимки
Отказ модели Может выдать «похожий на JSON» отказный текст Отдельное поле refusal

Официально Structured Outputs — эволюция JSON Mode: новым проектам лучше сразу идти через Schema, а не трижды ретраить после ошибки парсинга. Если параллельно сравниваете счета за токены у разных моделей, смотрите сравнение стоимости API Kimi K3 и GPT-5.5 — сам структурированный вывод почти не раздувает объём ответа; счёт разъезжается из‑за ретраев и слишком длинного reasoning.

Два входа, одни и те же правила Schema
В Chat Completions Schema лежит в response_format; в Responses API — в text.format. Пути полей разные, а ограничения strict, additionalProperties: false и «все ключи в required» одинаковы.

Минимальный рабочий запрос

Эта Schema для извлечения тикета закрывает около 80% продакшен-кейсов: строка, enum, целое и опциональное поле через union с null. Модель обязана вернуть все ключи; если ID аккаунта нет — явно null, а не пропуск ключа.

Chat Completions · response_format
{
  "model": "gpt-4o-2024-08-06",
  "messages": [
    {"role": "system", "content": "Извлеките из описания пользователя объект заявки."},
    {"role": "user", "content": "На странице оплаты сохранённая карта даёт 500. Началось сегодня. Аккаунт acct_8842."}
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "support_ticket",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "summary": { "type": "string" },
          "category": {
            "type": "string",
            "enum": ["billing", "bug", "account", "other"]
          },
          "severity": { "type": "integer" },
          "account_id": { "type": ["string", "null"] }
        },
        "required": ["summary", "category", "severity", "account_id"],
        "additionalProperties": false
      }
    }
  }
}

Эквивалент в Responses API — та же Schema в text.format, где type, name, schema и strict стоят соседними полями. При первом использовании Schema сервер компилирует ограничения, задержка может быть чуть выше; дальше та же Schema кешируется, и запросы возвращаются к обычной скорости.

Код на ноутбуке и среда отладки структурированного API
Относитесь к Schema как к контракту API: сначала прогоните те же определения локальными юнит-тестами, затем отдайте генерацию модели

Правила, без которых strict Schema не примут

После strict: true вы сдаёте не «любую JSON Schema», а поддерживаемое подмножество. Нарушение даёт 400, а не «постараемся соблюсти».

  • Корень — только object: корневой узел не может быть anyOf или массивом. Если Zod-discriminatedUnion порождает верхний anyOf, оберните его объектом, например { "result": ... }.
  • У каждого object нужен additionalProperties: false: включая вложенные. Пропустили один — запрос отклонят.
  • Все ключи из properties должны попасть в required: опциональность задают "type": ["string", "null"] или anyOf с null.
  • Поддерживаемые типы: string, number, integer, boolean, object, array, enum, anyOf.
  • Ограничения строк: pattern, format (email, date-time, uuid, ipv4 и др.).
  • Числа и массивы: minimum / maximum / multipleOf; minItems / maxItems.
  • Явно не поддерживается: allOf, not, if/then/else, dependentRequired и другие комбинаторы.
  • Лимиты размера: суммарно около 5000 свойств объектов и 10 уровней вложенности; суммарная длина имён свойств, имён определений и строк enum — не больше 120 тысяч символов; всего не более 1000 значений enum.
На fine-tuned моделях строже
На дообученных моделях pattern, format, minLength, minimum, minItems и похожие ограничения пока могут не работать. Сначала проверьте Schema на базовом снимке, потом решайте, нужен ли fine-tune.

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

Тот же strict работает и для параметров функций / tools. Разница в намерении: Schema инструмента описывает, «что модель должна вызвать»; response_format / text.format — «какой формы должен быть ответ пользователю». Извлечение, оценка, состояние UI — второе; погода, запись файла, команда в шелле — первое. Если сравниваете терминального Agent и управление GUI, см. связь Claude Code и OpenAI Computer Use — это другой слой «как действовать»; здесь речь о контракте данных до и после действия.

Разбор ответа SDK сразу в объект

Ручную JSON Schema легко забыть дополнить required. Официальный SDK даёт помощники Pydantic / Zod: Schema генерируется из типов, а на стороне ответа сразу приходит разобранный объект. Типичный Python-вариант ниже.

Python · Pydantic
from typing import Literal, Optional
from pydantic import BaseModel
from openai import OpenAI

class SupportTicket(BaseModel):
    summary: str
    category: Literal["billing", "bug", "account", "other"]
    severity: int
    account_id: Optional[str]

client = OpenAI()
completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "Извлеките заявку."},
        {"role": "user", "content": "Страница оплаты: 500, аккаунт acct_8842."},
    ],
    response_format=SupportTicket,
)
ticket = completion.choices[0].message.parsed
if completion.choices[0].message.refusal:
    raise RuntimeError(completion.choices[0].message.refusal)
print(ticket.category, ticket.severity)
Один источник правды для Schema
Одна и та же модель Pydantic / Zod обслуживает API-запрос, юнит-тесты и проверку ниже по пайплайну. Не копируйте список полей ещё раз в промпт — документация начнёт расходиться с кодом раньше, чем вы заметите.

Ошибки, отказ модели и инженерный чеклист

Перед релизом пройдите этот список — он снимает большинство случаев «локальный curl проходит, пайплайн отвечает 400».

  • 400 Unsupported schema: нет additionalProperties: false, поле не в required, корень — anyOf, или использован allOf.
  • Отказ модели: при срабатывании политики безопасности контент не запихивают в Schema; читайте refusal и показывайте его в UI, не ретрайте как ошибку JSON-парсера.
  • Опциональное поле превратилось в пустую строку: если в контракте null, принимайте null; перед записью в БД мапьте его в SQL NULL, не гадайте второй раз.
  • Слишком широкий enum: сведите бизнес-статусы к 5–8 значениям; сотни enum жрут контекст и приближают лимит в 1000.
  • Порядок ключей: ключи в ответе идут в том же порядке, что в Schema — при стриминге можно разбирать поля инкрементально.
  • Промпт всё ещё нужен: Schema задаёт форму, промпт — смысл. Например, «severity 1–5, 5 — полный отказ сайта» пишите в system; диапазон целого дополнительно зафиксируйте minimum / maximum.

На практике закрепите три вещи: Schema в Git; отдельные метрики на отказ, 400 и таймаут; регрессионный набор из реальных тикетов, а не игрушечный «Hello, верни JSON». Когда слой парсинга стабилен, можно заниматься точностью классификации и задержкой, а не чинить регулярки каждую неделю.

Конвейер извлечения на Mac mini проще отлаживать

Цикл отладки Structured Outputs короткий: поправили Schema, прогнали небольшую выборку, посмотрели разобранный объект. На macOS Python, Node.js, Docker и Homebrew работают сразу, без WSL. Единая память Apple Silicon позволяет одновременно держать редактор, локальные скрипты валидации и оценку с длинным контекстом — без ухода машины в swap на проверке типов.

Mac mini M4 в простое около 4 Вт — удобно гонять регрессионный набор всю ночь; сбои macOS редки, а Gatekeeper и SIP снижают риск «заражённых» скриптов зависимостей. По сравнению с Windows-коробкой той же цены и стабильность, и электричество при круглосуточной оценке Agent заметно дружелюбнее.

Если нужен постоянно онлайн узел именно для JSON-регрессии и сравнения моделей, посмотрите тарифы Kvmzen и перенесите контракт Schema с ноутбука на облачный Mac фиксированной конфигурации.

Ограниченное предложение

Больше чем Mac — ваша база разработки в облаке

Выделенные вычисления · Глобальные узлы · Ежемесячная подписка · Без покупки железа

На главную
Ограниченное предложение Посмотреть тарифы