Команда CVM B2B из МТС превратила Telegram-бота для подготовки клиентских менеджеров в ассистента, который отвечает на вопросы по более чем 100 B2B-продуктам и подкрепляет каждый ответ ссылкой на источник в документации. Весь стек — локальный, внешние API под запретом.
Разработка шла под тремя жёсткими требованиями: данные хранятся только в локальном стеке, информация поступает только из базы, а каждый ответ сопровождается «хлебными крошками» — путём до фрагмента в структуре документа. Если нужных сведений нет, ассистент должен честно ответить «не нашел».
Что было до RAG
Ранее команда реализовала Telegram-бота, помогавшего клиентским менеджерам готовиться к встречам. Его функционал включал разведку по ИНН (сбор информации о компании из внутренних систем), подбор продуктов под клиента с помощью классических моделей, LLM-комментарии — статические подсказки на основе шаблонов, то есть заранее заготовленные тексты, которые бот показывал менеджеру без обращения к языковой модели в момент ответа, — и готовые сценарии диалога под типовые задачи. Интерфейс был привычным Telegram-ботом с кнопками, рекомендациями и обратной связью через лайки и дизлайки.
Часть информации поступала из внутренних источников, другая обогащалась внешними порталами и веб-поиском. В какой-то момент безопасники запретили брать данные извне, и команде пришлось полностью пересмотреть подход, перейдя на работу исключительно с внутренними источниками знаний.
Масштаб задачи
Ассистент должен отвечать на вопросы по 100+ B2B-продуктам со ссылками на источник в документации. Внутренняя Wiki оказалась глубоко вложенной: у некоторых продуктов — до шести уровней иерархии, а рекорд по числу подстраниц на один продукт при такой структуре — 77. Такую структуру нельзя просто передать языковой модели.
Предобработка: от Wiki к Markdown
Данные для ассистента брались из внутренней базы знаний, у которой уже был собственный RAG, но не было исходных данных в виде обычного текста, поэтому их приходилось парсить. Парсили исключительно внутренний корпоративный сайт — это снимало ограничения со стороны системы безопасности.
Сначала команда выбрала Seleniumинструмент автоматизации браузера, который справлялся с задачей, но затем перешла на более современные инструменты:
- Playwright — движок для автоматизации браузера, работающий с динамическим контентом (JS), эмулирующий действия пользователя и обходящий SPA-ограничения.
- Crawl4AI — специализирован на быстрой выгрузке веб-страниц сразу в чистый Markdown, оптимизированный для контекста LLM. Позволяет загрузить структуру документа или предоставить несколько примеров, а затем самостоятельно обучается их разбирать и умеет обходить ограничения.
- Markdownify — конвертер HTML в Markdown, используется для финальной очистки данных от мусора (скрипты, стили, навигация) и приведения их к компактному виду. Полученный формат можно использовать для векторизации без дополнительной подготовки.
Заголовок первого уровня в Markdown соответствует исходному продукту, а последующие уровни отражают вложенность страниц и разделов.
Подготовка данных: чанки, таблицы, эмбеддинги
Предобработка сырого Markdown-документа включает два этапа. Первый — добавление «хлебных крошек», которые позволяют не потерять информацию о том, откуда был взят конкретный фрагмент текста. Второй — обработка таблиц: если граница чанкафрагмента, на которые разбивается документ для поиска и векторизации проходит посередине таблицы, нижняя часть теряет контекст и становится бесполезной. Поэтому перед загрузкой таблицы преобразуют в единый структурированный (линейный) вид, а ссылки удаляют как мусорные и занимающие токены без добавления контекста.
Схема разбиения двухуровневая. Сначала формируются parent-чанки — крупные фрагменты документа, обычно соответствующие отдельным разделам или подстраницам. Затем каждый такой фрагмент дополнительно разбивается на небольшие child-чанкимелкие фрагменты, на которые делится parent-чанк и по которым выполняется поиск. Child-чанки преобразуются в эмбеддингивекторные представления текста, по которым измеряется смысловая близость с помощью модели BGE-M3модель для построения эмбеддингов и сохраняются в pgvectorрасширение PostgreSQL для хранения и поиска векторов. Помимо самих векторов, там хранятся текст чанка и дополнительная метаинформация. В базе данных появились две связанные таблицы с parent-чанками и child-чанками.
Пайплайн на Langflow
Архитектура состоит из четырёх основных блоков. Подготовка данных (Preprocessing) превращает исходные документы в пригодный для поиска вид. Загрузка данных в векторную базу (Ingestion) помещает эти документы в хранилище, где они разбиваются на parent- и child-чанки в два этапа: сначала документы загружаются в векторную базу, а затем делятся на parent- и child-чанки. Поиск релевантных документов (Retrieval) находит в базе фрагменты, подходящие под запрос пользователя. Генерация ответа (Generation & Evaluation) формирует итоговый ответ и оценивает его качество. Часть элементов пайплайна команда реализовала самостоятельно, поскольку их не было в библиотеке Langflowвизуальный конструктор для сборки пайплайнов обработки данных и языковых моделей.
Retrieval и реранжирование
Простой поиск по векторам достаёт 60–70% нужного, а цепочка из четырёх шагов повышает качество до 85–90%. Пайплайн Retrieval состоит из четырёх этапов:
- Перегенерация запроса. Система получает пользовательский запрос и создаёт четыре переформулировки с помощью языковой модели. Это увеличивает полноту поиска (Recallдолю найденных релевантных документов от всех существующих в базе), поскольку один и тот же вопрос может быть сформулирован разными способами.
- Гибридный поиск. Параллельно ищут по смыслу (Vector) и по словам (BM25поиск по точным словам запроса, а не по их смыслу), чтобы закрывать разные типы запросов.
- Объединение результатов. Результаты двух поисков объединяются в общий массив с помощью алгоритма Reciprocal Rank Fusion (RRF)объединение двух ранжированных списков в один по позициям документов в них.
- Реранжирование — самый дорогой этап. Массив уходит в Rerankerмодель, которая пересортировывает найденные фрагменты по их реальной релевантности запросу. Например, из 20 найденных чанков Reranker оставляет пять, которые гарантированно содержат ответ.
После реранжирования остаётся три наиболее релевантных child-чанка, по ним восстанавливаются соответствующие parent-чанки, затем выполняется дедупликация, чтобы не передавать модели дубляжи.
Метрики и выводы
Команда сравнила две версии реранкера — локальную с BGE CrossEncoderмодель повторного ранжирования, которая попарно оценивает релевантность запроса и найденного фрагмента и продовую с metadata-эвристиками (реранкер, который упорядочивает найденные фрагменты по формальным признакам их метаданных, а не по смысловой близости к запросу). Локальная версия показала заметно лучшие результаты практически по всем показателям. В проде большинство метрик значительно упало, но достоверность ответа (Faithfulnessметрика, показывающая, насколько ответ модели опирается на предоставленный контекст) выросла.
Падение контекстуальных метрик объясняется тем, что модель стала чаще отвечать «Информация в документации не найдена». Если нужных сведений действительно нет в базе знаний, такой ответ считается технически корректным.
На конкретном примере: когда FlagEmbeddingбиблиотека для вычисления эмбеддингов и повторного ранжирования вывела на первое место раздел «Сравнение продуктов», модель сгенерировала ответ с почти идеальными метриками. Когда метод Reranker выводил на первое место раздел «Автосекретарь. Контакты и общие вопросы», чанк не содержал нужного контекста и метрики стремились к нулю.
Ограничения и открытые вопросы
Отдельная проблема — исходно-ложные запросы. Когда пользователь задаёт вопрос, ответа на который в документации нет, модель должна ответить, что подобной информации нет. Но модель отвечала утвердительно и генерировала ложную информацию. Эта проблема решается добавлением в системный промпт вопросов с отрицанием.