Учебник
Что такое AI Structured Output? Как JSON Schema делает JSON языковой модели надёжным
«Только JSON» в промпте лишь снижает вероятность. Structured Output закрепляет форму на декодировании; JSON Schema превращает поля, типы и перечисления в контракт.
Подключая модель к конвейеру, страшно не слабое письмо, а значение, которое код не съест: пропущенная запятая, переименованное поле, число, ставшее строкой. Structured Output как раз про это: модель говорит в объявленной форме, пока выдаёт токены, а не пишет прозу, из которой вы потом выдернете JSON регуляркой. JSON Schema — самый частый письменный вид этой формы. После текста вы поймёте, застряли ли вы на синтаксисе, форме или деловой правде — и что схема всё равно требует локальной проверки.
Почему JSON только из промпта ненадёжен
«Верни только JSON, без пояснений» — самый частый обходной путь. Иногда он срабатывает, потому что записывает предпочтение в контекст. Он ненадёжен, потому что предпочтение не ограничение: модель всё ещё может обернуть объект в markdown-ограждение, пропустить обязательный ключ или превратить priority в более «естественный» urgency. Как только ниже по потоку сработает JSON.parse, сбой — уже не вопрос текста, а остановка всей цепочки.
Более тихий сбой: «парсится, но форма не та». Вы ждали, что items будет массивом, а получили объект; числовой балл, а получили "0.91". Код читает undefined или склеивает строки, и ошибка всплывает гораздо позже. Промпт такую девиацию не остановит: он не отклоняет незаконные токены на декодировании.
Поэтому не считайте Structured Output «более строгим промптом». Это часть генерации: сервер компилирует схему в множество допустимых следующих токенов, и модель не может выдать непарные скобки или ключ вне списка. Промпт объясняет задачу; форма принадлежит схеме.
Какой слой на самом деле ограничивает Structured Output
Думайте тремя слоями. Смешаете любые два — покажется, что «модель творит что хочет». Первый слой: синтаксис — JSON, который парсится. Второй: форма — имена, типы, обязательные поля и перечисления совпадают со схемой. Третий: смысл — верный ли класс, настоящая ли сумма. Structured Output закрывает первые два. Третий всегда ваш.
| Уровень | Что вы задаёте | Что вы реально получаете |
|---|---|---|
| Договорённость в промпте | «Только JSON» | Предпочтение, не контракт |
| Режим JSON | MIME или json_object |
С высокой вероятностью валидный JSON; имена полей по-прежнему у модели |
| Режим схемы | JSON Schema плюс строгий флаг | Форма, типы, обязательные поля и перечисления следуют схеме |
OpenAI описывает Structured Outputs как следующий шаг после JSON mode: оба могут дать валидный JSON; только первый гарантирует вашу схему. Официальное сравнение: Structured model outputs. Gemini проводит ту же черту между «просто JSON» и «поля по схеме»; переключатели — в руководстве по JSON в Gemini API — здесь детали SDK не повторяем.
Есть ещё граница, которую легко пропустить: отказ по безопасности, обрезка или сбой вызова инструмента могут не влезть в объект успеха. Некоторые API отдают отдельный refusal или пустое содержимое. Схема ограничивает отрезок, который «говорит в формате», а не «этот вызов обязательно успешен».
Как JSON Schema становится контрактом
JSON Schema — словарь для JSON-документов: типы, обязательные поля, перечисления, числовые диапазоны, элементы массива. Стандартный разбор: Understanding JSON Schema. Подключённый к модели, тот же словарь получает вторую работу: это уже не только постпроверка — он сужает пространство поиска во время генерации.
Для инженерии схема — контракт компиляции и рантайма. Pydantic, Zod и генерируемые типы Swift обычно сходятся в один языконезависимый JSON Schema, который можно отдать облачной модели, записать в лог и проиграть фикстурой. Одна таблица полей, чтобы приложение не говорило totalCents, API — amount, а промпт — «сумма».
Пишите объект, который вы реально прочитаете, а не полную модель мира. Каждый лишний необязательный поле — ещё один шанс заполнить его неверно или оставить пустым. Обязательное — в required, замкнутые множества — в enum, числовые границы — в minimum / maximum. description у свойства обычно стабильнее, чем повторное объяснение в промпте: ограничение связано с декодированием.
В строгом режиме часто ещё одно правило: у объектов должно быть additionalProperties: false, и каждое объявленное поле входит в required. По-настоящему необязательное значение не «убирайте из required» — разрешите null. Иначе модель может выкинуть ключ, а ваш код всё ещё предполагает obj.field.
Как разные API подключают схему
У всех вендоров название Structured Output, поля-обёртки разные. Сначала два вопроса: эта схема ограничивает финальный ответ или аргументы инструмента? Поддерживает ли этот снимок модели строгий режим? Не думайте, что ключевое слово из документации ведёт себя одинаково на каждом эндпоинте.
OpenAI: json_schema плюс strict
В Chat Completions задайте response_format как json_schema и включите strict: true. Responses API пишет то же самое в поле текстового формата. Правила схемы те же, меняется только оболочка. Запрос ниже извлекает тикет: категория — перечисление, счёт может быть null.
{
"model": "gpt-4o-2024-08-06",
"messages": [
{ "role": "system", "content": "Extract the ticket into the schema." },
{ "role": "user", "content": "Checkout 500s on a saved card. Account 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": {
"anyOf": [{ "type": "string" }, { "type": "null" }]
}
},
"required": ["summary", "category", "severity", "account_id"],
"additionalProperties": false
}
}
}
}
Считайте ответ «целиком JSON». Не выдирайте сначала блок регуляркой. Помощник parse в SDK может десериализовать в типизированный объект; обрабатывайте и ветку отказа — срабатывание политики безопасности может не совпасть со схемой успеха.
Gemini и остальные стеки
Gemini объявляет «это JSON» через MIME и фиксирует поля через responseSchema или responseJsonSchema. Второй ближе к стандартному JSON Schema и подходит для anyOf, $ref и числовых диапазонов. Подробности и примеры на Python / JS: учебник Gemini Structured Output.
Он-девайсные модели Apple используют @Generable — форму времени компиляции, а не файл JSON Schema, который вы набрали руками. Как только выходите из процесса, бьёте HTTP или пишете лог, снова нужен сериализуемый JSON. Как делятся три цепочки вызовов: Apple AI Agent и JSON. Когда внешний агент идёт через MCP или REST, полезная нагрузка почти всегда JSON, и схема по-прежнему таблица, которую должен выровнять адаптер.
Схема, которую можно выкатить
Схема ниже моделирует классификацию тикета: закрытая категория, целый приоритет, строковое резюме. Вот чем должна владеть схема — формой, а не «надо ли пометить этот тикет срочным по бизнесу».
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "bug", "feature"],
"description": "Ticket category"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5
},
"summary": {
"type": "string"
}
},
"required": ["category", "priority", "summary"],
"additionalProperties": false
}
Элементы массива описывайте через items. Для списка фиксированной длины, ближе к кортежу, на пути JSON Schema смотрите prefixItems. Вложенные объекты сначала инлайном. Беритесь за $defs + $ref только когда та же структура появляется в третий раз или узел дерева ссылается на себя. Ранняя абстракция делает ошибки нечитаемыми: если сервер отверг схему, вы смотрите на развёрнутый ком объекта.
Не описывайте структуру и в промпте, и в схеме. Двойное описание качает модель между двумя историями и жжёт входные токены. Задача — в промпте; имена и типы — только в схеме. Если локальные типы сменили поле, схема запроса должна смениться вместе, иначе в проде тихо появятся лишние или пропавшие ключи.
После ограничения всё равно проверяйте локально
Structured Output сильно успокаивает слой разбора. Он не гарантирует верный класс и не гарантирует, что число уважает ваш деловой потолок. Перечисление ограничивает множество, но не мешает выбрать неверно. Приоритет 5 законен; «на самом деле должно быть 2» — тоже валидный JSON. Критический путь семплируйте или повесьте правила.
В проде смотрите на ответ модели как на обычный JSON. Сначала Форматирование JSON, чтобы увидеть вложенность, затем Проверка JSON, чтобы подтвердить разбор, и тот же объект в JSON Schema для второй локальной проверки. С золотым набором — JSON Diff, чтобы сразу увидеть сдвиг порядка ключей или nullable-полей.
Держите три фикстуры: ticket.valid.json, ticket.missing-field.json, ticket.wrong-enum.json. Первая подтверждает happy path; следующие две — что локальная проверка действительно отвергает. Ошибки, которые сторона модели уже ограничила, должны воспроизводиться локально — иначе в день смены модели или отключения строгого режима конвейер узнает об этом офлайн.
Частые вопросы FAQ
Чем Structured Output отличается от JSON mode?
JSON mode гарантирует JSON, который парсится. Structured Output сверх того гарантирует вашу JSON Schema: имена, типы, обязательные поля и перечисления по контракту. Если код читает именованные поля, берите второе.
Если есть схема, писать ли ещё «только JSON»?
Короткая фраза допустима. Не копируйте таблицу полей в промпт. Форма принадлежит схеме. Два описания снижают качество и жгут квоту.
Как в строгом режиме обозначить необязательное поле?
Большинство строгих реализаций закрывают лишние свойства и требуют все перечисленные поля. Необязательные значения делайте nullable — объединение строки и null — а не вычёркивайте ключ из required.
Может ли бизнес быть неверным, если схема прошла?
Да. Схема владеет формой, не истиной. Неверный класс, галлюцинированная сумма, принудительное заполнение вместо отказа — всё это может быть валидным JSON. В проде остаются правила, выборка или человеческая проверка.
Итог и следующие шаги
Structured Output — не трюк копирайта. Он превращает форму JSON из «надежды» в ограничение декодирования. JSON Schema — самый общий способ записать это ограничение: обязательные поля, перечисления, диапазоны и запрет лишних ключей решают, сможет ли downstream стабильно JSON.parse и прочитать ожидаемые поля. Имена переключателей у API разные; слои одни — синтаксис, форма, смысл. Не смешивайте.
Дальше напишите маленькую схему, которую вы реально прочитаете, прогоните один путь извлечения или классификации в строгом режиме и проверьте возврат в браузере. Примеры запросов Gemini — в предыдущем руководстве; как аргументы агента становятся JSON — в статье про Apple.