Галоўная / Артыкулы

Гібрыдная пам’ять агента: спаўненне BM25 і векторнага пошуку з RRF у Pythonе

Дазвольце дазнацься, чаму чыстая вектарная пашуковая сістэма не падходзіць як памяц агента, як Reciprocal Rank Fusion спаўнае BM25 і ўжоцьплненыя рэзультаты на Python, і калі падсумкі GraphRAG ўжоцьплненыя.

Выдавец: REACTAPP.TOP7 хв чытання1651 слоў

Крыніцы: State Checkpointing & Resiliency: Fault-Tolerant ReAct Engines

Агент, які выконваець длігі робочыя процесы працаваючы калькі дзён або тыяжоў, патрабуе памяці, яю можна было б надзейна выкарыстоўваць для пошуку, а стандартны варыянт — хранэнне вектораў, якое адбываецца за дапамогою падобнасці, шырока выкорыстоўванай у эмбеддінгах, быстра перестае працаваць у тэхнічных сферах. Коды абоханак і назвы функцый згубляюцца, запытанні, якія стосуюцца калькі дакументаў, застаюць без адказу, а запытанні заполняюцца нескаладзана сувязанымі фрагментамі. У гэтым кяле показана, як спалучыць пошук за ключоўымі словамі і ўжоцьвёрдзены пошук з методам Reciprocal Rank Fusion (RRF), прадстаўленая канпактная рэалізацыя на Python і яе недастаткі, а таксама пояснены, дзе шар GraphRAG можа быць корыстным.

Чаму сама пошукавая сістэма на векторах недастаткова

Чысты ўжоцьвёрдзены пошук, які часта называюць простым RAG, мае тры частыя слабасці як дзейнае памяць агента:

  • Точныя ідэнтыфікаторы стаюць нечытнымі. Методы усадкі можа зафіксаваць значэнне, але паслаба перакладаюць точныя елементы, такія як ID-ы аб’ектаў, номеры модэляў, ID-ы корыстнікаў і назвы функцый. Запит на адны код каштоўкі можа вернуць інфармацыю пра код каштоўкі з аднойчы схожым візуальным выглядам.
  • Няма глобальнага апылю. Процес выкарыстоўвання дадзеных вяртае ізольаваны фрагменты, близкія да запиту, і не можа сформаваць зв’язкі, якія распрастраняюцца па всім корпусе дадзеных.
  • Занадто больш контэксту. Уведчыце неапдатаваныя рэзультаты запыту ў інструкцію, вы выкарыстоўваеце токены без гарантіі ўзаемнасувязанасці.

Звычны адказ у працэўнікоў вырабнічых кампаній ёсць шаровы: гібрыдны пошук выканае з викорыстаннем BM25 разам з ўпакоўванымі вектарамі, яны з’едынаюцца за дапамогою RRF, а для шырокіх запитоў дадаецца граф-граф Раг для знанняў. Чым керуецца вектарны падход, можна дазнацца за лінкам «Як насправды працуюць вектарныя базы дадзеных, ад імбеддінга да гібрыднага пошуку».

