Перейти к содержанию
Публикация AiManual

Почему валидный JSON не гарантирует правильные данные в structured outputs LLM

Валидный JSON от LLM может пройти JSON Schema и Pydantic, но содержать неверный enum, галлюцинацию или противоречивые поля. Разбираем пять скрытых сбоев structu

Коротко

Что будет в материале

  1. 01

    Валидный JSON проверяет форму, а не смысл данных

  2. 02

    Что гарантируют constrained decoding и JSON Schema для LLM

  3. 03

    Пять сбоев, которые проходят проверку схемы

  4. 04

    Что ловят JSON-валидаторы и Pydantic, а что остается незамеченным

Валидный JSON подтверждает, что ответ можно разобрать как JSON. JSON Schema и constrained decoding добавляют проверку типов, обязательных полей, вложенности и перечислений enum. Эти механизмы не доказывают, что LLM правильно поняла входной документ, выбрала верную категорию или не добавила выдуманный факт.

Structured outputs LLM решают проблему формата ответа. Это критично для пайплайнов, где результат сразу попадает в базу, CRM, поиск, RAG-индекс или автоматическое действие. Но объект может пройти schema validation и при этом содержать неверный статус, несуществующий идентификатор, конфликтующие даты или список сущностей, которых нет в исходном тексте.

Главная граница проста: схема проверяет форму данных, прикладная логика и источники проверяют их смысл. Если после прохождения JSON Schema объект автоматически меняет состояние заказа, создает запись, отправляет уведомление или влияет на решение, нужен отдельный semantic gate.

Валидный JSON проверяет форму, а не смысл данных

Представим задачу извлечения статуса заявки из письма. Модель возвращает объект с нужными ключами, типами и допустимыми значениями. Входной текст говорит, что заявка ожидает уточнения, а модель ставит статус approved. Парсер и схема подтвердят корректность объекта, хотя бизнес-смысл результата ошибочен.

Проблема усиливается в цепочках обработки. Downstream-сервис не видит исходный промпт и документ, он получает готовый объект. Если у него нет собственных проверок, ошибка превращается в штатное действие системы.

Четыре уровня корректности ответа

УровеньЧто проверяетсяУсловный пример ошибки
СинтаксическийТекст разбирается как JSONПропущена закрывающая скобка или кавычка
СтруктурныйПоля, типы, вложенность, required-поля, enumПоле даты передано числом вместо строки
ЛогическийСогласованность значений внутри объектаДата окончания раньше даты начала
СемантическийСоответствие входному контексту и внешней реальностиВ объект попал выдуманный номер договора

JSON-валидатор покрывает первый и часть второго уровня. Логические ограничения можно описать в коде. Семантическая проверка требует доступа к документу, базе данных, API, правилам предметной области или ручному просмотру.

Почему правильный формат создает ложное чувство надежности

Структурированный объект удобно сериализовать, хранить и передавать между сервисами. Из-за этого успешный ответ выглядит надежнее свободного текста: поле status есть, его тип правильный, значение входит в enum. Визуальная аккуратность не добавляет фактической точности.

Особенно опасен сценарий, где статус или сумма становятся входом для автоматики. Схема превращает ответ LLM в технически удобный контракт, но контракт не заменяет контроль фактов. Похожую проблему извлечения данных с nullable-полями и evidence разбирает материал о валидном JSON с неверными данными.

Что гарантируют constrained decoding и JSON Schema для LLM

Constrained decoding в LLM ограничивает пространство допустимых токенов во время генерации. Если система ожидает JSON по заданной схеме, модель получает меньше возможностей закончить ответ пояснительным текстом, сломанной вложенностью или полем другого типа.

JSON Schema описывает контракт: имена полей, обязательность, типы, диапазоны, форматы, вложенные объекты, массивы и допустимые элементы enum. Конкретный набор гарантий зависит от API и режима strict, но граница остается общей: механизм контролирует соответствие формальному описанию, а не истинность утверждений.

