Привет! Меня зовут Алиса, я руковожу командой машинного обучения в Банки.ру и занимаюсь проектами, связанными с внедрением ИИ. В этой статье расскажу, как мы создавали чат-бота для работы с внутренней документацией: какие задачи решали, с какими сложностями столкнулись, что сработало, а что — нет. Надеюсь, наш опыт окажется полезным тем, кто только начинает путь или уже в процессе — возможно, это поможет сэкономить время и нервы.

Предпосылки создания бота

Началось всё с Confluence — инструмента, которым мы пользовались ежедневно. Поиск по нему, мягко говоря, оставлял желать лучшего. Например, чтобы найти описание объекта DWH, нужно было вбить полное его название, вместе со схемой(sic!). Одной опечатки хватало, чтобы оказаться в тупике и идти спрашивать коллег, где лежит нужная страница.

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

К концу 2023 года ИИ переживал очередной виток популярности: инструменты становились доступнее, мощнее и ближе к реальным задачам. По слухам, такие гиганты, как Amazon, уже тогда начали разрабатывать ИИ-ботов для проектной документации. При этом публичных кейсов почти не было - ни докладов, ни статей. Тогда мы решили сами поэкспериментировать с API ChatGPT-3.5.

Оказалось, что можно задать довольно общий вопрос по фрагменту текста и получить понятный, точный ответ. Это стало для нас отправной точкой. Родилась идея сделать бота, который бы отвечал бы в человекоподобном стиле на вопросы по нашей документации в Confluence.

Защита идеи перед руководством прошла без особых препятствий. Я представила концепт, объяснила, как это поможет команде, и получила поддержку.

Примерно в то же время параллельно развивался проект нашего корпоративного бота под названием Флексо. Мы поняли, что можем объединить усилия: использовать наработки платформы, а не собирать всё с нуля. Впоследствии это значительно упростило нам запуск, а вначале - позволило сразу перейти к реализации нашей идеи.

Реализация

Предварительно (и очень примерно) этапы реализации виделись так:

  1. Собрать наши данные

  2. Дообучить на них некую публичную модель

  3. Наладить промптирование

  4. Обернуть в сервис

  5. Всё готово, Enjoy

По факту старта ни у кого практического опыта с LLM особо не было, и пришлось начать с проработки подходов, а как это собственно делается. Выяснилось следующее

Добавление данных в модель

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

Вот три основных метода:

1. Fine-tuning
Это дообучение модели на собственных данных, чтобы адаптировать её поведение под специфические задачи. Часто предполагается, что можно просто взять публичную модель с открытыми весами - например, тогда была Llama 2 - и дополнить её знания, сконцентрировавшись на своей предметной области. Так подумали и мы и решили начать с этого подхода.
На практике всё оказалось сложнее. Современные LLM обучаются на колоссальных объёмах данных с применением десятков приёмов и оптимизаций. Без соответствующей инфраструктуры и экспертизы аккуратно встроить в такую модель свои данные — нетривиальная задача. Мы быстро поняли, что fine-tuning требует ресурсов, которыми мы не располагаем. Эта попытка заняла у нас около недели.

2. Prompt-based подходы (включая P-tuning)
Эти методы предполагают передачу примеров задач в теле запроса или использование обучаемых векторов-префиксов, которые подаются в модель вместе с вопросом. Такой способ удобен, если важно получить ответ в определенном формате или стиле. Но он плохо масштабируется, когда нужно передать большие объемы знаний, как в случае с технической документацией.

3. RAG (Retrieval-Augmented Generation)
В этом подходе пользовательский запрос преобразуется в эмбеддинг, затем ищется наиболее релевантная информация в базе знаний, и она подаётся в модель вместе с запросом. Модель формирует ответ, опираясь как на исходный вопрос, так и на найденный контекст.
Этот способ стал для нас оптимальным: он даёт гибкость, не требует изменения самой модели и позволяет быстро масштабировать решение на большие объёмы данных.

Сейчас такой подход считается де-факто стандартом для работы с внутренними базами знаний.

Сбор документов

Наша проектная документация включает более 44 тыс. различных документов, созданных разными командами. Для начала мы решили работать с их ограниченным пулом и выбрали пространство команды Data, частью которой является и наша ML-команда. Логичным казалось начать тестирование модели на “своих” документах.

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

Модель, очевидно, тоже столкнулась с трудностями. Несмотря на способность работать с техническими текстами, с такими объёмами и уровнем сложности она справлялась слабо. Попытки прогнать через неё наши документы не дали удовлетворительного результата, а валидировать ответы было непросто из-за специфики содержания.

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

В итоге, хоть изначально мы и хотели сделать инструмент для поиска по техническим спекам и структурам данных — той самой боли, с которой всё началось, — на первом этапе от этой идеи пришлось отказаться. Когда подошло время защищать MVP, мы сменили домен задачи и взяли данные из пространства HR: информация о больничных, отпусках и другие подобные документы. Они оказались гораздо более подходящими для работы.

