· 17 мин чтения

OCR + LLM-агент для 1С: как мы автоматизировали ввод счетов, накладных и договоров

Полгода назад мы решали для одного клиента конкретную задачу: бухгалтер тратит по 3-5 минут на каждый входящий счёт, накладную или договор, перебивая реквизиты в 1С руками. Первая идея — «отдать всё одной vision-модели» — не выдержала проверки на разнородности макетов: счета у каждого контрагента свои, ТОРГ-12 и УПД жёстко табличные, а договоры — это 20 страниц текста, а не картинка. В этой статье — как мы построили гибридный пайплайн OCR+LLM, где именно граница между «просто дать LLM картинку» и «сначала прогнать через OCR-движок» проходит, и как это заводится в 1С через OData и HTTP-сервисы.

Зачем гибрид, а не «одна vision-модель на всё»

Первый вариант, который приходит в голову — взять vision-модель уровня Claude Sonnet 5, скормить ей скан целиком и попросить вернуть JSON с реквизитами. Для одностраничного счёта это действительно работает и в большинстве случаев даёт корректный результат с первого раза. Но как только в работу попадают три реальных типа документов — счета, накладные ТОРГ-12/УПД и договоры — экономика и надёжность резко расходятся.

Три причины, почему мы не оставили только vision-LLM.

Поэтому мы строим не «AI, который читает документы», а маршрутизатор: для каждого документа система сама решает, использовать ли OCR-слой перед LLM, отправить ли сразу в vision-LLM, или для длинного договора вообще не сканировать, а вытащить текстовый слой напрямую из PDF.

Архитектура: четыре стадии пайплайна

Пайплайн живёт как отдельный сервис (Python), который забирает вложения из почтового ящика бухгалтерии или из папки обмена с 1С, и на выходе кладёт готовый документ в базу через OData/HTTP-сервис. Четыре стадии:

  1. Нормализация входа. PDF с текстовым слоем проверяется через pdftotext -layout (poppler-utils) — если текст извлекается и его объём разумный (не «мусорные» 3 символа на страницу от сканированного PDF без OCR-слоя), документ идёт в текстовый режим без всякого OCR. Если текстового слоя нет или PDF — это обёртка над сканом, страницы рендерятся в PNG через pdftoppm -r 300 -png — обязательно не ниже 300 dpi, потому что при 150 dpi точность Tesseract на мелком кегле счетов падает ощутимо.
  2. Классификация макета. Первая (уменьшенная, ~768px по длинной стороне) страница отправляется дешёвой модели — Claude Haiku 4.5 — с задачей определить тип документа (счёт / ТОРГ-12 / УПД / договор / прочее) и грубую оценку качества скана.
  3. Маршрутизация в слой распознавания. В зависимости от типа и качества документ уходит либо в OCR-слой (Tesseract 5 или Yandex Vision OCR) с последующим текстовым запросом к LLM, либо напрямую в vision-LLM с полным изображением страницы.
  4. Извлечение, валидация, запись в 1С. LLM возвращает строго типизированный JSON, значения сверяются со справочниками 1С (контрагенты, номенклатура) через OData, документ с высокой уверенностью проводится автоматически, документ с низкой — уходит в очередь ручной проверки бухгалтеру, и только после подтверждения записывается.

Важно: маршрутизация — не разовое решение «этот клиент = OCR, тот клиент = vision-LLM», а решение на уровне конкретного документа. У одного и того же контрагента типовой счёт может пройти вообще без OCR (LLM читает картинку целиком), а плохо отсканированная накладная от него же — через полный OCR-слой с ручной проверкой таблицы.

Считаете вручную счета и накладные? Посчитаем, сколько часов бухгалтерии это экономит

Мы разворачиваем такой пайплайн под конкретный набор макетов документов клиента — не универсальный шаблон, а настройку под то, как выглядят именно ваши счета, накладные и договоры, и под конкретную конфигурацию 1С. Опишите, сколько документов в месяц и какие типы преобладают — прикинем экономику и покажем, с чего начать.

Классификация макета: дешёвая модель как диспетчер

Классификатор — это не отдельная ML-модель, а запрос к Claude Haiku 4.5 со строгой JSON-схемой через tool use. На вход — уменьшенное изображение первой страницы (экономим токены на этапе, где нам нужен не текст, а общая структура), на выход — компактный объект, который дальше читает диспетчер маршрутизации:

{ "model": "claude-haiku-4-5-20251001", "max_tokens": 300, "tools": [{ "name": "classify_document", "input_schema": { "type": "object", "properties": { "doc_type": {"type": "string", "enum": ["invoice", "torg12", "upd", "contract", "other"]}, "page_count_estimate": {"type": "integer"}, "scan_quality": {"type": "string", "enum": ["digital_text", "clean_scan", "poor_scan", "handwritten_marks"]}, "has_dense_table": {"type": "boolean"} }, "required": ["doc_type", "scan_quality", "has_dense_table"] } }], "tool_choice": {"type": "tool", "name": "classify_document"} }