Синтаксис и структура: зона ответственности схемы

Схема хорошо отсекает предсказуемые технические ошибки. В типичном контракте можно запретить лишние поля, потребовать customer_id, ограничить priority набором значений, задать массив объектов и проверить, что дата передана в ожидаемом формате.

{
  "type": "object",
  "required": ["status", "items"],
  "properties": {
    "status": {"enum": ["approved", "rejected", "needs_review"]},
    "items": {"type": "array"}
  }
}

Такой контракт не позволит передать status со значением done, если его нет в enum. Он не определит, соответствует ли approved содержанию письма.

Смысл и истинность: зона ответственности приложения

Схема обычно не знает, упомянут ли клиент в исходном документе, существует ли артикул в каталоге, актуальна ли дата, верно ли модель связала причину отказа со статусом. Эти вопросы выходят за пределы проверки типа string, формата даты или списка разрешенных значений.

Приложение должно выразить проверяемые правила явно: сопоставить идентификатор с базой, проверить цитату из документа, сверить сумму с расчетом, отклонить несовместимые поля. Для части задач полезно отделить извлечение от интерпретации: первый вызов возвращает подтвержденные фрагменты, второй классифицирует их по правилам.

Почему enum не превращается в классификатор истины

Enum сужает набор ответов. Он не добавляет модели знания о правильном классе. В задаче маршрутизации обращения набор billing, technical, sales может быть описан безупречно, но сообщение о возврате средств легко получить в категории sales, если модель неверно интерпретировала контекст.

Проверка enum отвечает на вопрос: есть ли значение в списке? Контроль качества классификации отвечает на другой вопрос: подтверждают ли признаки входного текста выбранную категорию? Для второго вопроса нужны эталонные примеры, правила, повторная оценка, разметка или проверка оператором.

Пять сбоев, которые проходят проверку схемы

Все пять сценариев ниже выглядят для валидатора как успешные ответы. Поэтому журналировать только schema validation недостаточно: он покажет высокую долю корректных объектов даже при деградации содержательного качества.

Формально допустимый, но неверный enum

{
  "ticket_type": "technical",
  "reason": "Пользователь просит вернуть оплату за подписку"
}

Значение technical входит в разрешенный enum, тип строки корректен, поле причины заполнено. Ошибка видна при чтении самого текста: запрос касается оплаты, а не технической неисправности.

Проверка: сопоставлять класс с явными доменными признаками, держать набор ручной разметки для регрессии и отправлять конфликтующие пары ticket_type и reason на повторную оценку. В некоторых доменах полезен отдельный классификатор-арбитр, но его ответ тоже нуждается в контроле качества.

Правдоподобная, но выдуманная информация

{
  "contract_number": "CN-4821",
  "signed_at": "2026-08-14",
  "evidence": null
}

Каждое поле соответствует ожидаемому формату. При этом номер и дата могли отсутствовать во входном документе. Модель заполнила пропуск правдоподобными значениями, потому что контракт требует строку, а промпт не оставляет честного пути для ответа unknown.

Для извлечения фактов полезен контракт, где значение отделено от подтверждения: value, evidence, source_ref, verification_status. Если доказательства нет, система должна принять null, unknown или needs_review. Пустое подтверждение при заполненном критичном поле становится явным сигналом для semantic gate.

Противоречия между полями

{
  "status": "rejected",
  "rejection_reason": null,
  "starts_at": "2026-09-12",
  "ends_at": "2026-09-10"
}

Каждое значение имеет допустимый тип. Статус взят из enum, null разрешен, даты выглядят как даты. Объект описывает невозможную комбинацию: отказ не содержит обязательной причины, а окончание события наступает до его начала.

