Начало всех начальных начал 

Добрый день, уважаемые хабропоселенцы ;)) Сегодня мы будем говорить, снова о хакатонах и разработке RAG-моделей, вернее моделей с RAG-подходами и наших попытках выйти за рамки простого векторного поиска. Не так давно мы участвовали на всероссийском хакатоне “Альфа-Будущее”, организованным Альфа-Банком и посвящённому настройке RAG для вопросно-ответных систем. 

Нам необходимо было создать интеллектуальный pipeline RAG-системы, которая по пользовательскому запросу находит релевантные фрагменты в корпусе данных. Вообще, было на выбор две задачи, вторая звучала как “Разработка copilot приложения для клиентов микробизнеса”, но нам ближе оказалась вторая задача. И, конечно же, мы “запилили” своё “модное” решение, о котором вам спешим рассказать в этой статье. Мы проиллюстрируем, как выстраивали архитектуру, какие модели тестировали, на чём остановились и почему, именно такой подход оказался для нас наиболее удачным. Покажем, как работает весь пайплайн — от чанкования документов до гибридного поиска и поделимся результатами бенчмарков и планами развития системы в дальнейшем. Всех заинтересованных лиц приглашаю по традиции под кат ;))

Рис. 1. Вот так Альфа привествовал участников, ееее ;))
Рис. 1. Вот так Альфа привествовал участников, ееее ;))

Задачка, стеки и инфра

Говоря, по направлениям, здесь было всё стандартно: Data Science, NLP, AI Engeneering, ML, Frontend, Backend, Fullstack, Machine Learning. По формату первый этап онлайн, второй офлайн, который проходил в Москве. Забегая вперёд сразу скажу, что мы выступали двумя командами и набрали 38.5 балла это у одной команды и 32.0 у второй команды, у лидера было 40.3. Отрыв, не прям, большой, но выступили мы очень бодро и весьма достойно. 

Хакатон проходил в лучших традициях: понятное чёткое ТЗ, поддержка и саппорт были очень “бодрыми”, по призам было: за первое место 250, 2 – 150 и 3 – 100. Также после хака была возможность метнуться на фаст-трек и получить приглашение на собес. По таймлайну была неделька на сборку проекта, потом чекапы и проверка ещё неделька, потом давалось ещё 10 дней для финалистов на допил проектов и финалка “вылетала” на начало декабря. По стеку значилось следующее Python 3.8+; Jupyter / Colab / IDE; Pandas, Numpy; Matplotlib; Scikitlearn; CatBoost / XGBoost / LightGBM / PyTorch.   

Рис. 2. Полное описание задачи
Рис. 2. Полное описание задачи

Неповторимый оригинал решения и железный цех

Исходя из поставленной задачи, мы задумались над концептом, которое нам стоило “запилить” и затем как-то приумножить. Так вот, мы решили, что текущая постановка может быть больше чем, просто, лайтовая интересная, и мы пошли дальше: решили соединить два мегасильных концепта Retrieval-Augmented Generation и графы знаний. Такой гибрид позволил бы системе не просто находить релевантные фрагменты текста, а понимать структуру информации, улавливать взаимосвязи и давать ответы, которые гораздо ближе к тому, как рассуждает реальный человек.

Для начала мы пошли в сторону архитектуры и пайплайна и осветили, так сказать, общий план работы над задачей, которую необходимо было поднять. Итак, в основе нашего проекта лежала модельная архитектура, причём, мы здесь сознательно отказались от линейных пайплайнов в пользу системы, которая формировала граф связей прямо на “лету”, то бишь, на “горячую”. Этот подход позволил нам сразу, динамически, адаптироваться к родственным документам, расширяющимся базам знаний и источникам данных. 

За ключевые моменты мы выбрали Ingestion & Chunking. Вначале документы поступали в систему, проходили один из трёх алгоритмов “нарезки”, при этом каждый чанк получал собственный профит метаданных: web_id, url, kind, chunk_text. Затем полученные тексты преобразовывались в векторы – это принципиально. Мы, здесь, сразу выполняли L2-нормализацию для корректности будущих сравнений, эти моменты укладывались у нас для Embeding Generation. Далее шла индексация, полученные эмбединги, мы упаковывали в FAISS, причём, мы остановились на связке HNSW + IndexFlatIP, как на наиболее сбалансированном решении по времени ответа и качеству поиска. На финалке мы гордо дошли до Graph Construction и здесь мы вышли за привычные рамки и сделали классификацию через NLI. Следующим моментом, который мы учли в своей работы был Hybrid Search и здесь участвовало сразу два механизма: FAISS (векторный уровень), TF-IDF (точные совпадения), Cross-Encoder (точное переранжирование). Метаданные “жили” вне индекса, чтобы FAISS оставался максимально лёгким.

Рис. 3. Мысли, схемы
Рис. 3. Мысли, схемы

На следующем этапе нас ждали алгоритмы чанкования и построение графа: наш взор упал на рассмотрение стразу трёх возможных алгоритмов. При этом мы не стали ограничиваться одним подходом, так как разные документы требуют разных стратегий, а именно: фиксированная длина – простой и надёжный метод (600 слов + 100 overlap); рекурсивное чанкование – документ уменьшает сам себя, пока не уложится в лимит; семантическое чанкование - (самая интересная стратегия, по нашему мнению) близкие по смыслу предложения объединяются в чанк, не нарушая логики текста. 