С такими данными было проще и работать, и валидировать результат (в том числе “заказывать” валидацию). Всё пошло значительно лучше, и именно с этим материалом мы и защищали проект.

Очистка данных из Wiki

Тем не менее, даже эти данные нужно было дополнительно обработать. Например, мы решили использовать HTML-страницы из Wiki в качестве источников данных, однако очистка этих данных до простого текста оказалась не такой простой задачей. Мы использовали стандартные инструменты: Atlassian Python API для работы с нашим Wiki, а также BeautifulSoup для парсинга HTML.

Но даже после того как мы вырезали текст, обнаружилось, что он не всегда отражает истинный смысл. Например, заголовок “См. также:” и под ним список ссылок на другие страницы, который оказался полностью бесполезным без контекста. Визуально на странице всё выглядело логично, но после парсинга - просто обрывки фраз и служебный мусор.

Поэтому нам пришлось дорабатывать его вручную. Тут важно отметить: любые ручные манипуляции с таким объемом информации не могут привести к долговременному успеху. С этим объемом данных это уже неэффективно.

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

Размер чанков и ограничения модели

Для загрузки в векторную БД, документы разбиваются на чанки (chunks). Размер чанков напрямую влияет на качество поиска. Слишком маленькие - и модель теряет контекст, а результаты становятся слишком буквальными, зависят от точных совпадений слов. Слишком большие - может не поместиться нужный контекст, да и сама модель начнёт «спотыкаться» на входе. Мы выбрали параметры CHUNK_SIZE = 1000 и CHUNK_OVERLAP = 200 - это оказалось достаточно удобно: контекста хватало, а объём оставался в рамках допустимого для подачи в модель.

Эмбеддинги

А как залить данные в БД? Эмбеддинги — это векторные представления текста, которые позволяют сравнивать фразы и документы не по точному совпадению слов, а по их семантике, закодированной в числовом виде. Именно они лежат в основе поиска в RAG-архитектуре: чем ближе эмбеддинг запроса к эмбеддингу документа, тем выше их семантическая схожесть.

На старте мы использовали эмбеддинги от sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 — лёгкой и мультиязычной модели, поддерживающую в том числе и русский язык. Мы протестировали её на наших данных, и результаты были вполне приемлемыми для MVP. Внутри модель использует mean-агрегацию: она усредняет векторы всех токенов, чтобы получить один эмбеддинг на весь чанк текста. Это простой и универсальный подход, который дал нам возможность быстро запуститься.

Позже, когда мы начали добавлять в RAG новые разделы базы знаний — например, описания внутренних метрик и сокращений вроде MAU, DAU и других терминов из аналитики, — стало заметно, что модель справляется с ними не очень уверенно. Тогда мы перешли на эмбеддинги от Yandex Foundation Models. Они оказались лучше адаптированы под такие задачи и давали более устойчивые результаты при поиске по текстам с узкоспециализированной лексикой.

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

Хранение и поиск

Для хранения эмбеддингов и реализации поиска мы используем OpenSearch. Изначально у нас был один индекс, содержащий HR-документы, но по мере развития проекта появилась необходимость разделять данные по "доменам". Сейчас у нас несколько отдельных индексов — например, для HR, IT, продуктов и так далее. Архитектурно это устроено так: пользователь задаёт вопрос, и он сначала классифицируется по теме (домену), после чего поиск производится только по соответствующему индексу. Это позволило не только ускорить обработку, но и улучшить точность ответов.

Интеграция с корпоративным мессенджером

С самого начала мы планировали, что наш инструмент будет работать через корпоративный мессенджер — сначала это был Slack, позже его заменил Loop (это форк Mattermost). Разработкой бота и интеграцией с Loop занималась команда, отвечающая за инфраструктуру проекта. Наша часть — это модельный слой, оформленный в виде отдельного REST API-сервиса.

Обработка запроса и классификация домена

Как уже упомянула, каждый пользовательский запрос сначала проходит классификацию: мы определяем, к какому «домену» он относится. Это стало особенно важно после того, как к HR-документации добавились новые разделы — аналитика, IT, продукты и т.д.
На первом этапе мы использовали простую ручную иерархию на основе ключевых слов, но собрав пул первых диалогов, перешли на классификацию с использованием Yandex-классификатора с типом few-shot. Такой подход позволяет учитывать контекст вопроса и лучше обрабатывать пограничные случаи. На основе результата классификации выбирается нужный индекс для поиска. Если домен не удаётся определить однозначно — по умолчанию используется HR как наиболее общий.

Retrieval и генерация ответа

