Категория за 265 мс: как мы построили категоризатор на Jev
Пришёл платёж, Jev выбрал категорию. Рассказываем, как получили медиану 265 мс в небольшом dev-тесте и как прошлые исправления клиента помогают со следующей операцией.
- Категория
- Общее
- Обновлено
- Автор
- Stan Kharlap
Приходит платёж. Есть контрагент, сумма и, если повезёт, несколько полезных слов в назначении. Осталось выбрать категорию. Задача небольшая, но при импорте банковской выписки она повторяется снова и снова.
С 25 сентября 2026 года мы уже используем Jev для категоризации в продакшене. В небольшом dev-тесте на вымышленных платежах получили 265 мс по медиане и p95 302 мс. Все 30 запросов вернули категорию из списка, который мы передали модели.
Рассказываем, как это собрали: дали Jev понятный набор вариантов, добавили привычные инструкции по учёту и несколько похожих решений клиента из истории. И предусмотрели запасной вариант, чтобы у клиента была подсказка даже для платежа с непонятным назначением.
Дать Jev понятную задачу
Jev от TypeSafe умеет принимать решения. Его режим Choice получает список вариантов и выбирает один. Нам это подходит: план счетов уже есть, нужна категория из него. Подробнее о Jev.
Список собирает наш код ещё до обращения к модели. Категория должна быть активной, подходить компании, типу учёта и направлению платежа. В нужных случаях в список входят и допустимые счета капитала. Вместе с вариантами Jev получает контрагента, назначение платежа, сумму и немного информации о бизнесе.
Туда же передаём основные инструкции из нашего текущего категоризатора: примеры операций, подсказки по поставщикам и действующий промпт классификации. В нём уже накоплено полезное бухгалтерское знание. Сохраняем его и просим Jev выбрать один из предложенных вариантов.
Сократить путь до ответа
Сначала проверяем явные правила учёта. Подходящий шаблон контрагента, который клиент подтверждал несколько раз, тоже может сразу дать категорию. В таких случаях вызов модели вообще не нужен.
У этого короткого пути есть ограничения. Назначение платежа может объяснять, что именно купили, а у одного поставщика в истории могут быть разные категории. Тогда выбираем с учётом полного контекста.
Для каждого платежа, который дошёл до Jev, делаем один прямой запрос к TypeSafe. HTTP-клиент переиспользуем. Категории и историю загружаем один раз на группу импорта, а похожие примеры подбираем уже для каждого платежа. На каждую операцию по-прежнему приходится отдельный запрос к модели.
Получив ответ, проверяем ID категории по переданному списку. Формат запроса есть в документации TypeSafe. Основная идея в коде выглядит так:
options = eligible_categories(
company, direction,
)
examples = relevant_choices(
payment, limit=5,
)
choice = jev_choose(
payment, options, examples,
)
if choice in options:
return choice
return general_category(options)
Для краткости здесь опущены правила, трассировка и обработка ошибок. К резервной категории ещё вернёмся.
По имени поставщика не всё понятно
Возьмём вымышленную Atelier Nord GmbH. Один счёт у неё за лицензию на программу, другой за обучение, третий за аренду оборудования. Поставщик один, покупки разные. Если запомнить только последнюю категорию для этого имени, следующую операцию легко отправить не туда.
Исправления клиентов уже хранятся у нас в базе. Ещё есть изменения категорий в журнале аудита. Используем эти записи, чтобы найти полезные примеры для следующего запроса. Каждый пример должен относиться к той же компании и направлению платежа. Сохранённый выбор должен совпадать с текущей категорией операции, а сама категория должна быть в допустимом списке.

