Создание ДС с локальной LLM. Часть 2: Строгая структура данных (JSON Schema)

В первой части мы рассмотрели общую архитектуру решения для автоматической генерации Дополнительных Соглашений. Мы выяснили, что ключ к успеху — это не просто “поболтать” с моделью, а заставить её работать как часть жесткого алгоритма.

Сегодня мы углубимся в техническую реализацию этого “принуждения”. Как заставить творческую языковую модель (LLM) возвращать данные, которые можно безопасно вставить в базу данных или Word-документ, не опасаясь галлюцинаций или лишнего текста?

Ответ: JSON Schema и режим Structured Outputs.

Проблема: LLM любят поговорить

Обычный промпт: “Извлеки из договора номер и дату” часто приводит к ответу:

“Конечно! Я проанализировал текст. Номер договора — 123, а дата заключения — 1 января 2025 года.”

Для человека это понятно. Для кода — это катастрофа. Нам нужно писать регулярные выражения, чтобы вытащить данные из этого ответа. А если модель решит изменить формат ответа завтра? Весь пайплайн сломается.

Решение: Structured Outputs

Современные движки для запуска LLM (включая vLLM, llama.cpp и серверы, совместимые с OpenAI API) поддерживают параметр response_format. Мы можем передать схему данных, и движок будет гарантировать, что выход модели валиден относительно этой схемы.

В моем проекте я использую локальную модель Qwen, и для неё я определил строгую схему ADDENDUM_SCHEMA.

Реализация в коде

Вот как выглядит определение схемы на Python (фрагмент из generate_addendums.py):

ADDENDUM_SCHEMA = {
    "type": "json_schema",
    "json_schema": {
        "name": "addendum_placeholders_ru",
        "strict": True,
        "schema": {
            "type": "object",
            "properties": {
                # Основные реквизиты
                "номер_ДС": {"type": "string"},
                "дата_ДС": {"type": "string"},
                "город_подписания": {"type": "string"},
                "номер_договора": {"type": "string"},
                "дата_договора": {"type": "string"},
                
                # Стороны
                "наименование_заказчика": {"type": "string"},
                "наименование_исполнителя": {"type": "string"},
                
                # Подписанты (обычные и для грамматики)
                "подписант_заказчика": {"type": "string"},
                "подписант_исполнителя": {"type": "string"},
                "подписант_заказчика_в_родительном падеже": {"type": "string"},
                "подписант_исполнителя_в_родительном падеже": {"type": "string"},

                # Блок изменений (Предмет, Оплата, Сроки)
                "тип_единицы_предмет": {"type": "string"},       # например, "Статья"
                "номер_единицы_предмет": {"type": "string"},     # например, "3"
                "последний_пункт_предмет": {"type": "string"},   # "3.4"
                "добавляемый_пункт_предмет": {"type": "string"}, # "3.5"
                
                # Грамматические конструкции для вставки в текст
                "фраза_единицы_предмет_вин": {"type": "string"}, # "статью 3"
                "фраза_пункт_предмет_твор": {"type": "string"},  # "пунктом 3.5"
                "текст_цитаты_предмет": {"type": "string"}
            },
            "required": [
                "номер_ДС", "дата_ДС", "город_подписания",
                "номер_договора", "дата_договора",
                "наименование_заказчика", "наименование_исполнителя",
                "подписант_заказчика_в_родительном падеже", 
                # ... и все остальные поля обязательны!
            ]
        }
    }
}

Разбор архитектурных решений в схеме

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

1. Борьба с падежами

Русский язык сложен для автоматической генерации шаблонов. В преамбуле ДС мы пишем:

“…в лице Генерального директора Иванова И.И., действующего на основании…” (Родительный падеж).

В блоке подписей:

“Генеральный директор Иванов И.И.” (Именительный падеж).

Если просить модель вернуть просто “ФИО подписанта”, нам придется склонять его отдельной библиотекой (типа pymorphy2), которая часто ошибается с фамилиями.

enes