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, тот клиент = 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 — уже не для распознавания символов, а для смыслового разбора: сопоставить, что «Итого к оплате» и «Всего с НДС» — это одно и то же поле, отличить ИНН продавца от ИНН покупателя, привязать позицию строки таблицы к номенклатуре.
Модель выбирается по сложности документа, а не одна на всё:
- 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.