Привет, Хабр! На связи команда CVM (Customer Value Management) B2B из МТС — техлид Артём Каледин и ML‑инженер Александр Швайко. 

Менеджер должен быстро разбираться, как строить обсуждение с техническим директором компании. Но даже если информация о продукте есть, иногда бывает сложно быстро найти нужные материалы и выбрать плюсы для конкретного клиента. Раньше коллеги готовились к встречам с помощью Telegram‑бота, но недавно от бизнеса поступила задача: сократить время на подготовку и повысить качество рекомендаций и контента. 

В статье по мотивам доклада на Inside AI Meetup расскажем про архитектуру нашей системы и этапы работы, поделимся результатами и планами, а еще — как построили надежный RAG, который не галлюцинирует (ну, почти).

Что было до RAG

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

  1. Разведка по ИНН, то есть сбор информации о компании из внутренних систем.

  2. Подбор продуктов под клиента с помощью классических моделей.

  3. LLM‑комментарии — статические подсказки на основе шаблонов.

  4. Готовые сценарии диалога под типовые задачи клиентского менеджера.

Интерфейс представлял собой привычный Telegram‑бот с кнопками, рекомендациями и механизмом обратной связи через лайки и дизлайки.

Часть информации поступала из внутренних источников, другая — обогащалась внешними порталами и веб‑поиском. Но в какой‑то момент к нам пришли безопасники и сказали: «Ай‑ай‑ай, так делать нехорошо, нельзя брать данные извне». Пришлось полностью пересмотреть подход и перейти на работу исключительно с внутренними источниками знаний.

Какую задачу нам поставил бизнес

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

Воронка продаж B2B
Воронка продаж B2B

Например, если необходимо предложить клиенту продукт MWS GPT, менеджер должен быстро разобраться в сути решения и решить, как строить обсуждение с техническим директором компании. Даже если информация о продукте есть, менеджеру бывает сложно быстро найти нужные материалы и выбрать плюсы, которые продадут инструмент именно этому клиенту.

Из этой точки были определены две цели:

  1. Повысить эффективность продавцов через сокращение времени на подготовку ко встречам и на сопровождение сделок.

  2. Повысить конверсии из первичного контакта в продажу за счёт качества рекомендаций и контента для продавцов.

Совместно с бизнесом мы собрали список метрик, которые и попытались улучшить с помощью нашего подхода:

  • сокращение времени на подготовку ко встрече;

  • конверсии в продажу;

  • оценка релевантности рекомендаций продавцами;

  • готовность клиентов рекомендовать продавцов (NPS);

  • общая удовлетворённость;

  • время ответа Co‑Pilot.

Подробнее про задачу

Что нам предстояло сделать? Превратить Telegram‑бота в ассистента клиентского менеджера, который отвечает на вопросы по 100+ B2B‑продуктам со ссылками на источник в документации и тем самым ускоряет подготовку ко встречам.

У нас было три требования:

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

  2. Информация поступает только из базы. Данные отсутствуют на внутренних ресурсах — честно говорим «не нашел».

  3. Каждый ответ подкрепляется ссылкой на так называемые хлебные крошки, то есть путь до фрагмента в структуре документа. Менеджер должен иметь возможность проверить данные или углубить свои знания.

Как выглядят данные и система знаний

Чтобы отвечать на вопросы, системе нужны данные. И вот с чем мы работаем.

Слева – как КМ ищет в СУЗ сегодня. Справа – как будет выглядеть с ассистентом
Слева — как КМ ищет в СУЗ сегодня. Справа — как будет выглядеть с ассистентом

Кажется, ничего сложного. Стандартная Wiki‑страница с таблицами, схемами, PDF и вкладками. Но у некоторых продуктов вложенность до шести уровней, наш рекорд с такой структурой — 77 подстраниц. Как во всем этом хранилище найти нужный ответ с правильной ссылкой? Как понять отличие продукта А от продукта Б и узкую специфику продукта В?