Такую ошибку ловит cross-field-валидация. Она формулирует инварианты доменной модели: для rejected нужна причина, ends_at не раньше starts_at, при пустом списке совпадений статус не может утверждать наличие совпадений. Каждый инвариант должен возвращать понятную причину отказа.

Схлопывание распределения в безопасные значения

Модель может стабильно выбирать нейтральную категорию: needs_review, other, low или unknown. Каждый отдельный объект пройдет проверку схемы. На уровне потока данных возникнет distribution collapse: входы отличаются, а выходы начинают чрезмерно концентрироваться в одном безопасном значении.

Причиной бывает изменение промпта, схемы, модели, системной инструкции или характера входящих данных. Без мониторинга это легко принять за снижение риска, хотя система могла потерять чувствительность к важным признакам.

Проверка: строить частоты enum по версии модели, промпта, источнику и типу документа. Резкий сдвиг не доказывает ошибку, но требует проверки на контрольной выборке и анализа примеров из изменившейся категории.

Пустой массив заменяется несуществующими элементами

{
  "entities": [
    {"name": "ООО Альфа", "role": "поставщик"}
  ],
  "status": "complete"
}

Схема подтверждает, что entities содержит объект нужной формы. Но во входном тексте могла не упоминаться ни одна организация. Модель предпочла заполнить массив вместо признания отсутствия данных.

Ограничение minItems повышает риск такого поведения, если задача допускает честный пустой результат. Контракт должен разрешать [], а для каждого элемента стоит хранить фрагмент подтверждения. Если элемент нельзя связать с источником, его нельзя без проверки передавать в сущностный граф, базу клиентов или автоматический поиск.

Что ловят JSON-валидаторы и Pydantic, а что остается незамеченным

JSON Schema validator и Pydantic полезны как первая линия контроля. Они предотвращают падения парсеров, упрощают обработку ошибок и делают контракт между LLM и приложением явным. Проблема начинается, когда успешную валидацию трактуют как знак достоверности результата.

Типы, обязательные поля и форматы

Базовая валидация проверяет типы, required-поля, структуру вложенных объектов, ограничения чисел и строк, enum, сериализацию дат и размеры массивов. В Pydantic такие правила описываются типами полей и декларативными ограничениями модели.

Этот слой нужен всегда. Он защищает код от ответов, которые нельзя безопасно разобрать или использовать. При этом строка в поле email может соответствовать формату и принадлежать не тому человеку, а разрешенный идентификатор может отсутствовать в рабочей базе.

Логические инварианты и зависимости между полями

Кастомные валидаторы добавляют правила, которых нет в простом описании полей. Pydantic позволяет выразить такие проверки в валидаторах модели.

from datetime import date
from pydantic import BaseModel, model_validator

class Application(BaseModel):
    status: str
    rejection_reason: str | None = None
    starts_at: date
    ends_at: date

    @model_validator(mode="after")
    def check_invariants(self):
        if self.ends_at < self.starts_at:
            raise ValueError("ends_at раньше starts_at")
        if self.status == "rejected" and not self.rejection_reason:
            raise ValueError("для rejected нужна причина")
        return self

Этот код обнаружит две внутренние ошибки. Он не установит, действительно ли заявка была отклонена и верно ли модель извлекла даты из письма. Полнота логических правил зависит от того, какие инварианты разработчик описал и поддерживает.

Семантическая проверка требует контекста

Смысловая проверка сравнивает результат с источником истины. Источником может быть документ, таблица, CRM, каталог, база договоров, внутренний API или формализованные правила. Здесь проверяются происхождение факта, актуальность, существование объекта и соответствие утверждения входным данным.

Поле confidence не решает проблему само по себе. Число, сгенерированное той же LLM, описывает ее ответ в заданной форме, но не служит независимо калиброванной вероятностью. Для RAG и документооборота полезны типизированные контракты с цитатами, флагами подтверждения и отдельными этапами проверки, этот подход раскрыт в разборе Pydantic-контрактов для RAG.