Так уже хорошо, движемся дальше ;)))

Чтобы граф был не просто набором рёбер, а полноценной структурой знаний, мы использовали NLI-модель MoritzLaurer/mDeBERTa-v3-base-mnli-xnli. Она определяла характер связи между чанками (Strong, Medium​, Weak​). То есть, у нас было не просто похожие фрагменты, а фрагменты с различной степенью смысловой близости, и это очень влияло на финальное качество поиска.

Тесты, тесты, тесты и кофе, кофе, кофе

Наступил черёд испытать наш космолет подход в серьёзной битве и оценить модели по всем трём критериям: скорость → качество на русском → ресурсопотребление. Для начала мы начали с эмбеддингов. За основу мы взяли ai-forever/ru-en-RoSBERTa. Он оказался победителем среди эмбедеров, так как у него значились: 

- Билингвальность (отлично держит контекст и в RU, и в EN сегментах);

- Лёгкость (Она значительно быстрее тяжелых аналогов, что критично при пересчёте графа);

- Качество (Высокая точность семантического поиска в задачах, похожих с нашими).

Мы также сравнили его с intfloat / multilingual-e5-base и sentence-transformers / paraphrase-multilingual-*: они давали хорошие метрики, но оказались слишком медленными на больших корпусах данных. Также мы рассматривали Giga-Embeddings и крупные instruct-модели, но они были больше ориентированы на генерацию, а не на быстрый векторный поиск.

Следующим этапом мы пошли к reranking. Здесь мы подхватили DiTy/cross-encoder-russian-msmarco, который идеально подходил под русскоязычный домен и особенно оказался хорош для финансовой тематики. Из плюсов у него значились, то, что модель уже оптимизирована именно под русский язык, что даёт прирост точности сортировки топ-кандидатов. 

Cross-Encoder подход: Оценивает пару (query, chunk) напрямую, что точнее косинусного расстояния. Оптимизация: Лёгкая интеграция с кэшированием (_cached_predict) и батчингом. Здесь же мы также сравнили их с конкурентами IlyaGusev/cross-encoder-rubert-base-msmarco: Тоже отличная модель, но в наших тестах оказалась тяжелее и медленнее на CPU. 

Мультиязычные Cross-Encoder'ы: На финансовой тематике и русском языке часто показывают результаты хуже специализированных.

Далее мы приступили к выбору LLM. Мы прогоняли множество моделей, которые нашли на HuggineFace и GitHub, в том числе и достаточно экзотических, но по ряду тестов остановились на Qwen3-MAX 8B. В целом, модель оказалась более чем хорошая, и это был оптимальный выбор для Query Expansion по ряду параметров: 

- Во-первых: Баланс (8B параметров) - достаточная генеративная мощность для качественного перефразирования, но при этом "влезает" в доступные GPU-ресурсы с хорошей скоростью;

- Во-вторых: Модель корректно работает с финансовой терминологией, русским языком (критично для банка) и легко помещается в доступные GPU. Наконец, почему не GPT-4 / LLaMA-2-13B: Слишком ресурсоёмкие и медленные для интерактивного RAG (задержка ответа неприемлема). Маленькие модели (3B): Не обеспечивали нужного качества генерации и точности в финансовом контексте.

Испытания в поле и fine-tune на “горячую”

Наконец, мы подошли к самому затейному, а именно, описанию, как работает поиск (Step-by-Step). Итак, когда пользователь отправляет запрос, система запускает последовательность шагов: 

1.​ Query Expansion: Qwen3-MAX генерирует 3–5 альтернативных формулировок и ключевые термины; 

2.​ Parallel Search: ​FAISS (семантика),​ TF-IDF (точное совпадение текста);

3.​ Merge​: Результаты объединяются; 

4.​ Reranking.​ Cross-Encoder пересортировывает кандидатов и выводит наиболее релевантные; 5.​ Final Output. Возвращаются лучшие чанки.​

На выходе у нас получился полный набор артефактов: graph.json – структура графа;​ vectors.faiss – индекс;​ embeddings.npy – эмбеддинги;​ chunks.jsonl – тексты;​ tfidf.pkl – TF-IDF;​ expanded_queries.csv – история расширения запросов.​ Таким образом, мы создали внутренний LLM-бенчмарк, который показал высокую корреляцию с метриками Альфа-Банка, причём система стабильно сохранила качество при переходе от публичного лидерборда к приватному, что говорит о хорошей обобщающей способности.

В дальнейшем мы хотели бы масштабировать нашу систему, “прикрутить” к ней чат-бот и аналитику. При этом уже у нас есть задел на масштабирование: FAISS-шардирование или переход на Milvus / Weaviate и инкрементальная индексация новых документов.​ По интерфейсу и чат-боту: 

- Vector Chat Memory (память диалогов);

- Мультиканальность (Web, Mobile, мессенджеры); ​

- Персонализация и объяснимость;

- Подсветка источников.

- По части аналитики​: A/B-тестирование разных комплектов моделей, ​LLM-мониторинг качества данных, интеграция с внешними источниками знаний.​

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