Сначала мы нашли команду, которая отвечает за внутреннюю базу знаний. Оказалось, что у них уже есть собственный RAG, но нет исходных данных — обычного текста в том виде, в котором он хранится в базе знаний. Как быть? Парсить.

77 подстраниц на один продукт, до 6 уровней вложенности, такую структуру нельзя просто «скормить» LLM. API есть, но он бесполезен
77 подстраниц на один продукт, до 6 уровней вложенности, такую структуру нельзя просто «скормить» LLM. API есть, но он бесполезен

Наши методы парсинга и предобработки:

  1. Playwright — мощный движок для автоматизации браузера. Позволяет работать с динамическим контентом (JS), эмулировать действия пользователя и обходить SPA‑ограничения.

  2. Crawl4AI — это Playwright на стероидах для RAG. Специализирован на быстрой выгрузке веб‑страниц сразу в чистый Markdown, оптимизированный для контекста LLM.

  3. Markdownify — конвертер HTML в Markdown. Используется для финальной очистки данных от мусора (скрипты, стили, навигация) и приведения их к компактному виду.

Сначала мы выбрали инструмент автоматизации браузера Selenium. Он справлялся со своей задачей, но в 2026 году хочется использовать более современные инструменты. Например, Crawl4AI, который позволяет загрузить структуру документа или предоставить несколько примеров, а затем самостоятельно обучается их разбирать и умеет обходить ограничения. Мы работали исключительно с внутренним корпоративным сайтом, поэтому никаких ограничений со стороны системы безопасности не возникало.

После извлечения данных встала задача преобразования HTML в удобный формат для дальнейшей обработки. Для этого применили библиотеку Markdownify, которая конвертирует HTML в Markdown. Этот формат можно использовать для векторизации без дополнительной подготовки.

Четыре стадии архитектуры: от сырого Markdown до оцененного ответа

Верхнеуровневая архитектура решения
Верхнеуровневая архитектура решения

Теперь перейдем к самой архитектуре. На схеме видно, что она включает большое количество внутренних сервисов и вспомогательных компонентов. В рамках статьи мы рассмотрим только ту часть, которая отвечает непосредственно за работу RAG. Она выделена красной рамкой на визуализации выше.

Архитектура состоит из четырех основных блоков:

  1. Подготовка данных (Preprocessing).

  2. Загрузка данных в векторную базу (Ingestion).

  3. Поиск релевантных документов (Retrieval).

  4. Генерация ответа (Generation & Evaluation).

Карта пайплайна. Четыре стадии: подготовка данных, инджест, поиск, генерация и оценка
Карта пайплайна. Четыре стадии: подготовка данных, инджест, поиск, генерация и оценка

На этапе подготовки мы собираем исходные данные и приводим их к единому виду. После этого каждый документ разбиваем на чанки и загружаем в векторную базу данных.

Когда пользователь отправляет запрос, мы ищем подходящие фрагменты сразу несколькими способами: с помощью векторного и полнотекстового поиска — то есть гибридный поиск. Затем объединяем найденные результаты, ранжируем их и определяем наиболее релевантные документы. Последним этапом модель генерирует готовый ответ на запрос пользователя.

Получение, предобработка и загрузка данных по совести

Как вы уже поняли, страница в системе управления данными — это огромный массив со сложной иерархией. Поразмыслив, мы пришли к структуре, где заголовок первого уровня в Markdown‑формате соответствует исходному продукту, а последующие уровни отражают вложенность страниц и разделов.

Затем сырой Markdown‑документ проходит дополнительную предобработку в два этапа.

Первый этап — добавление «хлебных крошек». Они позволяют не потерять информацию о том, откуда был взят конкретный фрагмент текста.

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

Линеаризация таблиц + убираем URL+ breadcrumbs: делаем каждый чанк самодостаточным
Линеаризация таблиц + убираем URL+ breadcrumbs: делаем каждый чанк самодостаточным

После предобработки документы загружаются в векторную базу данных. Для этого разбиваем данные на чанки в два этапа.

