Ian Chou's Blog

把 GraphRAG 的「走圖」落實:graph.pkl 真遍歷、Local Search 合併擴展、Chat 支援 graph_hops

把 GraphRAG 的「走圖」落實:graph.pkl 真遍歷、Local Search 合併擴展、Chat 支援 graph_hops

目標很簡單:別讓「看起來在走圖」的 GraphRAG 變成幻覺。只要 graph.pkl 已經建立,我們就應該真的從圖上多跳擴展,並把擴展到的 chunks 以可控成本納入檢索結果與對話上下文。

這篇把這次做過的修復完整整理成一條「可運行、可驗證、可觀測」的閉環,順便補上之前容易遺漏的幾個關鍵點:結果合併、參數貫通、擴展上限、以及如何用 query 一眼看出走圖確實生效。


背景:為什麼「走圖」會變成幻覺

GraphRAG 的 Local Search 通常是:

  1. 先用向量/全文做 seed retrieval(Top-K 種子 chunks)
  2. 再從知識圖譜(graph.pkl)對種子做圖遍歷擴展(1-hop/2-hop)
  3. 把擴展到的 chunks 合併進候選集合,再排序/重排(rerank)

實際工程裡,「走圖變幻覺」通常來自三種斷點:

這次修復就是把這三件事對齊。


索引與圖:graph.pkl 裡的節點到底長什麼樣

這裡的圖是由 indexer.py 生成並 pickle 成 /.lancedb/graph.pkl

節點與邊的約定(簡化版):

也就是說,只要拿到 seed chunks 對應的 source + section,就應該能定位到 chunk:{chunk_idx} 節點,進而走到 entity,再走回其他 chunks。


修復一:Local Search 真的把圖擴展結果合併進 results

症狀

Local Search 端點即使調用了圖遍歷函數,也可能出現結果完全不變,或者「有走圖的日誌/程式碼路徑,但最終返回的 JSON 還是原本那 20 條」。

根因通常是:擴展拿到的是「chunk 索引集合」,但沒有把這些索引對應的 chunks 取回並合併到 results DataFrame。

修法

server.py 的 Local Search 流程裡:

關鍵實現位於 local_search 的 graph expansion 段落。

這裡有個隱含假設:chunk:{chunk_idx}chunk_idx 與 LanceDB 表的 row offsets 對齊。由於 index 是用同一份 all_chunks 生成並一次性建表,這個假設在「每次重建索引」的工作流裡是成立的。


修復二:用映射加速 seed chunk → graph node 的定位

症狀

一旦圖變大(上千節點起跳),每次查詢都在圖上做「全節點掃描找 seed chunk」,會讓走圖階段變得又慢又貴,而且不穩定。

修法

在 server 啟動時(lifespan)預先建立映射:

映射建立在 lifespan 載入 graph.pkl 之後,後續 expand_with_graph 優先用映射命中 seed chunks,避免每次都掃全圖。


修復三:Chat 端點真正吃進 graph_hops,並限制擴展數量

症狀

很多專案會先把 「Search 做對」 然後忘了 「Chat 也需要同樣的 retrieval 邏輯」。

典型情況是:

修法

在 chat 中:

這樣做的效果是:Chat 的回答會更容易提到「間接相關但不一定包含 query 字面詞」的知識片段,而且成本更可控。


觀測:用一個 query 一眼看出走圖生效

想驗證「真的走圖」,最好的方法不是看日誌,而是看結果差異:在 graph_hops=2 裡出現的新增 chunks,往往不會包含原始 query 字面詞。

推薦 query:

對照方式(Local Search):

curl -sG 'http://localhost:8000/api/search' --data-urlencode 'type=local' --data-urlencode 'q=bge-m3' --data-urlencode 'graph_hops=0'
curl -sG 'http://localhost:8000/api/search' --data-urlencode 'type=local' --data-urlencode 'q=bge-m3' --data-urlencode 'graph_hops=2'

你要看的不是「Top1 是否一樣」,而是:

這是最強的可觀測訊號:新增證據不是靠全文/向量直接命中,而是靠圖關係擴展進來的。


運行與健康檢查

本地啟動後先看 health:

curl -s 'http://localhost:8000/api/health'

你應該會看到類似:

對應實現:/api/health


仍可改進的地方(這次先不做,但要心裡有數)

  1. offset 的穩定性:目前用 chunk:{idx} 的 idx 當作 LanceDB row offset。只要索引是「全量重建」,這個約定很好用;如果未來改成增量寫入或 table compaction,需要把 「圖節點 -> chunk row」 的映射顯式存成欄位(例如 row_id)而不是依賴 offset。
  2. 走圖的排序訊號:目前擴展進來的 chunks 可能被賦予較低的 _relevance_score 以避免污染排序;長期來看可以把圖距離、entity overlap 等訊號做成可解釋的加權排序。
  3. debug 可視化:如果要讓前端更直觀看到「哪些結果來自走圖」,可以在 API response 裡加一個 debug 欄位(例如 retrieval_path: seed|graph)。這會改變 API schema,這次先保持不動。

小結

這次把 GraphRAG 「走圖」從概念落到可驗證的實現,核心就三件事:

  1. Local Search:擴展出來的 chunks 要真的合併進 results
  2. Chat:參數要貫通,走圖證據要真的進 context,而且要有上限
  3. 可觀測:用 graph_hops=0 vs 2 的「新增且不包含 query 字面詞」的結果差異來驗證