JSON Schema для структурированных ответов LLM
Structured outputs JSON Schema: как OpenAI, Gemini и Claude обеспечивают валидную схему, плюс настройка, проверки и подводные камни.
Структурированные ответы (structured outputs) ограничивают ответ LLM предоставленной вами JSON-схемой, поэтому модель возвращает машиночитаемый JSON, соответствующий вашим полям, типам и перечислениям, а не свободный текст, который приходится разбирать вручную.
Каждый, кто выпускал в продакшн функциональность на базе LLM, знает альтернативу: регулярка для удаления лишних markdown-ограждений, try/catch вокруг JSON.parse и цикл повторных попыток, который срабатывает чаще, чем хотелось бы. Всё это работает нормально ровно до того утра, когда тихо перестаёт работать.
Провайдер обеспечивает соблюдение схемы прямо во время генерации, что превращает «надеемся, что модель вернёт валидный JSON» в контракт. Сегодня это работает в OpenAI, Google Gemini и Anthropic Claude. Все три принимают JSON Schema, поэтому одна и та же схема переносима, и меняется только обвязка запроса, а не контракт. В этой статье разбираем, что такое структурированные ответы, как ими управляет JSON Schema, как принуждение к схеме работает «под капотом», как всё подключить у каждого провайдера и от каких сценариев отказа стоит подстраховаться.
Ключевые выводы
- JSON mode гарантирует только синтаксически валидный JSON; строгие структурированные ответы гарантируют JSON, соответствующий вашей конкретной схеме: это разница между «оно парсится» и «в нём есть нужные вам поля».
- Соблюдение схемы обеспечивается за счёт ограниченного декодирования (constrained decoding): на каждом токене модель может выдать только такие продолжения, которые сохраняют валидность вывода относительно вашей схемы, то есть соответствие обеспечивается во время генерации, а не проверяется постфактум.
- OpenAI, Gemini и Claude — все понимают JSON Schema, поэтому одна схема переносится между провайдерами; Pydantic и Zod компилируются в JSON Schema, и именно так на практике работает большинство команд.
- Даже при включённом строгом режиме отказ модели (refusal) или ответ, обрезанный по длине, возвращается как успешный, но не является JSON, валидным по схеме, поэтому проверяйте разобранный объект, прежде чем ему доверять.
- Каждый провайдер поддерживает лишь подмножество JSON Schema, поэтому ключевые слова вроде
minimum,patternили глубокая рекурсия могут быть отброшены или отклонены. Сверяйтесь с документацией каждого провайдера по поддерживаемому подмножеству.
От свободного текста к JSON, ограниченному схемой
Предоставленные сами себе, LLM выдают свободный текст, который ломает парсеры: они добавляют прозу вокруг JSON, пропускают кавычки или выдумывают поля. Структурированные ответы решают эту проблему, ограничивая ответ JSON-схемой, поэтому вывод оказывается машиночитаемым и надёжно разбираемым.
Это шаг вперёд по сравнению со старым режимом JSON mode. OpenAI представила JSON mode в 2023 году как способ добиться валидного JSON, но он обещает лишь то, что вывод разберётся, а не то, что он будет следовать какой-либо заданной вами схеме. Строгие структурированные ответы закрывают этот пробел, обеспечивая соблюдение самой схемы. По собственной оценке OpenAI на задаче следования сложным схемам модель gpt-4o-2024-08-06 со Structured Outputs набрала 100% против менее чем 40% у более старой gpt-4-0613. Это бенчмарк для конкретной модели, а не универсальная гарантия, но он иллюстрирует переход от «обычно парсится» к «соответствует схеме».
Как сюда вписывается JSON Schema?
Discover how at OpenReplay.com.
JSON Schema — это декларативный контракт для ваших данных: она описывает типы, обязательные поля, перечисления и ограничения на значения. Вы передаёте её вместе с запросом, и провайдер ограничивает генерацию так, чтобы она ей соответствовала. Компактная схема для извлечения контакта выглядит так:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"plan_interest": {
"type": "string",
"enum": ["starter", "pro", "enterprise"]
}
},
"required": ["name", "email", "plan_interest"],
"additionalProperties": false
}
Мало кто пишет такое вручную в продакшене. Распространённый подход — описать структуру в Pydantic (Python) или Zod (TypeScript) и позволить SDK сгенерировать JSON Schema. SDK от OpenAI поддерживают это напрямую: передайте им объект Pydantic или Zod, и они сгенерируют соответствующую JSON-схему, преобразуют ответ обратно в ваш типизированный объект и покажут отказы модели. Gemini добавила такое же удобство, распространив поддержку JSON Schema на все активно поддерживаемые модели Gemini, так что схемы Pydantic и Zod работают без промежуточного преобразования.
Если вы не хотите собирать схему или обёртку под конкретного провайдера вручную, конструктор JSON Schema от OpenReplay делает и то, и другое прямо в браузере. Вы добавляете поля с типами, описаниями, перечислениями и вложенностью либо вставляете пример JSON, чтобы получить исходную заготовку, а затем копируете результат из панели экспорта, которая предлагает JSON Schema, response_format для OpenAI, function tool для OpenAI, форматы Anthropic, Gemini, Zod и Pydantic. Ничего из введённого вами не покидает страницу.
Как работает принуждение к схеме?
Структурированные ответы работают потому, что провайдер ограничивает декодирование: на каждом шаге генерации модель может выдать только те токены, которые сохраняют валидность вывода относительно вашей схемы. Схема компилируется в грамматику (или конечный автомат), и токены, которые её нарушили бы, маскируются до этапа сэмплирования. В документации Anthropic описан тот же механизм: ваша схема компилируется в грамматику, а ограниченное сэмплирование удерживает генерацию внутри неё, поэтому у модели просто нет возможности выдать токен, ломающий схему.
Та же идея обобщается и за пределы JSON. Для локальных и self-hosted моделей llama.cpp использует файлы грамматик GBNF, а Outlines применяет ограничения на основе регулярных выражений и грамматик — оба обеспечивают произвольные форматы (SQL, собственные DSL или JSON) по тому же принципу маскирования токенов.
Структурированные ответы у разных провайдеров
Все три крупных провайдера понимают JSON Schema, поэтому подход переносим. Различаются обвязка запроса и сигналы об ошибках.
| Провайдер | Куда передаётся схема | Диалект схемы | Сигнал об отказе / неполном ответе |
|---|---|---|---|
| OpenAI | response_format (Chat Completions) или text.format (Responses API), с strict: true | подмножество JSON Schema | поле refusal; finish_reason: "length" |
| Gemini | responseFormat.text (mimeType + schema) внутри generationConfig | подмножество JSON Schema (вкл. anyOf, $ref) | обрезанный кандидат; отклонение слишком сложных схем |
| Claude | output_config.format либо strict: true в input_schema инструмента | подмножество JSON Schema | stop_reason: "refusal" / "max_tokens" |
OpenAI. Установите strict: true и передайте схему. Руководство OpenAI рекомендует начинать новые проекты на актуальных моделях и отмечает, что в Responses API параметр переехал: используйте response_format: { type: "json_schema", strict: true } в Chat Completions или text: { format: { type: "json_schema", strict: true } } в Responses API.
Gemini. Передайте схему через generationConfig:
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Extract the contact from this email...",
config={
"response_format": {
"text": {
"mime_type": "application/json",
"schema": person_schema,
}
},
},
)
Claude. Anthropic выпустила нативные структурированные ответы — это больше не обходной путь через вызовы инструментов. Есть две дополняющие друг друга возможности: JSON-вывод через output_config.format для тела ответа и строгое использование инструментов через strict: true для их входных данных; их можно применять по отдельности или вместе. Строгое использование инструментов гарантирует, что аргументы вызова соответствуют его input_schema, поскольку эта схема компилируется в грамматику, ограничивающую сэмплирование, — то же семейство приёмов, что применяют OpenAI и Gemini. Обратите внимание: при выходе в GA API изменился — параметр output_format переехал в output_config.format, а beta-заголовки больше не требуются.
Подводные камни и рекомендации
Строгий режим не гарантирует разбираемый вывод, а поддержка схем не универсальна. Подстрахуйтесь от следующего.
Держите схемы плоскими. Глубоко вложенные или рекурсивные структуры — самая частая причина ошибок «схема слишком сложная» и ухудшения качества рассуждений. Claude сообщает об этом напрямую, возвращая 400, когда скомпилированная грамматика становится слишком большой, а документация Gemini предупреждает, что очень крупные или глубоко вложенные схемы могут быть отклонены. Длинные имена свойств, большие массивы, перечисления с множеством значений и объекты, полные необязательных свойств, — всё это увеличивает стоимость. Разбивайте крупные задачи извлечения на более мелкие и плоские схемы.
Обрабатывайте отказы и обрезание. Даже при включённом строгом режиме отказ модели или ответ, обрезанный по длине, возвращается со статусом успеха, но не является JSON, валидным по схеме. OpenAI добавила для этого специальный сигнал: поле refusal в ответе сообщает, что модель отказалась отвечать, а не вернула что-то, соответствующее вашей схеме. Универсального лимита на выходные токены не существует. Ограничения зависят от модели, и любой ответ, упирающийся в лимит посреди объекта, даёт невалидный JSON, поэтому рассчитывайте max_tokens на худший случай и сверяйтесь с задокументированным потолком вашей модели.
Проверяйте поддерживаемое подмножество. В строгом режиме каждый провайдер поддерживает лишь часть JSON Schema. Руководство OpenAI прямо говорит, что покрыта значительная часть спецификации, но некоторые её элементы не поддерживаются — по соображениям производительности или по техническим причинам. Ключевые слова вроде minimum, pattern или значения по умолчанию могут быть отброшены или отклонены, поэтому сверяйтесь с документацией провайдера по поддерживаемому подмножеству, а не рассчитывайте на полную спецификацию.
Валидируйте в любом случае. Поскольку отказы и обрезание дают ответы со статусом успеха, но невалидным по схеме JSON, разбирайте объект и проверяйте его по вашей схеме, прежде чем ему доверять, — даже при strict: true.
Сначала рассуждение, потом вывод. Ограничение вывода может снижать качество рассуждений в некоторых задачах. Одно практическое руководство по структурированным ответам Claude рассматривает это как реальный компромисс с расширенным мышлением (extended thinking): если задача больше выигрывает от рассуждений модели, чем от гарантированного соответствия схеме, оставьте фазу размышления неограниченной. Практичный компромисс — дать модели порассуждать на этапе thinking, а ограничивать только итоговый JSON.
Структурированные ответы превращают ответы LLM в нечто, с чем можно работать как с типизированным API, и этот подход чисто переносится между OpenAI, Gemini и Claude, потому что все они принимают JSON Schema. Начните с описания структуры в Pydantic или Zod, включите строгий режим у своего провайдера, держите схему плоской и оберните разбор в валидацию, которая обрабатывает отказы и обрезание, — а затем подключите ту же схему к тому провайдеру, с которым разворачиваетесь.
Часто задаваемые вопросы
В чём разница между JSON mode и строгими структурированными ответами?
JSON mode гарантирует лишь то, что модель вернёт синтаксически валидный JSON, который разберётся без ошибок, но не гарантирует соответствия какой-либо конкретной схеме. Строгие структурированные ответы обеспечивают соблюдение вашей конкретной JSON-схемы во время генерации, поэтому возвращаемый объект содержит заданные вами поля, типы и перечисления. Разница между «оно парсится» и «в нём есть нужные вам поля». OpenAI представила JSON mode в 2023 году, а позже добавила структурированные ответы с принуждением к схеме, чтобы закрыть этот пробел.
Гарантирует ли включение строгого режима, что вы всегда получите валидный, разбираемый JSON?
Нет. Строгий режим ограничивает генерацию токенов вашей схемой при нормальном завершении, но отказ по соображениям безопасности или ответ, обрезанный по длине, всё равно возвращаются со статусом успеха, при этом вывод не является JSON, валидным по схеме. OpenAI предоставляет отдельное поле refusal и finish_reason со значением length; Claude сигнализирует об этом через stop_reason со значением refusal или max_tokens. Поскольку такие ответы возвращают 200 и тарифицируются, вам следует разбирать объект и проверять его по вашей схеме, прежде чем ему доверять.
Могу ли я переиспользовать одну и ту же JSON-схему в OpenAI, Gemini и Claude?
В основном да. OpenAI, Google Gemini и Anthropic Claude принимают JSON Schema, поэтому одна и та же схема переносима, и меняется только обвязка запроса, а не контракт. Различается место передачи схемы: OpenAI использует response_format или text.format с strict true, Gemini вкладывает схему в responseFormat.text внутри generationConfig, а Claude использует output_config.format либо строгое использование инструментов. Каждый провайдер поддерживает лишь подмножество JSON Schema, поэтому сверяйте неподдерживаемые ключевые слова с документацией каждого провайдера по поддерживаемому подмножеству, прежде чем рассчитывать на полную переносимость.
Почему мою схему отклоняют как слишком сложную и как это исправить?
Ограничения по сложности возникают из-за глубоко вложенных или рекурсивных структур, длинных имён свойств, больших лимитов массивов, перечислений с множеством значений или объектов со множеством необязательных свойств. Claude возвращает 400, когда скомпилированная грамматика становится слишком большой, а Gemini может отклонить очень крупные или глубоко вложенные схемы. Решение — держать схемы плоскими и разбивать крупные задачи извлечения на более мелкие и плоские схемы. Глубоко вложенные структуры также часто ухудшают качество рассуждений, поэтому упрощение структуры улучшает и приём схемы, и качество вывода.