← Назад к блогу

Полное руководство по OpenAI Structured Outputs: как заставить GPT стабильно выдавать JSON, соответствующий JSON Schema?

Полное руководство по OpenAI Structured Outputs: как заставить GPT стабильно выдавать JSON, соответствующий JSON Schema?

Материал для разработчиков, которым нужно безопасно записывать ответы GPT в базы данных, очереди, API и исполнители инструментов. В статье разобраны проектирование JSON Schema, различия между Structured Outputs и JSON mode, обработка отказов и обрывов, а также обязательная бизнес-валидация.

Для стабильного JSON используйте OpenAI Structured Outputs с strict: true, а не только инструкцию в промпте или обычный JSON mode; при этом заранее проверьте поддерживаемый поднабор JSON Schema и добавьте обработку отказа, обрыва генерации и бизнес-правил. Такой подход гарантирует соответствие структуры, но не доказывает, что даты, суммы, идентификаторы и классификация семантически верны.

Эта статья предназначена для:

  • backend-разработчиков, которым нужно надёжно разбирать ответ модели;
  • инженеров по данным, которые записывают результат в базу данных или очередь;
  • разработчиков AI Agent, разделяющих формат финального ответа и параметры вызываемых инструментов.

Сначала выберите правильный механизм вывода

У GPT API есть принципиально разные уровни контроля. Промпт может попросить модель вернуть JSON, JSON mode помогает получить синтаксически корректный JSON, а Structured Outputs связывает ответ с заданной схемой. В режиме строгого соответствия приложение получает не просто объект, который удалось распарсить, а объект, соответствующий разрешённой структуре схемы.

OpenAI описывает Structured Outputs в двух основных вариантах: через response_format или аналогичный формат структурированного текста для финального ответа и через определение функции с strict: true для аргументов инструментов. Подробные варианты зависят от используемого API и модели, поэтому перед внедрением необходимо свериться с актуальной документацией Structured Outputs. (openai.com)

Задача Предпочтительный механизм Что он контролирует Что остаётся на стороне приложения
Получить объект для записи в базу json_schema со строгим режимом Поля, типы, вложенность, допустимые значения Смысл данных, права записи, уникальность
Передать параметры инструмента Function Calling с strict: true Форму аргументов функции Разрешения, состояние ресурса, безопасность операции
Получить просто валидный JSON JSON mode Синтаксис JSON Наличие нужных полей и их типы
Вернуть обычный текст Обычный текстовый ответ Ничего, кроме инструкций Полный разбор и все проверки

Как OpenAI обеспечивает соответствие JSON Schema?
В Structured Outputs используется не только обучение модели, но и ограничение допустимых вариантов генерации на основе схемы. Однако это не означает универсальную поддержку всего стандарта JSON Schema: строгий режим работает с определённым подмножеством конструкции, которое необходимо проверять в документации для выбранного API и модели. (openai.com)

Исторический результат в 100 % на внутреннем тесте OpenAI относился к конкретной модели и набору схем, а не ко всем будущим моделям и любым бизнес-данным. Его нельзя превращать в обещание стопроцентной смысловой точности. (openai.com)

Спроектируйте закрытую схему для извлечения данных

В сценарии извлечения данных схема должна быть закрытой и достаточно небольшой. Если приложение получает сведения из письма, договора или протокола встречи, лучше заранее определить объект верхнего уровня, массивы, обязательные поля и политику неизвестных значений.

Минимальный пример:

{
  "type": "object",
  "properties": {
    "client_name": {
      "type": "string",
      "description": "Имя клиента в исходном документе"
    },
    "due_date": {
      "type": ["string", "null"],
      "description": "Дата в формате YYYY-MM-DD, если она явно указана"
    },
    "tasks": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "owner": {
            "type": ["string", "null"]
          }
        },
        "required": ["title", "owner"],
        "additionalProperties": false
      }
    }
  },
  "required": ["client_name", "due_date", "tasks"],
  "additionalProperties": false
}

Здесь важны не сами названия полей, а границы контракта:

  • required заставляет приложение явно решить, что делать с отсутствующим значением;
  • additionalProperties: false не позволяет незаметно расширять объект произвольными ключами;
  • массив имеет собственную схему элементов;
  • null лучше использовать для честного состояния «значение не найдено», чем заставлять модель придумывать строку;
  • описание поля объясняет модели смысл значения, но не заменяет проверку после ответа.

