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.

Принцип основан на следующих стадиях обработки данных:

  1. Токенизация текста
    Каждый документ и каждый поисковый запрос разбиваются на слова или токены (обычно с применением нормализации: лемматизация, удаление стоп-слов и т.д.).
    Документ: "Подключение роуминга за границей"
    Токены: ["подключить", "роуминг", "за", "граница"]
  2. Построение инвертированного индекса (Inverted Index)
    Создаётся индекс, в котором для каждого уникального слова хранится список документов, в которых оно встречается.
  3. json

{

  "обучение": [doc1, doc4, doc5],

  "медицина": [doc2, doc5],

  ...

  1. }
    Это позволяет быстро находить документы, содержащие определённые слова.
  2. Взвешивание токенов: 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 или активируйте в приложении.

Пошаговая реализация алгоритма:

  1. Токенизация текста
    Для каждого документа нужно выделить токены (слова), привести к нижнему регистру, удалить стоп-слова и знаки препинания.

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()}

  1. Построение инвертированного индекса (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],

 ...

}

  1. Взвешивание токенов: 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

  1. Поиск по запросу

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:

Минусы Sparse Retrieval:

Dense Retrieval — Векторный поиск

Это метод поиска, основанный не на ключевых словах, а на векторных представлениях текста. Вся информация (документы и запросы) представляется как многомерные векторы в общем векторном пространстве. Поиск осуществляется через сравнение (обычно по косинусной или евклидовой близости) между вектором запроса и векторами документов. Метод заключается в поиске информации, при котором как запрос, так и документы представляются в виде векторных эмбеддингов, обычно полученных с помощью нейросетевых моделей. Вместо совпадений по ключевым словам (как в sparse retrieval), dense retrieval оценивает семантическую близость между запросу и документами в многомерном пространстве.

В основе dense retrieval лежит нейросетевая модель, преобразующая текст в вектор фиксированной размерности (обычно 128–1024).

Принцип основан на следующих стадиях обработки данных:

  1. Фрагментация текста
    Каждый документ разбивается на небольшие фрагменты (chunks) для дальнейшей обработки.
  2. Векторизация текста
    Каждый фрагмент текста преобразуется в вектор при помощи эмбеддинг модели.
  3. Индексирование
    Каждый векторизованный фрагмент вносится в векторную базу данных.
  4. Векторизация запроса
    Запрос преобразуется в вектор при помощи той же эмбеддинг модели, которая использовалась для векторизации исходных фрагментов.
  5. Поиск ближайших векторов
    Сравнение между вектором запроса и векторами документов позволяет выявить ближайшие вектора.

Пример реализации метода с использованием 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

Запрос: Как подключить тариф ГигаМакс?

 

Найденные документы:

  1. Для активации тарифа 'ГигаМакс' наберите *123*5#. Абонентская плата — 600 рублей в месяц. (расстояние: 0.1234)
  2. Пополнить счёт можно через мобильное приложение, терминалы или команду *100#. (расстояние: 0.6821)
  3. Проверка остатка интернет-трафика осуществляется командой *111*3#. (расстояние: 0.7430)

Пошаговая реализация алгоритма:

  1. Фрагментация текста
    Большие документы разбиваются на семантические фрагменты (chunks), чтобы повысить точность поиска и уменьшить «размывание» смысла текста.

python

from langchain.text_splitter import RecursiveCharacterTextSplitter

 

text_splitter = RecursiveCharacterTextSplitter(

    chunk_size=300,  # символов

    chunk_overlap=50

)

 

chunks = text_splitter.split_text(doc_text)

  1. Векторизация
    Каждый фрагмент текста преобразуется в вектор с помощью Dense Embedding Model. Примеры моделей:

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-мерный вектор

  1. Индексирование
    Все эмбеддинги документов индексируются в векторной базе данных (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))  # добавляем документы

  1. Обработка запроса и поиск ближайших векторов
    Пользователь задаёт вопрос:
  2. python
  3. "Как подключить тариф ГигаМакс?"
    Этот запрос также кодируется в вектор:

python

query = "Как подключить тариф ГигаМакс?"

query_vector = model.encode([query], normalize_embeddings=True)

FAISS находит топ-k наиболее близких по вектору фрагментов:

python

k = 3

D, I = index.search(np.array(query_vector), k)

Результат:

python

closest_docs = [chunks[i] for i in I[0]]

Например:

python

[

  "Для активации тарифа 'ГигаМакс' наберите *123*5#. Абонентская плата — 600 рублей в месяц.",

  ...

]

Преимущества Dense Retrieval:

Недостатки Dense Retrieval:

