Классификация входящих документов LLM: классы и пороги
Классификация входящих документов с помощью LLM: как задать понятные классы, выбрать пороги уверенности и отправлять спорные случаи на ручную проверку.

Почему обычная классификация не работает
Классификация входящих документов редко сводится к четырем-пяти ярлыкам. В одном потоке лежат счета, акты, договоры и письма в свободной форме. Если просто дать модели список классов и ждать точного ответа, путаница начнется уже на первых десятках файлов.
Границы между классами часто размыты. Один PDF может содержать письмо на первой странице и счет на второй. Акт нередко похож на счет: те же реквизиты, суммы, печати и подписи. Договор и приложение к договору тоже легко спутать, особенно если модель видит только часть текста или плохой скан.
Чаще всего ломаются такие случаи:
- письмо с просьбой оплатить счет и сам счет в одном файле
- акт без явного заголовка, но с таблицей и подписями
- договор после OCR, где потерялись номер, дата и названия сторон
- скан, где печать закрывает сумму или реквизиты
Человек в таких примерах опирается на контекст. Он замечает структуру документа и понимает, где сопроводительное письмо, а где приложение. Модель часто видит только похожие признаки: дату, сумму, название компании, подпись. Когда классы пересекаются, этого мало.
Качество входа тоже сильно влияет на результат. OCR путает цифры, режет строки, склеивает колонки и теряет части таблиц. Из-за этого дата становится нечитаемой, сумма меняется на одну цифру, а реквизиты превращаются в шум. Для маршрутизации этого достаточно, чтобы документ ушел не туда.
Ошибка маршрута почти всегда обходится дороже, чем кажется. Если счет попал в договорной контур, оператор сначала откроет файл, поймет, что это не его тип, вернет документ назад, а потом кто-то разберет его еще раз. На одном файле это мелочь. На потоке в несколько тысяч документов в день это уже часы лишней ручной работы и задержки в обработке.
Схема "один документ - один класс - один уверенный ответ" в живом архиве работает плохо. Для реального потока нужны четкие классы, понятные правила для спорных случаев и отдельный маршрут туда, где решение принимает человек.
Как задать классы без серых зон
Задача ломается не на модели, а на названиях классов. Если сотрудники сами спорят, где заканчивается "счет" и начинается "договор", модель будет путаться еще чаще.
Каждый класс должен отвечать на один простой вопрос: что это за документ с точки зрения следующего шага? Его нужно оплатить, проверить исполнение, отправить юристам или просто сохранить без действий. Если один класс ведет в разные процессы, его почти всегда лучше разделить.
Плохая схема выглядит так: "договор_срочно", "счет_бухгалтерия", "акт_регион". Здесь смешаны тип документа, приоритет и получатель. Модель начинает угадывать сразу три вещи вместо одной. Тип, срочность и отдел лучше хранить в разных полях.
Для каждого класса зафиксируйте четыре пункта:
- что входит в класс
- что точно не входит
- по каким признакам человек его узнает
- какие пограничные примеры чаще всего вызывают путаницу
Например, класс "счет" может включать счет на оплату и счет-оферту, если оба документа идут в один маршрут. Но если счет-оферта всегда уходит юристам, а обычный счет - в оплату, это уже два класса, даже если по форме документы похожи.
С классом "договор" полезно сразу решить судьбу приложений и допсоглашений. Если команда обрабатывает их так же, как основной договор, держите один класс. Если допсоглашение проверяют отдельно, выделите его в свой класс. Не стоит надеяться, что модель сама поймет разницу по контексту.
Пограничные примеры нужны не для порядка в документации. Они убирают серую зону. Для "акта" таким примером может быть УПД, который иногда ведет себя как закрывающий документ, а иногда как счет. Для "договора" - коммерческое предложение с юридическими формулировками. Для "прочего" подойдут письма, сканы без структуры и файлы, где не хватает страниц.
Класс "прочее" держите узким. Если в него уходит каждый десятый документ, схема уже неудачна. Обычно он нужен для редких случаев, а не как корзина для всего непонятного.
Простая проверка выглядит так: человек без долгих споров должен разложить 20 случайных файлов по вашим классам почти без разногласий. Если этого не происходит, сначала поправьте схему классов, а уже потом промпт и пороги.
Какой ответ просить у модели
Если вам нужна предсказуемая классификация, просите у модели не "понятный ответ", а короткий и строгий объект. Практичный минимум такой: class, confidence, reason, needs_review.
class нужен для маршрута в системе. confidence показывает, насколько ответ уверенный. reason дает короткую причину выбора, чтобы сотрудник понял логику модели и быстрее проверил спорный случай. needs_review сразу отделяет сомнительные документы от тех, что можно обработать автоматически.
Список допустимых классов лучше зафиксировать прямо в промпте и не оставлять модели свободу придумывать свои варианты. Если ваши классы такие: invoice, act, contract, other, то только они и должны появляться в поле class. Иначе очень быстро появится хаос вроде bill, agreement, service_act и десятков почти одинаковых меток.
Свободный текст тоже лучше убрать. Требуйте строгий JSON и явно запрещайте любой текст до или после объекта. Это упрощает парсинг и снижает число сбоев в проде.
Пример схемы ответа
{
"class": "invoice",
"confidence": 0.91,
"reason": "Есть номер счета, реквизиты сторон, сумма и назначение оплаты.",
"needs_review": false
}
Для спорных случаев задайте простое правило: если документ смешанный, плохо читается, в нем не хватает страниц или признаки двух классов конфликтуют, модель ставит needs_review: true. Это полезнее, чем попытка любой ценой выбрать один класс.
Есть еще одна деталь, которую команды часто пропускают: сохраняйте версию промпта рядом с результатом классификации. Не просите модель возвращать ее сама. Пусть приложение пишет, например, prompt_version: doc_cls_v3 в метаданные запроса или в запись результата. Тогда проще понять, почему вчера документ ушел в contract, а сегодня в other.
Если вы отправляете такие запросы через единый шлюз вроде RU LLM, удобно держать одинаковый формат ответа и метаданные для разных моделей. Это особенно полезно, когда классификация должна работать в одном контуре и без лишней ручной разборки.
Как выбрать пороги уверенности
Порог уверенности - это не число "по умолчанию". Это бизнес-правило: когда система может принять решение сама, а когда ей нужен человек. Для входящих документов лучше начать с одного общего порога для автопринятия, а потом подстроить его по классам.
На старте часто хватает простого правила: если уверенность модели выше 0.85, документ уходит в нужный класс автоматически; если ниже, он попадает в ручную проверку. Это дает рабочую базу, но редко остается финальной настройкой. Уже через несколько дней становится видно, где ошибка обходится особенно дорого.
Если промах в одном классе стоит дороже, поднимайте порог именно там. Счет, который случайно попал в "акт", обычно исправят быстро. Договор, который система приняла за счет, может сломать маршрут согласования и создать лишний риск. Для таких классов разумно ставить 0.93 и выше, даже если это увеличит долю ручной обработки.
Класс "прочее" лучше держать отдельно. Модель часто использует его как безопасный ответ, когда сомневается. Если оставить для него тот же порог, что и для остальных, туда начнут проваливаться нормальные документы, которые просто не набрали уверенность. Обычно для "прочего" задают более высокий порог и отправляют сомнительные случаи оператору.
Средняя точность тут быстро успокаивает. Смотрите не на одно общее число, а на ошибки по каждому классу:
- где больше всего ложных срабатываний
- какие документы модель путает между собой
- сколько файлов уходит на ручную проверку
- сколько реально стоит одна ошибка по каждому классу
Проверять пороги нужно не на красивой тестовой выборке, а на живых пакетах документов. Берите сканы с плохим OCR, многостраничные файлы, документы со штампами, письма с вложениями, старые шаблоны. На чистом наборе порог 0.9 может выглядеть отлично, а в реальном потоке резко просесть.
Нормальная картина после проверки обычно такая: часть классов идет в автообработку при 0.85, более рискованные ждут 0.93, а "прочее" живет по своим правилам и часто уходит человеку. Так система ошибается реже и не забивает команду ручной сортировкой без причины.
Как отправлять спорные случаи на ручную обработку
Ручная проверка - не запасной выход, а часть процесса. Если модель дает низкий confidence, документ лучше не проталкивать дальше по цепочке. Ошибка на этом шаге часто дороже, чем лишняя минута оператора.
В ручную очередь стоит отправлять не только сомнительные ответы модели. Туда же обычно попадают документы с плохим OCR, оборванными страницами, странной версткой и новыми шаблонами, которых не было в примерах. Это обычная ситуация: поток меняется, поставщики присылают файлы в разных форматах, а модель видит не сам документ, а его текстовое представление.
Очередь лучше делить не только по классу документа, но и по типу проблемы. Тогда оператор быстрее понимает, что делать, а команда потом точнее меняет правила и пороги. На практике хватает нескольких причин: низкая уверенность, плохой OCR, новый шаблон, конфликт признаков и технический сбой вроде пустого ответа или ошибки парсинга.
Оператору нужен не только итог модели, например "счет" или "договор". Покажите ему исходный текст, фрагменты, на которых модель опиралась, confidence и причину отправки в ручную очередь. Когда человек видит только ярлык класса без контекста, он тратит время на повторный разбор с нуля. Когда перед глазами есть OCR и ответ модели, проверка идет заметно быстрее.
После ручной проверки сохраняйте финальное решение оператора рядом с исходным запросом: какой класс выбрали, почему исправили, был ли плохой OCR, это новый шаблон или ошибка порога. Потом эти записи помогают пересчитать порог уверенности, добавить новый класс или обновить промпт без догадок.
Если вы строите такой контур через RU LLM, полезно держать запрос, ответ модели и решение оператора в одном процессе. Это упрощает разбор спорных случаев и внутренний контроль, особенно когда документ влияет на оплату, договорный маршрут или проверки по 152-ФЗ.
Хороший признак рабочей схемы простой: ручная очередь не растет бесконечно, а становится чище. Старые типы ошибок исчезают, новые шаблоны быстро переходят в автоматический поток, а оператор занимается только тем, что модель пока разбирает нестабильно.
Пошаговая схема запуска
Начинайте не с огромного набора правил, а с живых документов за последние 1-3 месяца. Старые архивы часто портят тест: там другие шаблоны, другое качество сканов и уже неактуальные типы файлов. Для этой задачи свежая выборка обычно полезнее, чем большой, но пестрый архив.
Соберите по каждому классу несколько сотен недавних документов и разметьте их вручную. Если какого-то типа мало, лучше сразу отметить этот перекос. Перед прогоном уберите дубликаты, повторные отправки и почти одинаковые версии одного файла, иначе метрики будут красиво выглядеть только на бумаге. Персональные данные и чувствительные поля лучше замаскировать до теста. Для российских команд это обычная рабочая норма, особенно если логи и обработка остаются внутри РФ.
На первом шаге дайте модели простой промпт: список классов, короткие правила выбора и строгий формат ответа, например JSON с полями class, confidence, reason, needs_review. Сложные исключения пока не нужны. После первого прогона смотрите не только на общую точность. Гораздо важнее понять, сколько документов ушло в ручную очередь и где модель чаще путает похожие типы.
Один общий порог почти никогда не подходит всем классам. Счет, акт и договор ведут себя по-разному: где-то модель уверена почти всегда, а где-то постоянно колеблется между двумя метками. Поэтому пороги лучше настраивать по классам и после каждой правки прогонять тот же тест заново.
Не фиксируйте схему после одного удачного запуска. Возьмите еще несколько независимых пачек документов, повторите проверку и посмотрите, держится ли доля ручной обработки на ожидаемом уровне. Если метрики не прыгают, а спорные случаи уходят людям, а не в неверный класс, процесс можно переносить в прод.
Пример: поток счетов, актов и договоров
Компания получает из почты и ЭДО около 300 файлов в день: сканы, PDF, письма с вложениями. Для старта достаточно четырех классов: "счет", "акт", "договор" и "прочее". Такая схема покрывает большую часть потока и не заставляет модель угадывать слишком тонкие различия.
Маршрут можно задать очень просто:
- счет с суммой, ИНН, банковскими реквизитами и номером документа сразу идет в бухгалтерию
- акт уходит в учет только если модель видит сам акт и признаки завершенности документа
- договор, даже если к нему приложены спецификация или приложение, попадает в юротдел
- свободное письмо без явного шаблона получает класс "прочее"
На практике LLM для документов лучше работает не как "магическая кнопка", а как первый фильтр. Она быстро сортирует понятные случаи и отдает сомнительные человеку.
Хороший пример - акт без подписи. Допустим, файл называется "Акт выполненных работ за март", но внизу нет подписи заказчика. Модель отвечает так: "акт" с уверенностью 0.76, "счет" 0.14, "договор" 0.06, "прочее" 0.04. Если порог для класса "акт" стоит на 0.70, документ пройдет дальше как обычный акт. Для учета это уже риск.
Если поднять порог до 0.85, тот же файл не пройдет автоматически и уйдет на ручную проверку. Есть и более аккуратный вариант: оставить порог 0.70, но добавить простое правило - любой акт без подписи оператор проверяет вручную. Так команда не тормозит поток чистых документов, но ловит неполные.
С договором ситуация обычно проще. Если модель видит слова "договор", "стороны", "предмет" и отдельное приложение, она чаще всего дает высокий балл и отправляет пакет в юротдел. А вот письмо вроде "Коллеги, направляю материалы по обсуждению условий" лучше относить к классу "прочее", даже если внутри есть вложение.
Если контур обработки должен оставаться в РФ, такую маршрутизацию можно держать за OpenAI-совместимым API и не менять существующий SDK. Для команд, которые уже собрали пайплайн и не хотят переписывать интеграции, это просто удобный способ сохранить текущую схему работы.
Где команды ошибаются чаще всего
Проблемы обычно начинаются не в модели, а в схеме, которую ей дают. Если команда хочет сделать классификацию слишком подробной с первого дня, точность быстро падает. Модель путается там же, где путался бы и человек: между "счетом", "счетом на оплату", "закрывающим счетом" и другими почти одинаковыми ярлыками.
Лучше дать меньше классов, но сделать их понятными. Если два класса нельзя уверенно различить по тексту и структуре документа, их рано разделять в автообработке. Сначала объедините спорные варианты, а потом дробите их по данным из реального потока.
Еще одна частая ошибка - один порог уверенности для всех типов документов. Для типовых счетов можно держать высокий уровень автообработки, потому что их форма обычно повторяется. Для договоров, приложений и писем нужен другой порог: у них больше вариантов формулировок, страниц и шаблонов.
На практике это выглядит так: счет с уверенностью 0.93 можно сразу отправлять дальше по процессу, а договор с той же оценкой лучше еще проверить по правилам или отдать человеку. Один общий порог делает систему либо слишком осторожной, либо слишком самоуверенной.
Есть и более приземленная ошибка: команда не проверяет OCR до LLM, а потом спорит с классификатором, хотя проблема в исходном тексте. Если OCR потерял заголовок, перепутал таблицу или склеил строки, модель начнет гадать.
До классификации стоит ловить хотя бы такие случаи:
- слишком мало распознанного текста
- много мусорных символов
- пустая первая страница
- сильный разрыв между страницами по качеству
Ручная обработка тоже часто устроена плохо. Сотрудник исправил класс, но причина нигде не сохранилась. Через месяц команда видит только факт ошибки и уже не понимает, что именно сломалось: новый шаблон, плохой OCR, неясные классы или слабый промпт.
Если сохранять причину правки в нескольких простых категориях, качество растет заметно быстрее. Тогда видно, что именно нужно менять: список классов, пороги уверенности или этап распознавания.
И самый дорогой промах - сразу пускать новые шаблоны в автообработку. Новый формат счета от крупного поставщика, нестандартный акт или договор с чужой версткой лучше сначала отправить в ручной контур на несколько десятков примеров. После такой проверки автоматический маршрут включать намного спокойнее.
Быстрая проверка перед запуском
Перед тем как включать классификацию в рабочий поток, проверьте не модель, а правила вокруг нее. Большая часть сбоев начинается не с LLM, а с расплывчатых классов, разных порогов в разных сервисах и неудобного экрана для оператора.
Хорошая схема выглядит скучно, и это плюс. Для каждого класса команда пишет, что в него входит и что точно не входит. Если у вас есть классы "счет", "акт" и "договор", для каждого нужны простые признаки входа и выхода. Иначе модель начнет путать счет на оплату с приложением к договору, а оператор будет гадать, почему это произошло.
Перед запуском полезно пройти короткий чек-лист:
- у каждого класса есть короткое правило: что принимаем, что исключаем, куда отправляем спорный случай
- модель возвращает жесткий JSON без свободного текста, с полями
class,confidence,reasonиneeds_review - порог уверенности и маршрут на ручную обработку лежат в одном месте, а не в промпте, коде и админке сразу
- оператор видит сам документ, ответ модели и причину решения на одном экране
- команда считает ошибки по каждому классу раз в неделю, а не только общую точность
Жесткий JSON нужен не для порядка. Он убирает двусмысленность. Если модель иногда пишет "скорее всего договор", а иногда contract, пайплайн быстро ломается. Когда формат фиксирован, ответ можно валидировать и сразу решать, пропускать документ дальше или отправлять человеку.
Отдельно проверьте ручной маршрут. Он должен срабатывать по одному правилу, которое все понимают. Например, если confidence ниже 0.82 или reason указывает на смешанный пакет страниц, документ сразу уходит оператору. Не стоит прятать это условие в нескольких местах.
И последняя проверка: возьмите свежую выборку и посмотрите ошибки по классам. Общая цифра часто обманывает. Если модель хорошо ловит счета, но регулярно путает акты и договоры, вы увидите это только в разрезе классов.
Если вы запускаете такую схему через RU LLM, полезно держать одинаковый JSON-контракт и общие правила маршрутизации для разных моделей. Тогда смена провайдера не ломает классификатор и не создает новый набор ошибок.
Что делать после пилота
После пилота не спешите расширять схему. Лучше на время заморозить ее и посмотреть на реальные цифры. Часто кажется, что все уже работает хорошо, пока не видно, сколько файлов ушло людям и какие классы модель путает между собой.
Смотрите не только на общую точность. Она легко скрывает проблему. Если модель уверенно разбирает счета, но регулярно ошибается на договорах, бизнесу от этого не легче.
Лучше держать перед глазами простой набор метрик:
- доля документов, ушедших на ручную обработку
- число промахов по каждому классу
- доля ложной уверенности, когда модель поставила высокий балл, но выбрала не тот класс
- среднее время разбора одного спорного документа человеком
Эти цифры лучше смотреть по неделям, а не одним итогом за месяц. Так быстрее видно, какой класс проседает и не растет ли ручная очередь после смены промпта или модели. Если, например, из 100 договоров 12 попали в "прочее", проблема не в общем качестве, а в границе класса.
Полезно прогнать один и тот же набор документов через две-три модели. Сравнивайте их в одинаковых условиях: тот же промпт, те же классы, те же пороги. Обычно выигрывает не та модель, что дала лучший разовый результат, а та, что ведет себя ровно на повторных прогонах и реже отправляет понятные документы в серую зону.
Если команда уже использует OpenAI-совместимый SDK, такую проверку можно сделать через RU LLM: сменить base_url на api.rullm.com, оставить тот же код и сравнить несколько моделей на одном наборе. Для команд, которым важно хранить логи и обработку внутри РФ, это еще и практичный способ не переделывать интеграцию перед сравнением.
Новые классы добавляйте только после ровного пилота. Если текущие классы еще спорят друг с другом, расширение почти всегда делает схему хуже. Сначала добейтесь понятных правил, нормальной доли ручной обработки и предсказуемых ошибок по каждому классу. Потом уже можно дробить "договор" на подтипы или выносить редкие документы в отдельные ветки.
Такой порядок выглядит скучнее, чем быстрый рост схемы, но через месяц он обычно экономит команде гораздо больше времени.
Часто задаваемые вопросы
Сколько классов брать на старте?
На старте обычно хватает 3–5 классов. Возьмите только те типы, которые ведут в разные процессы: например, invoice, act, contract, other.
Если команда спорит о границах уже на этом уровне, не дробите схему дальше. Сначала добейтесь, чтобы люди без долгих обсуждений одинаково раскладывали свежие документы.
Как описать классы, чтобы модель меньше путалась?
Опишите класс через действие после распознавания, а не через абстрактное название. Полезная схема простая: что входит, что не входит, по каким признакам человек узнает этот тип и какие спорные примеры встречаются чаще всего.
Если один класс ведет в два разных маршрута, лучше разделить его сразу. Иначе модель начнет гадать там, где сама схема уже неясная.
Что делать, если в одном файле сразу несколько документов?
Не заставляйте модель выбирать один ярлык любой ценой. Если в файле письмо на первой странице и счет на второй, пусть она ставит основной класс только при явном правиле, а в спорном случае возвращает needs_review: true.
Так вы теряете пару минут на проверку, но не ломаете маршрут дальше по цепочке.
Нужен ли класс «прочее»?
Да, но держите его узким. other нужен для редких случаев, новых шаблонов, битых сканов и файлов без понятной структуры.
Если туда уходит заметная доля нормального потока, проблема обычно не в модели, а в схеме классов или слишком низких порогах для остальных типов.
Какой формат ответа лучше просить у модели?
Просите строгий JSON с четырьмя полями: class, confidence, reason, needs_review. Этого хватает для маршрута, проверки спорных случаев и разбора ошибок.
Не давайте модели свободу придумывать свои метки и не принимайте свободный текст вокруг JSON. Иначе парсинг и логика в проде быстро начнут сыпаться.
С какого порога уверенности начать?
Рабочий старт для автообработки — около 0.85. Все, что ниже, сначала отправляйте человеку.
Это не финальное число. Через несколько дней посмотрите, где промах обходится дороже, и поднимите порог для таких классов отдельно.
Почему не стоит ставить один порог для всех классов?
Потому что цена ошибки разная. Счет, который случайно ушел не туда, обычно быстро вернут. Договор, который система приняла за счет, ломает согласование и тянет больше лишней работы.
Поэтому для простых и повторяемых типов порог можно держать ниже, а для рискованных — выше. Один общий порог почти всегда или слишком строгий, или слишком смелый.
Что делать, если OCR распознал документ плохо?
Сначала проверьте вход. Если OCR потерял заголовок, склеил таблицу или оставил слишком мало текста, не гоните такой файл в обычную классификацию.
Лучше сразу отправить его в отдельную ветку ручной проверки с причиной вроде bad_ocr. Тогда команда не будет искать ошибку в модели там, где сломан сам текст.
Какие случаи лучше сразу отправлять на ручную обработку?
Отправляйте туда не только низкий confidence. В ручную очередь стоит класть смешанные файлы, документы с недостающими страницами, новые шаблоны и технические сбои вроде пустого ответа.
Оператору покажите сам документ, OCR-текст, класс модели, confidence и короткую причину. Тогда он проверит решение быстрее и оставит понятную правку.
Какие метрики смотреть после пилота?
Смотрите не на одну общую точность, а на ошибки по каждому классу. Полезнее всего считать долю ручной обработки, ложную уверенность, частые пары путаницы вроде act и contract, а также время разбора спорных файлов человеком.
Если ручная очередь не растет, а старые ошибки встречаются реже, схема движется в правильную сторону. Если other распухает или один класс постоянно проседает, меняйте правила, пороги или этап OCR.