雙路徑整合檢索(Ensemble Retrieval)提升 GraphRAG 種子節點品質:Vector + LLM + RRF 融合落地
雙路徑整合檢索(Ensemble Retrieval)提升 GraphRAG 種子節點品質:Vector + LLM + RRF 融合落地
GraphRAG 的成敗,常常不在「走圖」或「摘要」寫得多漂亮,而在更早的一步:Seed Nodes(種子節點)選得準不準。
如果種子起點偏掉,後面的 graph expansion 很容易把錯誤放大(error propagation);反過來,起點夠準,1-hop/2-hop 擴散就會變成高效的「上下文補齊器」。
這篇把一個很實用的優化落地成可跑的工程實作:雙路徑整合檢索(Ensemble / Dual-Channel Retrieval)。
問題:為什麼單靠 Vector/FTS 或單靠 LLM 都不穩?
路徑 A:向量/全文(既有做法)
- 優勢:快、便宜、召回率高(可以撈到大量相關 chunks)
- 弱點:遇到「隱喻 / 間接指代」會卡住
- 例如 query 是「Snake language」,資料裡寫的是「Python」,字面重疊不高,向量也可能因語境差而 miss
路徑 B:LLM 選擇(新增做法)
- 優勢:概念對齊 + 推理(能把隱喻對到正確概念)
- 例如「Snake language → Python」
- 或「前端框架 → React」
- 弱點:慢、貴,而且如果沒限制範圍,會產生「幻覺節點」
- 選到圖中不存在的節點,後續根本沒法走圖
結論很直接:把兩者並行整合,才更穩健。
解法:並行雙路徑 + 融合(Fusion)選出 Top-K 種子
整體流程長這樣:
flowchart TD Q["Query"] --> A["Path A: Vector/FTS Hybrid"] Q --> B["Path B: LLM Seed Selector"] A --> A1["Seed Chunks"] A1 --> A2["Chunk -> Entity 聚合"] B --> B0["Candidate Entities"] B0 --> B1["LLM Select + Confidence"] A2 --> F["Fusion (RRF)"] B1 --> F F --> E["Top-K Entity Seeds"] E --> C["Entity -> Chunk 回推"] C --> G["Graph Expansion (hops)"]
要點是:Path B 不直接創造節點,而是在候選集合裡選;Fusion 不直接把 distance 跟 confidence 相加,而是用更穩健的排名融合。
落地關鍵 1:限制 LLM 的選擇範圍(避免幻覺)
LLM 路徑如果直接面對「全局節點列表」,常見三個問題:
- 節點數過大 → prompt 變長、成本上升、品質下降
- LLM 會想「補全」 → 輸出圖裡不存在的節點
- 你無法把錯誤隔離 → 一旦錯 seed,後面走圖會放大
因此實作採用「兩段式」:
- 先生成候選 entity 列表(Top-N)
- 來源之一:Path A 從 top chunks 反推出來的 entities(高訊號、低噪音)
- 補強:用 query tokens 對 entity 名稱做簡單命中,補回一些可能被向量漏掉的概念
- LLM 只允許從候選清單裡挑 Top-K
- 並回傳
confidence(0..1) - 設定最低門檻(例如
<0.6直接丟棄)
- 並回傳
這樣做的本質是:用工程約束把「LLM 的創造力」改造成「LLM 的選擇能力」。
補強:Query Token 命中 Entity Name(Keyword / Entity Linking Lite)
這個補強的目的很務實:向量相似度擅長語意,但不擅長精確指名。只要 query 裡出現了某個專有名詞(或縮寫/型號),你通常會希望「至少把這個節點放進候選集合」,否則後面再怎麼 rerank / 走圖都救不回來。
在這個 repo 裡,這件事已經落在 Search Server 端,做法是:
- 先把 query 斷詞成 tokens(中文用
jieba,英文/數字用 regex 抽 ASCII tokens,並做停用詞與長度門檻) - 對圖譜裡所有 entity name 做掃描,計算「token 命中」與「片語(normalized phrase)命中」
- 把命中的 entity 補進候選集合,避免純向量/FTS 漏掉被指名的節點
對應程式在 [server.py:get_query_tokens_for_entities](file:///home/ianc/GitHub/2511-Eleventy-blog/search/server.py#L97-L127) 與 [server.py:get_entity_candidates](file:///home/ianc/GitHub/2511-Eleventy-blog/search/server.py#L130-L187),核心概念是「便宜的字面命中 → 先把候選集補齊」。
tokens = get_query_tokens_for_entities(query)
for entity in entity_meta:
token_hits = ...
phrase_hit = ...
if token_hits > 0 or phrase_hit:
scored.append(...)
這裡有兩層用途:
- 「候選生成」:把命中的 entity 補進 Path B 的候選池,讓 LLM 只能在圖中存在的節點裡挑
- 「Hard Match」:把明確被 query 點名的 entity 另外做成 Path C,並在融合時給更高權重,讓它更容易進 Top-K seeds
另外,你也有一個更偏 chunk 層的「硬字面」護欄:當 query 很像單一 keyword 時,會優先走 FTS,並用 query in content 做 boost([server.py:keyword query](file:///home/ianc/GitHub/2511-Eleventy-blog/search/server.py#L521-L589))。它補的是「內容字面命中」,而上面這段補的是「entity 名稱命中」。
不足之處與補強建議(讓命中更準、更像你想要的 Hard Match)
上面這個版本很便宜、很有效,但它是「簡化版」:夠用,不一定最準。實務上常見幾個不足點,可以視資料量與需求逐步補強:
-
缺少 alias / 同義詞
目前 entity name 主要來自抽取與 tag,小寫化後直接當唯一鍵。若你有別名(例如「OpenRouter」vs「Open Router」、縮寫、產品代號),建議在建圖時就把 alias 一併掛上,或在候選命中前先做 normalization。 -
子字串命中可能誤傷(false positive)
現在判斷是t in name,對英文短 token 或常見片段容易撞到不該中的 entity。常見做法是加護欄,例如:- token 長度門檻(太短的不算)
- 停用詞過濾(尤其是中英混雜時)
- 英文用 word boundary(避免
go命中mongodb這種)
-
多詞實體(phrase)需要更好的匹配
例如 entity 是「leiden algorithm」,query 可能只出現「leiden」,你會希望能命中;但同時你也會希望「完整片語出現時」能拿到更強的加權。這可以用 n-gram(把 query tokens 組 2-gram/3-gram)或 Aho–Corasick 之類的多模式匹配做 phrase scan。 -
Hard Match 的介面(本 repo 已落地)
這個 repo 已把「完整命中 entity 名稱」獨立成 Path C,並在融合時給更高權重,讓「使用者點名」更像你想要的 Hard Match。對應程式:
落地關鍵 2:分數融合別硬加,先解決不可比性
向量搜尋給你的是 distance / relevance;LLM 給你的是 confidence。這兩者尺度不同,硬相加會造成:
- 某一邊分數支配另一邊(權重失真)
- 分數分布漂移時結果不穩(模型換了就壞)
因此採用 RRF(Reciprocal Rank Fusion):只用排名,不用原始分數,對尺度差異很穩。
概念上是:
- Path A 給你一個 entity ranking
- Path B 給你一個 entity ranking
- 融合後:同時在兩邊排前面的 entity 會被自然加權
落地關鍵 3:把 entity seeds 轉回 chunk seeds(才能接上既有走圖)
本專案的走圖擴散入口是從 chunks 出發(chunk → entity → chunk),所以融合後的 Top-K entity 需要回推成 chunk seeds:
- 對每個
entity:{name},取它的 predecessors(哪些chunk:{idx}指向它) - 集合去重後,取前幾個 chunk 作為走圖起點
這一步的好處是:你可以用 LLM 把概念對齊做在 entity 層,最後仍回到 chunk 層去拿可引用的上下文證據。
在這個 repo 的具體實作(你可以直接跑)
本次落地包含兩部分:
- Search API 端點加入 ensemble seed selection
- Local Search:先 hybrid,再用 ensemble 生成額外 seeds,最後做 graph expansion
- Chat:在既有 rerank 後的 seed_df 上做同樣的 ensemble,再走圖補上下文
- 文件更新:把這個步驟寫入 core features checklist,讓架構圖與依賴關係一致
程式碼位置:
- Search server(ensemble + fusion + guardrails):[server.py](file:///home/ianc/GitHub/2511-Eleventy-blog/search/server.py)
- Core features 文件(新增 Ensemble Retrieval 模組):[core-features-checklist.md](file:///home/ianc/GitHub/2511-Eleventy-blog/docs/core-features-checklist.md)
護欄(Guardrails)清單:把不穩變成可控
這類設計在工程上最怕的不是「做不出來」,而是「做出來但不可控」。實務上建議至少要有:
- 候選集合上限(Top-N entities)
- LLM 最低信心門檻(min confidence)
- Path B 最大貢獻量(最多選幾個 entity)
- 融合方法選 RRF(避免尺度問題)
- entity → chunk 回推的上限(避免種子爆炸)
只要護欄齊了,這套雙路徑架構就能在成本、穩定性、品質之間取得平衡。
總結:這是一個從「堪用」到「好用」的升級點
雙路徑整合檢索(Ensemble Retrieval)的核心價值很單純:
- Path A 保留檢索系統的速度與召回
- Path B 補上概念對齊與推理能力
- Fusion 用穩健的排名融合避免尺度地雷
- 最終把更好的 seeds 交給 GraphRAG 的走圖擴散,讓上下文更精準,回答更可信
如果你正在做 GraphRAG,卻常覺得「走圖看起來很厲害,但答案就是差一點」,十之八九是 seed 品質不夠。從這一步下手,通常會是投資報酬率最高的優化。
- ← Previous
把 GraphRAG 想成全自動 Logseq/Roam:從雙向連結到社群摘要與走圖檢索 - Next →
Ensemble 不只在機器學習:把 RAG 做穩的多路徑檢索策略(BM25/FTS + Vector + GraphRAG)