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-подхода.
- Стоимость по страницам. Каждая страница-изображение в API Anthropic — это заметный фиксированный объём входных токенов, зависящий от разрешения, независимо от того, сколько там реально текста. Договор аренды на 18 страниц как набор картинок обходится дороже, чем прогон через слой OCR + один текстовый запрос на несколько тысяч токенов.
- Точность на плотных таблицах. ТОРГ-12 и УПД — это фиксированная табличная форма (для ТОРГ-12 — унифицированная форма №ТОРГ-12 из постановления Госкомстата №132, для УПД — форма на базе счёта-фактуры по приказу ФНС) с колонками «№ п/п», «Товар», «Ед. изм.», «Кол-во», «Цена», «Сумма без НДС», «НДС», «Сумма с НДС». На таких строго регулярных сетках специализированный табличный OCR даёт более стабильную привязку значения к колонке, чем vision-LLM, которая иногда «съезжает» на строку выше или ниже при плотной таблице из 30+ позиций.
- Качество скана. Часть входящих документов — это фото с телефона бухгалтера контрагента или скан с МФУ на 150 dpi. OCR-движок можно донастроить (передискретизация, бинаризация, deskew) и получить численную метрику уверенности по каждому слову. У vision-LLM такой встроенной метрики нет вообще — она просто выдаёт ответ, и по нему нельзя понять, «дожала» модель нечитаемый фрагмент или додумала его.
Поэтому мы строили не «AI, который читает документы», а маршрутизатор. Для каждого конкретного файла система сама решает: добавить OCR-слой перед LLM, отправить скан напрямую в vision-LLM — или, если это длинный договор в PDF, вообще не сканировать, а просто вытащить текстовый слой.
Архитектура: четыре стадии пайплайна
Пайплайн живёт как отдельный Python-сервис. Он забирает вложения из почтового ящика бухгалтерии или из папки обмена с 1С, а на выходе кладёт готовый документ в базу через OData или HTTP-сервис. Внутри — четыре стадии.
- Нормализация входа. PDF с текстовым слоем проверяется через
pdftotext -layout(poppler-utils) — если текст извлекается и его объём разумный (не «мусорные» 3 символа на страницу от сканированного PDF без OCR-слоя), документ идёт в текстовый режим без всякого OCR. Если текстового слоя нет или PDF — это обёртка над сканом, страницы рендерятся в PNG черезpdftoppm -r 300 -png— обязательно не ниже 300 dpi, потому что при 150 dpi точность Tesseract на мелком кегле счетов падает ощутимо. - Классификация макета. Первая (уменьшенная, ~768px по длинной стороне) страница отправляется дешёвой модели — Claude Haiku 4.5 — с задачей определить тип документа (счёт / ТОРГ-12 / УПД / договор / прочее) и грубую оценку качества скана.
- Маршрутизация в слой распознавания. В зависимости от типа и качества документ уходит либо в OCR-слой (Tesseract 5 или Yandex Vision OCR) с последующим текстовым запросом к LLM, либо напрямую в vision-LLM с полным изображением страницы.
- Извлечение, валидация, запись в 1С. LLM возвращает строго типизированный JSON, значения сверяются со справочниками 1С (контрагенты, номенклатура) через OData, документ с высокой уверенностью проводится автоматически, документ с низкой — уходит в очередь ручной проверки бухгалтеру, и только после подтверждения записывается.
Здесь важно понимать: маршрутизация — это не настройка один раз на уровне контрагента. Это решение для каждого конкретного документа. У одного и того же поставщика типовой счёт может пройти вообще без 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. Уже не для распознавания символов, а для смыслового разбора: понять, что «Итого к оплате» и «Всего с НДС» — одно и то же поле, отличить ИНН продавца от ИНН покупателя, привязать строку таблицы к позиции номенклатуры.
Модель выбирается под сложность конкретного документа — не одна на все случаи.
- Claude Haiku 4.5 — типовые счета с понятной структурой, где нужно вытащить 8-10 плоских полей без сложной логики;
- Claude Sonnet 5 — основная рабочая лошадка: накладные ТОРГ-12/УПД с табличной частью из 10-40 позиций, счета с нестандартной вёрсткой, где vision важнее скорости;
- Claude Opus (линейка 4.x) — договоры: там нужно не просто вытащить дату и сумму, а понять структуру документа — предмет, срок действия, условия пролонгации, ответственность сторон — и это оправдывает более высокую стоимость токена при низком объёме документов такого типа (в среднем на порядок меньше договоров, чем счетов, за месяц).
Извлечение всегда идёт через строгую 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 для документов, прошедших через Tesseract или Yandex Vision: считаем среднюю уверенность отдельно по каждому полю и отдельно по документу в целом.
- self-reported confidence LLM (поле
field_confidenceв JSON-схеме, отдельно на каждую строку таблицы) — это не «настоящая» вероятность, а признание модели «я не уверена», но на практике оно неплохо коррелирует с реальными ошибками, если явно просить модель занижать её при нечитаемых фрагментах; - Сверка со справочниками 1С через OData: ИНН найден среди существующих контрагентов, сумма строк совпадает с итоговой суммой документа — с точностью до копейки.
| Условие | Действие |
|---|---|
| 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С работает нестабильно и версионно-зависимо, а диагностика ошибок при отказе крайне скудная.
Именно поэтому для документов с табличными частями — например, «Поступление товаров и услуг» — мы разбиваем запись на два шага:
- POST в
Document_ПоступлениеТоваровУслуг— создаём шапку документа (контрагент, дата, склад, договор), получаемRef_Key; - Последовательные 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 секунд на подтверждение уже заполненного черновика. Для автоматически проведённых документов подтверждение вообще не нужно, только выборочный постфактум-контроль. Но честно предупреждаем: это цифры по конкретным внедрениям, не универсальный норматив. Доля ручной проверки и итоговая экономия времени сильно зависят от того, насколько качественные сканы присылают контрагенты конкретного клиента.
Грабли, которые стоит знать заранее:
- 150 dpi убивает Tesseract. Часть контрагентов присылает сканы с дешёвых МФУ на 150 dpi — confidence по мелкому шрифту падает резко. Мы принудительно рендерим PDF через
pdftoppm -r 300даже если исходный DPI ниже — это не восстанавливает потерянные детали, но стабилизирует поведение LSTM-движка. - Опечатки OCR в ИНН. Цифры 0/8, О/0, 3/8 путаются на плохих сканах. Прямое сравнение строки с 1С даёт ложные «контрагент не найден» — обязательна нечёткая проверка (допуск на 1 символ) прежде чем создавать нового контрагента, иначе плодятся дубли в справочнике.
- LLM «улучшает» цифры. Без явного запрета в промпте модель иногда пересчитывает НДС «как положено», а не берёт напечатанное значение — расхождение всплывает при сверке с итоговой суммой документа и выглядит как ошибка распознавания, хотя на самом деле это ошибка интерпретации.
- Рукописные правки на факс-копиях договоров. Договор, отсканированный с рукописной правкой суммы или даты, — это единственный класс документов, где мы держим обязательную ручную проверку без исключений: точность vision-LLM на смешанном печатном+рукописном тексте заметно ниже, чем на чистом печатном.
- Лимиты API на пике. В конце месяца, когда приходит пачка из 40-60 документов разом, легко упереться в лимиты запросов в минуту у используемого тарифа Anthropic API — нужна очередь с ограничением параллелизма и экспоненциальным бэкоффом, а не наивный цикл «отправить все сразу».
Если бы мы строили этот пайплайн с нуля — сразу заложили бы раздельные 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.
