Гібрыдны спосаб працы з RAG, выкарыстоўваючы pgvector, BM25 і крос-кодэр для пераранжавання
Дазвольце дазнаць, чаму чыстая вектарная пашуковая сістэма не знаходзіць номеры артыкулаў і коды каштоўкаў, а таксама як сумаваць pgvector, BM25 і механізм переранкавання ў LangChain для точнага аднаходжэння інформацыі за прынцыпам RAG.
Пратотып генеравання з падасцю даступных дадзейнаў (RAG), створаны на базе звычнага вектарнага пошуку, часта захопляе ў дэманстрацыях, але пасля цього разчароўвае рэальных корыстнікаў. Якщо запытаць яго пра графік тэхнічнага адтварожэння конкретнай моделі насоса, ён дае загальныя порады па насосах; якщо шукать точны код каштоўкі, адпаведны крок для усунення проблемы ніколі не праказуецца. У гэтым кяліку пояснюецца, чаму ўжоўтучны даступ не функцыонуе з точнымі ідэнтыфікаторамі, і паказана, як выправіць гэта за дапамогою гібрыднага падходу: pgvector для семантычнага пошуку, BM25 для падбору ключоўых слоў, а таксама прыстрой для пераранкавання, який вяршыць выбор тых частак дадзейнаў, якія насправды патрапляюць да LLM.
Чаму вектарны пошук працюе некорэктна з точнымі ідэнтыфікаторамі
Эмбеддынгі выказуюць ся як адзіны спосаб прыняць значэнне тэксту. Запит на слова "автомабіль" практычна завяршваецца дакументамі пра "машины" і "транспортныя засобы", таму што модель эмбеддынга размешчае супакоўаныя понятціі ў блізкай адстані ў высака-вымярнам прасторе. Аднак самая гэта якосц стае і слабасцю — схожасць характарызуецца сэмантычным блізкам, а не точнымі последованнямі симвалаў.
Калі корыстнік шукае номер часткі, напрыклад 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) гэта запазджае выдачу першага токена. Абліковыце гэты показнік окрема ў вашай системе стежэння за затрымкамі. Падышча точнасці зазвычай ўзрабатваецца, але налаштавайце колькісць кандыдатаў па вашам бюджэту; наша статыя пра тое, чаму reranking мусі заслужыць сваю затрымку, дэтальней розглядае гэты компроміс.
Рэалізацыя пайплайну з 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не даджае первыяся скорынкі. Ён спаўнае лісты па рангу за дапамою важкавага методу Reciprocal Rank Fusion, які пазбегае асаблівасцяў у масштабаванні, описанных вышэй. Ён таксама адчыняе дуплікаты, таму пры пераранжаванні можа быць менш чым 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, інструменты і агенты локальна — Навучыцеся ствараць ланцюгі, системы рэтрывалія-падкрэпленай генерацыі, інструменты та агентные RAG-системы з LangChain 1.x за дапамою безкоштовнай локальной наладкі Ollama, без неабяжнасці API-клучоў.
- Beyond Top-K: Праграмы адактуальнасці, гібрыдны пошук і переранкаванне ў RAG — Дазнаецеся, чаму база дадзэння вектароў плюс LLM не ёстся прыемным системай RAG, і як чанкаванне, праграмы супараднення, гібрыдны пошук, переранкаванне і ацэнка заполняюць гэты прыем.