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

Structured Outputs в LLM: почему идеальный JSON может содержать неверные данные

Разбираем, почему Structured Outputs в LLM возвращают валидный JSON, но не гарантируют корректность извлечённых данных. Показываем, как проектировать nullable-п

Коротко

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

  1. 01

    Structured Outputs: что это и чего они не гарантируют

  2. 02

    Как валидный JSON превращается в ошибку извлечения данных в LLM

  3. 03

    Nullable-поля: как дать модели возможность сказать «данных нет»

  4. 04

    Evidence: как проверить происхождение каждого значения

Structured Outputs: что это и чего они не гарантируют

Structured Outputs позволяют получить ответ LLM в заданной структуре: с нужными объектами, полями, типами и допустимыми значениями. Модель может вернуть JSON, который проходит проверку JSON Schema, но при этом содержит ошибочный факт, неподтверждённую дату или неверную интерпретацию исходного текста.

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

Например, строка 2024-05-17 подходит под формат даты. Это ещё не доказывает, что такая дата указана в исходном документе. Для надёжного извлечения данных нужны три слоя: корректный JSON, соответствие схеме и независимая проверка смысла, происхождения и взаимосвязей полей.

Подробные ограничения strict-режима и типичные сбои Structured Outputs разобраны в статье о подводных камнях strict-режима. Здесь фокус смещён на другую проблему: корректный по форме ответ может тихо передать ошибочные данные дальше по цепочке.

Формат ответа и содержание - разные задачи

Предположим, схема требует объект с названием продукта, производителем и датой выпуска:

{
  "product_name": "Альфа",
  "manufacturer": "Компания Бета",
  "release_date": "2024-05-17"
}

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

Строгая проверка JSON или YAML на well-formedness и применение JSON Schema связаны, но находятся на разных уровнях. Первая проверяет синтаксис. Вторая описывает контракт объекта. Ни одна из них сама по себе не сопоставляет каждое значение с утверждением в исходном документе.

Даже ограничение через enum не решает задачу полностью. Если статус может принимать значения active, paused и closed, схема проверит принадлежность к этому списку. Она не установит, какой статус подтверждает текст и не перепутала ли модель текущий статус с историческим.

Почему это особенно опасно в автоматизации

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

  • В базе оно выглядит как обычная запись.
  • В CRM оно может изменить сегментацию клиента.
  • В отчёте оно влияет на сортировку, фильтры и расчёты.
  • В RAG-индексе оно становится частью контекста для следующих ответов.
  • В пайплайне AI-агента оно может повлиять на выбор инструмента или действие.

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

Как валидный JSON превращается в ошибку извлечения данных в LLM

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

Обязательное поле без значения в источнике

Допустим, исходный фрагмент сообщает только следующее: продукт называется «Альфа», его производитель - «Компания Бета». Дата выпуска в тексте не указана.

При этом схема содержит обязательное поле:

{
  "type": "object",
  "required": ["product_name", "manufacturer", "release_date"],
  "properties": {
    "product_name": {"type": "string"},
    "manufacturer": {"type": "string"},
    "release_date": {"type": "string"}
  }
}

Модель может вывести дату из соседнего контекста, принять дату анонса за дату выпуска или сформировать правдоподобное предположение. Конкретный результат зависит от модели, API, промпта и исходного текста. Системный риск появляется уже в момент проектирования контракта: схема не оставляет легального варианта для состояния «неизвестно».

Так возникает одна из форм hallucination, или галлюцинации: значение выглядит аккуратно оформленным, но не имеет достаточного основания. Жёсткий формат не устраняет эту склонность, если задача и схема требуют заполнить поле любой ценой.

Почему ошибка остаётся незаметной

Автоматический валидатор видит тип, формат и структуру. Он не видит намерение документа. Строка с датой может быть допустимой, число может находиться в заданном диапазоне, а значение enum может существовать в списке, хотя все три значения не подтверждены источником.