Как построить многоуровневую семантическую валидацию в production

Надежный пайплайн принимает structured output только после последовательности проверок. Слои должны возвращать машиночитаемый итог: принять объект, повторить запрос, запросить уточнение, передать на human review или безопасно отказаться от действия.

Слой 1. Schema validation как базовый фильтр

Сначала парсится JSON, затем проверяются типы, required-поля, enum, форматы, ограничения длины и количества элементов. Объект, который не прошел этот этап, не должен достигать бизнес-логики.

Успех на этом слое означает только техническую пригодность для дальнейшей обработки. Лог должен хранить версию схемы, версию промпта, модель и точную причину отказа.

Слой 2. Нормализация и доменные правила

До проверки смысла значения приводятся к единому представлению: даты к одному часовому поясу и формату, суммы к единой валютной модели, идентификаторы к канонической записи, названия категорий к утвержденному словарю. Без нормализации одинаковый факт может выглядеть как расхождение.

После нормализации применяются правила предметной области. Например, артикул должен существовать в каталоге, сумма не может быть отрицательной для выбранного типа операции, а значение статуса должно быть допустимо для текущего этапа процесса.

Слой 3. Cross-field-валидация

Cross-field-валидация проверяет связи между значениями. Она находит невозможные сочетания статусов, дат, сумм, причин и массивов. Этот слой полезен даже без доступа к внешней базе, потому что отсекает внутренние противоречия объекта.

  • Для статуса rejected нужна причина отказа.
  • Для статуса approved причина отказа должна быть пустой или отсутствовать.
  • Дата завершения не может предшествовать дате начала.
  • Статус наличия совпадений не должен сопровождаться пустым массивом подтвержденных совпадений.
  • Сумма итоговой операции должна сходиться с суммой строк, если контракт содержит обе величины.

Правила должны быть наблюдаемыми. Система должна писать в лог идентификатор инварианта и поля, из-за которых возник отказ.

Слой 4. Проверка по источнику и evidence

Для извлекаемых фактов нужен проверяемый след: фрагмент документа, позиция в тексте, идентификатор записи или иной source reference. Поле evidence полезно лишь тогда, когда приложение может сопоставить его с реальным входом.

Хороший контракт разрешает отсутствие данных. Для неподтвержденного значения подходят null, unknown, insufficient_evidence и пустой массив, выбор зависит от семантики поля. Принудительное заполнение обязательной строкой создает стимул для галлюцинации.

Принцип отделения генерации от строгой внешней проверки полезен и за пределами LLM-пайплайнов. В материале о формальной проверке AI-кода и доказательств разобрана та же идея: генератор предлагает результат, независимый проверяющий принимает или отклоняет его по заданным правилам.

Слой 5. Неопределенность и безопасный отказ

Контракту нужны явные состояния неопределенности: needs_review, abstain, unknown, insufficient_evidence. Их нельзя добавлять формально: downstream-логика должна знать, что такие ответы не запускают необратимое действие.

Полезная маршрутизация выглядит так: схема не прошла - повторить или отклонить запрос; нарушен доменный инвариант - вернуть на повторную обработку; нет evidence для критичного факта - отправить на human review; источник подтвердил данные - разрешить следующий шаг. Порог неопределенности стоит калибровать на проверенных примерах, а не брать число confidence за готовую оценку риска.

Какие сигналы мониторить после запуска

Ошибки формата видны сразу: парсер вернул исключение, схема отклонила объект. Семантическая деградация часто развивается тихо. Все ответы остаются валидными, но меняются их распределения, число неподтвержденных полей и доля исправлений после проверки.

Распределение enum и категорий

Отслеживайте частоты значений enum во времени и в разрезе версии модели, промпта, схемы, типа документа и источника входных данных. Отдельно смотрите на категории other, unknown, needs_review и самые популярные классы.