Сначала формируются parent‑чанки — крупные фрагменты документа, обычно соответствующие отдельным разделам или подстраницам.

Затем каждый такой фрагмент дополнительно разбивается на небольшие child‑чанки, по которым впоследствии и выполняется поиск.

После этого child‑чанки преобразуются в эмбеддинги с помощью модели BGE‑M3 и сохраняются с расширением pgvector. Помимо самих векторов, в pgvector хранятся текст чанка и дополнительная метаинформация.

Берём подготовленный текст и кладём его туда, где потом будем искать
Берём подготовленный текст и кладём его туда, где потом будем искать

Весь процесс мы реализуем в Langflow! Пайплайн логически разделён на две части: верхняя отвечает за обработку parent‑чанков, нижняя — за создание child‑чанков и их векторизацию.

Вот так выглядит исходный пайплайн для загрузки данных
Вот так выглядит исходный пайплайн для загрузки данных

Langflow — очень удобен сам по себе, но для реализации нашей логики понадобилось написать несколько собственных компонентов. Так в базе данных появились две связанные таблицы с parent‑чанками и child‑чанками.

Теперь эти чанки можно искать!
Теперь эти чанки можно искать!

Поиск, Reranker и главный инсайт

Пайплайн Retrieval также состоит из четырех этапов. Простой поиск по векторам достает 60–70% нужного. Цепочка из четырех шагов повышает качество до 85–90%.

Retrieval – это не один шаг, а цепочка
Retrieval — это не один шаг, а цепочка

Первый этап — перегенерация запроса. Система получает пользовательский запрос и создаёт четыре переформулировки с помощью языковой модели. Такой подход позволяет увеличить полноту поиска (Recall), поскольку один и тот же вопрос может быть сформулирован разными способами.

Один вопрос → несколько формулировок = увеличиваем recall
Один вопрос → несколько формулировок = увеличиваем recall

Второй этап — гибридный поиск. Ищем параллельно по смыслу (Vector) и по словам (BM25), чтобы закрывать разные типы запросов.

Два поиска: по смыслу (Vector) + по словам (BM25) = закрываем разные типы запросо
Два поиска: по смыслу (Vector) + по словам (BM25) = закрываем разные типы запросо

Третий этап — объединение результатов. Результаты двух поисков объединяем в общий массив с помощью алгоритма Reciprocal Rank Fusion (RRF).

Четвёртый (и самый дорогой) этап — реранжирование. Массив уходит в Reranker, и на выходе мы получаем список наиболее релевантных кандидатов. Например, из 20 найденных чанков Reranker оставляет пять, которые гарантированно содержат ответ на запрос.

Зачем нужен Reranker

На третьем этапе, после объединения результатов с помощью RRF, мы получили ТОП-20 подходящих чанков. Однако Retrieval решил только первую часть задачи — из тысяч документов вытащил ТОП-20 кандидатов, которые с наибольшей вероятностью содержат ответ на запрос пользователя.

Задача Reranker — сузить поле поиска и расположить найденные фрагменты в порядке их реальной релевантности.

Что изменилось после реранкера?
Что изменилось после реранкера?

Смотрим на конкретном примере. После гибридного поиска формируется общий список чанков, и RRF объединяет результаты в ранжированный список. На первом месте оказывается чанк «Автосекретарь. Главное». После повторного ранжирования он смещается на третью позицию, а первое место занимает раздел «Сравнение продуктов», который содержит значительно более релевантную информацию.

Есть два подхода к реранжированию, которые мы сравниваем.

Первый подход — Cross‑encoder (BGE), на котором был построен наш изначальный вариант разработки. Модель читает пару (вопрос, документ) целиком и оценивает, насколько они совпадают по смыслу. Для работы нужна нейросеть и терпение, потому что подход медленный, но точный.

Локально эта модель показывала очень хорошие результаты, и качество ранжирования нас полностью устраивало. Однако при переносе решения в инфраструктуру МТС возникли ограничения, которые мы впоследствии побороли. Библиотека FlagEmbedding не содержала необходимой реализации, поэтому пришлось изобретать костыли.