Обычная инструкция вроде «верните имя, дату и список задач в JSON» может привести к разным результатам: дата окажется в разных форматах, массив будет заменён строкой, а дополнительное поле появится без согласования с базой. JSON mode способен оставить такой объект синтаксически корректным. Structured Outputs с поддерживаемой строгой схемой ограничивает форму, но не определяет, правильно ли модель поняла исходный документ.

Почему GPT с JSON Schema всё ещё может не пройти разбор?
Причины обычно находятся не в одной ошибке парсера. Приложение могло использовать неподдерживаемую конструкцию схемы, получить отказ вместо объекта, столкнуться с обрывом по лимиту вывода, выбрать модель без нужной возможности или пытаться разобрать не тот участок ответа. Поэтому логика должна сначала классифицировать результат, а уже потом передавать его валидатору.

Для команд, которые проектируют отдельные контракты данных, полезно заранее составить внутреннюю политику конфиденциальности и обработки API-данных, особенно если в документах есть персональные или коммерческие сведения.

Настройте классификацию через перечисления

Для маршрутизации заявок, тикетов и документов свободный текст создаёт лишнюю неопределённость. Если downstream-сервис ожидает один из нескольких маршрутов, поле лучше описать через enum.

{
  "type": "object",
  "properties": {
    "category": {
      "type": "string",
      "enum": [
        "billing",
        "technical",
        "security",
        "unknown",
        "human_review"
      ],
      "description": "Категория обращения"
    },
    "confidence_note": {
      "type": "string",
      "description": "Краткое объяснение выбранного маршрута"
    }
  },
  "required": ["category", "confidence_note"],
  "additionalProperties": false
}

Значения unknown и human_review здесь не являются декоративными. Они позволяют модели признать неопределённость, не разрушая контракт. Если схема содержит только billing, technical и security, модель может быть вынуждена выбрать один из маршрутов даже при недостатке данных. Формально JSON будет правильным, а операционная маршрутизация — ошибочной.

Признак схемы Подходящий вариант Риск при неправильном выборе
Небольшое фиксированное число маршрутов enum Минимальный дрейф названий
Открытый набор пользовательских меток Строка плюс отдельный статус проверки Разные варианты написания одной категории
Неуверенная классификация enum с unknown или human_review Принудительная ложная классификация
Маршрутизация с высокой ценой ошибки Категория плюс объяснение и ручная проверка Автоматическое действие на основании формально допустимого, но неверного значения

В описании поля стоит указывать критерии выбора, но не перегружать схему длинными правилами. Сложную логику маршрутизации надёжнее вынести в отдельный слой: модель предлагает категорию, а приложение применяет пороги, приоритеты и правила эскалации.

Разделите формат финального ответа и параметры инструмента

Когда модель должна вызвать API, структура аргументов функции — это не то же самое, что формат ответа пользователю. Например, агент может вызвать create_ticket с параметрами project_id, priority и summary, а затем отдельно вернуть пользователю сообщение о результате.

В определении функции строгий режим помогает ограничить форму аргументов:

{
  "type": "function",
  "function": {
    "name": "create_ticket",
    "description": "Создаёт заявку только после проверки разрешений приложения",
    "strict": true,
    "parameters": {
      "type": "object",
      "properties": {
        "project_id": {
          "type": "string"
        },
        "priority": {
          "type": "string",
          "enum": ["low", "normal", "high"]
        },
        "summary": {
          "type": "string"
        }
      },
      "required": ["project_id", "priority", "summary"],
      "additionalProperties": false
    }
  }
}

OpenAI указывает, что при strict: true аргументы функции должны соответствовать переданной JSON Schema, если запрос не был отклонён и генерация завершилась нормально. Но валидные аргументы не дают модели права выполнить операцию. Помощник может сформировать корректный project_id, который не принадлежит текущему пользователю, или указать допустимый приоритет для проекта, находящегося в архиве. (help.openai.com)

Перед реальным вызовом инструмента приложение должно проверить:

  • принадлежит ли ресурс текущему пользователю или сервисному аккаунту;
  • разрешена ли операция для текущей роли;
  • существует ли объект и находится ли он в допустимом состоянии;
  • не повторяет ли запрос уже выполненную операцию;
  • не превышены ли лимиты, квоты и внутренние политики;
  • нужно ли подтверждение человека перед необратимым действием.

Это особенно важно для AI Agent: Structured Outputs ограничивает сообщение и аргументы, но не заменяет авторизацию, транзакционную логику и аудит.