Правило маршрутизации у нас простое и жёстко зашито в коде, а не отдано на откуп модели: doc_type=invoice и scan_quality не хуже clean_scan — сразу в vision-LLM без OCR; doc_type in (torg12, upd) или has_dense_table=true — всегда через OCR-слой независимо от качества скана; doc_type=contract — сначала повторная попытка извлечь текстовый слой PDF, и только если её нет — OCR постранично с последующей склейкой текста. poor_scan и handwritten_marks в любом случае поднимают порог уверенности, который потребуется на выходе, чтобы документ провёлся автоматически.

OCR-слой: Tesseract 5 против Yandex Vision OCR

Когда маршрутизатор решил, что нужен OCR, дальше выбор — локальный движок или облачный. Мы держим оба и переключаемся по типу документа и требованиям к конфиденциальности (у части клиентов данные о поставщиках нельзя гонять во внешнее облако).

КритерийTesseract 5 (LSTM)Yandex Vision OCRПрямой vision-LLM (Claude)
Где работаетЛокально, без сетиОблако (Yandex Cloud)Облако (Anthropic API)
Лучше всего дляЧистые сканы 300 dpi+, типовые табличные формыТабличные формы (режим распознавания таблиц), PDF без предварительного рендераСвободные макеты счетов, нестандартная вёрстка
Метрика уверенностиConfidence 0-100 по слову (TSV-вывод)Confidence по слову/блоку в ответеНет нативной — только самооценка модели в JSON
ОграниченияТребует ручной preprocessing (deskew, бинаризация Otsu) при плохом сканеЛимит файла 10 МБ / до 200 страниц; многостраничные — через асинхронный методСторона изображения до 8000 px; при батче свыше 20 изображений размер каждого урезается
СтоимостьБесплатно (только CPU-время)Постраничная тарификация в Yandex CloudТокены на страницу-изображение

Для Tesseract мы всегда явно задаём движок LSTM: tesseract page.png out --oem 1 --psm 6 -l rus+eng -c tessedit_create_tsv=1. Флаг -l rus+eng обязателен даже для русскоязычных документов, потому что суммы, артикулы и часть служебных пометок в счетах у клиентов часто набраны латиницей. --psm 6 подходит для однородного блока текста; для табличных накладных с чёткими колонками мы переключаемся на --psm 4, который лучше держит колоночную структуру. По TSV-выводу считаем среднюю confidence по документу и отдельно по каждому распознанному полю; порог для «доверяем без перепроверки» мы установили на практике на уровне 75 — ниже него поле уходит на повторный OCR с более агрессивной бинаризацией или, если это не помогает, вырезается как кроп и отправляется на уточнение в vision-LLM.

Yandex Vision OCR используем там, где нужна табличная структура из коробки: в запросе можно выбрать модель распознавания таблиц, и ответ уже содержит строки/столбцы, а не плоский текст, который потом пришлось бы восстанавливать регулярками. Для многостраничных сканов — асинхронный метод распознавания, синхронный рассчитан на одну страницу за вызов. Авторизация — IAM-токен от сервисного аккаунта (обновляется, обычно на час) либо API-ключ; для продакшен-пайплайна мы используем сервисный аккаунт с ролью на использование Vision OCR.

LLM-слой: структурированное извлечение полей

После OCR (или вместо него для чистых счетов) в дело вступает LLM — уже не для распознавания символов, а для смыслового разбора: сопоставить, что «Итого к оплате» и «Всего с НДС» — это одно и то же поле, отличить ИНН продавца от ИНН покупателя, привязать позицию строки таблицы к номенклатуре.

Модель выбирается по сложности документа, а не одна на всё:

Извлечение всегда идёт через строгую JSON-схему (tool use с обязательными полями), а не через свободный текстовый ответ — иначе парсинг ответа сам становится источником ошибок. Пример схемы для счёта/накладной:

{ "name": "extract_invoice", "input_schema": { "type": "object", "properties": { "doc_number": {"type": "string"}, "doc_date": {"type": "string", "description": "YYYY-MM-DD"}, "seller_inn": {"type": "string"}, "seller_kpp": {"type": "string"}, "buyer_inn": {"type": "string"}, "currency": {"type": "string"}, "total_with_vat": {"type": "number"}, "vat_amount": {"type": "number"}, "line_items": { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "unit": {"type": "string"}, "qty": {"type": "number"}, "price": {"type": "number"}, "amount": {"type": "number"}, "field_confidence": {"type": "number", "description": "0-1, самооценка модели"} }, "required": ["name", "qty", "price", "amount"] } } }, "required": ["doc_number", "doc_date", "seller_inn", "total_with_vat", "line_items"] } }