Второй подход — Metadata‑эвристики. Здесь учитываются только теги документа (продукт, категория, тип секции). Нейросеть не требуется, Metadata выдает результат за миллисекунды, но ответ достаточно грубый.

В проде FlagEmbedding недоступен — используем metadata‑реранкер. Что это меняет в рамках одного вопроса? FlagEmbedding вывела на первое место раздел «Сравнение продуктов», и модель сгенерировала ответ с почти идеальными метриками. В то же время наш метод Reranker выводил на первое место раздел «Автосекретарь. Контакты и общие вопросы». Этот чанк не содержал нужного контекста, метрики стремились к нулю.

Один вопрос — две версии
Один вопрос — две версии

Оцениваем метрики

Локальная версия с BGE CrossEncoder показала заметно лучшие результаты практически по всем показателям. В проде большинство метрик значительно упало, но достоверность ответа (Faithfulness), наоборот, выросла.

Сравнение локал (BGE-Reranker) и прод (metadata-эвристики)
Сравнение локал (BGE‑Reranker) и прод (metadata‑эвристики)

Чем мы это объясняем? Модель стала чаще отвечать: «Информация в документации не найдена». Если нужных сведений действительно нет в базе знаний, такой ответ считается технически корректным, при этом контекстуальные метрики падают.

Почему метрики ведут себя странно? Мы нашли четыре причины.

  1. Значения RRF score схлопываются. Итоговые оценки оказываются слишком близкими друг к другу, буквально схлопываются в единицу и порядок решают только мелкие эвристические поправки ±0,05.

  2. Intent штрафует правильное. Например, при наличии слова «тариф» система искусственно штрафует раздел «Главное», хотя именно он может содержать нужную информацию.

  3. Fallback general бустит мусор. Секции без категории получают метку general, например, и страница «Контакты» (справочник телефонов) выходит в топ для вопросов «чем X отличается от Y».

  4. Dedup‑by‑parent фиксирует ошибку. После сортировки берем первый чанк на каждый parent_id. Если Reranker ошибся в порядке — неправильный parent фиксируется, правильный выбывает навсегда.

Теперь посмотрим на итоговые версии, которые мы получили через BGE‑модель. Для оценки было подготовлено 72 вопроса. Часть составлена вручную, часть — сгенерирована автоматически. Качество ответов также оценивалось с помощью LLM.

3 продукта, 5 категорий, 72 вопроса. Judge на той же Qwen3 с калиброванными промптами
3 продукта, 5 категорий, 72 вопроса. Judge на той же Qwen3 с калиброванными промптами

Разбор ошибок позволил выделить три проблемы.

Первая проблема — ошибка сравнения. Retrieval извлекал чанки, связанные со скидками и акциями, хотя вопрос касался различий между продуктами. Модель получала неподходящий контекст и галлюцинировала.

Retrieval достаёт не ту секцию — LLM выдумывает конкретные цифры, чтобы закрыть пробел
Retrieval достаёт не ту секцию — LLM выдумывает конкретные цифры, чтобы закрыть пробел

Вторая проблема — сама галлюцинация. Когда пользователь задает исходно‑ложный запрос, модель должна ответить, что подобной информации в документации нет. Но наша модель отвечала утвердительно и генерировала ложную информацию. Эта проблема решается, если в системный промпт исходной модели добавить вопросы с отрицанием.

Negation-вопросы — слабое место. Модель отвечает «да», когда правильный ответ — «только обходным путём»
Negation‑вопросы — слабое место. Модель отвечает «да», когда правильный ответ — «только обходным путём»

Третья проблема — ошибка автоматического оценщика. Сценарий оказался связан не с самим RAG, а с системой автоматической оценки. Например, на вопрос о ESS‑маркировке в автосекретаре модель сформировала очень большой и содержательный ответ, полностью соответствующий найденному контексту. Однако оценщик ожидал короткий ответ, основанный на маленьком контексте, и занизил итоговую оценку.

