Как я перестал верить нейросети и полюбил контрольные разряды: Zero-Trust для распознавания юридических документов. 1С.. 1С. llm.. 1С. llm. ocr.. 1С. llm. ocr. Open source.. 1С. llm. ocr. Open source. python.. 1С. llm. ocr. Open source. python. vlm.. 1С. llm. ocr. Open source. python. vlm. верификация.. 1С. llm. ocr. Open source. python. vlm. верификация. галлюцинации.. 1С. llm. ocr. Open source. python. vlm. верификация. галлюцинации. ИНН.. 1С. llm. ocr. Open source. python. vlm. верификация. галлюцинации. ИНН. искусственный интеллект.. 1С. llm. ocr. Open source. python. vlm. верификация. галлюцинации. ИНН. искусственный интеллект. ФССП.

Покажите 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) не проходит.

Стена четвёртая — честность статусов. Статусная таксономия — это язык, на котором система разговаривает с оператором:

Статус

Смысл

Что делать

zero_trust_verified

все применимые проверки прошли

выборочный контроль

partially_verified

проверок было мало

выборочный контроль

gate_not_executed

эталона для сверки не было

ручная проверка

discrepancy_detected

неверный разряд, расхождение, лимит

ручная проверка

heuristic_fallback

суммы восстановил regex, не модель

сверка сумм

ocr_low_confidence

скан < 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. Оператор проверяет исключения, а не всё. Автоимпорт в 1С — только для чистых документов; расхождения, эвристики и сбои маркируются requires_human_review и кодом возврата. Бухгалтер открывает только помеченное.

  2. Ошибки ловятся на входе, а не в банке. Неверный разряд ИНН, счёт с неправильным ключом, превышенный лимит удержания, «ИП возбуждено раньше решения суда» — это алгоритм, ему всё равно, как сегодня настроилась модель.

  3. Каждый старый провал закрыт регресс-тестом. Ошибка в 10 раз давала 98,32 % и метку «excellent» (сравнение сумм строкой); провальный документ уходил в реестры 1С как COMPLETED; прогон оставлял 56 файлов на 13 документов, из которых три четверти — байтовые дубликаты. Всё это — страницы чейнжлога с номерами фиксов, не истории у костра.

  4. Интеграция без 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

Источник