Резкий сдвиг распределения может отражать реальное изменение входящего потока. Он же может сигнализировать о том, что модель стала избегать конкретных решений или начала применять новую интерпретацию инструкции. Проверка на контрольной выборке отделяет один случай от другого.

Пустые массивы, null и принудительно заполненные поля

Сравнивайте долю [], null, unknown и объектов с элементами без evidence. Если после изменения схемы пустые массивы почти исчезли, а доля неподтвержденных элементов выросла, контракт мог начать подталкивать модель к заполнению результата.

Сигнал полезен только вместе с контекстом задачи. Для документов с редкими сущностями пустой массив часто корректен. Для обязательного списка товаров он может говорить о сбое извлечения или проблеме входного парсинга.

Противоречия и результаты downstream-проверок

Собирайте число нарушений cross-field-правил, отклонений по источнику, повторных вызовов LLM, передач на ручную проверку и ручных исправлений. Причины отказов лучше хранить отдельно, иначе команда увидит общий процент ошибок, но не поймет, где именно ломается цепочка.

Если растет число отказов на проверке источника при стабильной schema validation, проблема лежит в семантике ответа, ретривале, OCR, парсинге входных документов или изменении модели. Формат structured output в этом сценарии может оставаться идеальным.

Данные для регрессионных проверок

Поддерживайте небольшой набор вручную проверенных примеров. В него должны входить нормальные случаи, неоднозначные формулировки, отсутствующие поля в источнике, пустые результаты, конфликтующие признаки и документы с похожими категориями.

Перед сменой модели, промпта или схемы проверяйте на этом наборе больше, чем JSON-парсинг: правильность enum, наличие evidence, согласованность полей, корректную обработку null и способность вернуть пустой массив. Это дает воспроизводимый сигнал о качестве до выхода изменений в основной поток.

Когда достаточно JSON Schema, а когда нужен отдельный semantic gate

Глубина контроля зависит от цены ошибки и последствий автоматического действия. Один и тот же schema-only подход может быть приемлем для черновика и опасен для обработки критичных данных.

Низкий риск: формат важнее точности

JSON Schema часто достаточно как технический фильтр, когда результат служит черновиком, внутренним поисковым признаком, промежуточным представлением или подсказкой для человека. Условие одно: объект не должен сам менять важное состояние и запускать необратимое действие.

Даже здесь нужны логирование, выборочная проверка и мониторинг распределений. Иначе ухудшение качества обнаружится лишь после того, как результаты начнут использоваться шире.

Высокий риск: валидный объект не должен сразу выполнять действие

Отдельный semantic gate нужен для персональных, финансовых, медицинских, юридически значимых данных, управления доступами, платежей, договоров и автоматических решений с заметными последствиями. В таких задачах перед действием проверяются источник, межполевая логика, актуальность записи, уровень неопределенности и условия эскалации.

Порог confidence может стать вспомогательным сигналом, но не заменяет доказательство. Для критичных полей важнее подтвержденное происхождение значения и понятный fallback: повторный запрос с уточнением, сверка с системой учета или human review.

Минимальный production-чеклист

  • Проверяется ли JSON-синтаксис и JSON Schema до бизнес-логики?
  • Есть ли cross-field-правила для статусов, дат, сумм и зависимых полей?
  • Может ли модель честно вернуть unknown, null или пустой массив?
  • Есть ли evidence или другой проверяемый след для извлеченных фактов?
  • Проверяются ли критичные идентификаторы и значения по внешнему источнику?
  • Мониторятся ли распределения enum, пустые массивы, отказы и ручные исправления?
  • Есть ли регрессионный набор перед сменой модели, схемы или промпта?
  • Что произойдет с сомнительным объектом до автоматического действия?

Structured outputs нужны для надежного обмена данными между LLM и кодом. Их нельзя считать доказательством истинности. Чем дороже ошибка, тем больше проверок должно стоять между валидным JSON и действием системы.

Подписаться на канал