Как проверить, что LLM действительно следует JSON Schema

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

Схема проверки ответа LLM: JSON проходит синтаксический разбор, проверку JSON Schema и семантические тесты
Схема проверки ответа LLM: JSON проходит синтаксический разбор, проверку JSON Schema и семантические тесты
Amiroooo.jpg | by Siramirb | wikimedia_commons | CC BY-SA 4.0

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

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

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

JSON Schema описывает допустимую форму документа: типы, обязательные свойства, ограничения на значения и вложенность. В спецификации Draft 2020-12 отдельно определены ключевые слова для валидации, например `type`, `required`, `enum` и `minimum`.

Для практической проверки важно различать три результата:

Синтаксический разбор: строка вообще является корректным JSON.

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

Например, схема может требовать поле `confidence` типа `number` в диапазоне от 0 до 1. Она не определит, насколько обоснована оценка модели. Аналогично, поле `summary` может быть строкой по схеме, но содержать пустой текст, если это отдельно не запрещено.

Составьте схему под контракт, а не под удачный ответ

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

json
{
«type»: «object»,
«properties»: {
«label»: {
«type»: «string»,
«enum»: [«billing», «technical», «other»]
},
«confidence»: {
«type»: «number»,
«minimum»: 0,
«maximum»: 1
},
«summary»: {
«type»: «string»,
«minLength»: 1
}
},
«required»: [«label», «confidence», «summary»],
«additionalProperties»: false
}

`required` нужен для полей, без которых потребитель не сможет работать. `enum` ограничивает метку заранее заданными вариантами. `additionalProperties: false` запрещает неожиданные ключи — полезно, если downstream-код рассчитывает на закрытый формат. Если дополнительные поля допустимы, не включайте это ограничение автоматически.

Проверьте, что выбранная версия валидатора поддерживает именно тот диалект схемы, который заявлен. Разные версии JSON Schema могут по-разному трактовать отдельные конструкции; спецификация 2020-12 задаёт собственные правила и словарь ключевых слов.

Проверьте синтаксис и схему отдельно

Для локального теста установите Python-пакеты:

bash
python -m pip install jsonschema

Затем сохраните схему в переменную и проверьте ответ:

python
import json
from jsonschema import Draft202012Validator

schema = {
«$schema»: «https://json-schema.org/draft/2020-12/schema»,
«type»: «object»,
«properties»: {
«label»: {
«type»: «string»,
«enum»: [«billing», «technical», «other»],
},
«confidence»: {
«type»: «number»,
«minimum»: 0,
«maximum»: 1,
},
«summary»: {
«type»: «string»,
«minLength»: 1,
},
},
«required»: [«label», «confidence», «summary»],
«additionalProperties»: False,
}

raw = «»»
{
«label»: «billing»,
«confidence»: 0.83,
«summary»: «Вопрос о платеже»
}
«»»

try:
result = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f»Ответ не является корректным JSON: {exc}») from exc

Draft202012Validator.check_schema(schema)
errors = list(Draft202012Validator(schema).iter_errors(result))

if errors:
for error in errors:
print(f»{list(error.path)}: {error.message}»)
else:
print(«Ответ соответствует схеме»)

Первый этап ловит синтаксические ошибки, второй — нарушения контракта. Проверка самой схемы через `check_schema` полезна при запуске тестов или сборке приложения: она обнаруживает ошибку в спецификации до обращения к модели. Библиотека `jsonschema` также позволяет получить все нарушения через `iter_errors`, а не останавливаться на первом.

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

Добавьте негативные тесты на типовые поломки

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

python
invalid_cases = [
# Нет обязательного поля
{«label»: «billing», «confidence»: 0.83},

Неизвестная метка
{«label»: «urgent», «confidence»: 0.83, «summary»: «Вопрос»},

Значение вне диапазона
{«label»: «billing», «confidence»: 1.4, «summary»: «Вопрос»},

Неподходящий тип
{«label»: «billing», «confidence»: «high», «summary»: «Вопрос»},

Лишнее поле
{
«label»: «billing»,
«confidence»: 0.83,
«summary»: «Вопрос»,
«debug»: True,
},

Пустое резюме
{«label»: «billing», «confidence»: 0.83, «summary»: «»},
]

validator = Draft202012Validator(schema)

for index, case in enumerate(invalid_cases, start=1):
assert list(validator.iter_errors(case)), f»Тест {index} неожиданно прошёл»

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

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

Структурированный режим API не отменяет проверку

Некоторые API предлагают режим структурированного вывода и принимают схему в запросе. Документация OpenAI описывает Structured Outputs как способ ограничить формат ответа заданной схемой; у Gemini API также есть режим структурированного вывода с поддержкой JSON Schema и собственными ограничениями. Конкретные поддерживаемые подмножества и поведение зависят от API и модели, поэтому перенос схемы между поставщиками надо проверять отдельно.

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

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

Семантические проверки ловят то, чего нет в схеме

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

Добавьте прикладные проверки после JSON Schema:

python
def validate_business_rules(result):
if result[«label»] == «other» and «ошиб» in result[«summary»].lower():
raise ValueError(«Проверьте классификацию: описание может указывать на ошибку»)

if result[«confidence»] > 0.9 and len(result[«summary»].split()) < 3:
raise ValueError(«Высокая уверенность требует более содержательного объяснения»)

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

Измеряйте не только долю валидных ответов

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

  • Доля синтаксически корректных ответов: сколько ответов удалось разобрать как JSON.
  • Доля ответов, прошедших схему: сколько разобранных объектов выполнили весь контракт.

Дополнительно измеряйте семантическую точность на примерах с эталонными ответами и отдельно отмечайте отказы, пустые ответы и тайм-ауты. Не смешивайте эти исходы в одну цифру: если доля валидных объектов выросла, это ещё не значит, что модель стала лучше решать задачу.

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

Перед подключением результата к приложению

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

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

Источники