Последствия проявляются в обычных операциях с данными:

  • неверная дата меняет сортировку и возраст записи;
  • ошибочное число попадает в сумму или среднее;
  • неверный идентификатор ломает дедупликацию;
  • перепутанный статус запускает неправильную ветку процесса;
  • выдуманный атрибут ухудшает поиск и классификацию.

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

Nullable-поля: как дать модели возможность сказать «данных нет»

Если исходные документы часто неполны, схема должна описывать это состояние. Nullable-поле разрешает вернуть null, когда подтверждённого значения нет или текст допускает несколько трактовок.

Это не превращает любой ответ в достоверный. Зато неизвестность получает явное представление и перестаёт маскироваться под случайно выбранную строку, нулевое число или дату.

Null, пустая строка и значение по умолчанию - не одно и то же

ЗначениеСмыслРиск при смешивании состояний
nullПодтверждённого значения нет, либо его нельзя надёжно извлечьМинимальный, если downstream-система умеет обрабатывать отсутствие данных
Пустая строкаТекстовое поле сознательно содержит ноль символовПустое значение можно ошибочно принять за пропуск или результат очистки
Значение по умолчаниюЗаранее заданная настройка системыСистема может принять default за факт из документа

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

Концептуально схема может выглядеть так:

{
  "type": "object",
  "properties": {
    "product_name": {"type": "string"},
    "release_date": {
      "type": ["string", "null"]
    }
  },
  "required": ["product_name", "release_date"]
}

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

Какие поля стоит делать nullable

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

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

  • Обязательное поле: идентификатор операции, статус обработки, версия контракта.
  • Nullable-поле: дата, которая может не быть указана; сумма, которую нельзя восстановить; характеристика с неоднозначным значением.
  • Поле с отдельным статусом: атрибут, для которого нужно различать «нет в источнике», «найдено несколько вариантов» и «извлечение не удалось».

Слишком мягкая схема тоже создаёт проблемы. Если разрешить null для каждого поля, система перестанет замечать действительно обязательные данные. Nullable нужно сочетать с проверкой полноты и маршрутизацией записей, которые нельзя передавать дальше как подтверждённые.

Evidence: как проверить происхождение каждого значения

Поле value отвечает на вопрос «что извлекла модель». Evidence отвечает на вопрос «на каком фрагменте это основано». Такая связь делает structured output проверяемым человеком и программой.

Минимальный состав evidence

Для каждого факта удобно хранить значение и компактное подтверждение:

{
  "release_date": {
    "value": null,
    "evidence_quote": null,
    "document_id": "doc-1842",
    "page": null,
    "location": null
  }
}

Если дата отсутствует, value и подтверждающие поля остаются пустыми. Для найденного факта объект может содержать короткую цитату, идентификатор документа, номер страницы, номер абзаца, диапазон символов или другой указатель позиции.

  • value - нормализованное значение для дальнейшей обработки.
  • evidence_quote - короткий фрагмент исходного текста.
  • document_id - идентификатор документа в хранилище.
  • page или location - место, где найдено подтверждение.
  • confidence - дополнительный сигнал уверенности, если он нужен для маршрутизации.

Confidence не заменяет доказательство. Высокая оценка уверенности может помочь отправить запись в автоматическую ветку, но не подтверждает факт сама по себе.

Evidence не заменяет валидацию

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

Минимальный набор программных проверок выглядит так:

  1. Найти evidence_quote в исходном тексте или проверить её по нормализованным позициям.
  2. Убедиться, что идентификатор документа существует и относится к текущей операции.
  3. Проверить, что значение соответствует цитате: дата, число, имя или статус должны следовать из неё.
  4. Отметить запись как сомнительную, если подтверждение отсутствует или связь нельзя установить.

Для чисел полезно сохранять исходное написание рядом с нормализованным значением. Например, строка «1 250,50 руб.» может превратиться в число 1250.50, но проверяющему нужен исходный фрагмент, чтобы отличить корректную нормализацию от ошибки в разделителе.

Бизнес-валидация в коде, а не в промпте

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

Какие правила относятся к бизнес-логике

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

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

