Система приняла счёт на 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. Новый шаблон поставщика проходит по типам, но меняет смысл колонок. Самая неприятная ошибка выглядит правдоподобно и не вызывает исключения.
Правило: LLM предлагает значение, а система принимает его только вместе с происхождением и проверками предметной области.
Практикум 49.1
Возьмите 50 документов минимум трёх шаблонов. Разметьте поля и координаты. Измерьте exact match по каждому полю и долю документов, прошедших без человека. Затем испортите десять файлов: поворот, размытие, пропавшая строка, конфликт итогов. Настройте очередь проверки так, чтобы опасные ошибки уходили человеку, а не превращались в чистый JSON.