Сравнение основных поисковых методов

Как видно из таблицы Sparse и Dense Retrieval подходы взаимно дополняют друг друга и их совместное использование позволяет извлекать релевантные данные в рамках поискового запроса практически в большинстве случаев.

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

Альтернативные методы поиска

Кроме основных методов поиска приведённых выше, распространены также и альтернативные подходы поиска и извлечения данных:

Поиск по табличным, временным и мультимодальным данным

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

  1. Табличные данные — данные, структурированные в виде таблиц (например, SQL-базы, Excel-таблицы).
  2. Временные данные — данные, упорядоченные по времени, например, временные ряды, логи, датчики IoT.
  3. Мультимодальные данные — данные, включающие несколько типов информации одновременно: текст, изображения, звук, видео, сенсорные данные.

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

Поиск по табличным данным

В рамках метода реализуются следующие задачи:

Данные задачи решаются следующими подходами:

  1. Semantic Table Embedding (Dense)
    Каждая строка или таблица кодируется в вектор:
  2. Hybrid Retrieval: SQL + Dense
    Используется SQL-запрос для фильтрации, затем векторный поиск по описанию или метаданным:
  3. LLM + SQL Agent
    LLM генерирует SQL-запрос на основе пользовательского ввода

sql

SELECT * FROM tariffs WHERE price < 500 ORDER BY internet_data DESC;

Временные ряды

В рамках метода реализуются следующие задачи:

Данные задачи решаются следующими подходами:

  1. Time Series Embeddings
    Преобразование временных рядов в вектор:
  2. Vector Index + Metadata Filtering
    Пример: IoT-сенсоры → эмбеддинги по 24ч-окну + фильтр по device_id или location
  3. LLM-Assisted Analytics
    LLM получает метаописание временного ряда и сгенерированные признаки:

Мультимодальные данные

В рамках метода реализуются следующие задачи:

Данные задачи решаются следующими подходами:

  1. Multimodal Embedding Models
    Модели, преобразующие разные типы данных в общее векторное пространство:
  2. Cross-modal Retrieval
    Запрос в одном модальности (текст), поиск в другой (изображения, звук):
  3. Multimodal RAG
    Используется LLM с внешним поиском по изображениям, PDF, аудио:

Внешние API запросы

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

Сервисы могут предоставлять доступ к «живым» данным например посредством API сервисов Google легко извлекаются такие данные как погода, поисковая выдача, почта, контакты, диск, и т.д.

Сервисы через API также могут открывать доступ к данным хранящимся в базах данных например API сервиса геокодирования nominatim позволит извлечь данные о координатах местоположения по указанному адресу.

API специализированных баз знаний позволяют извлекать справочно-информационные данные например:

(прямые SQL-запросы, Semantic Table Search (семантический поиск по таблицам), Поиск по значениям и структуре таблиц, Vector Search для табличных данных), поиск по временным данным (Time Series Search) (поиск по шаблонам (Pattern Matching, поиск аномомалий и событий, индексация временных рядов, семантический поиск и векторные эмбеддинги (мультимодальные эмбеддинги, Cross-modal Retrieval (кросс-модальный поиск), Интеграция векторных и классических индексов, Мультимодальные базы данных и фреймворки)

Задачи ранжирования

После получения нескольких наборов результатов (ранжированных списков документов) из разных поисковых методов необходимо:

  1. Объединить (merge) результаты в единый список.
  2. Ранжировать документы так, чтобы наиболее релевантные оказались сверху.
  3. Учесть особенности каждого источника и веса их значимости.

Основные подходы к ранжированию в гибридном поиске:

  1. Линейное комбинирование скоров (Score Fusion)
    Каждый документ получает баллы от каждого поискового метода (например, BM25 score и cosine similarity). Итоговый скор вычисляется как взвешенная сумма по формуле.
    Плюсы:
  2. Минусы:
  3. Ранжирование через Learning-to-Rank (LTR)
    Использование ML-моделей, обученных на размеченных данных с релевантностью. В качестве признаков используются скоры из разных методов, метаданные, дополнительные тригеры. Модели: Gradient Boosted Trees, LambdaMART, нейронные сети.
    Плюсы:
  4. Минусы:
  5. Ранжирование с помощью переобучения (Re-ranking)
    Сначала выбирается расширенный список из разных источников (например, топ-100 документов). Затем мощная модель (например, LLM или cross-encoder) переоценивает каждый документ по запросу. Итоговый список сортируется по оценкам переоценки.
    Плюсы:
  6. Минусы:
  7. Каскадное ранжирование (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 прямо сейчас!