Совпадения в назначении платежа получают больший вес, чем совпадения в имени поставщика. Перед поиском убираем даты, номера и типовой банковский текст. Если назначения явно разные, одинаковое имя поставщика само по себе не сделает платежи примерами друг для друга.
В промпт попадает до пяти похожих примеров. Нашли меньше, значит отправим меньше. Сейчас поиск работает по словам, поэтому может пропустить синонимы и переводы. Здесь уже понятно, что улучшать дальше.
Основу мы описали в статье о памяти категоризатора. В этой версии точнее выбираем, какие прошлые решения взять с собой. Новое сохранённое исправление может помочь уже следующему запросу: обновляется контекст, веса модели остаются прежними.
Дать клиенту отправную точку
Пустая категория оставляет клиенту всю работу. Мы хотим предложить вариант, который можно проверить и при необходимости поправить, даже если назначение платежа мало о чём говорит.
Когда AI-подсказки включены и есть хотя бы одна допустимая категория, принимаем корректный выбор Jev даже при низкой вероятности. Если ответ невалидный, запрос закончился по таймауту или API недоступен, наш код выбирает резервную категорию локально. Предпочтение отдаём общей категории расходов или доходов. Если список категорий пуст, сначала нужно исправить настройку.
Неуверенные ответы и резервные варианты отдельно отмечаем в трассировке. TypeSafe возвращает вероятность выбранного варианта и сигнал confidence, о них можно почитать в документации. Они помогают разбирать ответы модели; бухгалтерскую точность мы измеряем отдельно.
Результат остаётся предложением: клиент может его проверить и изменить. НДС и финализация операции обрабатываются отдельно. Правила, требующие проверки, имеют приоритет, а отключённая AI-автоматизация остаётся отключённой.
Откуда взялись 265 мс
25 сентября 2026 года мы взяли шесть вымышленных сценариев расходов и прогнали каждый пять раз через jev-1.13.0. В каждом запросе было 68 категорий на выбор. Запросы шли последовательно из dev-бэкенда, порядок сценариев менялся от раунда к раунду.
| Что измерили | Результат |
|---|---|
| Медианное время адаптера | 265 мс |
| p95 по методу ближайшего ранга | 302 мс |
| Самый медленный запрос | 791 мс |
| Ответы HTTP 200 | 30 из 30 |
| Вернулась допустимая категория | 30 из 30 |
Первый запрос оказался самым медленным, 791 мс, и тоже вошёл в расчёт. HTTP-соединения переиспользовали, историю передали в памяти, запись трассировок отключили. Таймер охватывал сборку промпта, подбор примеров и вызов API по сети. Браузер, импорт банка, OCR, чтение истории из базы и сохранение операции в замер не входили.
Это небольшой dev-тест с повторяющимися данными. Кеширование у провайдера мы не контролировали. Он показывает скорость этой части системы; время в продакшене, работу под нагрузкой и сравнение с другими продуктами ещё нужно проверять отдельно. Исходные данные и методика доступны для просмотра.
Следить, становится ли лучше
Все шесть сценариев в этом прогоне получили ожидаемые категории. Чтобы понять, как это переносится на клиентские платежи, нужны независимые примеры с проверенными ответами.
Мы отдельно считаем покрытие, то есть как часто есть категория для проверки, и совпадение с проверенным ответом. Первое показывает, движется ли процесс дальше. Второе показывает, насколько полезны подсказки.
Наш инструмент оценки прогоняет один и тот же случай с историей и без неё. Тестовые операции исключаются из всех промптов. Подтверждённые примеры должны быть старше проверяемого платежа. Записи, изменённые после контрольной даты, и сводные шаблоны без надёжной даты тоже убираем. Иначе легко незаметно подложить модели правильный ответ и порадоваться отличному результату.
В нынешней dev-выборке после этих фильтров не осталось независимой прошлой истории, поэтому измеренного прироста точности пока нет. Это следующий шаг. А сейчас у нас есть быстрая подсказка, прошлые решения клиента для контекста и способ проверить, делают ли дальнейшие изменения эти подсказки лучше.
Частые вопросы
- И насколько это быстро?
- В небольшом dev-тесте 25 сентября 2026 года мы получили медиану 265 мс и p95 302 мс на 30 запросах. Шесть вымышленных сценариев повторили по пять раз, каждый раз с выбором из 68 категорий. Замер включает наш адаптер и вызов API; загрузка истории и сохранение операции остаются за его пределами.
- Jev учится на исправлениях клиента?
- Мы добавляем в запрос до пяти похожих прошлых решений той же компании. Новые сохранённые исправления становятся доступны для следующего запроса. Саму модель мы при этом не переобучаем. Насколько это помогает, ещё нужно измерить на независимых примерах.
- А если Jev сомневается или API недоступен?
- Если AI-подсказки включены и есть допустимые категории, мы принимаем корректный выбор даже при низкой уверенности. При ошибке или невалидном ответе выбираем резервную категорию локально, обычно общую категорию расходов или доходов. Клиент может её проверить; НДС и финализация обрабатываются отдельно.
Norman берет операционную финансовую работу на себя
От invoicing до bookkeeping: Norman организует повторяющиеся финансовые процессы так, чтобы вы успевали к дедлайнам с меньшим объемом ручной работы.