Как проверить JSON Schema в ответах LLM: контрактные тесты для структурированного вывода

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

Схема проверки ответа LLM по JSON Schema: структурированный JSON проходит валидацию перед передачей приложению
Схема проверки ответа LLM по JSON Schema: структурированный JSON проходит валидацию перед передачей приложению
File:Envisioning emerging technology for 2012 and beyond.png | by Michell Zappa | openverse | by-sa

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

Ниже — узкий практический сценарий: модель извлекает из обращения клиента тему и срочность, а приложение ожидает два поля с ограниченным набором значений. Разберём схему, тесты на Python и границы метода. Это полезно для классификаторов, извлечения сущностей и промежуточных шагов агента, где ошибка формата может остановить весь процесс.

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

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

Предположим, классификатор должен возвращать:

json
{
«category»: «billing»,
«priority»: «high»
}

Одной проверки `json.loads()` недостаточно. Она поймает синтаксическую ошибку, например незакрытую кавычку, но примет и такой объект:

json
{
«category»: 17,
«priority»: «urgent»,
«debug»: true
}

JSON корректен, однако структура не соответствует контракту приложения: `category` не строка, `urgent` не предусмотрено, а поле `debug` может быть неожиданным. Схема позволяет выразить эти ограничения явно.

Как задать контракт для классификатора

Установите библиотеку `jsonschema` и сохраните схему в тесте или отдельном файле:

bash
python -m pip install jsonschema

python
SCHEMA = {
«type»: «object»,
«properties»: {
«category»: {
«type»: «string»,
«enum»: [«billing», «technical», «account», «other»]
},
«priority»: {
«type»: «string»,
«enum»: [«low», «normal», «high»]
}
},
«required»: [«category», «priority»],
«additionalProperties»: False
}

`type` задаёт тип верхнего уровня, `properties` — правила для полей, а `required` делает эти поля обязательными. `enum` ограничивает ответы известными категориями и уровнями приоритета. `additionalProperties: false` отклоняет ключи, которых контракт не предусматривает.

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

Как написать контрактный тест на Python

Метод `validate` из пакета `jsonschema` проверяет экземпляр данных и выбрасывает `ValidationError`, если он не соответствует схеме. Документация библиотеки описывает также проверку самой схемы и сбор ошибок валидации: python-jsonschema.

python
from jsonschema import validate

def check_model_result(result: dict) -> None:
validate(instance=result, schema=SCHEMA)

check_model_result({
«category»: «billing»,
«priority»: «high»
})

Для набора тестов полезно проверить не только один допустимый ответ, но и несколько конкретных отказов:

python
import pytest
from jsonschema import ValidationError, validate

def test_valid_result():
validate(
{«category»: «technical», «priority»: «normal»},
SCHEMA
)

@pytest.mark.parametrize(«result», [
{«category»: «technical»},
{«category»: 5, «priority»: «high»},
{«category»: «technical», «priority»: «urgent»},
{«category»: «technical», «priority»: «high», «trace»: «x»},
])
def test_invalid_results(result):
with pytest.raises(ValidationError):
validate(result, SCHEMA)

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

Как проверять настоящий ответ модели

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

python
import json
from jsonschema import ValidationError, validate

def parse_and_validate(raw: str) -> dict:
result = json.loads(raw)
validate(instance=result, schema=SCHEMA)
return result

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

Это измеряет соответствие формату, а не качество классификации. Ответ `{«category»: «other», «priority»: «low»}` может пройти схему и всё равно неверно описывать срочную проблему. Для проверки смысла нужны отдельные эталонные примеры и метрики задачи, например точность по категориям или доля пропущенных срочных случаев.

Что дают встроенные режимы структурированного вывода

Некоторые API позволяют передать схему в запросе и ограничить генерацию структурированным форматом. Например, OpenAI описывает Structured Outputs и ограничения режима `strict` в документации; Google — режим структурированного вывода для Gemini в руководстве. Anthropic также документирует структурированные выходы Claude и их ограничения в разделе для разработчиков.

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

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

Попросить API сформировать ответ в структурированном режиме, если он доступен.

Проверить полученные данные локальным валидатором до их использования.

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

Как встроить проверку в цепочку обработки

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

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

Получить ответ модели.

Разобрать его как JSON; отдельно обработать синтаксическую ошибку.
3. Проверить объект по схеме.
4. Передать валидный результат следующему шагу.
5. Для отказа записать безопасную диагностическую информацию и применить заранее выбранную политику: повторный запрос, ручную проверку или остановку операции.

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

Где метод перестаёт помогать

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

Для семантических ограничений нужны дополнительные проверки. Например, если поле `order_id` должно соответствовать существующему заказу, приложение должно сверить его с базой. Если два поля должны быть согласованы, добавьте бизнес-правило в код или используйте возможности схемы, если они поддерживаются вашим валидатором и режимом API.

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

Источники