Как проверить structured outputs в LLM: тесты для JSON Schema и семантики

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

Схема проверки структурированного ответа LLM: JSON проходит JSON Schema и дополнительные проверки значений
Схема проверки структурированного ответа LLM: JSON проходит JSON Schema и дополнительные проверки значений
18.4.2015 Ceremonia de entrega de los Premios HO 2015 | by HazteOir.org | openverse | by-sa

Структурированный вывод LLM упрощает передачу данных в код, но сам по себе не гарантирует правильный ответ. Модель может вернуть корректный JSON с несуществующим идентификатором, перепутать валюту или неверно извлечь дату — и пройти проверку схемы. Поэтому проверка structured outputs должна состоять как минимум из двух уровней: соответствие формату и корректность значений относительно задачи.

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

Что именно проверяет JSON Schema

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

Это ловит ошибки вроде отсутствующего поля, строки вместо числа или неизвестного кода валюты. Спецификация JSON Schema 2020-12 также определяет ограничения для массивов, строк и объектов. Однако схема не знает, соответствует ли сумма исходному тексту, правильно ли модель поняла намерение пользователя и существует ли указанный в ответе товар.

Разделяйте три вопроса:

Синтаксис: ответ разбирается как JSON?

Контракт: он соответствует схеме?
3. Смысл: значения подтверждаются входными данными и правилами приложения?

Первые два удобно проверять автоматически стандартным валидатором. Третий требует предметных правил, эталонных примеров или проверки человеком — в зависимости от риска.

Проверка structured outputs: минимальный контракт

Возьмём задачу извлечения суммы покупки из текста. Контракт должен различать найденную сумму и случай, когда её нет. Не заставляйте модель угадывать: предусмотрите явное значение `null` и ограничьте валюту поддерживаемым списком.

Пример схемы JSON Schema:

json
{
«type»: «object»,
«properties»: {
«amount»: {
«type»: [«number», «null»],
«minimum»: 0
},
«currency»: {
«type»: [«string», «null»],
«enum»: [«USD», «EUR», «RUB», null]
},
«evidence»: {
«type»: «string»
}
},
«required»: [«amount», «currency», «evidence»],
«additionalProperties»: false
}

`required` требует присутствия полей, но не запрещает `null`: это отдельное решение, заданное типом. `additionalProperties: false` не допускает неожиданные ключи. Такая строгость особенно полезна, если результат напрямую обрабатывает код, но её нужно согласовать с конкретным API и способом формирования схемы.

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

Валидируйте ответ до передачи в бизнес-логику

Ниже — небольшой тест на Python с Pydantic. Он проверяет типы и допустимые значения, а затем отдельным условием связывает сумму с валютой.

python
from typing import Literal
from pydantic import BaseModel, ConfigDict, ValidationError

class Purchase(BaseModel):
model_config = ConfigDict(extra=»forbid»)

amount: float | None
currency: Literal[«USD», «EUR», «RUB»] | None
evidence: str

def validate_purchase(data: dict) -> Purchase:
result = Purchase.model_validate(data)

if (result.amount is None) != (result.currency is None):
raise ValueError(«amount и currency должны быть заданы вместе»)

return result

good = {
«amount»: 19.99,
«currency»: «USD»,
«evidence»: «The total is $19.99.»
}

print(validate_purchase(good))

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

Если модель использует режим структурированного вывода, проверьте также состояния API, в которых содержимого по ожидаемой схеме может не быть: например, отказ, прерванная генерация или ошибка запроса. Документация OpenAI отдельно описывает structured outputs и обработку отказов; конкретный формат зависит от используемого API.

Формат прошёл, а ответ всё равно неверен

Валидатор не сверяет ответ с исходным текстом. Добавьте проверки, которые отражают реальные последствия ошибки:

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

Проверка вида «evidence непустой» доказывает только наличие строки. Она не доказывает, что эта строка подтверждает сумму. Для надёжной проверки сопоставляйте данные с источником: например, ищите точное значение в процитированном фрагменте или проверяйте его отдельным алгоритмом. Если такой проверки нет, не трактуйте поле `evidence` как гарантию достоверности.

Соберите небольшой регрессионный набор

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

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

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

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

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

Где различаются JSON Schema и форматы API

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

На стороне приложения стандартные валидаторы, например Pydantic или Ajv для JavaScript, полезны даже при использовании встроенного режима. Они позволяют проверить итоговый объект независимо от обещаний генератора и не связывать безопасность бизнес-логики только с поведением модели.

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

Как не превратить проверку в источник новых ошибок

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

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

Проверка не должна молча исправлять неоднозначные значения. Автоматическое преобразование строки `»19,99″` в число может быть уместным при заданной локали, но опасным, если формат неизвестен. Любую нормализацию опишите явно и покройте тестами.

Практический порядок внедрения

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

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

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

Источники