Гибридный метод поиска RAG с использованием pgvector, BM25 и ранжировщика на основе кросс-энкодера
Узнайте, почему чистый векторный поиск упускает номера деталей и коды ошибок, и как сочетать pgvector, BM25 и механизм переранжирования в LangChain для точного поиска с использованием RAG.
Прототип генерации с усилением через поиск (RAG), построенный на обычном векторном поиске, обычно производит впечатление на демо-демонстрациях, но затем разочаровывает реальных пользователей. Если спросить его о графике технического обслуживания конкретной модели насоса, он дает общие рекомендации по насосам; при поиске точного кода ошибки соответствующий шаг устранения не находится. В этом руководстве объясняется, почему плотный поиск терпит неудачу с точными идентификаторами, и показано, как это исправить с помощью гибридной системы: pgvector для семантического поиска, BM25 для поиска по ключевым словам и алгоритм переранжирования для определения того, какие фрагменты действительно попадут к большой языковой модели.
Почему векторный поиск упускает точные идентификаторы
Эмбеддинги отлично справляются с передачей смысла. Запрос на слово «автомобиль» попадает рядом с документами о «машинах» и «транспортных средствах», поскольку модель эмбеддингов располагает связанные понятия близко друг к другу в высокомерноменной пространстве. Однако именно эта особенность является и слабостью. Похожесть определяется семантической близостью, а не точными последовательностями символов.
Когда пользователь ищет номер детали вроде TX-99402 или код ошибки вроде E-404, эмбеддинг этой строки может оказаться очень близко к TX-99401 или к общим инструкциям по устранению неисправностей. Механизм поиска возвращает документы, «относящиеся к одному и тому же», а не тот, в котором содержится точно такой же идентификатор, который ввел пользователь. Для технических руководств, каталогов продуктов и баз знаний поддержки, где идентификаторы несут основную часть смысла, это является наиболее распространенным способом сбоя.
Гибридный поиск: плотный и разреженный одновременно
Решение заключается в использовании двух взаимодополняющих поисковых механизмов и объединении полученных результатов:
- Плотный поиск (векторный поиск) позволяет учитывать контекст и смысл. Вместо отдельной векторной базы данных можно хранить эмбеддинги в PostgreSQL с использованием расширения
pgvector. Многие приложения уже работают на Postgres, поэтому добавление векторного столбца позволяет сохранить компактную архитектуру и использовать знакомые инструменты резервного копирования, контроля доступа и транзакций. - Разреженный поиск (поиск по ключевым словам) обеспечивает поиск точных совпадений, аббревиатур и специфической терминологии данной области. Стандартным алгоритмом является BM25 — давно используемая функция ранжирования, которая оценивает документы в зависимости от частоты появления терминов запроса, учитывая их редкость в корпусе текстов и нормализуя результаты с учетом длины документа.
Полезная модель мышления: векторный поиск находит подходящий район, а BM25 — точный номер дома. Необходимо взять лучшие результаты из каждого метода и объединить их. Чтобы узнать больше о том, в каких случаях преимущество у каждого подхода, ознакомьтесь с нашей статьей о гибридном поиске для технических знаний.
Почему объединенные результаты требуют дополнительной классификации
Гибридный поиск создает немедленную проблему: у вас теперь есть два ранжированных списка, оценки по которым нельзя сравнивать. Оценка BM25 зависит от частоты вхождения терминов и не имеет верхнего предела, в то время как сходство векторов определяется по косинусному расстоянию на совершенно другой шкале. Семантическая оценка 0.82 не является ни лучше, ни хуже, чем оценка BM25 14.5; сортировка объединенного набора по первоначальной оценке бессмысленна.
Реранкер решает эту проблему путем игнорирования первоначальных оценок. Это отдельная модель, обычно кросс-энкодер, которая считывает запрос и кандидатский документ вместе и выдает единую оценку релевантности для этой пары. Поскольку она видит оба текста одновременно, она может с гораздо большей точностью определять релевантность, чем при сравнении двух независимо вычисленных эмбеддингов.
Порядок действий следующий:
- Получить по 10 кандидатов из каждого инструмента поиска — pgvector и BM25.
- Объединить их, получив до 20 фрагментов.
- Оценить каждый фрагмент по отношению к запросу с помощью реранкера.
- Оставить лучшие 3 фрагмента и передать только их в LLM.
Меньшее количество, но более качественных фрагментов также означает более короткий промпт, меньше шума, который модель должна игнорировать, и ниже стоимость токенов.
Затраты на задержку
Кросс-кодеры стоят дорого. Обработка 20 документов занимает значительно больше времени для каждого запроса, а в API потокового чата (например, построенном с использованием FastAPI) это приводит к задержке первого токена. Отслеживайте это время отдельно в системе мониторинга задержек. Повышение точности обычно оправдано, но настраивайте количество кандидатов в соответствии с вашим бюджетом; наша статья о том, почему переупорядочивание должно оправдывать свою задержку, более подробно рассматривает этот компромисс.
Реализация конвейера с помощью LangChain
LangChain предоставляет необходимые элементы для каждого этапа, поэтому весь конвейер помещается в две небольшие функции на Python. Приведенные ниже примеры носят концептуальный характер; адаптируйте строку подключения, модели и пути к файлам под свою среду.
Прием данных: разбиение на части, встраивание информации и индексация дважды
Функция обработки загружает текстовый файл, деля его на фрагменты по 1 000 символов с перекрытием в 100 символов, затем индексирует эти фрагменты двумя способами. Сначала они генерируются с использованием модели OpenAI text-embedding-3-small и сохраняются в коллекции pgvector с помощью PGVector.from_documents. Затем к фрагментам применяется механизм BM25Retriever, а результат сериализуется на диск с помощью pickle, поскольку BM25 формирует свой индекс в памяти.
import pickle
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_postgres.vectorstores import PGVector
from langchain_community.retrievers import BM25Retriever
CONNECTION_STRING = "postgresql+psycopg://user:password@localhost:5432/mydb"
COLLECTION_NAME = "hybrid_docs"
def ingest_documents(file_path: str):
# 1. Load and chunk the document
loader = TextLoader(file_path)
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
chunks = text_splitter.split_documents(docs)
# 2. Store dense embeddings in pgvector
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
PGVector.from_documents(
embedding=embeddings,
documents=chunks,
collection_name=COLLECTION_NAME,
connection=CONNECTION_STRING,
)
# 3. Fit and save the BM25 sparse retriever
bm25_retriever = BM25Retriever.from_documents(chunks)
with open("bm25_retriever.pkl", "wb") as f:
pickle.dump(bm25_retriever, f)
print(f"Successfully ingested {len(chunks)} chunks.")
# Example usage:
# ingest_documents("technical_manual.txt")
Важные моменты:
- Индекс BM25 представляет собой моментальную копию данных. При изменении документов его необходимо пересоздать и снова сохранить, иначе он потеряет синхронизацию с хранилищем векторов.
- Распаковывайте только те файлы, которые вы сами создали. Загрузка файла в формате pickle выполняет код, поэтому подделанный файл представляет угрозу безопасности.
Поиск: ансамбль, затем переранжирование
Функция поиска воссоздает оба механизма поиска и связывает их вместе. Механизм pgvector возвращает 10 лучших семантических результатов (k=10), а механизм BM25 также настроен возвращать 10 результатов. Класс EnsembleRetriever объединяет их, присваивая каждому вес 0.5. Компрессор CohereRerank с параметром top_n=3 оборачивает этот ансамбль в класс ContextualCompressionRetriever, так что каждый запрос проходит через поиск, объединение результатов и переранжирование в одном вызове invoke.
import pickle
from langchain_openai import OpenAIEmbeddings
from langchain_postgres.vectorstores import PGVector
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain_cohere import CohereRerank
CONNECTION_STRING = "postgresql+psycopg://user:password@localhost:5432/mydb"
COLLECTION_NAME = "hybrid_docs"
def setup_hybrid_retriever():
# 1. Initialize Vector Retriever
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = PGVector(
connection=CONNECTION_STRING,
embeddings=embeddings,
collection_name=COLLECTION_NAME,
)
# Fetch top 10 semantic matches
pgvector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
# 2. Load Keyword Retriever (BM25)
with open("bm25_retriever.pkl", "rb") as f:
bm25_retriever = pickle.load(f)
# Fetch top 10 exact keyword matches
bm25_retriever.k = 10
# 3. Merge pools with EnsembleRetriever (50/50 weighting)
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, pgvector_retriever],
weights=[0.5, 0.5]
)
# 4. Rerank the combined 20 chunks to output the absolute top 3
reranker = CohereRerank(cohere_api_key="YOUR_COHERE_API_KEY", top_n=3)
advanced_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=hybrid_retriever
)
return advanced_retriever
def query_system(query: str):
retriever = setup_hybrid_retriever()
best_docs = retriever.invoke(query)
for i, doc in enumerate(best_docs):
print(f"\n--- Result {i+1} ---")
print(doc.page_content)
# Example usage:
# query_system("What is the warranty period for the TX-99402 sensor?")
Некоторые детали, которые легко упустить:
EnsembleRetrieverне добавляет исходные оценки. Он объединяет списки по рангу с использованием взвешенного метода объединения по обратному рангу, что позволяет избежать проблемы несоответствия масштабов, описанной выше. Кроме того, он удаляет дубликаты, поэтому инструмент переранжирования может получить меньше 20 фрагментов, если оба поисковика найдут один и тот же результат.- Никогда не включайте ключ API в исходный код. Читайте ключ Cohere из переменной окружения или менеджера секретов.
setup_hybrid_retriever()выполняется при каждом запросе: каждый раз восстанавливается подключение к Postgres и перезагружается модель BM25. В реальном сервисе поисковик следует создать один раз при запуске и использовать его повторно.
EnsembleRetriever и ContextualCompressionRetriever, могут находиться в другом пакете в вашей версии. Проверьте актуальную документацию LangChain, если возникают проблемы с импортом.Основные выводы
- Чистый векторный поиск слабо справляется с точными токенами, такими как номера деталей, SKU и коды ошибок; BM25 заполняет этот пробел.
- pgvector позволяет добавить функцию интенсивного поиска в существующую структуру PostgreSQL без необходимости использования отдельной векторной базы данных.
- Оценки от разных механизмов поиска несопоставимы, поэтому их следует объединять по рангу, а окончательная сортировка должна выполняться с помощью кросс-кодера.
- Пересортировка повышает точность, но увеличивает задержку; следует тщательно определять размер пула кандидатов и контролировать его.
- Рассматривайте индекс BM25 как элемент конфигурации, который необходимо обновлять вместе с данными, и храните учетные данные вне кода.
Гибридный метод поиска не гарантирует идеальных ответов, но устраняет наиболее распространённую причину, по которой системы RAG в реальных условиях возвращают правдоподобный, но ошибочный контекст.
Связанные статьи
- Проектирование четырёхуровневой памяти агента с использованием LangGraph и Amazon Bedrock — Узнайте, как создавать для агентов на основе LLM рабочую эпизодическую, семантическую и процедурную память на платформах Bedrock и LangGraph, а также защищать её от вредоносного воздействия, утечек персональных данных и проблем, связанных с разными пользователями.
- LangChain 1.x на практике: цепочки, технологии RAG, инструменты и агенты локально — Научитесь создавать цепочки, системы генерации с усилением через поиск информации, инструменты и агентные решения с использованием LangChain 1.x в локальной среде Ollama без необходимости использования API-ключей.
- Beyond Top-K: Пороги релевантности, гибридный поиск и переранжирование в RAG — Узнайте, почему векторная база данных в сочетании с LLM не является готовой к использованию системой RAG, и как чанкирование, пороги сходства, гибридный поиск, переранжирование и оценка помогают преодолеть эти недостатки.