Покажите VLM скан исполнительного листа — и она уверенно вернёт реквизиты. Формат будет идеальным, тон — безупречным. Одна беда: среди реквизитов с ненулевой вероятностью окажется несуществующий ИНН с правильным форматом, сумма, которую никто не сверил с документом, а рядом в сводке будет гореть «качество: 100 %» — на скане 745×1024 @ 96 DPI, с которого в принципе нельзя прочитать реквизиты.
Это не гипотеза и не страшилка из интернета — это логи реального проекта. Я строю конвейер, который читает сканы российских юридических документов (исполнительные листы, постановления ФССП, удержания из зарплаты, УПД, договоры) и готовит реестры для 1С. Он построен вокруг одного принципа: вывод модели никогда не принимается на веру. Нейросеть предлагает, алгоритм проверяет, человек смотрит только то, что не сошлось.
Под катом расскажу, как это вообще началось, как устроены четыре стены проверок — контрольные разряды, арифметика и закон, кросс-модальный гейт и честные статусы, — какие реальные баги они поймали, что показывают замеры ложных срабатываний и при чём тут пара десятков строк арифметики, которые заменяют доверие нейросети.
Как это вообще началось
Реализация проекта получилась очень спонтанной. Меня позвали поучаствовать в одной из встреч внутренней команды, чтобы подсказать варианты решения проблемы: там уже работал другой инструмент распознавания, и в его данных ловились расхождения и постоянные ошибки.
Я решил подготовиться и накидать первый набросок. За два дня он получился — проверял на всём, что было доступно в интернете: исполнительные листы, удержания из зарплаты. Цель изначально была именно такой, узкой.
На встрече показали мой вариант и вариант коллег из смежной команды. Мой подошёл несколько больше — он не требовал отправки данных наружу, работал внутри закрытого контура из коробки и был заточен под конкретные типы документов, которые как раз и были запрошены.
А дальше включилась педантичность и желание довести всё до Отличного — насколько это вообще возможно в рамках одного человека. Ну и полетели версии: 1-2-5-10… Какая конкретно сейчас, честно, уже и не скажу — на GitHub я сразу не выкладывал, разрабатывал и тестил локально.
Отдельная история — существенное ограничение: только LLM-модели внутри периметра. Всё, что попадает в этот контур, строго конфиденциально, передача даже другим сотрудникам недопустима — про наружу и речи нет. Тут пришлось поплясать.
Но, если честно, я был удивлён уровнем качества моделей в этом контуре: да, не быстро — скорость генерации и обработки не такая, к какой мы привыкли на подписках, — но добиться хорошего результата определённо можно. И я считаю, нужно: мы снимаем с себя любые ограничения и получаем отличный инструмент без страха выноса информации «наружу».
Побочный эффект такой архитектуры — она вообще не привязана к конкретной модели (любой OpenAI-совместимый шлюз), так что проект легко проверить дома на Ollama с Qwen2.5-VL, что я и делал при разработке.
Проект сейчас применяется в production и тестируется бравыми ребятами, которые не побоялись довериться энтузиасту)
Дальше — техническое мясо. Кому интересна только суть: скан на входе, проверенные реквизиты и реестры для 1С на выходе, нейросети не доверяем вообще.
Что получилось по факту
Конвейер «скан → извлечённые и проверенные реквизиты → реестры 1С/Excel»:
-
9 типов документов из коробки: постановления ФССП, исполнительные документы (листы, судебные приказы), удержания из зарплаты, УПД/накладные, договоры, кадровые приказы, доверенности, претензии/иски, акты выполненных работ. Всё это — изолированные плагины, новое добавляется папкой, ядро про типы ничего не знает;
-
любая модель через OpenAI-совместимый API: корпоративные, локальные Ollama/vLLM, при желании облако;
-
детерминированная верификация: контрольные разряды, арифметика сумм, статутные лимиты, хронология, кросс-модальная сверка с исходным текстом (об этом ниже, это главное);
-
вывод: два JSON на документ (
_Full— канон с аудитом,Flat— плоская запись для 1С), общий реестрRegistryFull.jsonи Excel-книга; -
CLI с осмысленными кодами возврата, JSON-RPC-сервер для интеграций.
Стек скромный: Python 3.9+, Pydantic v2, Pillow/PyMuPDF, openpyxl, опционально rapidocr.
Как устроен конвейер
Пакетная обработка — шесть этапов подряд:
скан → [1] окружение → [2] поиск файлов → [3] классификация
→ [4] экстракция (VLM + кэш + чекпоинты) → [5] метрики
→ [6] реестр + Excel
Классификация — каскадом: сначала эвристики по имени файла и якорям (бесплатно), потом VLM-роутер (дорого). Замер на «грязных» бухгалтерских именах вида Скан0001.pdf и IMG_20240115_143022.jpg: первая ступень закрывает 53,7 %, один мисраут, остальное уходит в модель.
Предобработка сканов закрывает то, что видно на реальных документах: разрешение ниже 1500 px апскейлится 2× Lanczos, многостраничный PDF режется лимитом в 20 страниц. Причём замер честно показывает: в 45-страничном документе реквизиты с 31-й страницы в лимит не попадают — и это видно в отчёте, а не теряется молча.
Экстракция идёт по плагину: промпт, Pydantic-схема, постобработка чинит типовые болезни VLM (например, восстановление формата номера ИП NNNNN/NN/NNNNN-ИП, когда модель потеряла слэши). Ответ модели всегда проходит безопасный JSON-парсинг — с вычищением «рассуждений», которые любят выдавать Qwen и DeepSeek.
А вот дальше начинается то, ради чего проект вообще существует.
Про доверие: четыре стены
Стена первая — контрольные разряды. ИНН (10 и 12 знаков), СНИЛС, ОГРН/ОГРНИП, БИК и 20-значный счёт (ключ ЦБ РФ по 565-П) проверяются алгоритмом. «Красивое», но выдуманное значение отбивается до того, как попадёт в реестр. Вот ядро проверки — 10-значный ИНН юрлица, как есть, из src/scan_reader/verifier/checksums.py (12-значные ИНН физлиц и ИП считают контрольный разряд дважды, с другими весами — полный код в репозитории):
if len(nums) == 10:
# Legal entity: 10th digit is check digit
weights = [2, 4, 10, 3, 5, 9, 4, 6, 8]
control_sum = sum(w * n for w, n in zip(weights, nums[:9])) % 11 % 10
if control_sum != nums[9]:
return False, f"Invalid 10-digit INN checksum: expected {control_sum}, got {nums[9]}"
return True, "Valid 10-digit INN (Legal Entity)"
# ... 12-значный ИНН физлица/ИП: контрольный разряд считается дважды,
# с другими весами — полный код в репозитории
Несколько строк умножения с остатком — и несуществующий ИНН не пройдёт, как бы уверенно модель его ни «прочитала». Аналогично СНИЛС: там, кстати, есть историческая льгота ПФР — номера до 1998 года не имеют корректной контрольной суммы, и проверка обязана это знать, иначе она строже стандарта. Сверка знает и другие исключения: счета ГРКЦ Банка России и казначейские БИК УФК не должны фейлиться стандартной проверкой ключа — иначе половина постановлений ФССП ложится ложными ошибками.
Стена вторая — арифметика и закон. До этой стены суммы 157 611,62 + 7 004,39 + 60 000,00 и подставные 999 999 проходили через логику одинаково — деньги никто не сверял. Теперь сверяются все, с допуском (включая легитимные нули — да, x or y молча съедает законный 0.0, я это тоже поймал), плюс хронология: дата акта ≤ дата исполнительного документа ≤ возбуждение ИП.
Лимиты удержания — отдельная юридическая часть: 50 % и 70 % по ст. 99 229-ФЗ, 20 % по ст. 138 ТК РФ, дробные доли (1/4, 1/3, 1/2, 2/3). И исключение по 314-ФЗ: к возмещению вреда здоровью процентный потолок не применяется вообще — юридическая ловушка, которую я когда-то сам же и огрёб в собственном докстринге.
Стена третья — кросс-модальный гейт (анти-галлюцинации). «Кросс-модальная» — потому что сверяются два разных представления одного документа: структурированные поля от модели против его же текста. Каждый критичный реквизит должен найтись в эталонном тексте документа: текстовый слой, независимый OCR или (с явной пометкой «не независимая проверка») транскрипция той же модели. Тут всплывает реальный русский язык, и гейт про него знает:
-
документ склоняет фамилии, а модель отдаёт именительный падеж: «Иванов» подтверждается по «Взыскать с должника Иванова Ивана Ивановича». При этом короткий «Иван» не подтверждается внутри «Иванов», а «Иванов» — не внутри «Ивановский», потому что это другой человек;
-
модель возвращает сумму JSON-числом
5075.0, а документ печатает «5 075,00» — суммы сверяются числом. Десятикратная ошибка (507500 против 5 075,00) не проходит.
Стена четвёртая — честность статусов. Статусная таксономия — это язык, на котором система разговаривает с оператором:
|
Статус |
Смысл |
Что делать |
|---|---|---|
|
|
все применимые проверки прошли |
выборочный контроль |
|
|
проверок было мало |
выборочный контроль |
|
|
эталона для сверки не было |
ручная проверка |
|
|
неверный разряд, расхождение, лимит |
ручная проверка |
|
|
суммы восстановил regex, не модель |
сверка сумм |
|
|
скан < 150 DPI |
пересканировать |
Подчеркну: zero_trust_verified означает «применимые проверки прошли», а не «правильно на 100 %». Формулировки «100 % подтверждено» в доке проекта запрещены — и это проверяется тестами, потому что однажды я уже видел, как Excel показывает «отлично» рядом с расхождением. CLI дублирует статус кодами возврата: 0 — чисто, 3 — расхождение или невыполнившийся гейт (автоимпорт недопустим), 4 — эвристика, 1 — сбой. Провальная экстракция возвращает 1, а не «успех с пустыми данными» — тоже исправление реального бага.
Про цифры
Сразу оговорки — что эти цифры утверждают и чего не утверждают. Утверждают: на детерминированном стенде (фиксированный seed, воспроизводится одной командой) при конкретном наборе эталонов доля ложных срабатываний выглядит вот так. Не утверждают: что это сравнение с другими системами — публичного бенчмарка по извлечению российских юридических документов я найти не смог, все проценты ниже это внутренние измерения. Эталонов 23, у 6 из 9 типов ровно по одному документу, так что цифры честные, но компактные. Шум вносится только в эталонный текст, извлечение всегда чистое — это сценарий «OCR ошибся, а модель была права».
Ложные срабатывания гейта — главная операционная метрика, потому что это и есть «сколько честных документов я пошлю человеку зря»:
|
Доля ошибок распознавания в эталоне |
Ложных срабатываний |
|---|---|
|
0 % (чистый скан) |
0,0 % |
|
1 % |
21,5 % |
|
2 % |
38,5 % |
|
5 % |
73,5 % |
|
10 % |
89,1 % |
Как читать: на чистом тексте гейт не шумит вообще. Кривая дальше — цена доверия: чем хуже скан, тем больше документов уходит человеку. 460 проверок на уровень, 20 повторов.
Качество кода — тоже метрика, за которую мне не стыдно: 667 автотестов (в версии 0.8.0 было 75 — рост в ~9 раз), покрытие 73,5 % при пороге 70 %, ruff и mypy чистые, запись артефактов атомарная, секреты маскируются в логах (включая случай logger.info("токен %s", secret) — этот секрет классически утекает через record.args, а не через сообщение).
Что это даёт на практике
-
Оператор проверяет исключения, а не всё. Автоимпорт в 1С — только для чистых документов; расхождения, эвристики и сбои маркируются
requires_human_reviewи кодом возврата. Бухгалтер открывает только помеченное. -
Ошибки ловятся на входе, а не в банке. Неверный разряд ИНН, счёт с неправильным ключом, превышенный лимит удержания, «ИП возбуждено раньше решения суда» — это алгоритм, ему всё равно, как сегодня настроилась модель.
-
Каждый старый провал закрыт регресс-тестом. Ошибка в 10 раз давала 98,32 % и метку «excellent» (сравнение сумм строкой); провальный документ уходил в реестры 1С как
COMPLETED; прогон оставлял 56 файлов на 13 документов, из которых три четверти — байтовые дубликаты. Всё это — страницы чейнжлога с номерами фиксов, не истории у костра. -
Интеграция без GUI. Тихий CLI (stdout — путь к 1С-записи, служебное в stderr, коды возврата) уже совместим со сценарием 1С через
WScript.Shell; JSON-RPC-сервер — то же для сервисов.
Чего я не обещаю: «замены бухгалтера», «100 % точности», «работает на любом скане». Ожидание реалистичное: машина извлекает и проверяет, человек разбирает исключения.
Как обстоят дела с open source-аналогами
Прямого аналога — OSS, которая извлекает реквизиты российских юридических документов с верификацией — я не нашёл. Соседи по задачам такие:
|
Категория |
Проекты |
Что дают |
Чего нет |
|---|---|---|---|
|
OCR-движки |
Tesseract, PaddleOCR, EasyOCR/RapidOCR, docTR, Surya |
текст, и всё |
извлечение полей, семантика, верификация |
|
Разбор документов |
unstructured, marker, Apache Tika |
текст/блоки/таблицы |
юридическая модель данных |
|
Шаблонные экстракторы |
invoice2data |
поля по yaml-шаблонам |
падают на варьирующихся макетах, аудита нет |
|
Открытые VLM |
Qwen2.5-VL, InternVL |
сами модели (мы их и используем) |
доверять нельзя — см. начало |
|
SaaS |
Azure Document Intelligence, Google Document AI |
готовые пайплайны |
данные наружу, нет 229-ФЗ/ИНН/1С, нет статусов достоверности |
Так что вклад проекта — не «ещё один OCR», а слой верификации над VLM + плагины без зашитых типов в ядре + дисциплина честных измерений. Внутри при этом те же открытые компоненты.
Проект
Проект открытый, лежит тут: github.com/Riper21/ScanReader — Python, MIT. Если захотите покопаться: 667 тестов и пороги покрытия в CI, отчёты измерений в docs/measurements/, честный чейнжлог на двух языках, из которого видно, как проект рос через собственные баги.
Обратная связь, находки и обезличенные сканы-провокации приветствуются — они у меня превращаются в регресс-тесты.
P.S. Коллеги из той команды, к сожалению, не могут поделиться своими документами — NDA. Так что если интересно — прогоняйте на своих сканах
Автор: AntonUdyurminsky


