Задача звучала просто. Поставщик присылает накладную — фотографией с телефона, PDF-сканом или Excel-файлом, как ему удобнее. Надо получить из этого документ «Поступление товаров и услуг» в 1С: с правильным контрагентом, правильными суммами и всеми позициями в табличной части. Без ручного набора.
Формулировка «просто скормить фото нейросети» держится ровно до первой реальной накладной. Дальше начинается инженерия, и в ней я успел дважды поменять решение — сначала сменил модель целиком, потом отказался от идеи выбрать одну модель вообще.
Сразу отвечу на вопрос, который задал бы сам
«Зачем это вообще, когда есть 1С:РПД?»
Вопрос справедливый, и отмахиваться от него нечестно. 1С:Распознавание первичных документов стоит порядка 600 рублей в год за сотню страниц (актуальные тарифы стоит проверять отдельно, они меняются), поддерживается вендором, умеет накладные, счета-фактуры, УПД, акты, сопоставляет контрагентов и номенклатуру с базой. Есть и другие решения — Entera, продукты на базе SberScan, готовые обработки на Инфостарте. Ниша не пустая, она освоенная.
Я не считаю, что сделал более дешёвую или более функциональную замену. Если задача — просто закрыть распознавание первички в типовой бухгалтерии, честный совет: возьмите готовое решение от вендора, это дешевле любой разработки и надёжнее одиночного проекта.
Меня интересовало другое, и вопрос был инженерный, а не продуктовый.
Все существующие решения строились в эпоху, когда распознавание документа означало специализированный конвейер: OCR, извлечение структуры, обученные под конкретные типы документов модели, правила разбора. Долго, дорого, требует данных для обучения. За последние пару лет появились универсальные мультимодальные модели, которым можно просто дать картинку и словами объяснить, что нужно достать. И у меня был вопрос, ответ на который я нигде не нашёл в готовом виде: насколько такая универсальная модель сегодня закрывает эту задачу без специализированного конвейера вокруг неё? Не в демо на идеальном скане, а на реальных накладных.
Это вопрос, на который нельзя ответить чтением. Только собрав и разбив об реальность.
Второе соображение — прикладное. Готовые сервисы работают как чёрный ящик с тарификацией по страницам. Мне хотелось конструкцию, где видно всё: какая модель вызвана, что она вернула, почему результат признан сомнительным. Не потому что чёрный ящик плох, а потому что открытую конструкцию можно допилить под свой поток документов, а тарифицируемый сервис — нет.
Так что ниже — не история продукта-конкурента. История о том, что выяснилось, когда я попробовал решить типичную задачу нетипичным для неё способом. Кое-что из этого пригодится любому, кто прикручивает LLM к рабочему процессу, независимо от накладных.
Почему первым был GigaChat
Выбор был не про сравнение возможностей, а про знакомство с инструментом. Я уже работал с GigaChat API в другом проекте и знал его особенности: OAuth-токен живёт 30 минут и его надо не забывать обновлять, обязателен заголовок RqUID с уникальным идентификатором на каждый запрос, авторизация и сам чат живут на двух разных серверах. Всё это уже было отлажено и лежало готовым.
Первые тесты на синтетических накладных обнадёжили. Аккуратный документ — шапка, четыре товарные позиции, итоговая сумма — GigaChat-2-Pro распознавал полностью верно с одного вызова. Все поля, все цифры. Я мысленно поставил галочку «модель подходит» и пошёл собирать вокруг неё остальной конвейер.
Галочка оказалась преждевременной.
Что пошло не так на реальных документах
Реальные накладные отличаются от синтетических примерно всем: качеством печати, вёрсткой, расположением блоков, наличием штампов и подписей поверх текста. И на них вылезли две ошибки, которые я не могу назвать иначе как системными.
Первая — модель путала местами продавца и покупателя. В накладной оба реквизита выглядят одинаково: название организации, ИНН, КПП, адрес. Отличает их только подпись поля и положение на листе. Когда вёрстка нестандартная, модель уверенно брала не тот блок.
Вторая — ИНН. То задвоенный (цифры повторялись, и число выходило длиннее положенного), то просто невалидный — не 10 и не 12 знаков.
Отдельно неприятно тут не то, что ошибки были, а то, как они выглядят. Неверный ИНН — ошибка честная, она сама себя выдаёт длиной. А вот перепутанные продавец и покупатель — ошибка правдоподобная. Документ выглядит нормально: организации настоящие, ИНН валидные, суммы сходятся. Просто стороны стоят наоборот. Если такое проскочит мимо глаз и уйдёт в проводки, разбираться будут сильно позже и сильно дороже.
Три слоя, которыми я пытался это закрыть
Логичный первый ход — не менять модель, а обстроить её проверками. Я построил три слоя, каждый против конкретной слабости.
Двойной вызов на самопроверку (self-consistency). Шапка документа распознаётся дважды, независимыми запросами. Совпали ответы — доверяем. Разошлись — модель сама себе противоречит, документ идёт на ручную проверку. Стоит это ровно вдвое дороже по токенам на каждую шапку.
OCR-подсказка в промпте. Раз модель читает изображение неуверенно, логично дать ей второй источник того же текста. Перед отправкой фото прогоняется через классический OCR, и распознанный текст добавляется в промпт: вот что обычное распознавание увидело на этой картинке, учитывай. Специализированный OCR читает символы точнее, но не понимает, что из прочитанного чем является; модель понимает смысл, но хуже читает. Идея была сложить сильные стороны обоих.
Якорная проверка по координатам. Самый тяжёлый слой. Известно, где на листе физически стоит блок «Продавец», и известно, какие координаты у распознанного текста. Значит, можно проверить: то, что модель назвала ИНН продавца, действительно лежит рядом с подписью «Продавец», а не в середине товарной таблицы? Если координаты не сходятся — модель прочитала не то, что думает.
Все три слоя работали. Ошибок стало заметно меньше. Но в какой-то момент я посмотрел на это со стороны и увидел неприятную картину: половина конвейера существует не для того, чтобы решать задачу, а чтобы компенсировать слабости конкретной модели. Это не архитектура, это подпорки.
Забавно, что двигался я ровно туда, откуда хотел уйти: строил специализированный конвейер вокруг распознавания — то самое, чем занимаются классические решения, только хуже и в одиночку.
Тест, который перевернул решение
Проверка была прямая: один и тот же документ, один и тот же промпт, два движка — GigaChat и Claude. Никакой обвязки, чистый вызов против чистого вызова.
Один вызов Claude вернул верные роли сторон и корректные ИНН. Без двойного запроса, без OCR-подсказки, без якорной проверки — то, ради чего было построено три слоя, решалось моделью само.
Оговорюсь про методику, чтобы не выдавать это за большее, чем оно есть. Тестовая база была устроена так: масса синтетических документов, чтобы убедиться, что механика вообще работает, и два реальных — оба отвратительного качества, на 6 и на 21 позицию. Реальных документов мало сознательно: на чистом скане с ровной вёрсткой справляются все модели, разница между ними видна только там, где документ плохой. Смысла набирать десяток хороших сканов не было — они не различают модели.
Чего у меня нет и чего я утверждать не буду: числовой статистики вида «модель А точнее модели Б на столько-то процентов». Для этого нужен прогон по десяткам документов с сохранённым протоколом расхождений. Есть однозначный результат на худших случаях и подтвердившая его последующая эксплуатация. Скрипты сравнения остались в проекте, так что при желании вывод перепроверяется.
После этого теста весь путь фото и PDF переехал на Claude. Двойной вызов на самопроверку, OCR-подсказка в промпте и пост-хок сверка с внешним OCR ушли из основного маршрута — код остался в проекте, но вызывается только из тестовых скриптов сравнения. А вот якорная проверка выжила, причём в новом качестве: вместо того чтобы контролировать ответ модели, она теперь работает до неё и в удачном случае избавляет от AI-вызова для шапки целиком. Об этом ниже.
Вот, собственно, и ответ на исходный вопрос — тот самый, ради которого всё затевалось: да, на сегодня универсальная модель действительно способна заменить специализированный конвейер на этой задаче. Но разброс между моделями по качеству зрения оказался больше, чем я ожидал — и это не про «умеет / не умеет», а про то, насколько уверенно модель читает документ, когда он далёк от идеального. Из чего следует вторая половина истории.
Оговорка, без которой рассказ был бы нечестным
Из вышесказанного легко сделать вывод, что GigaChat вообще не годится для работы с изображениями. Формулировка неточная: он видит, но видит плохо — распознаёт неуверенно и нестабильно, особенно там, где смысл поля определяется его положением на листе.
Мои же синтетические тесты это подтверждают с другой стороны: на простых документах с обычной вёрсткой он отрабатывал идеально, с одного вызова. Пока картинка чистая и структура предсказуемая, зрения хватает. Как только вёрстка уезжает, появляются штампы поверх текста, а блоки продавца и покупателя стоят не там, где обычно — качество распознавания начинает сыпаться.
Отдельный урок из этого: тест на аккуратном синтетическом документе почти ничего не говорит о поведении модели в продакшене. Он показывает, что задача в принципе решаема, и только.
Почему Excel остался на GigaChat
Может показаться, что после такого перехода логично перевести на Claude вообще всё. Но Excel-накладные так и остались на GigaChat, и это не непоследовательность, а разные задачи.
Фото и PDF — это компьютерное зрение: модель должна увидеть изображение и понять, где что расположено. Excel — уже структурированные данные, зрение не нужно вообще; нужно разобрать содержимое ячеек и понять, какая колонка чему соответствует. Другая работа, другой промпт, и слабое место, найденное в vision, сюда не переносится.
Грабли на Excel-пути свои. Модель нестабильна по формату ответа — для одной и той же структуры может вернуть то словарь, то список, и код должен быть готов к обоим вариантам. Плюс путается в числовых индексах колонок, когда листов много. Лечится это не сменой модели, а фолбэками на стороне обработки: проверка типа пришедшей структуры перед разбором, явная нормализация индексов.
Побочный, но приятный эффект: обработка Excel-накладной обходится кратно дешевле обработки фото. Если поставщики шлют преимущественно Excel — это прямая экономия, а не просто техническая деталь.
Второй раунд: Haiku или Sonnet
Внутри линейки Claude встал новый вопрос. Haiku дешевле и быстрее, Sonnet дороже и сильнее. Соблазн взять дешёвую моделью по умолчанию был очевидный.
Проверил на реальном документе — плохой скан, 21 позиция. Haiku ошиблась крупно: неверно определила продавца, покупателя оставила вообще без имени, итоговые суммы обнулила, а из 21 товарной позиции распознала только 12. Девять позиций просто исчезли.
На простом синтетическом документе та же Haiku отработала безупречно — точное совпадение по всем полям, и ответ короче, чем у Sonnet, то есть ещё и дешевле по токенам.
Ровно та же картина, что была с GigaChat раньше, и это уже похоже на закономерность, а не совпадение: на чистом документе разницы между моделями почти нет, на грязном она решающая. Слабая модель на хорошем входе выглядит как сильная. Что делает тестирование на аккуратных примерах почти бесполезным для выбора.
То есть Haiku — не слабая модель. Она отлично справляется там, где документ читаемый и вёрстка обычная, и экономит на этом реальные деньги. Проблема только в том, что заранее неизвестно, какая накладная придёт следующей.
Решение: не выбирать модель, а маршрутизировать по качеству входа
Ключевая мысль оказалась не в том, чтобы попробовать дешёвую модель и переделать при неудаче, а в том, чтобы оценить документ до первого обращения к AI.
Оценку делает OCR — один прогон на страницу, результат используется дважды. Считается доля «слабых» блоков: тех, где уверенность распознавания ниже 0,7. Метрика грубая, но она отвечает ровно на нужный вопрос — насколько вообще читаем этот документ.
Дальше три ветки.
Если слабых блоков больше 30% — пайплайн отказывает сразу, ни одна модель не вызывается. Смысл простой: на таком фото ошибётся любая модель, а платить за распознавание мусора и потом объяснять пользователю, почему в документе ерунда, — худший вариант из возможных. Лучше честно сказать «переснимите» до того, как потрачены деньги и время.
Если слабых блоков от 10 до 30% — сразу Sonnet. Haiku в этой ветке не пробуется вообще. Это ровно тот случай, где она ломается, и тратить вызов на заведомо провальную попытку смысла нет.
Если слабых блоков меньше 10% — идёт Haiku. И вот только здесь появляется эскалация: результат проверяется на признаки сбоя — расхождение суммы позиций с итогом, противоречие в итогах, неподтверждённый ИНН, пустые обязательные поля — и при срабатывании делается один повторный вызов на Sonnet. Один, без рекурсии.
Отдельно про то, что оказалось приятным сюрпризом. Если якорное извлечение уверенно взяло все обязательные поля шапки — продавца, покупателя, оба ИНН, договор — шапка в AI не отправляется вообще. Модель работает только с товарными позициями. Тот самый якорный слой, который строился как проверка поверх ответа модели, в итоге стал первичным путём для шапки, до модели. Из подпорки превратился в несущую конструкцию.
Честные границы этой схемы
Первое: пороги 10% и 30% откалиброваны на двух документах. Двух. В коде они помечены как временные, и я не буду делать вид, что это выверенные эмпирические константы — это разумные первые приближения, которые почти наверняка сдвинутся, когда наберётся статистика.
Второе: детерминированные проверки почти никогда не блокируют результат. Они превращаются в предупреждения и понижают уверенность по полям, но документ всё равно доезжает до пользователя. Это осознанный выбор — правдоподобный, но тихо неполный результат опаснее явного предупреждения.
Третье, и самое важное для честности: эскалация Haiku → Sonnet работает не во всех ветках. В якорной ветке, где шапку взял OCR, модель для позиций выбирается заранее, и повторного вызова при провале проверок не происходит — будет только предупреждение. Так что фраза «если что-то не сошлось, подключится модель посильнее» верна не всегда, и мне это ещё предстоит доделать.
И четвёртое, общее: проверки ловят арифметику и контрольные суммы — то, что математически видно. Если модель правильно сложила суммы, но переврала название товара, ни одна проверка этого не заметит. Поэтому документ создаётся черновиком и не проводится сам: глаза бухгалтера остаются последним контуром, и убирать их из схемы я не собираюсь.
Про проверку ИНН — точная формулировка
Проверка устроена в два уровня, и первый вообще не требует интернета: контрольная цифра считается по стандартному алгоритму самого ИНН прямо на месте. Это мгновенно отсекает опечатки и те самые задвоенные номера ещё до любого внешнего запроса.
Второй уровень — сверка с реальными данными через сервис dadata.ru, который агрегирует данные ФНС. Формулировать это стоит именно так: не прямой запрос в реестр налоговой, а обращение к стороннему сервису-агрегатору. Если организация найдена — название подставляется официальное, а не то, что прочитала модель. Если не найдена — уверенность по этому полю резко понижается и выводится предупреждение.
Отдельная мелочь, которая экономит вызовы: если ИНН продавца и покупателя совпали — во внешний сервис не идём вообще, сразу предупреждение. Совпадение означает ровно одну вещь: модель перепутала стороны, и проверять тут нечего.
Чего эта штука не делает — до того, как вы спросите
Работает только с «Поступлением товаров и услуг». Это не универсальный распознаватель любых документов, и делать вид, что универсальный, я не буду — вот здесь разрыв с решением от вендора по охвату типов документов принципиальный, и не в мою пользу.
Новые элементы справочников — контрагенты, номенклатура — создаются только по явному подтверждению, никогда молча. На пустых справочниках первый документ потребует нескольких подтверждений подряд. Это осознанно: молча плодить дубли контрагентов — худшее, что автоматика может сделать с чужой базой.
Документ создаётся черновиком. Счёт учёта, способ учёта НДС, склад заполняет человек — это учётные решения, а не задача распознавания.
И то, о чём стоит сказать прямым текстом: фото и файлы уходят на сторонний сервер для распознавания через внешний AI-сервис. В момент обработки через чужую инфраструктуру проходят ИНН контрагентов, названия организаций, суммы, номенклатура. Постоянно они не хранятся, но если у организации жёсткая политика по данным — это надо знать заранее, а не выяснять постфактум.
Что в итоге
Два раза за проект я менял решение, и оба раза не потому, что первое было глупым, а потому что появлялись данные, которых на момент выбора не было. Первый выбор сделан по знакомству с API — разумно на старте, неверно по сути. Второй — попытка выбрать «правильную модель» вместо того, чтобы признать, что правильной одной модели тут нет.
Главное, что я вынес: подпорки вокруг модели — это диагноз, а не архитектура. Но диагноз не означает «выбросить всё». Три слоя из четырёх ушли, а четвёртый — якорное извлечение — оказался не костылём, а самостоятельным механизмом, который в удачном случае вообще избавляет от обращения к модели. Разница в том, что раньше он страховал модель, а теперь работает вместо неё.
Второе по важности — про тестирование. Выбирать модель по аккуратным примерам бессмысленно: там все хороши. Разница вылезает на худших документах, и именно их надо брать в тест, даже если их всего два. С той же оговоркой: два документа хороши для того, чтобы увидеть разницу, и недостаточны для того, чтобы калибровать пороги. Я это знаю и держу в голове как долг.
А на исходный вопрос — «зачем, когда есть готовое» — у меня ответ такой. Затем, что готовое отвечает на вопрос «как ввести первичку», а мне был интересен вопрос «изменилось ли что-то в том, как такие задачи вообще решаются». Оказалось, изменилось: там, где раньше нужен был специализированный конвейер, теперь местами хватает одного грамотного вызова. Не везде и не всегда — но граница сдвинулась, и это стоило проверить руками.
Собрано в обработку SmartDocs AI, лежит на Инфостарте за 2 стартмани, сейчас в стадии сбора обратной связи: //infostart.ru/1c/tools/2747047/. Особенно интересны случаи, где распознавание ошибается — именно они определяют, куда двигать пороги маршрутизации дальше.
Вступайте в нашу телеграмм-группу Инфостарт