Ключевое правило промпта, которое мы формулируем явно: модель должна переносить напечатанные значения, а не пересчитывать их. LLM склонна «помогать» — если сумма НДС в документе округлена нестандартно, модель может незаметно подставить «правильно посчитанное» число вместо того, что реально напечатано в счёте. Мы прямо пишем в системном промпте: «извлекай значения как они напечатаны в документе, не выполняй пересчёт и округление; если поле нечитаемо — верни null, не угадывай; если видишь арифметическое расхождение — зафиксируй его в отдельном поле discrepancy_note».

Три макета: что меняется в промпте и в маршруте

Разница между документами — не только визуальная, она диктует, что именно OCR-слой должен отдать LLM.

Тип документаОсобенность макетаСтратегия распознаванияКуда в 1С
Счёт на оплатуСвободная вёрстка, у каждого контрагента своя; итоги иногда на второй страницеОбычно напрямую в vision-LLM (Sonnet 5/Haiku 4.5) без OCR — макет слишком разный для регулярных правилСчёт на оплату (внутренний документ или основание для Поступления)
ТОРГ-12 / УПДЖёсткая унифицированная форма, плотная таблица позицийOCR с табличной моделью (Yandex Vision или Tesseract --psm 4) → LLM только нормализует текст и мапит на номенклатуру 1СПоступление товаров и услуг (Document_ПоступлениеТоваровУслуг) с табличной частью «Товары»
ДоговорМногостраничный текст, юридические формулировки, редко таблицыИзвлечение текстового слоя PDF напрямую (без OCR, если возможно) → LLM (Opus) вычленяет ключевые условия из полного текстаСправочник «Договоры» + карточка контрагента (реквизиты, срок действия, сумма)

Для ТОРГ-12/УПД отдельная тонкость: сопоставление позиций таблицы с номенклатурой 1С. OCR может вернуть «Кабель ВВГнг 3х2.5» с опечаткой («ВВГнг» распознаётся как «ВВГиг» на плохом скане), поэтому мы не ищем точное совпадение в справочнике Номенклатуры, а делаем нечёткий поиск (расстояние Левенштейна с порогом, обычно достаточно ≤2 правок на строку короче 30 символов) и, если совпадение неоднозначно, помечаем строку для ручного выбора номенклатуры бухгалтером — не создаём новую позицию справочника автоматически.

Confidence-пороги и человек в контуре

Ни один слой пайплайна не имеет права провести документ в 1С «вслепую». У нас три независимых источника уверенности, и решение принимается по их пересечению, а не по одному числу:

УсловиеДействие
OCR-confidence поля < 75 и нет vision-LLM подтвержденияКроп поля отправляется отдельным запросом в vision-LLM как перепроверка
Сумма строк таблицы ≠ итоговая сумма документаДокумент не проводится, уходит в очередь ручной проверки с пометкой расхождения
ИНН контрагента не найден в справочнике 1СЧерновик документа создаётся, но привязка контрагента — «требует подтверждения», новая карточка не создаётся автоматически
Все проверки пройдены, confidence ≥ порогаДокумент проводится автоматически, бухгалтер получает уведомление постфактум

Очередь ручной проверки — простой веб-интерфейс или телеграм-бот: скан документа с подсвеченным полем рядом с распознанным значением, кнопки «принять» / «исправить». По нашей практике на первых неделях внедрения в проверку уходит примерно 15-20% документов (в основном плохие сканы и накладные с рукописными правками), и доля снижается по мере того, как накапливается словарь известных контрагентов и типовых позиций номенклатуры для нечёткого поиска.

Как это попадает в 1С: OData и HTTP-сервисы

Готовый JSON от LLM — это ещё не документ в базе. Мы используем стандартный интерфейс OData (публикуется в конфигураторе, путь вида /odata/standard.odata/), но с одной важной оговоркой: «глубокая вставка» (deep insert) шапки документа вместе с табличной частью через один POST-запрос в OData 1С работает нестабильно и версионно-зависимо, а диагностика ошибок при отказе крайне скудная.

Поэтому для документов с табличными частями (Поступление товаров и услуг) мы делаем это в два шага:

  1. POST в Document_ПоступлениеТоваровУслуг — создаём шапку документа (контрагент, дата, склад, договор), получаем Ref_Key;
  2. Последовательные POST в подчинённый набор Document_ПоступлениеТоваровУслуг_Товары с указанием Ref_Key родителя и LineNumber для каждой строки таблицы.