Модель может иногда выдавать интересные ответы, больше, чем ожидал оценщик
Модель может иногда выдавать интересные ответы, больше, чем ожидал оценщик

Разбираем пайплайн на Langflow

Смотреть демо

Карта пайплайна
Карта пайплайна

На этапе загрузки данных используется уже рассмотренный ранее пайплайн. Часть элементов мы реализовали самостоятельно, поскольку их не было в библиотеке Langflow.

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

Исходный запрос автоматически переформулируется в альтернативные варианты → выполняется гибридный поиск → результаты передаются в Reranker → остается три наиболее релевантных child‑чанка и по ним восстанавливаются соответствующие parent‑чанки → выполняется дедупликация, чтобы не передавать модели дубляжи → итоговый промпт отправляется в LLM → генерируется финальный ответ.

Нужно объяснить, почему мы выбрали именно Langflow.

Во‑первых, у нас уже есть продовый Langflow, куда мы просто загрузили подготовленную JSON‑схему и гиперпараметры. После этого система сразу готова принимать пользовательские запросы и возвращать ответы в интегрированные системы.

Во‑вторых, к интерфейсу можно предоставить доступ пользователям, которые протестируют систему, зададут вопросы и оставят обратную связь.

Спидран по итоговой архитектуре

Данные преобразуются в Markdown, таблицы приводятся к линейному виду, добавляются хлебные крошки, удаляются ссылки и выполняется дополнительная предобработка. После этого данные загружаются в векторную базу и разбиваются на parent‑ и child‑чанки.

Если у вас есть ресурс, можно искать не по одному запросу, а по комбинации оригинального и модифицированных. Это позволяет находить больше релевантных документов. Например, один и тот же продукт могут называть по‑разному: «Автосекретарь 2.0», «автосек», «секретарь» и так далее. Различные формулировки повышают полноту поиска.

Далее используем гибридный поиск для объединения и ранжирования кандидатов, чтобы из тысяч вариантов остались десятки наиболее релевантных. После этого умный Reranker выполняет точное ранжирование, оно добавляется в промпт, и LLM формирует итоговый ответ. Все результаты логируются для последующего анализа качества и расчета метрик.

Выводы и наши планы

В ближайших планах — масштабировать число продуктов, интегрировать полноценный Reranker в инфраструктуру вместо текущей реализации. А для этого нужно починить провалы, расширить покрытие и подготовиться к проду. 

Отдельная задача на этом пути — проверить, как технические улучшения отражаются на бизнес‑метриках, с которых мы начинали эту историю. Сейчас данных недостаточно, чтобы уверенно говорить о влиянии на конверсию, NPS или время подготовки менеджера к встрече: системе нужно накопить статистику в реальных сценариях. Поэтому после выхода в прод мы сосредоточимся не только на качестве Retrieval и ответов модели, но и на том, помогает ли Co‑Pilot менеджерам быстрее находить нужную информацию и насколько полезны его рекомендации. 

Главный вывод, который мы сделали по итогу работы над этим проектом: RAG ломается не на LLM и генерации ответа, а на поиске информации, то есть на этапе Retrieval.

Экспериментируйте с разными подходами и делитесь своим опытом исследования RAG в комментариях. Мы рады пообщаться.

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


  1. Devpiligrim
    10.09.2026 10:07

    Две идеи вдогонку. Переформулировку запросов для продуктов с устоявшимися названиями ("Автосекретарь 2.0", "автосек") можно заменить обычным словарём синонимов - это проще, быстрее и не зависит от настроения модели. И эвристики реранка, которые сейчас живут в проде, стоит прогонять по вашим 72 вопросам перед каждым изменением - тогда сюрпризы вроде штрафа за слово "тариф" всплывут раньше.

    По оценке качества - судья на той же модели, что и отвечает - не очень удачная практика, особенно если в той-же самой сессии. LLM склонны хвалить сами себя, ваш кейс с ESS-маркировкой это показал.