Галоўная / Артыкулы / Гібрыдны спосаб працы з RAG, выкарыстоўваючы pgvector, BM25 і крос-кодэр для пераранжавання

Гібрыдны спосаб працы з RAG, выкарыстоўваючы pgvector, BM25 і крос-кодэр для пераранжавання

Дазвольце дазнаць, чаму чыстая вектарная пашуковая сістэма не знаходзіць номеры артыкулаў і коды каштоўкаў, а таксама як сумаваць pgvector, BM25 і механізм переранкавання ў LangChain для точнага аднаходжэння інформацыі за прынцыпам RAG.

1524 слоў

Пратотып генеравання з падасцю даступных дадзейнаў (RAG), створаны на базе звычнага вектарнага пошуку, часта захопляе ў дэманстрацыях, але пасля цього разчароўвае рэальных корыстнікаў. Якщо запытаць яго пра графік тэхнічнага адтварожэння конкретнай моделі насоса, ён дае загальныя порады па насосах; якщо шукать точны код каштоўкі, адпаведны крок для усунення проблемы ніколі не праказуецца. У гэтым кяліку пояснюецца, чаму ўжоўтучны даступ не функцыонуе з точнымі ідэнтыфікаторамі, і паказана, як выправіць гэта за дапамогою гібрыднага падходу: pgvector для семантычнага пошуку, BM25 для падбору ключоўых слоў, а таксама прыстрой для пераранкавання, який вяршыць выбор тых частак дадзейнаў, якія насправды патрапляюць да LLM.

Чаму вектарны пошук працюе некорэктна з точнымі ідэнтыфікаторамі

Эмбеддынгі выказуюць ся як адзіны спосаб прыняць значэнне тэксту. Запит на слова "автомабіль" практычна завяршваецца дакументамі пра "машины" і "транспортныя засобы", таму што модель эмбеддынга размешчае супакоўаныя понятціі ў блізкай адстані ў высака-вымярнам прасторе. Аднак самая гэта якосц стае і слабасцю — схожасць характарызуецца сэмантычным блізкам, а не точнымі последованнямі симвалаў.

Калі корыстнік шукае номер часткі, напрыклад TX-99402, або код каштоўкі, напрыклад E-404, эмбедд гэтага строкі можа апынуцца ў вельмі блізкай адстані ад TX-99401 чы рэгулярнага тэксту для адлучэння прычын каштоўкі. Система аднаходжэння дакументаў вяртае тыя, якія "пра тое ж самае", а не тыя, якія маюць точна той токен, які запісаў корыстнік. Для тэхнічных інструкцый, каталагаў продуктав і баз знаёмых падтрымкі, дзе ідэнтыфікаторы несу большую частку значэння, гэта є ўласнае асновная прычына некарэткага результата.

Гібрыдны рэтрыўванне: ўжоўкленае і розрэджанае паралельна

Рашэнняя — запуск двух взаімадопамагаючых рэтрыўвальных алгорытмоў і сукупнае выкарыстоўванне ўсьго, што яны знаходзяць:

  1. Ужоўкленае рэтрыўванне (пошук вектараў) фіксуе контэкст і значэнне. Уместо таго, каб апераваць околачную базу дадзенаў вектароў, можна зберагаць эмбедынгі ў PostgreSQL за дапамогою расшырэння pgvector. Большасць прыкладнаеў вжо работае на Postgres, таму даданне вектарнай столбцоў памагае залічыць структуру мінімальной і дае можлівасць выкарыстоўваць звычныя аптэнатывання, кантроль даступу і транзакцыі.
  2. Розрэджанае рэтрыўванне (пошук ключоўых слоў) фіксуе точныя зусваймленні, акронімы і спецыялізаваную тэрміналогію. Стандартны алгорытм — BM25, давно выкананая функцыя ранжыравання, якая оцінюе дакументы па тым, насколькі часта з’яўляюцца тэрміны запыту, з урахоўваннем таго, насколькі рэдкая ёсць гэтая тэрміны ў всім корпусе, і пасля нормалізацыі па дужыне дакумента.