Тры методы адналокавання і тое, у чаму кожны з яных суперяўніць

  • Пошук упакоўванымі вектарамі (HNSW або плоскія індэксы) перакладае тэкст у простор імбеддінга і знаходзіць супадання за намерам і концэптуальным супаданнем, нават калі формулювання разныя.
  • Пошук розрэдзенымі ключовымі словамі (BM25 або TF-IDF) оцэнюе точныя супадання токенав, што є незаменным для кодаў каштоўкаў, унікальных ID, сігнатур функцый і власных імен.
  • GraphRAG выкарыстоўваець энтыты і адносы, каб стварыць граф знанняў, і падходзіць для синтэзы запытаў уроду "Як усія службы ў Кластэре B распрацоўваюць праханні на завершэнне роботы базы дадзенаў?" Такія графы ўсунуто направленыя графы, але не павінны быць акрычнымі.
  • Форма запыту гібрыднай памяці

    Агент, які запытае сваю памяць, адправляе ўсёўны запыт, які параметрызуе оба двайніка:

    {
      "query_id": "mem_req_99214",
      "search_text": "HTTP 504 gateway timeout postgres connection pool",
      "top_k": 5,
      "hybrid_params": {
        "dense_weight": 0.6,
        "sparse_weight": 0.4,
        "rrf_k_constant": 60
      },
      "metadata_filter": {
        "environment": "production",
        "service": "billing-service"
      }
    }
    

    Паралельна з тэкстам запыту і top_k пакет дадзеных несе вагі для кожнага двайніка, сталя RRF k і фільтр метададзей, які обмежвае рэзультаты аднам сераўісу і средавішчу. Фільтрацыя мае важлівую ролю: на запыт пра інцыдэнт у працоўным сераўісе не павінна даходзіць адпаведь з нотатак стэйджынгу.

    З’едынаная адпаведь вяртае часткі з оцэнкамі і ўсімі дадзенымі пра ўсходнікі:

    {
      "query_id": "mem_req_99214",
      "total_retrieved": 2,
      "results": [
        {
          "chunk_id": "doc_8832_p4",
          "rrf_score": 0.0322,
          "text": "PostgreSQL connection pool exhaustion causes HTTP 504 timeouts. Increase max_connections to 200.",
          "source": "wiki/infrastructure_troubleshooting.md"
        }
      ]
    }
    

    rrf_score ўзначальваецца як маленькі значэння за прызначэннем. Калі k = 60, элемент, які займае першае месца ў адным списку і трэцяе — у іншым, атрыбуецца значэнне 1/61 + 1/63, што станавіць апошней 0,032. Гэтыя значэння маюць сенс толькі у спылку з іншымі значэннямі той жа запит, таму трэба ухіліцца ад абсалютных порогаў. (У прыкладзе total_retrieved паказана як 2, але пераказваецца толькі адна рэзультат; спрыяйце гэта як ілюстрацыю.)

    Адкалічэнне гібрыднага пошуку і RRF у Python

    У прыкладзе адкалічэння викорыстоўваюцца Pydantic і asyncio з сімуляваными бэкендамі, таму ён деманструе логіку злучэння, а не готовы компонент.

    Моделі дадзеных для фрагментаў і рангаваных рэзультатаў

    SearchResult — гэта тое, што вяртае кожны двайнер: яго пазіцыя (rank) і первасны бал; MemoryChunk — гэта саўязаны выход, які передаецца агенту. Зберагчы raw_score дапамагае у адлучэнні бягаў, нават якщо процес саўязывання яго ігнаруе.

    import math
    from typing import List, Dict, Any, Optional
    from pydantic import BaseModel, Field
    
    
    class MemoryChunk(BaseModel):
        chunk_id: str
        content: str
        metadata: Dict[str, Any] = Field(default_factory=dict)
        score: float = 0.0
    
    
    class SearchResult(BaseModel):
        chunk_id: str
        content: str
        rank: int
        raw_score: float
    

    Двайнер саўязывання

    RRF ацэнівае кожны дакумент як суму 1 / (k + rank) для кожнага списку, у якім ён з’яўляецца. Клас прымае гэты падход да вектарных і BM25-списоў, запамячае содержымак кожнага фрагмента, сортавае па сумарным балу і вяртае найлепшыя рэзультаты:

    class ReciprocalRankFusionEngine:
        """
        Combines ranked result lists from Dense Vector and BM25 Sparse Search
        using the RRF algorithm: RRF_Score = sum(1 / (k + rank))
        """
        def __init__(self, k_constant: int = 60):
            self.k_constant = k_constant
    
        def fuse_rankings(
            self,
            vector_results: List[SearchResult],
            bm25_results: List[SearchResult],
            top_k: int = 5
        ) -> List[MemoryChunk]:
    
            rrf_scores: Dict[str, float] = {}
            content_map: Dict[str, str] = {}
    
            # Process Vector Search Ranks
            for item in vector_results:
                rrf_scores[item.chunk_id] = rrf_scores.get(item.chunk_id, 0.0) + (
                    1.0 / (self.k_constant + item.rank)
                )
                content_map[item.chunk_id] = item.content
    
            # Process BM25 Search Ranks
            for item in bm25_results:
                rrf_scores[item.chunk_id] = rrf_scores.get(item.chunk_id, 0.0) + (
                    1.0 / (self.k_constant + item.rank)
                )
                content_map[item.chunk_id] = item.content
    
            # Sort by combined RRF Score descending
            sorted_chunks = sorted(
                rrf_scores.items(), key=lambda x: x[1], reverse=True
            )
    
            output: List[MemoryChunk] = []
            for chunk_id, score in sorted_chunks[:top_k]:
                output.append(
                    MemoryChunk(
                        chunk_id=chunk_id,
                        content=content_map[chunk_id],
                        score=round(score, 5)
                    )
                )
    
            return output
    

    Фрагмент, які знайшоўся як у адном, так і ў другім двайнерах, отрымае два вклады, таму адпаведнасць автаматычна награджваецца. Заўважкай ёсьць тое, што вагі пакета данных не выкарыстоўваюцца: гэта звычны RRF. Ёсьць можлівасць, каб dense_weight і sparse_weight мелі значэнне, якшо прайсці вкладу кожнага списку на яго вагу; такая версія зазвычай называецца RRF з вагамі.

    Скоўванне абох пошукавых процэсаў

    Алгорытм запрашвае кожны пошукавы механізм на два разы больш кандыдатаў, чым яму трэба, тады дае больш матэрыялу для обробкі, а пасля перадае обе спісы механізму RRF:

    class HybridMemoryRetriever:
        """
        Orchestrates Vector and BM25 search queries in parallel and applies RRF.
        """
        def __init__(self, vector_db_client: Any, bm25_index: Any):
            self.vector_db = vector_db_client
            self.bm25_index = bm25_index
            self.rrf_engine = ReciprocalRankFusionEngine(k_constant=60)
    
        async def retrieve_memory(self, query: str, top_k: int = 5) -> List[MemoryChunk]:
            # Fetch rankings from vector space and BM25 index
            vector_hits = await self._fetch_vector_results(query, fetch_k=top_k * 2)
            bm25_hits = await self._fetch_bm25_results(query, fetch_k=top_k * 2)
    
            # Merge and re-rank using Reciprocal Rank Fusion
            fused_memory = self.rrf_engine.fuse_rankings(
                vector_results=vector_hits,
                bm25_results=bm25_hits,
                top_k=top_k
            )
    
            return fused_memory
    
        async def _fetch_vector_results(self, query: str, fetch_k: int) -> List[SearchResult]:
            # Simulated vector DB query
            return [
                SearchResult(chunk_id="doc_102", content="Postgres timeout handling guide", rank=1, raw_score=0.92),
                SearchResult(chunk_id="doc_883", content="Database connection pooling configuration", rank=2, raw_score=0.85)
            ]
    
        async def _fetch_bm25_results(self, query: str, fetch_k: int) -> List[SearchResult]:
            # Simulated BM25 keyword query
            return [
                SearchResult(chunk_id="doc_883", content="Database connection pooling configuration", rank=1, raw_score=14.2),
                SearchResult(chunk_id="doc_401", content="HTTP 504 status codes and gateways", rank=2, raw_score=11.1)
            ]
    

    Докстрынг обяцвае паралельную експанацыю, але два вызову await выконваюцца абоўсумова; для супаралелізацыі выконання трэба вжыць asyncio.gather. У рэальных системах методы _fetch_ запрашваюць вашу базу дадзейнаў у вектарнай форме і індэкс BM25, прыменяюць фільтры на метаданы і дазволяюць выкарыстоўваць параметр fetch_k. У сімуляваных дадзенах doc_883 прысутны ў обох спісах, таму я знаходзіцца на першай пазіцыі ў скаўнанай рангаванні, незважаючы на тое, што ў аднам зі спісаў ён ўжо ў першай пазіцыі.

    Дадаўанне GraphRAG для запытак на всю корпус

    Гібрыдны пошук яшчэ і так запрашвае локальныя фрагменты дадзеных. Для рассуджэння над тыямі тысячамі дакументаў GraphRAG стварае структураваны шар у трох этапах:

    • Экстракцыя элементаў. Фонавы процес выклікаее элементы, такія як службы, типы памылак і схемы SQL, выявляе явныя зв’язкі і зберагае іх у базе дадзэння графа, такой як Neo4j, або ў бібліятэцы для памяці, такой як NetworkX.
    • Адкрытыця спільнатэкаў. Алгорытмы кластаравання, такія як Leiden group, агрупаваець ўзаемна з’яўлены вузлы ў іерархію спільнатэкаў.
    • Іерархічнае стасумаванне. LLM піша звястку для кожнай спільнатэкі, і шырокія запытанні адпаведзяюцца на адной з гэтых стасумаўкаў, а не на мільйонах первісных фрагментаў.

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

    Частае запытанні

    Чаму вжываць RRF заместо сярэднічага расчытку балав?

    Значэнні паказчыкаў з высокай і низкой ўтрамбоўкай належаць да незв’язаных шкалаў. У практыцы косай схожасці зазвычай дзейстуюць значэнні межа 0 і 1, тады калі значэнні BM25 не маюць верхней межы і часта перасягаюць 50. Їх нормалізацыя прыводзіць да викорыстання чутлівых параметраў, якія выключаюцца праз змены дадзэнняў. RRF выкарыстоўвае толькі пазіцыі, што робіць яго стойкім да разных набораў дадзэнняў без падбору налашоўкаў.

    Як GraphRAG зменшае размах запиту?

    Уместо вставкі дзесяткаў необработаных фрагментаў, скажам, 50, агент чытае адну або два заздалегідь вырахаваныя падсумкі спільнот. Указваецца, што так можна заощадзіць прыбліжна 70 процэнтав токенав, але рызыкты залежаць ад вашаг корпусу дадзэнняў і запытанняў, таму пераканайцеся самі.

    Ключовыя выводы

    • У тэхнічных сферах трэба спалучаць эмбеддынгі з BM25 і аднаўляць ранжыраванні за дапамогою RRF.
  • k = 60 — это стандартная сталя; яна зменшае ўплыв аднаго з верхняях рангаў, таму дакументы, якія знаходзяцца ў высокіх рангах у обох спісах, перамагаюць тыя, якія знаходзяцца на першым месцы толькі ў аднам з іх.
  • RRF не берае ў расчыт суровыя балы; якщо трэба, каб адзін механізм меў большы вага, трэба дадаць явныя вагі, а таксама фільтрацыяя метаданых перш чым яны будуць з’еднаны.
  • Адкройце оба пошукі адночасна і пераканайцеся, што код дзейсна ў режыме „паралельнага“ адкрыцця.
  • Для системнага аналізу велікіх колекцыяй выкарыстоўвайце падсумкі спяльнатыя GraphRAG, і врачуйце расходы на індексаванне.