Проверка cross-field validation сравнивает несколько полей одновременно. Например, каждая дата по отдельности может быть корректной строкой, но их порядок способен нарушать правило процесса. Формат не ловит такую ошибку, а код ловит.

if item.end_date and item.start_date:
    if item.end_date < item.start_date:
        errors.append('end_date раньше start_date')

if item.total is not None and item.parts:
    if item.total != sum(item.parts):
        errors.append('total не совпадает с суммой компонентов')

Это иллюстративная логика. Реальные сравнения должны учитывать часовые пояса, округление, валюту, правила включения налогов и точность хранения чисел.

Что делать при провале проверки

Ошибка валидатора должна превращаться в управляемое состояние. Запись можно:

  • повторно отправить модели с уточнённым контекстом;
  • обогатить справочником или дополнительным фрагментом документа;
  • поставить в очередь ручной проверки;
  • отклонить с сохранением причины;
  • сохранить со статусом uncertain, если дальнейшая обработка допускает неполные данные.

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

Для действий с заметным ущербом полезна отдельная очередь подтверждений. Практика human-in-the-loop, пороги риска и ограничение последствий для AI-агента подробно разобраны в материале о системе подтверждений для AI-агента.

Тихие ошибки в рабочих пайплайнах: где ставить контрольные точки

Надёжный пайплайн не сводится к проверке «JSON принят или сломан». У результата должны быть разные статусы, потому что техническая ошибка, отсутствие факта и нарушение бизнес-правила требуют разных действий.

Разделяйте extracted, uncertain и invalid

СтатусСмыслСледующее действие
extractedСтруктура корректна, значения подтверждены, бизнес-проверки пройденыПередать downstream-системе
uncertainЕсть пропуск, неоднозначность или неподтверждённое значениеПовторная обработка, обогащение или ручная проверка
invalidНарушена схема, формат evidence или бизнес-правилоОтклонить, исправить по явному правилу или отправить в очередь ошибок

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

Практическая последовательность контроля:

  1. Разобрать ответ и проверить JSON или YAML на корректность.
  2. Проверить соответствие JSON Schema.
  3. Проверить наличие evidence для каждого утверждённого факта.
  4. Сопоставить цитату и значение с исходным документом.
  5. Запустить бизнес-валидацию в коде.
  6. Передать дальше только подтверждённые записи, остальные маршрутизировать по статусу.

Такой каскад можно расширять проверкой справочников, дедупликацией, оценкой полноты и ручным подтверждением. Важен сам принцип: каждый слой отвечает за свой класс ошибок.

Что сохранять для аудита и повторной проверки

Минимальный audit trail должен позволять восстановить путь записи от документа до результата:

  • исходный документ или его стабильный идентификатор;
  • сырой ответ модели;
  • нормализованный JSON;
  • evidence по каждому факту;
  • версия JSON Schema;
  • идентификатор модели и параметры обработки, если они нужны для воспроизведения;
  • результаты структурных и бизнес-валидаторов;
  • финальный статус и причина маршрутизации.

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

JSON Schema в продакшене: где хранить контракт и что делать при изменениях

Когда JSON Schema применяется к сохранению результатов, она становится контрактом системы. Появляются вопросы о владельце схемы, области действия, версиях, правах и судьбе уже записанных данных.

Одна schema на namespace или отдельная для окружения и версии

Единая schema на namespace проще для понимания: все потребители работают с одним контрактом. Такой вариант подходит публичному набору данных, если приложения действительно готовы следовать одинаковым правилам.

Отдельная схема для environment, кластера или релиза даёт больше свободы при миграциях. Цена этой свободы - риск расхождения окружений. Один и тот же объект может приниматься в тестовой системе и отклоняться в рабочей, а дефект проявится только после публикации.

ВариантПлюсРиск
Одна schema на namespaceЕдиный контракт и простая диагностикаИзменение затрагивает всех потребителей
Schema на окружение или кластерМиграции можно проводить постепенноОкружения могут разойтись по правилам
Schema на релизИзменения привязаны к версии поставкиНужно хранить и поддерживать несколько контрактов
Наследование с переопределениямиОбщий базовый контракт и локальные исключенияСложнее определить итоговое правило

