Hybrid RAG - основы архитектуры
Автор: Артур Хайруллин | Дата публикации: 2025-08-14
Hybrid RAG - основы архитектуры.
С ростом популярности Retrieval-Augmented Generation (RAG), как архитектуры для построения систем генерации контента на основе извлечённых данных, стало очевидно, что односложный подход к выбору источников знаний ограничивает качество результатов. В этой связи особый интерес представляют Hybrid RAG подходы, сочетающие различные методы поиска и представления данных, в целях улучшения полноты, точности и релевантность ответа.
В данной разделе я поделюсь своим опытом в реализации Hybrid RAG систем, его архитектуры и практических методов реализации.
Что такое Hybrid RAG
Hybrid RAG (гибридная генерация дополненная извлеченными из базы источников данными) — это расширение базового RAG-подхода, в концепции которого для поиска знаний используются несколько различных источников и стратегий извлечения. Основная идея — комбинировать способы поиска и извлечения информации для дополнения запроса.
Тогда как RAG системы основаны исключительно на дополняемых данных извлеченных из документов, Hybrid RAG предлагает подход, комбинирования и сложного ранжирования результатов поиска дополняемых данных. Это могут быть не только источники в виде документов, разбитых на фрагменты, но также и мультимодальные данные. Методы поиска и извлечения также намного шире, например данные могут быть получены получены путем запроса через локальное или внешнее API сервиса, SQL запроса к БД, таким образом запрос Hybrid RAG можно дополнить например сведениями из сервиса геокодирования или результатами SQL запроса из CRM системы.
Архитектура HybridRAG
В основе архитектуры лежит базовая архитектура RAG дополненная альтернативными поисковыми методами, такими как: Ключевой поиск (Sparse retrieval), Векторный/семантический поиск (Dense Retrieval), Поисковые движки специализированные API, базы данных с табличными, временными или мультимодальными данными, графовые базы данных.
Схема HybridRAG
Этап извлечения данных завершается ранжированием, которое обеспечивает уравновешивание весов данных в общем массиве переданном LLM.
Основные методы поиска
Sparse retrieval — Ключевой поиск
Этот метод извлечения информации основан на разреженном представлении текста. Он предполагает использование индекса, в котором документы представлены в виде набора отдельных слов (токенов) или терминов, и поиск выполняется путём сопоставления токенов запроса и документов.
Это наиболее традиционный метод поиска, лежащий в основе классических поисковых систем (например, Google до внедрения BERT) и реализуемый с помощью моделей вроде BM25, TF-IDF, Boolean Retrieval.
Принцип основан на следующих стадиях обработки данных:
- Токенизация текста
Каждый документ и каждый поисковый запрос разбиваются на слова или токены (обычно с применением нормализации: лемматизация, удаление стоп-слов и т.д.).
Документ: "Подключение роуминга за границей"
Токены: ["подключить", "роуминг", "за", "граница"] - Построение инвертированного индекса (Inverted Index)
Создаётся индекс, в котором для каждого уникального слова хранится список документов, в которых оно встречается. - json
{
"обучение": [doc1, doc4, doc5],
"медицина": [doc2, doc5],
...
- }
Это позволяет быстро находить документы, содержащие определённые слова. - Взвешивание токенов: TF, IDF, TF-IDF
TF (Term Frequency) — насколько часто термин встречается в документе.
Пример реализации с использованием библиотеки rank_bm25:
python
from rank_bm25 import BM25Okapi
# Корпус документов
docs = [
"Чтобы сменить тариф, воспользуйтесь приложением или отправьте команду *111#.",
"При утере телефона SIM-карту можно заблокировать через горячую линию.",
"Для подключения роуминга отправьте SMS на номер 1234 или активируйте в приложении."
]
# Токенизация (очень простая)
tokenized_docs = [doc.lower().split() for doc in docs]
# Создание индекса BM25
bm25 = BM25Okapi(tokenized_docs)
# Запрос пользователя
query = "как подключить роуминг за границей"
tokenized_query = query.lower().split()
# Получение релевантности
scores = bm25.get_scores(tokenized_query)
# Вывод самых релевантных документов
import numpy as np
best_doc_idx = np.argmax(scores)
print(f"Наиболее релевантный документ:\n{docs[best_doc_idx]}")
Вывод:
text
Наиболее релевантный документ:
Для подключения роуминга отправьте SMS на номер 1234 или активируйте в приложении.
Пошаговая реализация алгоритма:
- Токенизация текста
Для каждого документа нужно выделить токены (слова), привести к нижнему регистру, удалить стоп-слова и знаки препинания.
python
import re
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
stop_words = set(stopwords.words("russian"))
def preprocess(text):
tokens = word_tokenize(text.lower())
return [t for t in tokens if t.isalpha() and t not in stop_words]
docs = {
1: "Как подключить безлимитный интернет на тарифе Супер Макс",
2: "Инструкция по смене тарифа через личный кабинет",
3: "Что делать если не работает мобильный интернет",
4: "Подключение роуминга в Европе шаг за шагом"
}
tokenized_docs = {doc_id: preprocess(text) for doc_id, text in docs.items()}
- Построение инвертированного индекса (Inverted Index)
python
from collections import defaultdict
inverted_index = defaultdict(list)
for doc_id, tokens in tokenized_docs.items():
for token in set(tokens): # уникальные токены
inverted_index[token].append(doc_id)
Результат:
python
{
'интернет': [1, 3],
'тарифе': [1],
'подключить': [1],
'мобильный': [3],
'роуминга': [4],
...
}
- Взвешивание токенов: TF, IDF, TF-IDF
Определяем TF (Term Frequency) — сколько раз токен встречается в документе следующей функцией:
python
def compute_tf(tokens):
tf = defaultdict(int)
for token in tokens:
tf[token] += 1
for token in tf:
tf[token] /= len(tokens)
return tf
Определяем IDF (Inverse Document Frequency) — насколько редкое слово в корпусе:
python
import math
N = len(tokenized_docs)
df = {token: len(docs) for token, docs in inverted_index.items()}
idf = {token: math.log(N / (1 + df[token])) for token in df}
Определяем TF-IDF — итоговая важность токена в документе:
python
doc_vectors = {}
for doc_id, tokens in tokenized_docs.items():
tf = compute_tf(tokens)
tf_idf = {token: tf[token] * idf[token] for token in tf}
doc_vectors[doc_id] = tf_idf
- Поиск по запросу
python
query = "не работает интернет"
Преобразуем запрос как документ:
python
query_tokens = preprocess(query)
query_tf = compute_tf(query_tokens)
query_vector = {token: query_tf[token] * idf.get(token, 0) for token in query_tokens}
Сравниваем с документами по косинусному сходству:
python
from numpy import dot
from numpy.linalg import norm
def cosine_similarity(v1, v2):
common_tokens = set(v1.keys()) & set(v2.keys())
numerator = sum(v1[t] * v2[t] for t in common_tokens)
norm1 = math.sqrt(sum(x*x for x in v1.values()))
norm2 = math.sqrt(sum(x*x for x in v2.values()))
return numerator / (norm1 * norm2 + 1e-10)
results = {
doc_id: cosine_similarity(query_vector, vec)
for doc_id, vec in doc_vectors.items()
}
top_result = sorted(results.items(), key=lambda x: -x[1])
Результат:
python
# top_result может вернуть:
[(3, 0.65), (1, 0.40), ...]
То есть документ 3 («Что делать, если не работает мобильный интернет») — наиболее релевантен запросу.
Поиск и ранжирование обеспечивают поиск документов содержащих хотя бы один из терминов запроса. Затем по формуле (TF-IDF или BM25) вычисляется оценка релевантности каждого документа. Документы сортируются по убыванию этой оценки, и пользователь получает top-k результатов.
Плюсы Sparse Retrieval:
- Быстрый поиск — благодаря инвертированному индексу.
- Интерпретируемость — понятно, почему документ релевантен (термины совпали).
- Эффективность на больших корпусах.
- Простой деплой и настройка.
- Легко интегрируется с фильтрами, булевыми операциями и фасетами (в отличие от dense retrieval).
Минусы Sparse Retrieval:
- Плохо работает при синонимах и переформулировках (не найдёт «врач», если запрос «доктор»).
- Не учитывает контекст слов (bag‑of‑words).
- Зависит от точного совпадения слов, не умеет «понимать» текст.
- Ограничен в мультиязычности и переносе на новые домены без переиндексации.
Dense Retrieval — Векторный поиск
Это метод поиска, основанный не на ключевых словах, а на векторных представлениях текста. Вся информация (документы и запросы) представляется как многомерные векторы в общем векторном пространстве. Поиск осуществляется через сравнение (обычно по косинусной или евклидовой близости) между вектором запроса и векторами документов. Метод заключается в поиске информации, при котором как запрос, так и документы представляются в виде векторных эмбеддингов, обычно полученных с помощью нейросетевых моделей. Вместо совпадений по ключевым словам (как в sparse retrieval), dense retrieval оценивает семантическую близость между запросу и документами в многомерном пространстве.
В основе dense retrieval лежит нейросетевая модель, преобразующая текст в вектор фиксированной размерности (обычно 128–1024).
Принцип основан на следующих стадиях обработки данных:
- Фрагментация текста
Каждый документ разбивается на небольшие фрагменты (chunks) для дальнейшей обработки. - Векторизация текста
Каждый фрагмент текста преобразуется в вектор при помощи эмбеддинг модели. - Индексирование
Каждый векторизованный фрагмент вносится в векторную базу данных. - Векторизация запроса
Запрос преобразуется в вектор при помощи той же эмбеддинг модели, которая использовалась для векторизации исходных фрагментов. - Поиск ближайших векторов
Сравнение между вектором запроса и векторами документов позволяет выявить ближайшие вектора.
Пример реализации метода с использованием FAISS:
python
from sentence_transformers import SentenceTransformer
import faiss
import numpy as np
# Пример исходных данных базы знаний
documents = [
"Для активации тарифа 'ГигаМакс' наберите *123*5#. Абонентская плата — 600 рублей в месяц.",
"Пополнить счёт можно через мобильное приложение, терминалы или команду *100#.",
"Если вы находитесь в роуминге, стоимость звонков определяется тарифом 'Мир Онлайн'.",
"Вы можете отключить платные подписки через личный кабинет или набрав *456#.",
"Проверка остатка интернет-трафика осуществляется командой *111*3#."
]
# Загружаем ембеддинг модель
model = SentenceTransformer("all-MiniLM-L6-v2")
# Векторизируем
doc_embeddings = model.encode(documents, normalize_embeddings=True)
doc_embeddings = np.array(doc_embeddings)
# Индексируем
dimension = doc_embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(doc_embeddings)
# Обрабатываем запрос
query = "Как подключить тариф ГигаМакс?"
query_embedding = model.encode([query], normalize_embeddings=True)
query_embedding = np.array(query_embedding)
# ищем ближайшие фрагменты
k = 3
distances, indices = index.search(query_embedding, k)
# Выводим результат
print(f"\nЗапрос: {query}\n")
print("Найденные документы:")
for i, idx in enumerate(indices[0]):
print(f"{i+1}. {documents[idx]} (расстояние: {distances[0][i]:.4f})")
Пример вывода:
text
Запрос: Как подключить тариф ГигаМакс?
Найденные документы:
- Для активации тарифа 'ГигаМакс' наберите *123*5#. Абонентская плата — 600 рублей в месяц. (расстояние: 0.1234)
- Пополнить счёт можно через мобильное приложение, терминалы или команду *100#. (расстояние: 0.6821)
- Проверка остатка интернет-трафика осуществляется командой *111*3#. (расстояние: 0.7430)
Пошаговая реализация алгоритма:
- Фрагментация текста
Большие документы разбиваются на семантические фрагменты (chunks), чтобы повысить точность поиска и уменьшить «размывание» смысла текста.
python
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=300, # символов
chunk_overlap=50
)
chunks = text_splitter.split_text(doc_text)
- Векторизация
Каждый фрагмент текста преобразуется в вектор с помощью Dense Embedding Model. Примеры моделей: - sentence-transformers/all-MiniLM-L6-v2 (open-source)
- text-embedding-ada-002 (OpenAI)
- cohere.embed()
python
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
document_vectors = model.encode(chunks, normalize_embeddings=True)
Теперь каждый чанк имеет числовое представление, например:
python
[0.12, -0.05, 0.34, ..., 0.07] # 384 или 768-мерный вектор
- Индексирование
Все эмбеддинги документов индексируются в векторной базе данных (FAISS, Qdrant, Pinecone, Weaviate и др.).
python
import faiss
import numpy as np
dimension = document_vectors.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(np.array(document_vectors)) # добавляем документы
- Обработка запроса и поиск ближайших векторов
Пользователь задаёт вопрос: - python
- "Как подключить тариф ГигаМакс?"
Этот запрос также кодируется в вектор:
python
query = "Как подключить тариф ГигаМакс?"
query_vector = model.encode([query], normalize_embeddings=True)
FAISS находит топ-k наиболее близких по вектору фрагментов:
python
k = 3
D, I = index.search(np.array(query_vector), k)
- D: расстояния до ближайших векторов
- I: индексы найденных фрагментов
Результат:
python
closest_docs = [chunks[i] for i in I[0]]
Например:
python
[
"Для активации тарифа 'ГигаМакс' наберите *123*5#. Абонентская плата — 600 рублей в месяц.",
...
]
Преимущества Dense Retrieval:
- Понимает семантику — находит релевантные документы даже при синонимах и переформулировках.
- Не зависит от совпадений токенов.
- Поддержка мультиязычности — можно искать на одном языке, а документы — на другом (если модель мультилингвальна).
- Гибкость и адаптивность — модель можно дообучать под домен.
- Лучше масштабируется с ростом словаря, т.к. нет токенов, как в sparse.
Недостатки Dense Retrieval:
- Требует значительных ресурсов для обучения/инференса.
- Индексация и обновление коллекции — медленнее, чем в sparse.
- Переиндексация при изменении модели — нужно пересоздавать векторное хранилище.
- Меньшая интерпретируемость — сложно выявить прямые закономерности релевантной выборки.
Сравнение основных поисковых методов
Как видно из таблицы Sparse и Dense Retrieval подходы взаимно дополняют друг друга и их совместное использование позволяет извлекать релевантные данные в рамках поискового запроса практически в большинстве случаев.
Важной особенностью векторного подхода является возможность дообучения модели векторизации для адаптации к домену данных базы источников.
Альтернативные методы поиска
Кроме основных методов поиска приведённых выше, распространены также и альтернативные подходы поиска и извлечения данных:
Поиск по табличным, временным и мультимодальным данным
Современные системы работы с данными охватывают не только классические текстовые документы, но и сложные типы данных:
- Табличные данные — данные, структурированные в виде таблиц (например, SQL-базы, Excel-таблицы).
- Временные данные — данные, упорядоченные по времени, например, временные ряды, логи, датчики IoT.
- Мультимодальные данные — данные, включающие несколько типов информации одновременно: текст, изображения, звук, видео, сенсорные данные.
Поиск и извлечение релевантной информации из таких данных требует специализированных методов и архитектур. Приведу ключевые подходы и технологии для каждой из этих категорий.
Поиск по табличным данным
В рамках метода реализуются следующие задачи:
- Поиск информации в SQL/CSV/Parquet таблицах
- Объединение табличных и текстовых данных
- Генерация на основе таблиц (вопросы, анализ, отчёты)
Данные задачи решаются следующими подходами:
- Semantic Table Embedding (Dense)
Каждая строка или таблица кодируется в вектор: - Модели: TAPAS, TaBERT, TUTA, TabFormer
- Используются Transformer-модели с инжекцией структуры таблицы
- Для поиска: запрос преобразуется в вектор и сравнивается с векторами строк/таблиц
- Hybrid Retrieval: SQL + Dense
Используется SQL-запрос для фильтрации, затем векторный поиск по описанию или метаданным: - Пример: найти строки по product_id → затем искать релевантные записи по description
- LLM + SQL Agent
LLM генерирует SQL-запрос на основе пользовательского ввода
sql
SELECT * FROM tariffs WHERE price < 500 ORDER BY internet_data DESC;
- Применяется в LangChain, LlamaIndex, OpenAI function calling
- Часто используется RAG-подход: text-to-SQL → поиск → SQL-to-text ответ
Временные ряды
В рамках метода реализуются следующие задачи:
- Найти похожие паттерны
- Сравнить интервалы, аномалии, тренды
- Семантический поиск по поведению во времени
Данные задачи решаются следующими подходами:
- Time Series Embeddings
Преобразование временных рядов в вектор: - Модели: TS2Vec, TSiT, MiniRocket, InceptionTime
- Поддерживают обработку и сравнение временных последовательностей
- Vector Index + Metadata Filtering
Пример: IoT-сенсоры → эмбеддинги по 24ч-окну + фильтр по device_id или location - Индексация через FAISS, Qdrant, Vespa
- LLM-Assisted Analytics
LLM получает метаописание временного ряда и сгенерированные признаки: - "Временной ряд показал резкий рост нагрузки на сеть с 18:00 до 21:00."
Мультимодальные данные
В рамках метода реализуются следующие задачи:
- Поиск по изображениям с текстом
- Комбинированные запросы: «Фото SIM‑карты с повреждением»
- Аудиозапросы: голосовой поиск, запись звонка
Данные задачи решаются следующими подходами:
- Multimodal Embedding Models
Модели, преобразующие разные типы данных в общее векторное пространство: - CLIP (OpenAI) — текст + изображение
- BLIP / GIT / Flamingo — мультимодальная генерация
- Whisper + CLIP — для аудио + текст
- Cross-modal Retrieval
Запрос в одном модальности (текст), поиск в другой (изображения, звук): - Запрос: "Неисправная SIM-карта"
→ Поиск по изображениям клиентов, загруженных в CRM - Multimodal RAG
Используется LLM с внешним поиском по изображениям, PDF, аудио: - Подключается OCR, ASR, Vision-модель
- Пример: клиент загрузил скриншот экрана с ошибкой → CLIP ищет похожие случаи
Внешние API запросы
Поиск через специализированные API — это подход, при котором вместо либо в дополнение построения собственной инфраструктуры (индексация, хранение, ранжирование) используются готовые API от внешних сервисов, предоставляющие доступ к сырым либо уже проиндексированным и обработанным данным.
Сервисы могут предоставлять доступ к «живым» данным например посредством API сервисов Google легко извлекаются такие данные как погода, поисковая выдача, почта, контакты, диск, и т.д.
Сервисы через API также могут открывать доступ к данным хранящимся в базах данных например API сервиса геокодирования nominatim позволит извлечь данные о координатах местоположения по указанному адресу.
API специализированных баз знаний позволяют извлекать справочно-информационные данные например:
- Wikipedia API / MediaWiki API — Позволяет получать статьи, категории, ссылки и т. д.
- PubMed API / Entrez E‑utilities — Медицинские публикации, аннотации, авторы
- ArXiv API — Научные публикации, метаданные и PDF‑ссылки
- StackExchange API — Вопросы и ответы с StackOverflow
(прямые SQL-запросы, Semantic Table Search (семантический поиск по таблицам), Поиск по значениям и структуре таблиц, Vector Search для табличных данных), поиск по временным данным (Time Series Search) (поиск по шаблонам (Pattern Matching, поиск аномомалий и событий, индексация временных рядов, семантический поиск и векторные эмбеддинги (мультимодальные эмбеддинги, Cross-modal Retrieval (кросс-модальный поиск), Интеграция векторных и классических индексов, Мультимодальные базы данных и фреймворки)
Задачи ранжирования
После получения нескольких наборов результатов (ранжированных списков документов) из разных поисковых методов необходимо:
- Объединить (merge) результаты в единый список.
- Ранжировать документы так, чтобы наиболее релевантные оказались сверху.
- Учесть особенности каждого источника и веса их значимости.
Основные подходы к ранжированию в гибридном поиске:
- Линейное комбинирование скоров (Score Fusion)
Каждый документ получает баллы от каждого поискового метода (например, BM25 score и cosine similarity). Итоговый скор вычисляется как взвешенная сумма по формуле.
Плюсы: - Простота реализации и интерпретируемость.
- Минусы:
- Требует нормализации скора каждого метода, иногда сложно подобрать веса.
- Ранжирование через Learning-to-Rank (LTR)
Использование ML-моделей, обученных на размеченных данных с релевантностью. В качестве признаков используются скоры из разных методов, метаданные, дополнительные тригеры. Модели: Gradient Boosted Trees, LambdaMART, нейронные сети.
Плюсы: - Высокое качество ранжирования, возможность учитывать множество факторов.
- Минусы:
- Требует обучающих данных и ресурсов.
- Ранжирование с помощью переобучения (Re-ranking)
Сначала выбирается расширенный список из разных источников (например, топ-100 документов). Затем мощная модель (например, LLM или cross-encoder) переоценивает каждый документ по запросу. Итоговый список сортируется по оценкам переоценки.
Плюсы: - Улучшает качество выдачи, учитывая глубокий контекст.
- Минусы:
- Вычислительно дорого, обычно применяется к ограниченному числу кандидатов.
- Каскадное ранжирование (Cascading ranking)
На первом этапе используется быстрый метод (BM25 или dense retriever) для отбора. На втором — более точный, но медленный метод (реранжирование, LTR). Позволяет балансировать скорость и качество.
Пример простого гибридного ранжирования:
python
alpha = 0.6 # вес sparse поиска
results_sparse = get_sparse_results(query) # [(doc_id, score), ...]
results_dense = get_dense_results(query) # [(doc_id, score), ...]
# Нормализация скоров
norm_sparse = normalize_scores(results_sparse)
norm_dense = normalize_scores(results_dense)
# Объединение результатов
combined_scores = {}
for doc_id, score in norm_sparse:
combined_scores[doc_id] = combined_scores.get(doc_id, 0) + alpha * score
for doc_id, score in norm_dense:
combined_scores[doc_id] = combined_scores.get(doc_id, 0) + (1 - alpha) * score
# Сортировка по итоговому скору
final_ranking = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
Метод ранжирования в гибридном поиске это основа объединения сильных сторон разных поисковых методов. Правильно выбранная стратегия позволяет улучшить полноту и точность поиска, адаптироваться под разные задачи и обеспечить более релевантные и разнообразные результаты.
В следующей статье мы подробно расскажем как о методах реализации RAG систем
Что дальше?
Протестируй прямо сейчас
Добавьте файлы и протестируйте RAG прямо сейчас!