После определения домена запускается цепочка обработки на основе ConversationalRetrievalChain из Langchain. Мы используем retriever с MMR-поиском в OpenSearch - после ряда экспериментов остановились на нем, так как он оказался заметно лучше, чем cosine similarity. В частности, MMR позволял избежать повторяющихся результатов в выдаче — он стремится к балансу между релевантностью и разнообразием возвращаемых фрагментов. Метрика accuracy hit@3 получилась от 0.71 до 1, в зависимости от домена. В общем, поиск возвращает три наиболее релевантных чанка, эти чанки «склеиваются» с помощью StuffDocumentsChain (модуль langchain.chains.combine_documents.stuff) и подаются в промпт, на основе которого вызывается генеративная модель — в нашем случае, это YandexGPT Pro 4. Ответ, сгенерированный моделью, отправляется обратно пользователю в Loop, наряду с ссылками на найденные документы.

Хранение истории и кэширование диалогов

История всех диалогов хранится у нас в обычной PostgreSQL-базе: туда пишутся вопросы, ответы и ссылки на документы, а также оценка пользователем качества ответа (для потенциального дальнейшего RLHF).

Кроме того, мы подключили Redis для хранения текущего контекста диалога. Этот контекст подаётся в модель вместе с новым вопросом: сначала формируется уточнённый запрос с учётом предыдущих реплик, затем запускается поиск и генерация. Функциональность обогащения вопроса контекстом есть в LangChain прямо из коробки, что удобно. Что оказалось неудобным - что пользователи прыгают с вопроса на вопрос, и хранение контекста оказалось, по крайней мере на нашем этапе и для нашей задачи, больше злом чем добром, и в конечном счете его отключили. И так бывает.

Промптирование

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

Сейчас по сути осталось два момента которые нам оказались важны:

Первое — это позиционирование модели. Мы хотим, чтобы модель отвечала так, как это сделал бы сотрудник из отдела HR: вежливо, профессионально и с учетом контекста запроса. Это важно для того, чтобы ответы звучали органично и соответствовали ожиданиям пользователя.

Второй момент — это ограничение галлюцинирования модели. Retriever всё равно вернёт что-то ближайшее, даже если это далеко от темы. Чтобы избежать этого, мы попросили модель не отвечать вообще, если она не уверена в правильности ответа. Это помогает снизить вероятность ошибок и делает ответы более точными.

Эти скриншоты наглядно демонстрируют результат до промптирования и после:

Как это работает

На иллюстрации можно наглядно увидеть процесс работы чат-бота:

  1. Пользователь вводит запрос.

  2. Запрос проходит процесс эмбеддинга, где текст преобразуется в вектор.

  3. Если нам нужна история диалога, обращаемся к базе данных Redis, чтобы получить предыдущие сообщения.

  4. Все данные — запрос пользователя и история диалога — объединяются в один промпт.

  5. Этот объединенный промпт подается в YandexGPT, который генерирует ответ.

  6. Ответ передается пользователю.

Архитектура решения

На схеме ниже показано, как это реализовано технически. Чат-бот реализован на PHP, а основной сервис (Q&A) написан на Python. Когда чат-бот получает запрос от пользователя, он передает его в сервис на Python, который ищет подходящий документ. Затем сервис обращается к истории общения с пользователем, чтобы продолжить диалог, отправляет запрос в YandexGPT и транслирует полученный ответ пользователю.

Результаты работы

На данный момент чат-бот доступен в корпоративном мессенджере, и используется сотрудниками. Обращений не много - около 200 в месяц, но планируем расширение на другие домены, и более широкое использование. Latency около 2.5 секунд - что конечно имеет потенциал для оптимизации.

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

Наш проект уже расширяется: мы масштабируем его на новые домены и типы данных. Сегодня у нас есть готовый фреймворк для подключения новых документов — простой и эффективный. В данный момент подключены 4 пространства данных.

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

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

Ключевой результат также — создание «Финансового помощника» для пользователей Банки.ру. Этот ИИ-ассистент помогает пользователям мобильного приложения отвечать на вопросы по платформе. В случае необходимости подключается оператор техподдержки. Финансового помощника мы запустили в конце декабря 2024 года.

Выводы

Вот что мы поняли из опыта создания чат-бота:

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

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

  3. Мы добавили классификатор тематики запросов, так как пространство поиска документов оказалось очень широким. Это помогло нам четко определить, из каких типов данных следует искать информацию.

  4. Кэширование диалогов не всегда полезно. В нашем случае пользователи часто меняли темы, поэтому мы отключили эту функцию.

  5. Обновление закодированных векторов должно происходить регулярно. Мы пришли к выводу, что лучше избегать ручной подготовки данных.

Главное: сейчас, в 2025 году, создание такого бота является уже решенной в индустрии задачей, все кирпичики решения которой широко известны. Я лишь привела в пример детали нашей реализации.

Надеюсь, наш опыт вдохновит вас на создание собственных ИИ-решений для ваших компаний.

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