Для публичного namespace, подключённого к нескольким приложениям, нужно заранее решить, остаётся ли schema единым общим контрактом. Если потребители имеют разные требования, лучше использовать адаптеры на границах систем или явные версии, чем незаметно менять смысл общих полей.

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

Что происходит с уже сохранёнными данными после ужесточения схемы

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

Есть два базовых режима:

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

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

Хранение версии schema рядом с результатом позволяет выбрать правильный валидатор задним числом. Без версии невозможно надёжно отличить ошибку исходной обработки от нарушения нового контракта.

Expand-contract как практичный режим совместимости

Expand-contract снижает число breaking changes без отдельной схемы для каждого окружения. Сначала добавляют новое поле или новый вариант значения так, чтобы старые потребители продолжили работу. Затем переводят потребителей на новый контракт. После проверки удаляют старое поле или прекращают его публикацию.

Пример последовательности:

  1. Добавить normalized_name, сохранив старое name.
  2. Начать заполнять оба поля и сравнивать результаты.
  3. Перевести читателей на normalized_name.
  4. Проверить логи, миграцию и обратную совместимость.
  5. Удалить старое поле только после завершения перехода.

Подход похож на режимы совместимости, известные по Avro, Protobuf и Confluent Schema Registry, но это именно аналогия на уровне процесса. Конкретные правила совместимости зависят от используемой системы и её валидатора.

Кто может менять schema

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

Для общего контракта полезны четыре ограничения:

  • изменение схемы проходит через версию и журнал;
  • перед публикацией запускается проверка совместимости;
  • для breaking change создаётся план миграции или перехода;
  • изменение привязывается к ответственному владельцу и окружениям.

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

Надёжная связка Structured Outputs, схемы и валидации

Надёжность извлечения данных появляется из нескольких независимых проверок. Structured Outputs задают форму ответа, nullable-поля представляют неизвестность, evidence связывает значение с документом, а код проверяет бизнес-смысл.

Минимальный контракт для извлечения фактов

Концептуальный объект для документного пайплайна может выглядеть так:

{
  "record_id": "record-1842",
  "product_name": {
    "value": "Альфа",
    "evidence_quote": "Продукт Альфа выпускает Компания Бета",
    "document_id": "doc-1842",
    "page": 1
  },
  "release_date": {
    "value": null,
    "evidence_quote": null,
    "document_id": "doc-1842",
    "page": null
  },
  "validation_status": "uncertain"
}

В этом примере record_id и validation_status нужны самому пайплайну, поэтому их можно сделать обязательными. Дата выпуска зависит от полноты источника и допускает null. Evidence для подтверждённого названия сохраняется рядом со значением, а для отсутствующей даты остаётся пустым.

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

Проверочный список перед запуском

  1. Может ли модель явно вернуть отсутствие данных через null или отдельный статус?
  2. Разделены ли обязательные технические поля и факты, которые могут отсутствовать в источнике?
  3. Есть ли evidence для каждого значения, которое система считает подтверждённым?
  4. Проверяется ли цитата по исходному документу?
  5. Отделены ли JSON Schema и бизнес-валидация в коде?
  6. Есть ли отдельные статусы для подтверждённых, сомнительных и невалидных записей?
  7. Предусмотрены ли retry, обогащение и ручная проверка?
  8. Сохраняются ли сырой ответ, нормализованный результат и версия схемы?
  9. Понятно ли, что произойдёт со старыми данными после изменения контракта?
  10. Есть ли правила совместимости и права на публикацию новой schema?

Structured Outputs в LLM решают проблему формы ответа. JSON Schema добавляет формальный контракт. Nullable фиксирует неопределённость, evidence показывает происхождение, а валидатор в коде проверяет предметные ограничения. Только связка этих уровней позволяет передавать данные дальше без ложного ощущения, что аккуратный JSON автоматически означает правильный факт.

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