О проекте. Я разрабатываю систему ИИ-поддержки первой линии — «Финлоджик. Контур Поддержки» (FinlogiQ AI Support). Бот принимает обращения через веб-чат и Telegram, понимает суть вопроса, ищет ответ в базе знаний и передаёт сложные случаи живому оператору. Делаю один: начиналось как заказная разработка под клиента, затем выросло в самостоятельный продукт. Это первая статья из цикла о внутреннем устройстве системы.
Пара терминов: LLM (large language model) — большая языковая модель, нейросеть вроде YandexGPT или DeepSeek; RAG (retrieval-augmented generation) — подход, когда боту перед ответом подкладывают найденный в базе знаний фрагмент текста.
Когда строишь чат-бота поддержки на больших языковых моделях, самый очевидный путь выглядит обманчиво простым: берём вопрос пользователя, вытаскиваем из базы знаний подходящий кусок документации, заворачиваем всё это в запрос к модели и отправляем. На демо и на небольшом объёме тестов это работает прекрасно. Но стоит выкатить систему в реальную эксплуатацию, как схема начинает давать сбои.
Модель ошибается на пограничных формулировках, путает похожие продукты или тарифы, уверенно придумывает несуществующие функции там, где нужно было просто переспросить, и вдобавок стоит денег на каждом запросе — включая банальные «здравствуйте» и «спасибо», где правильный ответ известен заранее.
При проектировании системы я с самого начала заложил обратный принцип: вопрос пользователя должен доходить до LLM в самую последнюю очередь. Сначала задачу пытаются решить жёсткие, полностью контролируемые слои с заранее заданной логикой, и только тот остаток, который они не смогли однозначно закрыть, передаётся нейросети.
Ниже — как устроен этот конвейер (pipeline) и почему такая гибридная архитектура на практике оказалась заметно стабильнее «чистого» RAG.
Доменная модель вместо плоского индекса
Главная ошибка классического RAG — относиться к базе знаний как к плоскому набору текстов или векторов. Я отказался от этой идеи и ввёл ключевую структурную единицу — причину обращения (ContactReason). Это строго описанный объект, который охватывает один конкретный класс проблем пользователя.
Внутри каждой причины заданы:
Многоуровневые маркеры (
verbs,nouns,numeric_tags,phrase_masks) — наборы слов, чисел и фраз для точного распознавания темы.Специфичные правила классификации, удержания контекста и условия перевода на оператора.
Тематические разделы с точечными парами «вопрос — ответ».
Прямые эталоны ответов (
ExampleQA) для обработки точных совпадений.Флаг полного контекста (
full_context_llm), который решает, нужно ли вообще подключать тяжёлую модель.
При таком подходе векторный поиск — это не фундамент системы, а лишь один из вспомогательных инструментов. Вся маршрутизация опирается на строгую бизнес-логику.
Конвейер принятия решения: от L0 до L3
Каждое входящее сообщение проходит сквозной конвейер. Его цель — закрыть обращение как можно быстрее, дешевле и предсказуемее.
L0: Глобальная эскалация
Прежде чем запускать алгоритмы классификации, система прогоняет текст по критическим признакам. Угрозы, юридические претензии, требования позвать регулятора или прямые жёсткие просьбы «переключи на человека» не должны анализироваться моделью. Система не тратит время и сразу переводит диалог на оператора.
L1: Скоринг по маркерам
На этом этапе работает собственный движок сопоставления. У каждого типа маркеров свой вес:
phrase_mask = 10 # Фразовые маски и регулярные выражения numeric_tag = 5 # Числовые теги, коды ошибок noun = 2 # Существительные (ключевые сущности) verb = 1 # Глаголы (действия) global_min_score = 5 # Минимальный порог для прохождения
Логика простая: уникальная фразовая маска или конкретный код ошибки — это в разы более надёжный сигнал, чем общие слова вроде «не работает» или «сломалось». Если алгоритм не видит явного лидера или разрыв между двумя похожими причинами слишком мал, система не угадывает ответ наугад. Она передаёт управление дальше — либо на уточняющий вопрос, либо на классификацию через LLM.
LLM как «санитар» текста. Если сообщение пользователя перегружено шумом (приветствия, эмоциональные отступления, куча опечаток), я могу подключить языковую модель. Но не для генерации ответа, а только для нормализации текста. Очищенный от мусора запрос повторно прогоняется через тот же прозрачный классификатор L1.
L1.1 – L1.5: Локальные правила причины
У каждой отдельной причины ContactReason могут быть свои жёсткие условия: повышенный порог прохождения, требование обязательного маркера (например, ИНН или номера договора) или собственный сценарий мгновенного перевода на специалиста.
L2: Поиск ответа внутри причины
Когда причина обращения определена, система ищет ответ внутри неё по строгому порядку приоритетов:
ExampleQA (прямые совпадения): если вопрос пользователя совпадает с заготовленным шаблоном (совпадение ≥ 0.7), бот мгновенно отдаёт заранее выверенный текст. Никаких обращений к нейросети, никакой задержки, полная стабильность и ноль затрат.
Complaint (типовые жалобы): срабатывает при совпадении с известными формулировками недовольства (совпадение ≥ 0.6) и возвращает выверенную реакцию.
ThematicSection (тематические разделы): считается комбинированная оценка:
0.3 * средний балл всего подраздела + 0.7 * лучший найденный вопрос-ответ.
L3: Финальная генерация через LLM
Сюда попадают только сложные, нестандартно сформулированные вопросы, которые не удалось перехватить раньше. Но важная деталь: модель получает на вход не всю огромную базу знаний компании, а строго ограниченный, точечный контекст, отобранный на шагах L1 и L2. Это сводит вероятность выдумок практически к нулю.
Почему гибридный подход выигрывает у чистого RAG
Переход от идеи «спросим модель обо всём» к жёсткому конвейеру дал три измеримых результата:
Предсказуемость. Все типовые, частые вопросы всегда получают один и тот же выверенный ответ. Качество первой линии больше не зависит от случайных колебаний в ответах модели или со стороны провайдера.
Экономия и скорость. Большой пласт рутинных обращений закрывается на слоях L1–L2 вообще без запросов к LLM. Это снижает среднюю стоимость обращения и даёт мгновенный отклик в интерфейсе.
Управляемость. Если бот ответил неправильно, у меня есть понятная точка вмешательства. Не нужно переписывать огромный системный запрос к модели в надежде, что она поймёт скрытый смысл. Я просто добавляю перефразировку в
ExampleQA, точнее настраиваю маркеры на уровне L1 или дополняю базу знаний конкретной парой «вопрос — ответ». Это предсказуемая работа со структурой, а не алхимия.
Главный вывод
Большая языковая модель — это мощный, но дорогой и непредсказуемый исполнитель. Если выпустить её на передовую без жёстких рамок, она неизбежно начнёт ошибаться там, где ошибаться нельзя.
Мой опыт разработки «Контура Поддержки» показал: сначала нужно выстроить предсказуемую структуру данных, и только потом подключать генеративные модели. Модель должна включаться лишь там, где её способность понимать живую человеческую речь действительно незаменима. Во всех остальных случаях правила, структура и логика бэкенда справляются быстрее, дешевле и надёжнее.