Добавьте семантическую проверку для интерфейсов и API

Тип string не говорит, что строка является корректной датой, существующим идентификатором или безопасным значением для SQL-запроса. Тип number не гарантирует правильную валюту, точность округления или соответствие сумме заказа.

После проверки JSON Schema следует добавить второй слой:

  1. синтаксическая десериализация;
  2. валидация схемы;
  3. нормализация форматов;
  4. бизнес-правила;
  5. ограничения базы данных;
  6. ручная проверка для рискованных случаев;
  7. запись результата и версии схемы.

Примеры семантических правил:

  • due_date должна быть календарной датой и не противоречить дате создания записи;
  • currency должна соответствовать разрешённому справочнику;
  • amount не может быть отрицательной, если бизнес-операция не поддерживает возвраты;
  • project_id должен существовать и быть доступным текущему пользователю;
  • start_date не может быть позже end_date;
  • связанные поля должны быть согласованы между собой.

Нужна ли бизнес-проверка после успешной JSON Schema-валидации?
Да. Схема отвечает на вопрос «имеет ли объект допустимую форму», а бизнес-валидация — на вопрос «можно ли доверять этому значению в конкретной операции». Эти уровни нельзя объединять: модель может правильно вывести JSON, но неверно распознать дату, сумму, статус или связь между сущностями.

Важно: не следует исправлять подозрительные значения прямо в обработчике ответа молча. Лучше сохранить исходный ответ, причину отклонения и версию схемы, а затем отправить запись на повторную обработку или ручную проверку.

Обработайте отказ и неполный ответ до парсинга

Structured Outputs не отменяет защитные ограничения. OpenAI предусматривает отдельный признак refusal, когда модель отказывается выполнять запрос вместо формирования объекта по схеме. Кроме того, генерация может завершиться до построения полного JSON из-за ограничения длины или другого условия остановки. (openai.com)

Для Chat Completions приложение должно проверять как минимум:

message = completion.choices[0].message
finish_reason = completion.choices[0].finish_reason

if getattr(message, "refusal", None):
    handle_refusal(message.refusal)
elif finish_reason != "stop":
    handle_incomplete_response(finish_reason)
elif message.parsed is None:
    handle_parse_failure(message)
else:
    validate_business_rules(message.parsed)

Для Responses API аналогичная проверка должна учитывать статус ответа и сведения о неполной генерации. Названия полей зависят от интерфейса и версии SDK, поэтому обработчик следует проверять на минимальных тестовых запросах против фактически используемой версии.

Что делать при refusal в Structured Outputs?
Не нужно безусловно повторять тот же запрос. Сначала определяется причина отказа, затем сохраняются идентификатор запроса, исходные входные данные, схема и версия модели. Если запрос действительно запрещён, повтор не изменит результат. Если отказ возник из-за некорректной постановки задачи, исправляется вход или маршрут; если данные критичны, запись переводится в ручную очередь.

Можно ли автоматически повторять любой сбой парсинга?
Нет. Повтор имеет смысл при временной ошибке сети, исчерпании инфраструктурного лимита или контролируемом неполном ответе, но не при отказе безопасности, ошибке схемы или нарушении бизнес-правил. Бесконечные повторы превращают дефект контракта в скрытые расходы и дублирование операций.

Соберите минимальный рабочий вызов GPT API

Для отдельного структурированного ответа Python SDK позволяет описывать ожидаемую модель данных и получать разобранный объект. Минимальный пример должен оставаться небольшим, чтобы его можно было использовать как smoke-тест после обновления SDK или модели.

from typing import Optional
from pydantic import BaseModel
from openai import OpenAI

class Task(BaseModel):
    title: str
    owner: Optional[str]

class Extraction(BaseModel):
    client_name: str
    due_date: Optional[str]
    tasks: list[Task]

client = OpenAI()

result = client.beta.chat.completions.parse(
    model="gpt-4o-mini-2024-07-18",
    messages=[
        {
            "role": "system",
            "content": "Извлеките данные из заметки. Не придумывайте отсутствующие значения."
        },
        {
            "role": "user",
            "content": "Клиент: АО Север. Задача: проверить договор. Ответственный не указан."
        }
    ],
    response_format=Extraction
)

message = result.choices[0].message

if message.refusal:
    print("Отказ:", message.refusal)
elif message.parsed:
    print(message.parsed.model_dump())
else:
    print("Ответ не прошёл обработку")

