Почему валидный JSON может испортить учёт
Когда локальную языковую модель подключают к счетам, заявкам или актам, первая техническая победа выглядит убедительно: вместо свободного текста модель возвращает аккуратный JSON. Поля на месте, типы совпадают, объект разбирается программой. Команда решает, что теперь результат можно писать прямо в CRM или ERP.
Именно здесь начинается опасная часть. Ограниченное декодирование может не дать модели вывести лишнюю запятую или неизвестное поле, но не мешает ей положить в разрешённое поле неверный ИНН, перепутать дату поставки с датой счёта или получить правильный итог из неправильных строк. Формат становится надёжнее, смысл — не обязательно.
Правильная архитектура разделяет четыре разных свойства:
- синтаксическая валидность: ответ действительно является JSON;
- соответствие схеме: есть обязательные поля, типы и допустимые значения;
- фактическая верность: каждое значение подтверждается исходным документом;
- допустимость действия: запись не нарушает бизнес-правила и может быть проведена.
Только последнее свойство разрешает изменение рабочей системы. Первые три — проверки на пути к нему.
Что дают vLLM и llama.cpp
Современные локальные серверы инференса умеют ограничивать пространство следующего токена. В vLLM для структурированного вывода доступны JSON Schema, выбор из фиксированных значений, регулярные выражения и грамматики. llama.cpp поддерживает GBNF и преобразование части JSON Schema в грамматику. Это полезнее просьбы в промпте «ответь строго JSON»: невозможные по грамматике символы отсекаются во время генерации, а не исправляются после неё.
Но поддержка стандарта не одинакова. Документация llama.cpp прямо говорит о подмножестве JSON Schema и перечисляет ограничения: некоторые комбинации `anyOf` или `oneOf`, вложенные ссылки, строковые форматы и другие возможности могут не поддерживаться. Более того, отдельные неподдерживаемые элементы могут быть пропущены без ошибки. Поэтому схема, которая работает в одном сервере, не должна считаться переносимой без теста в другом.
Сам JSON Schema тоже имеет версии, или диалекты. Официальная документация рекомендует указывать `$schema` в корне, чтобы валидатор и читатель понимали выбранную семантику. Неизвестное пользовательское ключевое слово может быть проигнорировано реализацией: документ пройдёт проверку, хотя автор схемы ожидал дополнительное ограничение.
Вывод для проекта прост: схема должна быть версионирована вместе с моделью и сервером инференса, а её фактическая поддержка — проверена набором положительных и отрицательных примеров.
Семь ступеней между документом и ERP
Надёжный контур лучше строить как конвейер, в котором модель не имеет прямых прав на рабочую систему.
1. **Приём документа.** Файл получает неизменяемый идентификатор, контрольную сумму, время поступления и источник. Оригинал сохраняется отдельно от результатов распознавания.
2. **Извлечение текста.** OCR или парсер возвращает текст с координатами страницы. Низкое качество, неизвестный формат и защищённый файл уходят в карантин.
3. **Структурированный вывод.** Локальная LLM получает короткую схему, ясные определения полей и извлечённый текст. Сервер ограничивает ответ выбранной грамматикой.
4. **Независимая валидация.** Обычная библиотека повторно проверяет JSON по закреплённой версии схемы. Проверка внутри сервера инференса не заменяет эту ступень.
5. **Проверка смысла.** Детерминированный код пересчитывает суммы, НДС, даты, справочники, дубликаты и допустимые сочетания полей.
6. **Маршрутизация решения.** Безопасные и полностью подтверждённые записи попадают в очередь автопринятия; спорные — человеку с подсветкой источника.
7. **Идемпотентная запись.** Отдельный исполнитель пишет в CRM или ERP по стабильному ключу и сохраняет результат. Повтор не создаёт вторую операцию.
Так модель остаётся преобразователем, а не обладателем бухгалтерских прав. Даже успешная генерация не может самостоятельно вызвать платёж, провести документ или изменить карточку клиента.
Как проектировать схему, которую можно проверить
Первая версия схемы должна быть короткой. Один огромный объект на все документы увеличивает число необязательных ветвей, усложняет промпт и затрудняет поиск ошибки. Практичнее сначала классифицировать тип документа фиксированным `enum`, затем применять отдельную схему для счёта, акта или заказа.
Для каждого поля полезно определить:
- тип и допустимость `null`;
- формат нормализованного значения;
- обязательность для конкретного типа документа;
- ссылку на страницу и фрагмент-основание;
- причину отсутствия, если значение не найдено;
- версию правила нормализации.
Не просите модель выдумывать «уверенность 97%». Такое число без калибровки мало помогает решению. Лучше требовать доказательство: номер страницы, координаты или короткий фрагмент исходного текста. Затем программа проверяет, что фрагмент действительно существует в зафиксированной версии документа.
Поле для отказа тоже является частью контракта. Модель должна иметь возможность вернуть `needs_review` и код причины: размытый скан, несколько кандидатов, несовпадение итогов, неизвестный контрагент. Если схема разрешает только «успех», ограниченное декодирование заставит выбрать синтаксически допустимый ответ даже при недостатке данных.
Проверки, которые нельзя отдавать модели
Бизнес-правила лучше писать обычным кодом. Для счёта это могут быть:
- сумма строк плюс налоги должна сходиться с итогом в пределах заданного допуска;
- валюта должна принадлежать разрешённому справочнику;
- дата не может выходить за допустимый период;
- поставщик должен однозначно находиться по ИНН или другому стабильному идентификатору;
- банковские реквизиты, появившиеся впервые, требуют отдельного подтверждения;
- номер документа вместе с поставщиком и датой не должен дублировать ранее принятую запись;
- итог выше установленного порога всегда направляется человеку.
Эти правила воспроизводимы, тестируемы и объяснимы аудитору. LLM может подсказать кандидата, но не должна пересчитывать то, что точно считает десять строк кода.
Особенно важно не смешивать нормализацию и источник. Храните исходное значение рядом с нормализованным: «1 234,50» и `1234.50`, исходную дату и ISO-дату, написание контрагента и найденный идентификатор справочника. Тогда исправление правил не требует заново угадывать, что было в документе.
Почему нужна вторая проверка схемы
Исследование JSONSchemaBench собрало 10 тысяч реальных схем и оценивает движки ограниченного декодирования по трём осям: скорости, покрытию возможностей стандарта и качеству результата. Это полезное напоминание: фраза «поддерживает JSON Schema» не описывает ни полноту поддержки, ни накладные расходы, ни качество заполнения.
Вторая, независимая проверка после генерации решает сразу несколько задач. Она фиксирует единый диалект схемы для всех моделей, ловит разницу реализаций, не пропускает неизвестные ключевые слова и позволяет вернуть точный код ошибки. Если сервер инференса обновился, этот слой остаётся стабильным контрактом приложения.
Перед обновлением vLLM, llama.cpp, backend ограниченного декодирования или модели прогоняйте один и тот же корпус. В нём должны быть не только обычные документы, но и пустые поля, длинные массивы строк, смешанные языки, запятые в числах, одинаковые названия поставщиков, плохие сканы и намеренно конфликтующие итоги.
Идемпотентность важнее красивого демо
Документ может попасть в обработку повторно из-за ретрая очереди, восстановления после сбоя или повторной загрузки пользователем. Поэтому исполнитель строит ключ не из ответа модели, а из стабильных входов: идентификатора компании, хеша исходного файла, типа операции и версии назначения.
Перед записью он проверяет журнал операций. Если ключ уже завершён, возвращается прежний результат. Если предыдущая попытка остановилась после обращения к ERP, но до ответа приложению, выполняется сверка по внешнему идентификатору, а не слепой повтор.
Какие метрики показывать руководителю
Доля валидного JSON — техническая метрика, а не бизнес-результат. Для пилота нужны как минимум четыре независимые группы:
- **форма:** процент ответов, прошедших схему без повторной генерации;
- **смысл:** точность каждого поля на размеченной выборке и доля документов с полностью верным набором полей;
- **решение:** ложное автопринятие, лишняя ручная проверка и пропущенный дубликат;
- **операция:** стоимость одного принятого документа, p95 времени обработки, число повторов и расхождений с ERP.
Самая опасная метрика — доля ошибочно принятых документов. Её нельзя прятать внутри средней точности полей: один неверный банковский счёт важнее десяти правильно извлечённых необязательных комментариев.
Экономику считайте на принятом результате. Модельная стоимость документа равна сумме амортизации сервера, энергии, эксплуатации, OCR, инференса и ручной проверки, делённой на число документов, которые прошли все проверки и были приняты без последующего исправления. Повторные генерации, карантин и разбор инцидентов входят в числитель.
Пилот на две недели
Начать можно без доступа на запись. Возьмите 200–500 исторических документов одного типа, удалите из выборки точные дубли и разметьте критические поля двумя сотрудниками. Зафиксируйте схему, модель, промпт, сервер инференса и правила нормализации.
Первую неделю запускайте конвейер в теневом режиме и сравнивайте с уже проведёнными документами. На второй неделе дайте оператору интерфейс подтверждения, но оставьте ERP только для чтения. Автоматическую запись рассматривайте лишь после того, как отдельный тестовый набор покажет приемлемую долю ошибочного автопринятия и команда проверит восстановление после повторов.
Успешный пилот — не тот, где локальная модель всегда выдаёт фигурные скобки в правильных местах. Успех — когда неверный смысл останавливается до рабочей системы, оператор видит источник каждого значения, а повторный запуск не создаёт вторую запись. JSON Schema делает выход предсказуемым. Доверие создаёт весь остальной конвейер.
