Система приняла счёт на 1 280 000 рублей как счёт на 128 000. OCR потерял ноль у сгиба, LLM аккуратно заполнила JSON, Pydantic подтвердил тип Decimal. Формат был правильный, деньги нет.
Вся цепочка отработала безупречно и синхронно: сканер увидел то, что увидел, модель прочла то, что было, валидатор проверил то, что умеет. Никто не соврал. Просто ни один участник не отвечал за вопрос «а бывает ли такой счёт у этого поставщика».
Документ проходит несколько независимых проверок
файл -> антивирус -> определение типа -> OCR/layout
-> извлечение по схеме -> нормализация
-> арифметические и справочные проверки -> человек при сомненииСохраняйте исходный файл, текст OCR, координаты фрагментов, версию модели и итоговый объект. Для каждого поля возвращайте доказательство, а не только уверенность модели.
class Evidence(BaseModel):
page: int
quote: str
bbox: tuple[float, float, float, float] | None = None
class Invoice(BaseModel):
number: str
date: date
supplier_tax_id: str
total: Decimal
currency: Literal["RUB", "USD", "EUR"]
evidence: dict[str, Evidence]
@model_validator(mode="after")
def plausible(self):
if self.total <= 0:
raise ValueError("total must be positive")
return selfПосле schema validation проверьте контрольные суммы: сумма строк плюс налог должна совпасть с итогом в пределах округления; ИНН сверяется со справочником; дата не может быть позже даты загрузки без явного допуска. Цитата должна действительно встречаться на указанной странице. Confidence полезен только после калибровки на ваших документах.
Что ломается
Скан повёрнут, таблица продолжается на следующей странице, две суммы стоят рядом, рукописная правка перекрывает печать. Prompt injection может находиться прямо в PDF. Новый шаблон поставщика проходит по типам, но меняет смысл колонок. Самая неприятная ошибка выглядит правдоподобно и не вызывает исключения.
Проверки предметной области дороже модели
Качество экстрактора определяется не столько промптом и моделью, сколько набором проверок после них.
Полезный минимум для счёта:
- Арифметика. Сумма строк плюс налог сходится с итогом в пределах округления. Ловит потерянный ноль лучше любой модели.
- Справочники. ИНН существует, поставщик известен, валюта разрешена договором.
- История. Сумма отличается от типичной для этого поставщика больше чем в N раз — повод для проверки человеком, а не для проводки.
- Дубликаты. Тот же номер счёта от того же поставщика уже загружался.
- Даты. Дата документа не из будущего и не из позапрошлого десятилетия.
Каждая проверка — десять строк кода и ноль токенов. Вместе они превращают «модель обычно права» в систему, где ошибка модели не становится ошибкой бухгалтерии.
Правило главы
LLM предлагает значение, а система принимает его только вместе с происхождением и проверками предметной области.
Чеклист главы
Практикум
Задача 49.1 — экстрактор с доказательствами. ⭑⭑⭑
Возьмите 50 документов минимум трёх шаблонов, разметьте поля и координаты, соберите извлечение с доказательством на каждое поле.
Критерии приёмки:
Подсказки:
- Разметку координат делайте хотя бы для полей с деньгами: именно они дают самые дорогие ошибки.
- Отдельно посмотрите на документы, где модель уверена и неправа. Это ваш список проверок предметной области.
Задача 49.2 — проверки, которые ловят потерянный ноль. ⭑⭑
Добавьте к экстрактору набор доменных проверок и измерьте, сколько ошибок они ловят.
Критерии приёмки:
Подсказки:
- Проверка «сумма строк сходится с итогом» ловит непропорционально много: начните с неё.
- История поставщика — самый дешёвый детектор аномалий. Даже грубое сравнение с медианой работает лучше, чем интуиция.