Этот пример нельзя считать универсальной гарантией для любой модели: поддержка форматов, SDK и подмножества схемы меняется, поэтому рабочий проект должен закреплять модель, версию SDK и контракт ответа в тестовой конфигурации. OpenAI также предоставляет нативную поддержку разбора структурированных ответов через Python и Node SDK в соответствующих сценариях. (openai.com)

Зафиксируйте производственный контур и регрессионные тесты

Надёжность определяется не одной настройкой strict, а тем, как команда сопровождает контракт после запуска. Для каждого Schema должны существовать тестовые группы:

  • нормальные документы с ожидаемым извлечением;
  • пустые и частично заполненные документы;
  • неоднозначные даты, суммы и идентификаторы;
  • длинные входы, способные привести к обрыву;
  • вредоносные или конфликтующие инструкции внутри исходного текста;
  • старый формат для проверки обратной совместимости;
  • новые примеры после каждого найденного дефекта.

Версию схемы следует сохранять вместе с результатом. Если таблица базы обновилась, нельзя незаметно переиспользовать прежнее имя схемы с другой семантикой. Надёжнее выпускать новую версию, прогонять оба контракта на регрессионном наборе и только затем менять маршрут записи.

OpenAI отдельно отмечает, что первый запрос с новой схемой может иметь дополнительную задержку на подготовку схемы: для типичных схем в объявлении указано менее 10 секунд, а для более сложных — вплоть до минуты. Это исторически опубликованное ориентировочное ограничение, а не SLA; его необходимо повторно измерять на фактической модели, регионе и схеме. (openai.com)

Для постоянных пакетных проверок и длительных Apple-разработческих конвейеров команда может использовать панель управления удалённой инфраструктурой, но выбор между временным и постоянно работающим Mac-окружением следует делать по длительности задач, требованиям к интерфейсу и необходимости физического доступа.

Чек-лист перед запуском

  • [ ] Выбран Structured Outputs, а не только инструкция «верните JSON».
  • [ ] Проверена совместимость модели, API и SDK с требуемым форматом.
  • [ ] Включён strict: true, когда сценарий и поддерживаемый поднабор схемы это допускают.
  • [ ] У каждого объекта задано additionalProperties: false.
  • [ ] Все поля, необходимые контракту, включены в required.
  • [ ] Для неизвестных значений предусмотрены null, unknown или human_review.
  • [ ] Свободный текст заменён на enum, если набор состояний закрытый.
  • [ ] Финальный ответ отделён от параметров Function Calling.
  • [ ] Перед выполнением инструмента проверяются права, ресурс и состояние операции.
  • [ ] После Schema-валидации выполняются проверки дат, сумм, идентификаторов и связей.
  • [ ] Отказы, обрывы и ошибки парсинга имеют разные обработчики.
  • [ ] Сохраняются исходный ответ, тип сбоя, модель, версия SDK и версия схемы.
  • [ ] Есть регрессионные примеры для нормальных, граничных и вредоносных входов.
  • [ ] Повторная обработка ограничена и не запускается для каждого типа ошибки.

Текущая схема с промптовым JSON или JSON mode может казаться проще, но у неё остаются дрейф полей, ручные повторы, неодинаковый разбор и риск записи формально корректного, но неподходящего объекта. Structured Outputs требует более дисциплинированного контракта, зато лучше подходит для задач, где ответ должен пройти через валидатор, очередь, базу данных или исполнитель инструмента. Для команд, которым нужны массовые тесты схем, длительные регрессионные прогоны или постоянная Apple-разработка, аренда Mac через nuvcloud может быть удобнее локальной машины: не требуется заранее покупать отдельное оборудование, поддерживать его доступность и выделять рабочее место под временную задачу. Если же нагрузка постоянная, длительная и требует физического интерфейса или локальной периферии, собственный Mac может оказаться рациональнее.

Перед запуском нового пайплайна стоит сверить требования с руководством по помощи и эксплуатации, затем провести небольшой приёмочный прогон на реальных обезличенных данных. Для временных задач, миграции или проверки совместимости достаточно арендовать среду на период тестов; для непрерывного конвейера решение следует принимать после сравнения фактического времени выполнения, требований к доступу и стоимости простоя.

Тестируйте Structured Outputs в стабильной среде macOS

Арендуйте удалённый Mac в nuvcloud для разработки, тестирования и запуска сервисов, работающих с JSON и API.

Подключайтесь к macOS удалённо через VNC и проверяйте интеграции без привязки к собственному оборудованию.

Дополнительное чтение

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