Прыгодная ментальная модель: вектарный пошук знаходзіць правы район, а BM25 — точны номер будынка. Вы беразеце найлепшыя рэзультаты з кожнага з іх і з’едначаеце іх. Чырпакаваць больш па таму, дзе кожны падход мае перавагі, можна ў нашай статыцэ па гібрыднаму пошуку тэхнічных знаёмасцей.

Чаму з’едначаныя рэзультаты патрабуюць пераранжавальніка

Гібрыдны пошук стварае негледзячы проблему. Тепер у вас є два ранжаваныя спискі, чыёі балы немагчыма пораўняць. Бал BM25 залежыць ад частоты выкарыстоўвання тэрмінаў і є безгранічны, тады як схожасць вектараў вырачваецца за дапамою косай вялічыны на абсалютна іншай шкале. Семантычны бал 0.82 не ўлепшы, ані не пагоршы за бал BM25 у розмяре 14.5; сортаванне аб’еднанага списку па чыстаму балу є беззначным.

Реранкер рашыяе гэта праблему, ігнаруючы первісныя балы. Це адзінэцкі модель, як правило, крос-анкодар, який чытае запит і кандыдатскі дакумент адночасова і выдае адзін бал рэлевантнасці для гэтай пары. Паколькі ён бачыць оба тексты адночасова, ён можа болей тачна ацэніць рэлевантнасць, чым парабяляючы два незалежна вырахаваныя эмбеддінгі.

Практычны план ў такім порядку:

  1. З’явіць 10 кандыдатаў з кожнага інструмента ахавання дакументаў — pgvector і BM25.
  2. Аб’еднаць іх, атрымаваючы да 20 фрагментаў.
  3. За дапамогою реранкера ацэніць кожны фрагмент па ступеню рэлевантнасці да запиту.
  4. Заставіць 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 выконвае код, таму падменены файл ёсць рызыкам для безпекі.
  • Строка з’єднання містить аутентифікацыйныя даны; ўвозьце яе з канфігурацыі, а не задавайце яе напрама.
  • Якщо вы хочаце застаўляць пошук па ключовым словам і ў базе дадзенаў, вбудоўаны пошук па повным тексту ў PostgreSQL являецца альтернатываю індэксу BM25, пры чым спосаб ранжавання разны.
  • Адзысканне: ансамбль, пасля чаго переранжаванне

    Функцыя адзыскання перабудоввае як самыя засобы адзыскання, так і ўз’єднвае іх. Засіб адзыскання 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 кожны раз. У рэальнай службе стварыце ретрывер аднойчы пад час запуску і вяртаецеся да яго.
  • LangChain пераранжавае своі пакеты ў разныя версіі, і класы такія як EnsembleRetriever і ContextualCompressionRetriever у вашай версіі можаць знаходзіцца ў іншым пакете. Пераканайцеся, чы не відбуваецца адмова ў імпортэ, і перагляньце актуальную документацыю LangChain.
  • Галоўныя выводы

    • Чысты пошук вектараў є слабы для точных токэнаў, такіх як номеры дзялянак, SKU і коды адказаў; BM25 падпірае гэты недастатак.
    • pgvector дазволяе адключыць функцію інтэнсівнага пошуку да існуючай структуры PostgreSQL без неабходнасці наявнасці окалічнай базы дадзенаў вектараў.
    • Рэйтынгі з разных прыстроў пошуку няма можлівасці пораўняваць, таму іх трэба з’едынаць за рангамі, а для фінальнага аранжавання выкорыстаць крос-кодэрактара.
    • Пераранжавання падвышае точнасць, але збільшвае час выканання; рэштрыктуўце колькасць кандыдатаў і старайцеся за ёю.
    • Спрыяйце індэксу BM25 як артыфакту практычнай рэалізацыі, які неабходна апдэйтаваць разам з дадзеннямі, і не включайце аутантыфікацыйныя данні ў код.

    Гібрыдны спосаб рэтрывалія не гарантуе ідеальных адказоў, але ён усунея найбольш часты прычыну, калі системы RAG вяртаюць правдападобны, але некоректны контэкст.

    Спадневаная літаратура