Для клиентов, где такая двухшаговая запись через OData давала расхождения в нумерации строк или не проходила проверку заполнения при проведении, мы вместо OData пишем в 1С отдельный HTTP-сервис — это надёжнее, потому что вся бизнес-логика (заполнение табличной части, проверка учётной политики, расчёт НДС по ставке из документа) остаётся на стороне 1С и вызывается одним атомарным запросом:

POST /hs/invoice_import/create HTTP/1.1 Content-Type: application/json { "doc_type": "upd", "contractor_inn": "7712345678", "doc_number": "УПД-4521", "doc_date": "2026-07-02", "warehouse": "Основной склад", "lines": [ {"nomenclature_match": "Кабель ВВГнг 3х2.5", "qty": 200, "unit": "м", "price": 48.5} ], "source_confidence": 0.94 }

Перед созданием документа агент всегда сначала обращается в OData на чтение — GET .../Catalog_Контрагенты?$filter=ИНН eq '7712345678' — чтобы найти существующего контрагента по ИНН, а не полагаться на текстовое совпадение наименования (оно у одного и того же контрагента в разных документах пишется по-разному: с кавычками, с ООО впереди или сзади, сокращённо). Отдельно перед записью проверяем дубль по связке номер+дата+контрагент, чтобы повторная присылка того же скана не задвоила поступление и НДС к вычету. Если контрагент не найден — документ создаётся, но помечается флагом на подтверждение, создание новой карточки в справочник остаётся за бухгалтером.

Экономика и грабли, на которые мы уже наступили

По нашим наблюдениям на внедрённых пайплайнах, время бухгалтера на один документ сокращается с 3-5 минут ручного ввода до примерно 30 секунд на подтверждение уже заполненного черновика (для автоматически проведённых документов подтверждение вообще не нужно, только выборочный постфактум-контроль). Это оценка по практике конкретных внедрений, а не универсальная цифра — доля ручной проверки и итоговая экономия сильно зависят от качества входящих сканов у конкретных контрагентов клиента.

Грабли, которые стоит знать заранее:

Если бы мы строили пайплайн заново — начали бы сразу с раздельных confidence-метрик по полю, а не по документу целиком: изначально мы считали одну общую уверенность на весь счёт, и из-за одного нечитаемого поля в очередь ручной проверки уходил весь документ целиком, хотя остальные 90% полей были распознаны верно.

Частые вопросы

Можно ли обойтись без OCR-движков и отдать всё Claude?
Для однотипных чистых счетов — да, это рабочий и самый простой вариант. Но на плотных таблицах ТОРГ-12/УПД и на многостраничных договорах гибрид с OCR-слоем и текстовым извлечением из PDF выходит дешевле и стабильнее, потому что каждая страница-изображение в vision-LLM стоит фиксированный объём токенов независимо от объёма текста на ней.
Как выбрать между Tesseract и Yandex Vision OCR?
Tesseract — бесплатный локальный вариант, хорош на чистых сканах от 300 dpi и там, где данные нельзя передавать во внешнее облако. Yandex Vision OCR выигрывает на табличных формах за счёт встроенного режима распознавания таблиц и меньше требует ручной предобработки изображения, но это платный внешний сервис с постраничной тарификацией и лимитом на размер файла.
Что делать, если LLM неправильно посчитала НДС или итоговую сумму?
Промпт должен явно запрещать модели пересчитывать значения — задача LLM только перенести напечатанные цифры, а не проверять арифметику документа. Расхождение между суммой строк и итоговой суммой — это отдельная проверка на этапе валидации, а не повод довериться пересчитанному моделью числу.
Нужно ли переписывать структуру справочников в 1С под этот пайплайн?
Нет, пайплайн работает поверх существующей структуры — обращается к тем же справочникам Контрагенты и Номенклатура через OData на чтение и создаёт стандартные документы (Поступление товаров и услуг и другие) тем же способом, каким их создал бы бухгалтер вручную. Изменения нужны только в части публикации OData/HTTP-сервисов, если они ещё не включены на информационной базе.
Как пайплайн реагирует на новых контрагентов, которых ещё нет в 1С?
Автоматически новую карточку контрагента система не создаёт — при отсутствии совпадения по ИНН документ создаётся как черновик с пометкой «контрагент требует подтверждения», и решение о создании новой карточки остаётся за бухгалтером, чтобы избежать дублей от опечаток OCR.
📄
Скачайте подробный разбор в PDF Кейсы, статистика, типовые ошибки и чек-лист самопроверки — 11 страниц
Скачать PDF

Подпишитесь на разборы ITfresh

Раз в неделю — практичные материалы по ИТ для бизнеса: без спама, только польза.