Мы нормализуем миллионы строк НСИ с точностью 98% на обычном сервере с 8 ядрами и 16 ГБ RAM. У нас нет Золотой Рыбки, у нас есть джинн, которого мы посадили в лампу. И я хочу рассказать, как у нас это получилось.
Привет, Хабр!
На связи снова Алексей Арустамов, директор компании‑разработчика low-code платформы продвинутой аналитики.
В прошлой статье я порассуждал о том, почему «чистый» ИИ горит в атмосфере реальных бизнес-процессов и как мы переносили издержки с вероятностной модели на детерминированный контур с помощью композитного подхода.
Сегодня я предлагаю перейти от рассуждений к практике и расскажу о том, как подходы нашей команды эволюционировали от регулярных выражений к локальным нейросетям, как мы искали баланс с LLM в enterprise-задачах и как теория информационных архитектур помогает считать реальную стоимость владения таким решением.
Иллюзия «волшебной лампы» и суровая реальность MDM
Представьте строку: Ручка дверная деревянная, длина 150 мм. Для кладовщика это просто текст.
А для MDM-системы (системы управления мастер-данными, которая должна хранить «единую версию правды» о товарах) это пакет задач:
определить класс («Ручка дверная»),
извлечь атрибуты («Материал»: Дерево, «Длина»: 150, «Ед. изм.»: мм) и
привести их к каноническому виду.
В нашем проекте классификатор содержит около 1000 классов (из них 500 наиболее востребованы). В среднем на класс приходится 7 атрибутов, но у сложных технических изделий их может быть 20–30. При этом точность автоматической нормализации должна быть не менее 95%, ибо ошибка в классе или атрибуте создает дубликаты, искажает оценку складских запасов и попадает в управленческую отчетность.
Мы поставили задачу создать конвейер, который преобразует эти неформализованные «цифровые следы» деятельности в строгую информационную модель. Мы протестировали три подхода.
Спойлер: в чистом виде проиграл каждый из них.
Подход 1. Ловушка регулярных выражений
К началу проекта у нас уже был опыт использования связки TF-IDF + SVM для классификации. Значения атрибутов извлекались с помощью обобщенных регулярных выражений. Для каждой пары «класс — атрибут» мы создавали набор правил, который называли грамматикой.
Как это работало
Высокая скорость обработки.
Интерпретируемость 100%, т. е. мы точно знали, почему парсер выделил именно эту цифру.
Где мы уперлись в потолок
Основной проблемой, с которой мы столкнулись, оказались трудозатраты на поддержку. Например, базовая настройка грамматики для одного класса, т. е. изучение примеров, написание регулярных выражений, проверка и уточнение правила, занимала в среднем 4 дня.
Для контекста: с технической точки зрения класс — это группировка атрибутов. Результат классификации (отнесение номенклатуры к определенному классу) дает нам максимально общее представление о товаре. Например, если мы знаем, что позиция относится к классу «Труба металлическая», мы сразу представляем что-то длинное из металла с дыркой посередине. А вот атрибуты (диаметр, толщина стенки, материал) уже уточняют это представление. Поскольку наборы атрибутов для разных классов совершенно разные, правила их извлечения для каждого класса приходилось писать с нуля.
Это напоминает попытку вычерпать море чайной ложкой: вода вроде бы уходит, но окончание работы не просматривается.
Итак, если умножить 4 дня на 1000 классов, получим 4000 человеко-дней только на первичную настройку, не считая бесконечной поддержки. Получается, что попытка описать живой текст с помощью жестких алгоритмов неизбежно ведет к росту издержек на сопровождение, и ресурсы команды уходят не на развитие системы, а на бесконечную адаптацию правил под новые форматы данных.
Давайте посмотрим, как это работает на практике.
Регулярное выражение — это, по сути, однострочная программа с железной логикой. Пока данные идут ровно по заложенному шаблону, все работает. Но стоит тексту сделать шаг влево или вправо — и конвейер встает.
Например, вы написали регулярку, и она отлично вытаскивает Длина 150. Вы ее протестировали, все ок. Но на следующей неделе приходит накладная от нового поставщика, который привык писать Д. 150 или система оптического распознавания выдает Дочка 150. Парсер ломается.
В итоге, вместо чистого и предсказуемого механизма (т. е. набора грамматик/регулярок) приведения данных к классификатору как целевой структуры классов и атрибутов, приходится постоянно дописывать новые правила. В коде разрастается настоящий «зоопарк» из специфичных грамматик, исключений и «костылей», который со временем становится просто невозможно поддерживать.
Подход 2. Смена парадигмы: от написания кода к разметке данных
Мы устали от того, что каждый новый поставщик или малейшая опечатка ломают наш конвейер. Но еще несколько лет назад нейросетевые модели были требовательными к ресурсам и нуждались в сложной подготовке для обучения с нуля, поэтому мы опирались на классический machine learning.
Индустрия тем временем развивалась: появились предобученные нейросетевые архитектуры, которые сделали переход к новому подходу технически реализуемым, поэтому мы решили изменить сам подход. Вместо того чтобы бесконечно дописывать правила для каждого отклонения, мы перешли к нейросетевым моделям. Теперь мы могли просто показывать моделям пример, а они уже сами находили закономерности в тексте.
Как мы это собрали:
1. Взяли предобученную модель на базе модификаций BERT и дообучили ее на своих данных, чтобы решить задачу классификации.
2. Для выделения блоков с атрибутами (NER) универсальные решения оказались менее эффективными, чем наше, поэтому модели собственной архитектуры мы обучали с нуля.
3. Нейросетям мы отдали только поиск нужных кусков текста, а вот приведение значений к каноническому виду (например, превращение разговорного «деревянная» в строгое словарное «Дерево») мы оставили за жесткими правилами.
Время подготовки данных для одного класса сократилось с 4 дней до 1–2 дней. При этом для старта модели достаточно всего 100–200 записей, если они репрезентативно вариативны и покрывают основные паттерны написания (например, длина 150, Д. 150, d=150), а не состоят из однотипных дубликатов.
Здесь произошел главный сдвиг: мы перестали писать код и начали оцифровывать экспертизу. Разметку теперь мог выполнять не программист, знающий синтаксис регулярных выражений, а предметный эксперт (товаровед). Ему не нужно понимать, как внутри работает нейросеть. Достаточно открыть интерфейс и просто показать: «вот здесь длина, а вот здесь диаметр».
Мы, по сути, стали трансформировать опыт экспертов и их рутины в структурированные цифровые следы, т. е. формальное описание предметной области. А сами размеченные данные превратились в самостоятельный актив компании, который можно спокойно переиспользовать, если завтра мы решим сменить архитектуру моделей.
Подход 3. Искушение LLM и столкновение с реальностью
Локальные модели решили проблему скорости, но добавление новых классов все равно требовал сбора и разметки данных. И тут на рынок вышли большие языковые модели (LLM) с возможностями few-shot и zero-shot. Это было похоже на сказку: потри лампу — напиши промпт — и твое желание исполнено!
Мы решили передать модели сам классификатор в качестве контекста, т. е. скормить LLM готовую бизнес-экспертизу — описание классов, атрибутов и ограничений — и получить универсальный инструмент, которому для старта вообще не нужна предварительная разметка.
Первые наблюдения на данных нашего масштаба показывали, что подход работает. Поскольку LLM — это предобученные модели, которые, по сути, уже обладают частью экспертных знаний, необходимых для решения задачи нормализации, нам оставалось лишь дополнить эти знания через промпты. Поэтому настройка оказалась менее трудоемкой: описывать классы и атрибуты на естественном языке оказалось проще, чем собирать обучающие наборы, а при новых вариантах записи инструкции приходилось менять реже. При этом модель улавливала смысл требования без перечисления всех формулировок, которыми атрибут может встретиться во входных данных.
Но чем глубже мы погружались в проверку результатов на реальных данных, тем яснее становилось, что для промышленного сервиса этих «чудес» недостаточно.
Начнем с банальной скорости.
Инференс одной записи занимал около трех секунд, тогда как наш локальный ML-конвейер к тому моменту укладывался в 100 миллисекунд, т. е. в 30 раз быстрее. Если для единичного запроса в чате это незаметно, то при обработке миллионов строк номенклатуры это превратится в недели простоя. Конечно, можно попытаться распараллелить запросы, но по факту это означает, что обработка миллиона строк на LLM обойдется в 30 раз дороже по вычислительным ресурсам. Использовать тяжелую генеративную модель для извлечения подстроки — это как выпиливать из бревна зубочистку .
Кроме того, хотя некоторых заказчиков не смущает трансграничная передача данных, в enterprise-сегменте чаще всего приходится разворачивать решения внутри закрытого контура, поэтому облачные API сразу отпадали. Нам приходилось разворачивать локальные open-source модели ограниченного размера.
Здесь важно сделать техническое уточнение. Когда мы говорим о локальных моделях в нашем конвейере, мы не имеем в виду «урезанные» версии больших языковых моделей (малые LLM). Это архитектурно иной класс решений.
Большие языковые модели (LLM) по своей природе генеративны. Они предсказывают следующий токен, пытаясь сделать ответ максимально связным. Именно поэтому они склонны к галлюцинациям: если в строке не указана толщина стенки трубы, LLM может ее «додумать», основываясь на статистических закономерностях, чтобы ответ выглядел логично. Для творческих задач это фича, но для нормализации НСИ — критический баг.
Специализированные локальные ML-модели (например, архитектуры для NER — извлечения именованных сущностей) работают иначе. Они извлекающие, а не генерирующие. Модель анализирует входную текстовую последовательность и выделяет из нее фрагменты, присваивая им метки. Она физически не может выдать на выходе атрибут, которого не было во входной строке. Она действует не как соавтор, а скорее как внимательный контролер в рамках заданного классификатора.
Итак, мы пришли к выводу, что генеративный подход в локальном контуре не срабатывает. Все генеративные модели «догадываются» и галлюцинируют — вопрос лишь в ценности этой догадки. Фронтирные модели, опираясь на огромный объем знаний, выдают корректные предположения. Локальные же open-source LLM без жестких рамок часто уходят в откровенный вымысел, и чтобы заставить их работать предсказуемо, каждую ситуацию приходилось прописывать в промпте. Однако мы старались уйти от слишком детальных пояснений в промптах, чтобы не менять шило на мыло: трудоемкую подготовку большой выборки для обучения ML-модели на не менее трудоемкую детальную проработку промпта для каждого кейса.
Но даже с идеальным промптом начинались проблемы с самим форматом ответа. Один и тот же атрибут «длина» модель могла вернуть числом 150, строкой «диаметр 15 мм» или целым предложением «длина изделия составляет 15 миллиметров». Человеку понятно, о чем речь, а вот следующему этапу конвейера — слою стандартизации — приходилось разбирать этот хаос и учитывать все варианты, которые модель сочла уместным.
Хуже того, модель упорно пыталась заполнить атрибуты, которых в исходном тексте вообще не было. Мы пытались жестко запрещать это в инструкции, но результат был предсказуем: мы переписывали промпт, модель исправляла один кейс, но тут же ломала поведение на других записях.
В этот момент нас накрыло дежавю.
Мы снова пытались учесть все возможные отклонения и исключения, просто как в старые добрые времена с регулярными выражениями. Разница была лишь в том, что вместо синтаксиса regex мы использовали естественный язык. Промпт-инжиниринг оказался той же самой хрупкой грамматикой, только написанной словами.
За месяцы экспериментов мы пришли к выводу, что у LLM пока нет предметной экспертизы в узких доменах, поэтому они додумывают атрибуты. Следовательно, наивное представление о том, что в настоящее время достаточно скормить системе сырые данные и получить готовую карточку, в enterprise-среде, как, пожалуй, и в любой другой среде (по крайней мере, пока), не работает. Качество результата напрямую зависит от контекста вокруг запроса. И готовить этот контекст нужно с той же тщательностью, с какой раньше мы собирали обучающую выборку.
Я даже нашел подходящую теорию, чтобы как-то понять непредсказуемость модели: семантический треугольник Огдена-Ричардса. Если представить упрощенно, у любого слова есть три грани: само слово (термин), его смысл (концепт) и реальный физический объект, который оно описывает (денотат).
LLM живет только в мире терминов и концептов. Она отлично улавливает, как слова связаны в тексте, но физическую «трубу с толщиной стенки 3 мм» она не видит. Она опирается исключительно на тот контекст, который мы ей предоставили. А связь слова с реальным объектом удерживается жесткими правилами. Если этих правил в промпте нет, модель теряет опору и начинает угадывать смысл. Для демо на слайде этого достаточно. Но когда результат обработки попадает в учет запасов или управленческую отчетность, любое «угадывание» превращается в проблему. Здесь не нужна креативность, здесь нужна строгая детерминированность.
Гибридный low-code конвейер
Я уже писал, что мы не стали выбирать между «старым добрым ML» и «модным ИИ», и уж точно не стали отдавать весь процесс на откуп одной универсальной модели. Вместо этого мы собрали конвейер, где каждый инструмент занимается своим делом: нейросети распознают образы и классифицируют, а жесткие правила гарантируют, что на выходе не будет хаоса.
В рамках такой универсальной модели на данном этапе мы разделили эти обязанности следующим образом:
Задача | Выбранный подход | Метрики |
Классификация позиции | Дообученная модель (BERT) | Точность > 95% |
Выделение блоков с атрибутами | Локальные модели (Custom NN) | Инференс ~100 мс |
Приведение значений к каноническому виду | Правила стандартизации | Детерминированный результат |
Проверка результата | Пользователь в MDM + пост-валидация | Итоговая точность 98% |
Отдельно хочу отметить, что такой композитный подход не только дал нужную точность, но и оказался крайне нетребовательным к железу. Нам не понадобилось покупать ферму видеокарт, потому что решение полностью работает на CPU и нам достаточно обычного сервера с 8 ядрами и 16 ГБ RAM. Данные при этом не покидают контур заказчика.
Для сборки и оркестрации этого конвейера мы архитектурно разделили решение на две независимые части: переиспользуемое ядро (непосредственно ML-инференс) и надстройку с бизнес-логикой (интеграции, проверки, обогащение).
Эту надстройку мы реализовали на нашей аналитической low-code платформе Loginom, которая обеспечила визуализацию надстройки и ее доступность. Визуальное представление сценариев дало возможность отслеживать трансформацию данных на каждом этапе без необходимости отлаживать исходный код. В итоге сопровождение и настройка правил стали доступны не только разработчикам, но и бизнес-аналитикам, а также профильным специалистам по НСИ. При этом аналитики и специалисты по НСИ могут визуально посмотреть на данные в каждой точке сценария, отследить трансформацию от входа к выходу и найти ошибку, не копаясь в исходном коде.
На практике это означает, что если завтра у заказчика изменится формат интеграции с MDM или добавится новый источник данных, аналитик просто пересоберет сценарий из готовых компонентов за несколько часов, а не будет отлаживать код неделями.
Главный инсайт: предсказуемость ошибок
Как измерить успешность нормализации? Мы считаем запись правильной, только если верно определен класс и все ее атрибуты. Одно пропущенное значение или лишний символ — и вся карточка уходит в брак.
Работу с новым классом мы всегда начинаем с валидационного набора. В среднем это 120 записей. С первого прогона модель или правила корректно обрабатывают 70–80% данных. Дальше вступает в работу обратная связь: мы смотрим на ошибки, добавляем примеры в разметку или уточняем словари. Со второй итерации мы, как правило, преодолеваем порог в 95%, а итоговый средний результат по уже обработанным 100+ классам составляет 98%.
Цифры радуют, но главный инсайт, который мы вынесли из этого проекта — это предсказуемость исправления ошибок.
В случае ошибки локальной модели мы просто берем и добавляем 10 новых примеров в обучающую выборку. Если ошибка в правилах стандартизации — корректируем словарь. Путь от замечания до исправления абсолютно прозрачен, логичен и детерминирован.
Совсем иначе обстоит дело с LLM. Здесь мы сталкиваемся с их стохастической природой: ответы на один и тот же запрос могут отличаться. В одном запуске модель выдаст ошибку, а в другом — правильный ответ. А для диагностики и устранения багов в enterprise-системах принципиально важна воспроизводимость. Вы не просто гадаете, какое именно слово в промпте нужно изменить, чтобы исправить конкретный кейс и не сломать остальные, вы еще и не можете быть уверены, что это изменение сработает стабильно при следующем прогоне.
В нашем конвейере действует «презумпция виновности» модели. По умолчанию мы не доверяем результатам автоматического разбора.
Важно уточнить: в текущем промышленном контуре мы используем именно специализированные ML-модели (второй вариант решения), а не генеративный ИИ, который пока остается на этапе экспериментов. Тем не менее, даже высокоточный ML-результат сначала проходит жесткую пост-проверку бизнес-правилами и словарями стандартизации. И только если данные успешно прошли этот фильтр, они считаются валидными. Если же возникают сомнения или срабатывают исключения — карточка отправляется на ручную проверку к эксперту.
Что дальше?
Результаты таких проектов показывают текущий расклад сил. В самом конвейере обработки промышленного трафика большим языковым моделям пока тесно из-за требований к скорости и детерминизму, зато они становятся отличными ассистентами, которые отлично выполняют подготовительные и творческие задачи. На данный момент LLM в нашей архитектуре — это тот самый джинн на подхвате, который отлично помогает на подготовительном этапе: размечает данные, генерирует черновики правил и подсказывает варианты для экзотических описаний.
Специализированные локальные ML-модели (в нашем контексте — узкоспециализированные алгоритмы, натренированные на конкретном датасете для решения одной строгой задачи с фиксированным форматом входа, например, дообученные BERT или кастомные архитектуры для извлечения сущностей) берут на себя основной объем рутинных операций: они быстрые, работают на обычном CPU и, в отличие от универсальных генераторов, физически не могут «галлюцинировать» и выдумать атрибуты, которых нет в исходной строке.
Однако и этот баланс не прибит гвоздями. Гибридная архитектура подразумевает поиск оптимальной комбинации технологий, которая сильно зависит от их текущих параметров. Как только скорость и точность локальных LLM вырастут, а стоимость инференса упадет, сфера применения LLM расширится и джинн может вырваться из лампы. И мы к этому готовимся, распределяя обязанности так, чтобы система оставалась гибкой.
Коллеги, а как у вас выстроена граница между детерминированным кодом и вероятностными моделями в задачах очистки данных? Приходилось ли вам откатываться от чистых LLM в пользу специализированных ML-моделей (или малых локальных архитектур), когда сталкивались с жесткими требованиями по скорости или необходимостью работать в закрытом контуре?
Делитесь своими граблями и находками в комментариях — давайте обсудим, где в ваших пайплайнах заканчивается магия ИИ и начинается суровая реальность продакшена.
Теоретическая база (для тех, кто хочет копнуть глубже):
Тезисы, использованные в статье, опираются на работы Ю.П. Липунцова (завкафедрой экономической информатики МГУ) по разработке информационных архитектур экосистем:
Разрыв между архитектурой деятельности и информационным слоем.
Концепция «цифровых следов» как основы для интеграции данных.
Трансакционные издержки при формализации взаимодействий (влияние Р. Коуза на ИТ-архитектуры).
Подход, основанный на знаниях: рутины как гены организации и их оцифровка.
Семантический треугольник Огдена-Ричардса в контексте коммуникативных актов и потери денотата при стохастической генерации.
Платформенная архитектура и стандартизация интерфейсов для снижения издержек на сопровождение.

