Structured Outputs в API: как проверить, что JSON от LLM соответствует схеме
Если приложение просит языковую модель вернуть JSON, одного указания «ответь в JSON» недостаточно: синтаксически корректный объект может не содержать нужное поле или использовать неподходящий тип. Structured Outputs в API OpenAI позволяет задать JSON Schema и потребовать соответствия ответа этой схеме. Это полезно для извлечения данных, классификаторов и вызовов функций, где результат передаётся дальше в код.
Но соответствие формату не означает, что значения верны. Ниже — узкая практическая инструкция: как задать схему, проверить ответ на отказ и не спутать структурную гарантию с проверкой фактов.
Structured Outputs и обычный JSON mode решают разные задачи
В JSON mode модель должна вернуть корректный JSON, но это само по себе не гарантирует совпадения с конкретной схемой. Structured Outputs добавляет ограничение на структуру: набор полей, их типы и допустимые варианты задаются разработчиком. OpenAI описывает режимы `json_schema` для форматирования ответа и `strict` для строгого соблюдения схемы в поддерживаемых случаях.
Это различие важно на границе между моделью и приложением. Допустим, сервис ожидает объект с полями `category` и `confidence`. Валидный JSON вроде `{«result»:»urgent»}` всё равно не подходит. При заданной схеме с обязательными полями ошибка формы должна предотвращаться самим структурированным режимом, а не обнаруживаться лишь после попытки прочитать отсутствующий ключ.
При этом остаются отдельными задачи проверки смысла, бизнес-правил и происхождения данных. Если схема разрешает `confidence` от 0 до 1, модель может вернуть число в этом диапазоне, не имея надёжных оснований для оценки. Формат — не доказательство правильности.
Как задать JSON Schema для ответа
В Responses API формат задают в `text.format`; в Chat Completions соответствующая настройка находится в `response_format`. Названия и параметры зависят от используемого API, поэтому сверяйтесь с актуальной документацией для конкретного endpoint.
Пример для Responses API на Python:
python
from openai import OpenAI
client = OpenAI()
schema = {
«type»: «object»,
«properties»: {
«category»: {
«type»: «string»,
«enum»: [«billing», «technical», «other»]
},
«needs_human»: {«type»: «boolean»},
«summary»: {«type»: «string»}
},
«required»: [«category», «needs_human», «summary»],
«additionalProperties»: False
}
response = client.responses.create(
model=»YOUR_SUPPORTED_MODEL»,
input=»Пользователь сообщает: списали оплату дважды, просит проверить возврат.»,
text={
«format»: {
«type»: «json_schema»,
«name»: «support_ticket»,
«strict»: True,
«schema»: schema
}
}
)
print(response.output_text)
`YOUR_SUPPORTED_MODEL` здесь намеренно оставлен как заменяемое значение: доступность функций зависит от модели и endpoint. Перед запуском проверьте документацию и используемую версию SDK.
Схема фиксирует три поля: категорию из закрытого списка, логический признак передачи человеку и краткое резюме. `required` перечисляет обязательные свойства, а `additionalProperties: false` запрещает незаданные ключи в объекте. Для классификатора закрытый `enum` обычно надёжнее, чем инструкция просить модель «выбирать подходящую категорию» без машинно проверяемого списка.
Как проверить результат в приложении, а не только в примере
Не ограничивайтесь печатью `output_text`. В реальном коде нужно разобрать ответ, проверить состояние завершения и обработать случаи, когда обычного объекта с результатом нет. API может вернуть отказ или незавершённый ответ; не следует считать любой текст в выходе готовыми данными.
Упрощённый контрольный шаблон:
python
import json
if response.status != «completed»:
raise RuntimeError(f»Ответ не завершён: {response.status}»)
if getattr(response, «refusal», None):
raise ValueError(«Модель отказалась выполнять запрос»)
data = json.loads(response.output_text)
if set(data) != {«category», «needs_human», «summary»}:
raise ValueError(«Неожиданный набор полей»)
if data[«category»] not in {«billing», «technical», «other»}:
raise ValueError(«Недопустимая категория»)
if not isinstance(data[«needs_human»], bool):
raise TypeError(«needs_human должен быть boolean»)
if not isinstance(data[«summary»], str):
raise TypeError(«summary должен быть строкой»)
Это иллюстрация проверок на стороне приложения, а не универсальный готовый обработчик всех вариантов SDK. Форма объекта ответа и доступ к признаку отказа зависят от версии библиотеки. Приведите проверки в соответствие с фактическим типом ответа, а для критичных операций сохраняйте исходный ответ и диагностические данные.
Ручные проверки здесь частично дублируют ограничения схемы, но полезны как защитный слой при смене модели, SDK или конфигурации. Для более сложных схем можно валидировать результат библиотекой JSON Schema; отдельно тестируйте ограничения, которые схема выразить не может или которые важны именно для вашего домена.
Какие ограничения схемы проверить до подключения модели
JSON Schema способна описать типы, обязательные свойства, перечисления и некоторые ограничения на строки, числа и массивы. Однако поддержка конкретных конструкций в Structured Outputs может отличаться от полной спецификации JSON Schema. Не переносите сложную схему из другого валидатора и не предполагайте, что API принимает каждую её возможность: сверяйте схему с перечнем поддерживаемых ограничений в документации OpenAI.
Полезно начинать с минимального контракта:
- объект верхнего уровня;
- ограниченный перечень полей;
- корректные типы;
- перечисления для конечного набора вариантов;
- обязательность нужных полей;
- запрет дополнительных свойств, если это соответствует сценарию.
Затем добавляйте вложенность и ограничения по одному. Для каждого изменения запускайте тест формирования запроса: неподдерживаемая схема должна быть выявлена на этапе интеграции, а не после деплоя. При изменении схемы проверьте старые потребители ответа — они могли зависеть от прежнего набора полей.
Почему дополнительные проверки нужны даже при strict
`strict: true` относится к соответствию заданной структуре, а не к истинности содержания. Модель всё ещё может неверно классифицировать обращение, упустить существенную деталь или сформулировать резюме, которого нет во входных данных. Schema не превращает вероятностный вывод в проверенный факт.
Особенно осторожно следует обращаться с полями, которые запускают действие: возврат денег, блокировка аккаунта, изменение записи или отправка сообщения. Для них добавьте независимые правила приложения. Например, разрешать изменение заказа можно только после проверки его существования в базе и прав пользователя, независимо от того, что указал ответ модели.
Если значение должно быть обосновано входным текстом, полезно хранить рядом исходный фрагмент или ссылку на запись, из которой оно извлечено. Это не гарантирует корректность вывода, но помогает аудиту и разбору ошибок. Не просите модель подтверждать собственный ответ второй фразой и не считайте такое подтверждение независимой проверкой.
Тестируйте отказ, пустой ввод и неоднозначные запросы
Перед внедрением подготовьте небольшой фиксированный набор входов: обычный пример, неполные данные, конфликтующие инструкции, нерелевантный текст и случаи, где модель должна отказаться или передать решение человеку. Проверяйте не только то, что JSON разбирается, но и то, как приложение ведёт себя при отказе, тайм-ауте и незавершённом ответе.
Для классификатора сохраните эталонные ответы, написанные человеком, и измеряйте ошибки по категориям. Если категории несбалансированы, одна общая доля совпадений может скрыть провал на редком, но рискованном классе. Разделяйте качество классификации и качество формата: первое оценивает содержательное решение, второе — пригодность ответа для парсинга.
При изменении модели, системных инструкций или схемы прогоняйте тот же набор повторно. Сравнение на одинаковых примерах выявляет регрессии лучше, чем несколько удачных ручных запросов. Для воспроизводимости фиксируйте версию модели и параметры вызова, если API позволяет это сделать, а также версию самой схемы.
Когда выбрать формат ответа, а когда function calling
Если нужно получить структурированный результат для приложения — например, классификацию или извлечённые поля, — формат JSON Schema подходит как контракт ответа. Если модель должна выбрать и вызвать инструмент с аргументами, используйте механизм вызова функций и описывайте схему параметров инструмента. Это разные сценарии: первый задаёт форму результата, второй связывает вывод модели с доступным действием.
И в том, и в другом случае проверяйте аргументы перед исполнением. Наличие корректного JSON не даёт модели права на действие. Авторизация, проверка существования ресурса, лимиты и подтверждение пользователя должны оставаться в обычной логике приложения.
Что проверить перед запуском
Сначала убедитесь, что выбранные модель и endpoint поддерживают нужный режим. Затем проверьте, что схема использует поддерживаемые конструкции, все критичные поля объявлены обязательными, а неожиданные свойства обрабатываются предсказуемо. Отдельно реализуйте ветки для отказа и незавершённого ответа.
Наконец, протестируйте не только валидный пример, но и ошибки предметной области: неверную категорию, пропущенную деталь, неоднозначный запрос и недопустимое действие. Structured Outputs помогает сократить класс ошибок формата; качество решения всё равно нужно измерять на собственных данных, а последствия — контролировать кодом приложения.








