Shanraq.org Shanraq.org
Структурированный ответ: JSON от модели и его проверка
IT

Python: от данных до своей сводки Урок 50 из 56

Структурированный ответ: JSON от модели и его проверка

Сорок девятый урок курса по Python. Просим модель вернуть не свободный текст, а JSON по заданной схеме, разбираем его стандартным модулем `json` и проверяем библиотекой `jsonschema`. Три отдельные границы — синтаксис, структура и факты — не дают красивой строке незаметно попасть в отчёт.

Зачем это нужно

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

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

У проверки три ступени:

  1. json.loads отвечает: это вообще JSON?
  2. jsonschema отвечает: у него ожидаемая форма?
  3. правила проекта отвечают: числа действительно взяты из наших данных?

Сегодня строим первые две ступени. Третью добавим в следующем уроке.

Сразу целиком

Установите закреплённую версию:

python -m pip install jsonschema==4.26.0

Файл structured.py работает с сохранённым ответом, поэтому Ollama для запуска не нужна.

"""Урок 49: разобрать и проверить структурированный ответ."""

import json

from jsonschema import Draft202012Validator

OUTPUT_SCHEMA = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
        "country": {"type": "string", "minLength": 1},
        "year": {"type": "integer", "minimum": 2000, "maximum": 2100},
        "value": {"type": "number"},
        "unit": {"type": "string", "enum": ["percent"]},
    },
    "required": ["country", "year", "value", "unit"],
    "additionalProperties": False,
}


def parse_and_validate(raw):
    """Вернуть только JSON, соответствующий нашему договору."""
    payload = json.loads(raw)
    Draft202012Validator(OUTPUT_SCHEMA).validate(payload)
    return payload


raw = '{"country":"Казахстан","year":2025,"value":11.4,"unit":"percent"}'
result = parse_and_validate(raw)

print("страна:", result["country"])
print("год — целое:", isinstance(result["year"], int))
print("значение — число:", isinstance(result["value"], (int, float)))
print("единица:", result["unit"])
print("лишних полей нет:", set(result) == {"country", "year", "value", "unit"})

Вывод:

страна: Казахстан
год — целое: True
значение — число: True
единица: percent
лишних полей нет: True

Сначала разбор, потом доверие

json.loads превращает JSON-строку в значения Python. Неверная запятая или текст до фигурной скобки вызывает JSONDecodeError. Не вырезайте фигурные скобки регулярным выражением: так можно случайно принять обломок ответа за целый документ.

Лучше потребовать от API чистый структурированный результат. В запросе Ollama схему можно передать полем format:

request["format"] = OUTPUT_SCHEMA

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

Что обещает схема

type: object требует объект. required перечисляет поля, без которых ответ неполон. additionalProperties: False запрещает незаявленные поля — опечатка county не спрячется рядом с правильными.

Типы важны: JSON-число 11.4 и JSON-строка "11.4" — разные значения. Схема не преобразует одно в другое и не должна угадывать намерение модели.

enum ограничивает единицу словом percent. Так программа не смешает %, доля и процент. Диапазон года ловит очевидный мусор, но не доказывает, что 2025 год есть в исходной таблице.

Перед использованием схемы в приложении полезно один раз проверить и её саму:

Draft202012Validator.check_schema(OUTPUT_SCHEMA)

Ошибка — часть результата

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

from json import JSONDecodeError
from jsonschema import ValidationError

try:
    result = parse_and_validate(raw)
except JSONDecodeError:
    print("Ответ не является JSON")
except ValidationError:
    print("JSON не соответствует схеме")
else:
    save_for_review(result)

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

Чего схема не знает

Схема подтвердит, что value — число. Она не знает, было ли в таблице 11.4, не перепутала ли модель страну и не выдумала ли причину. Правильная форма не означает правильный факт.

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

Карта урока

Карта урока: строка, JSON, схема и проверенные поля

Восстановите опорный сигнал: сырой ответ → json.loads → JSON Schema → данные для проверки фактов.

Скажите своими словами

  1. Почему просьба «верни JSON» не заменяет проверку?
  2. Чем JSONDecodeError отличается от ValidationError?
  3. Зачем одновременно нужны required и additionalProperties: False?
  4. Почему прошедшее схему число ещё нельзя публиковать?

Разминка

1. Предскажите. Пройдёт ли строка проверку типа number?

import json

value = json.loads('{"value": "11.4"}')["value"]
print(isinstance(value, (int, float)))

2. Заполните пропуск. Запретите поля, которых нет в договоре.

schema = {"type": "object", ...: False}

3. Почините. Сейчас ошибка структуры превращается в якобы пригодный ответ.

try:
    result = parse_and_validate(raw)
except Exception:
    result = {}
save_for_review(result)

Задание

Обязательное. Напишите check_many(raw_items). Для каждой строки отдельно посчитайте один из трёх исходов: valid, invalid_json, invalid_schema. Используйте только JSONDecodeError и ValidationError; неправильные ответы не должны попадать в список принятых.

принято: 1
не JSON: 1
не по схеме: 2
годы: [2025]

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

По желанию. Передайте эту же схему в format запроса Ollama. Сравните десять запусков со схемой и без неё, заранее определив метрику: доля ответов, прошедших локальную проверку.

Куда это встанет в проекте

Теперь ответ модели может попасть только в список структурно проверенных результатов. В уроке 50 дадим модели числа нашей сводки и сверим каждое возвращённое число с источником. Только после этой проверки появится текст для отчёта.

Ответы

Показать ответы
  1. Модель может добавить пояснение, пропустить поле или вернуть число строкой; пожелание не является проверкой кода.
  2. Первая означает неверный синтаксис JSON, вторая — правильный JSON неправильной формы.
  3. Первое ловит пропуск, второе — опечатку или незаявленное поле.
  4. Схема знает тип и диапазон, но не источник и истинность числа.
False
  1. Ключ — "additionalProperties".
schema = {"type": "object", "additionalProperties": False}
print(schema["additionalProperties"])
False
  1. Перехватите ожидаемую ошибку, запишите отказ и не вызывайте следующий этап.
from jsonschema import ValidationError, validate

raw = {}

try:
    validate(raw, {"type": "object", "required": ["year"]})
except ValidationError:
    print("отклонено")
else:
    print("принято")
отклонено

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

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

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.