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" до гарантированных типизированных контрактов.

Предварительные знания

  • Production Prompt Patterns: system/user/assistant, Few-Shot, Chain-of-Thought

Проблема: 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
Structured Output: заставляем LLM возвращать JSON, схемы, типизированные данные

0

1

Войти