О проекте. Я разрабатываю систему ИИ-поддержки первой линии — «Финлоджик. Контур Поддержки» (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: Поиск ответа внутри причины

Когда причина обращения определена, система ищет ответ внутри неё по строгому порядку приоритетов:

  1. ExampleQA (прямые совпадения): если вопрос пользователя совпадает с заготовленным шаблоном (совпадение ≥ 0.7), бот мгновенно отдаёт заранее выверенный текст. Никаких обращений к нейросети, никакой задержки, полная стабильность и ноль затрат.

  2. Complaint (типовые жалобы): срабатывает при совпадении с известными формулировками недовольства (совпадение ≥ 0.6) и возвращает выверенную реакцию.

  3. ThematicSection (тематические разделы): считается комбинированная оценка: 0.3 * средний балл всего подраздела + 0.7 * лучший найденный вопрос-ответ.

L3: Финальная генерация через LLM

Сюда попадают только сложные, нестандартно сформулированные вопросы, которые не удалось перехватить раньше. Но важная деталь: модель получает на вход не всю огромную базу знаний компании, а строго ограниченный, точечный контекст, отобранный на шагах L1 и L2. Это заметно снижает вероятность выдумок, хотя и не устраняет её полностью.

Почему гибридный подход выигрывает у чистого RAG

Переход от идеи «спросим модель обо всём» к жёсткому конвейеру дал три измеримых результата:

  1. Предсказуемость. Все типовые, частые вопросы всегда получают один и тот же выверенный ответ. Качество первой линии больше не зависит от случайных колебаний в ответах модели или со стороны провайдера.

  2. Экономия и скорость. Большой пласт рутинных обращений закрывается на слоях L1–L2 вообще без запросов к LLM. Это снижает среднюю стоимость обращения и даёт мгновенный отклик в интерфейсе.

  3. Управляемость. Если бот ответил неправильно, у меня есть понятная точка вмешательства. Не нужно переписывать огромный системный запрос к модели в надежде, что она поймёт скрытый смысл. Я просто добавляю перефразировку в ExampleQA, точнее настраиваю маркеры на уровне L1 или дополняю базу знаний конкретной парой «вопрос — ответ». Это предсказуемая работа со структурой, а не алхимия.

Главный вывод

Большая языковая модель — это мощный, но дорогой и непредсказуемый исполнитель. Если выпустить её на передовую без жёстких рамок, она неизбежно начнёт ошибаться там, где ошибаться нельзя.

Мой опыт разработки «Контура Поддержки» показал: сначала нужно выстроить предсказуемую структуру данных, и только потом подключать генеративные модели. Модель должна включаться лишь там, где её способность понимать живую человеческую речь действительно незаменима. Во всех остальных случаях правила, структура и логика бэкенда справляются быстрее, дешевле и надёжнее.

Комментарии (4)


  1. ENick
    29.06.2026 17:55

    """выстроить предсказуемую структуру данных, """ это реализация алгоритма "Ключ" - "Значение"?


    1. drkfx Автор
      29.06.2026 17:55

      Частично — да, но это только один из слоёв. Прямые совпадения (ExampleQA) действительно работают как key-value: точное совпадение → готовый ответ. Но «структура» шире: над этим есть доменная модель причин обращения со взвешенными маркерами (фразы, числовые теги, существительные, глаголы) и каскад приоритетов L0→L3. То есть key-value закрывает точные попадания, а всё неоднозначное уходит на классификацию и только в крайнем случае в LLM. Если интересно — как раз про устройство этих слоёв будут следующие статьи цикла.


  1. Feargin
    29.06.2026 17:55

    В целом мысль классная. Модель не обязана отвечать на каждый чих, если часть сценариев быстрее и надежнее закрывается правилами, поиском, шаблонами или прямой выборкой.

    Проблема обычно не в RAG как таковом, а в наивном RAG: “нашли похожий кусок, отдали модели, надеемся на лучшее”. В реальных продуктах почти всегда приходится комбинировать подходы.

    Я сейчас как раз разрабатываю продукт, где тоже приходится искать разные способы оптимизации: графы связей, умный RAG, прямую выборку, фильтры, контекстные ограничения (и по факту пока не нашел идеального баланса). По опыту это больше похоже не на спор “LLM против правил”, а на сборку нормального конвейера: где-то модель действительно нужна, а где-то обычная логика работает быстрее, дешевле и более предсказуемо.

    Единственное, я бы аккуратнее формулировал про “почти ноль галлюцинаций”. Риски снижаются, но не исчезают. Тут многое от модели зависит.


    1. drkfx Автор
      29.06.2026 17:55

      Спасибо, очень в точку — особенно про «не LLM против правил, а сборка нормального конвейера». У меня ровно то же ощущение: идеального баланса нет, есть набор инструментов под разные классы запросов, и graph/smart-RAG/прямая выборка живут вместе, а не вместо друг друга. И насчёт «почти ноль галлюцинаций» — согласен, формулировка слишком сильная. Корректнее: жёсткий контекст и предварительная классификация заметно снижают риск, но не убирают его полностью — остаточная зависимость от модели есть всегда. Поправлю акцент в тексте, спасибо за замечание. А что у вас за продукт, если не секрет? Интересно, к какому балансу графов и RAG вы в итоге склоняетесь.