AI-инжиниринг
Structured Output: заставляем LLM возвращать JSON, схемы, типизированные данные
Цели урока
- Понять проблему непредсказуемого формата ответа LLM и почему промпт-инструкции недостаточны
- Освоить JSON Mode (response_format) как первый уровень гарантий
- Научиться использовать Zod-схемы с OpenAI SDK для type-safe structured output
- Разобраться в function calling - механизме вызова внешних функций через LLM
- Построить надёжную систему обработки ошибок: retry, fallback, валидация
JSON mode не гарантирует валидный JSON. Это гарантирует что модель ПОПЫТАЕТСЯ вернуть валидный JSON. Разница - продуктовый инцидент в 3 часа ночи: `JSON.parse` падает, pipeline встаёт, пейджер орёт. Structured output - это контракт. LLM нарушает контракты. Именно поэтому нужен Zod, а не надежда.
- Linear AI извлекает structured данные из свободного текста - заголовок, приоритет, assignee - и создаёт issue без единого клика. Structured Output + Zod-схема
- Notion AI парсит неструктурированные заметки в базы данных с 99.9% надёжностью. Без Structured Output - 80%, что неприемлемо для продукта
- GitHub Copilot использует function calling для интеграции с IDE - поиск файлов, запуск тестов, навигация. Модель решает, что вызвать; IDE выполняет
- Vercel v0 генерирует React-компоненты через function calling - модель "вызывает" функцию создания UI с параметрами компонента
Эволюция structured output в OpenAI API
**Июнь 2023**: function calling - модель может выбирать функции и возвращать структурированные аргументы. Первый шаг к AI-агентам. **Ноябрь 2023**: JSON Mode (`response_format: { type: 'json_object' }`) - гарантированно валидный JSON, но без контроля схемы. **Август 2024**: Structured Outputs - constrained decoding на уровне токенов. Модель физически не может нарушить схему. Zod-интеграция через `zodResponseFormat`. **Антропик** параллельно добавил `tool_use` (аналог function calling) и prefill-трюк для JSON. За полтора года индустрия прошла путь от "попроси вернуть JSON" до гарантированных типизированных контрактов.
Предварительные знания
Проблема: LLM возвращает строку, а backend ожидает объект
LLM по умолчанию возвращает **свободный текст**. Это принципиальная проблема: backend-система не работает с произвольными строками - ей нужен распарсенный объект с предсказуемой структурой. Попросить модель "вернуть JSON" - это попросить её постараться. Постарается ли она в 3 часа ночи при нагрузке на прод?
Модель отлично знает JSON - проблема не в этом. Проблема в **отсутствии контракта**. Модель воспринимает "верни JSON" как стилистическую рекомендацию, а не инженерное требование. В production нужна 100% надёжность, а не 90%. Разница между 90% и 100% - это `JSON.parse` в обработчике ошибок против инцидента в Slack в 3 ночи.
| Подход | Надёжность | Когда использовать |
|---|---|---|
| Промпт "верни JSON" | ~80-90% | Прототипы, не для production |
| JSON Mode (response_format) | ~99% | Когда нужен валидный JSON без строгой схемы |
| Structured Output (Zod schema) | ~99.9% | Production: гарантированная структура |
| Function Calling | ~99.9% | Когда модель должна вызывать функции |
**Даже с JSON Mode и Structured Output бывают edge cases.** Модель может вернуть `null` в required-поле или пустую строку вместо значения. Валидация на стороне приложения - обязательна всегда.
JSON mode гарантирует валидный JSON
JSON mode гарантирует, что модель ПОПЫТАЕТСЯ вернуть валидный JSON. Валидацию структуры всё равно нужно делать на клиенте
OpenAI JSON mode с `response_format: { type: 'json_object' }` убирает markdown-обёртку и поясняющий текст. Но структуру полей не контролирует: модель может вернуть `{"person": "Анна"}` вместо `{"name": "Анна"}`. Для гарантии структуры нужен Structured Output с Zod-схемой. Без этого - надежда, а не инженерия.
Почему подход "попросить в промпте вернуть JSON" ненадёжен для production?
JSON Mode и response_format: первый уровень гарантий
Июнь 2023. OpenAI добавляет `response_format: { type: 'json_object' }` - **JSON Mode**. Модель перестаёт оборачивать ответ в markdown и добавлять пояснительный текст. Только чистый JSON. `JSON.parse` больше не падает... при условии, что структура не важна.
**JSON Mode требует упоминания JSON в system prompt.** Если в промпте нет слова "JSON", API вернёт ошибку. Это защита от случайного включения JSON Mode.
JSON Mode - это только первый уровень. Валидный JSON != правильный JSON. Модель может вернуть `{"person": "Анна", "mail": "anna@mail.ru"}` вместо `{"name": "Анна", "email": "anna@mail.ru"}` - и `JSON.parse` это проглотит без ошибки. Структуру полей контролирует только следующий уровень - Structured Output.
**Prefill-трюк для Claude** работает, но неэлегантен. Anthropic добавил поддержку tool_use (function calling), который даёт структурированный вывод надёжнее. Об этом - в следующих концепциях.
Чем JSON Mode (`response_format: { type: 'json_object' }`) отличается от Structured Output?
Zod + OpenAI: type-safe structured output
Август 2024. OpenAI выпускает Structured Outputs - механизм **constrained decoding**: модель физически не может сгенерировать токен, нарушающий схему. Не "постарается вернуть", а "не может не вернуть". Разница принципиальная - это уже не договорённость, а физическое ограничение генерации.
OpenAI SDK поддерживает **Zod schemas** напрямую через `zodResponseFormat`. Zod-схема описывает точную структуру ожидаемого ответа, SDK конвертирует её в JSON Schema для API, constrained decoding гарантирует соответствие. На выходе - уже типизированный объект, без ручного парсинга.
`.parsed` уже типизирован по Zod-схеме - TypeScript знает какие поля есть и какого они типа. Никакого `JSON.parse`, никаких `as any`. Это то, к чему backend-разработчик привык в любом другом месте кодовой базы.
**Ограничения Structured Output:** не поддерживает все возможности JSON Schema. Нельзя использовать `minItems`, `maxItems`, `pattern`, `format` (кроме `date-time`). Валидацию таких constraints нужно делать на стороне приложения после получения ответа.
Zod-схема определяет поле `age: z.number().min(18).max(100)`. Structured Output гарантирует, что модель вернёт число от 18 до 100?
Function Calling: когда модели нужно действовать
Июнь 2023. OpenAI добавляет function calling. Это меняет всё: LLM перестаёт быть чат-ботом и становится компонентом, способным **выбирать действия**. Именно тогда началась эпоха AI-агентов - не в смысле маркетинга, а в смысле архитектуры.
Важное уточнение: модель **не вызывает** функцию сама. Она возвращает JSON с именем функции и аргументами. Вызов выполняет приложение. Модель - "решатель", приложение - "исполнитель". Это принципиально для безопасности: LLM никогда не имеет прямого доступа к вашим системам.
**Function calling vs Structured Output.** Structured Output - это "верни данные в такой структуре". Function calling - "выбери действие и укажи параметры". Structured Output идеален для extraction, function calling - для агентов и tool use. Часто используются вместе.
При function calling модель возвращает `tool_calls` с именем функции и аргументами. Кто выполняет саму функцию?
Обработка ошибок и retry-стратегии для structured output
Structured Output - это контракт. LLM нарушает контракты. Не потому что плохая, а потому что вероятностная. Даже с constrained decoding бывают `refusal` (отказ по safety-причинам), `null` в required-поле, данные за пределами бизнес-правил. Надёжная обработка ошибок - не опционал, это часть контракта со стороны инженера.
Function calling добавляет ещё один уровень хрупкости: аргументы приходят как строка JSON. Модель с Structured Output всё равно может сгенерировать технически валидный JSON с неожиданными значениями. Валидация через Zod - обязательный шаг.
**Порядок предпочтения в production:** 1. Structured Output с Zod - используется по умолчанию. 2. При refusal - логирование + fallback. 3. При parse error - retry с экспоненциальным backoff. 4. Если всё сломалось - fallback на regex-extraction. Чем больше слоёв защиты, тем надёжнее система.
Модель вернула `refusal` (отказ по safety-причинам) при structured output запросе. Правильная стратегия?
JSON mode = гарантия валидного JSON
JSON mode гарантирует что модель не добавит текст вокруг JSON. Структура полей по-прежнему произвольная - нужна валидация
С `response_format: { type: 'json_object' }` `JSON.parse` не упадёт. Но модель может вернуть `{"человек": "Анна"}` вместо `{"name": "Анна"}` - и это будет валидный JSON. Для контроля структуры нужен Structured Output с Zod-схемой. JSON mode убирает markdown-обёртку. Structured Output убирает непредсказуемость полей. Это разные уровни гарантий.
Ключевые концепции
- Промпт "верни JSON" - ненадёжен (~80%). JSON Mode - валидный JSON без схемы (~99%). Structured Output с Zod - гарантированная структура (~99.9%)
- Constrained decoding (Structured Outputs, август 2024) - модель физически не может нарушить схему на уровне токенов
- zodResponseFormat + openai.beta.chat.completions.parse = type-safe extraction, без JSON.parse и as any
- Structured Output не валидирует Zod constraints (min, max, pattern) - обязательна двухуровневая валидация: API-схема + строгая бизнес-схема
- Function calling (июнь 2023): модель выбирает функцию и аргументы, приложение выполняет - основа AI-агентов. LLM никогда не имеет прямого доступа к системам
- Production: контракт со стороны инженера - retry с backoff + refusal-хендлинг + regex-fallback = многослойная защита
Что дальше
Structured output обеспечивает предсказуемый формат ответов. Следующий шаг - научиться получать эти ответы в реальном времени через streaming.
- Streaming: SSE и real-time ответы — Получение structured output в потоковом режиме - по чанкам
- Tool Calling (глубже) — Function calling из этого урока -> полноценный tool use и цепочки вызовов
- AI-агенты — Function calling + цепочки решений = автономные агенты
Вопросы для размышления
- В каких местах текущего или прошлого проекта JSON.parse мог упасть из-за неожиданного формата ответа от внешнего сервиса? Structured Output решил бы эту проблему?
- Чем constrained decoding принципиально отличается от просьбы "верни JSON" в промпте? Почему это разные уровни гарантий?
- Когда имеет смысл использовать function calling вместо Structured Output, и наоборот? Какой критерий выбора?
Связанные уроки
- aie-06-prompt-patterns — Промпты настраивают модель до ограничения её вывода
- aie-16-tool-calling — Function calling здесь вырастает в полноценный tool use
- aie-17-agent-fundamentals — Типизированный вывод это основа для решений агента
- aie-32-error-handling-llm — Сбои валидации по схеме требуют аккуратной обработки
- ts-01-why-typescript — Валидация по JSON Schema похожа на статическую проверку типов
- plt-25-parser