把 GraphRAG 的「走圖」落實:graph.pkl 真遍歷、Local Search 合併擴展、Chat 支援 graph_hops
把 GraphRAG 的「走圖」落實:graph.pkl 真遍歷、Local Search 合併擴展、Chat 支援 graph_hops
目標很簡單:別讓「看起來在走圖」的 GraphRAG 變成幻覺。只要
graph.pkl已經建立,我們就應該真的從圖上多跳擴展,並把擴展到的 chunks 以可控成本納入檢索結果與對話上下文。
這篇把這次做過的修復完整整理成一條「可運行、可驗證、可觀測」的閉環,順便補上之前容易遺漏的幾個關鍵點:結果合併、參數貫通、擴展上限、以及如何用 query 一眼看出走圖確實生效。
背景:為什麼「走圖」會變成幻覺
GraphRAG 的 Local Search 通常是:
- 先用向量/全文做 seed retrieval(Top-K 種子 chunks)
- 再從知識圖譜(graph.pkl)對種子做圖遍歷擴展(1-hop/2-hop)
- 把擴展到的 chunks 合併進候選集合,再排序/重排(rerank)
實際工程裡,「走圖變幻覺」通常來自三種斷點:
- 圖有載入,但擴展結果沒合併進最終 results
- Chat 端點沒有使用
graph_hops,導致對話上下文永遠是純向量/全文的 Top-K - 圖遍歷擴展沒有成本上限,導致 context 爆炸(token 失控)或響應不穩定
這次修復就是把這三件事對齊。
索引與圖:graph.pkl 裡的節點到底長什麼樣
這裡的圖是由 indexer.py 生成並 pickle 成 /.lancedb/graph.pkl。
節點與邊的約定(簡化版):
- 節點
article:{source}:文章chunk:{chunk_idx}:chunk(chunk_idx來自 indexer 裡的 enumerate 順序)entity:{entity_name}:實體(tag、概念、程式碼片段等,統一 lower)
- 邊
article -> chunk:CONTAINSchunk -> entity:HAS_ENTITYentity <-> entity:CO_OCCURS(同一篇文章共現)
也就是說,只要拿到 seed chunks 對應的 source + section,就應該能定位到 chunk:{chunk_idx} 節點,進而走到 entity,再走回其他 chunks。
修復一:Local Search 真的把圖擴展結果合併進 results
症狀
Local Search 端點即使調用了圖遍歷函數,也可能出現結果完全不變,或者「有走圖的日誌/程式碼路徑,但最終返回的 JSON 還是原本那 20 條」。
根因通常是:擴展拿到的是「chunk 索引集合」,但沒有把這些索引對應的 chunks 取回並合併到 results DataFrame。
修法
在 server.py 的 Local Search 流程裡:
expand_with_graph(results, q, hops=graph_hops)返回擴展到的 chunk 索引- 用
chunks_table.take_offsets(expanded_indices)把這些 offsets 對應的 rows 拉回來 - 跟原本 hybrid search 的
results合併,並按(source, section)去重
關鍵實現位於 local_search 的 graph expansion 段落。
這裡有個隱含假設:chunk:{chunk_idx} 的 chunk_idx 與 LanceDB 表的 row offsets 對齊。由於 index 是用同一份 all_chunks 生成並一次性建表,這個假設在「每次重建索引」的工作流裡是成立的。
修復二:用映射加速 seed chunk → graph node 的定位
症狀
一旦圖變大(上千節點起跳),每次查詢都在圖上做「全節點掃描找 seed chunk」,會讓走圖階段變得又慢又貴,而且不穩定。
修法
在 server 啟動時(lifespan)預先建立映射:
- key:
(source, section) - value:對應的 graph node id(例如
chunk:123)
映射建立在 lifespan 載入 graph.pkl 之後,後續 expand_with_graph 優先用映射命中 seed chunks,避免每次都掃全圖。
修復三:Chat 端點真正吃進 graph_hops,並限制擴展數量
症狀
很多專案會先把 「Search 做對」 然後忘了 「Chat 也需要同樣的 retrieval 邏輯」。
典型情況是:
/api/search支援graph_hops/api/chat雖然看似也有圖擴展,但沒有貫通參數,或者擴展結果沒進 context
修法
在 chat 中:
- 從
request.parameters讀取graph_hops(預設 1) - 對 rerank 後的 seed_df 做
expand_with_graph - 用
MAX_GRAPH_EXPANDED_CHUNKS限制圖擴展追加的 chunks 數量,避免 context 失控
這樣做的效果是:Chat 的回答會更容易提到「間接相關但不一定包含 query 字面詞」的知識片段,而且成本更可控。
觀測:用一個 query 一眼看出走圖生效
想驗證「真的走圖」,最好的方法不是看日誌,而是看結果差異:在 graph_hops=2 裡出現的新增 chunks,往往不會包含原始 query 字面詞。
推薦 query:
bge-m3responses apileidenalg
對照方式(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 是否一樣」,而是:
graph_hops=2的 topN 是否出現graph_hops=0完全沒有的(source, section)- 這些新增條目的 snippet 是否「不包含 query 字面詞」
這是最強的可觀測訊號:新增證據不是靠全文/向量直接命中,而是靠圖關係擴展進來的。
運行與健康檢查
本地啟動後先看 health:
curl -s 'http://localhost:8000/api/health'
你應該會看到類似:
index_loaded: truegraph_loaded: truegraph_nodes / graph_edges有數字(不是 0)
對應實現:/api/health
仍可改進的地方(這次先不做,但要心裡有數)
- offset 的穩定性:目前用
chunk:{idx}的 idx 當作 LanceDB row offset。只要索引是「全量重建」,這個約定很好用;如果未來改成增量寫入或 table compaction,需要把 「圖節點 -> chunk row」 的映射顯式存成欄位(例如row_id)而不是依賴 offset。 - 走圖的排序訊號:目前擴展進來的 chunks 可能被賦予較低的
_relevance_score以避免污染排序;長期來看可以把圖距離、entity overlap 等訊號做成可解釋的加權排序。 - debug 可視化:如果要讓前端更直觀看到「哪些結果來自走圖」,可以在 API response 裡加一個 debug 欄位(例如
retrieval_path: seed|graph)。這會改變 API schema,這次先保持不動。
小結
這次把 GraphRAG 「走圖」從概念落到可驗證的實現,核心就三件事:
- Local Search:擴展出來的 chunks 要真的合併進 results
- Chat:參數要貫通,走圖證據要真的進 context,而且要有上限
- 可觀測:用
graph_hops=0 vs 2的「新增且不包含 query 字面詞」的結果差異來驗證
- ← Previous
企業級 GraphRAG 架構選型:LanceDB + Graph vs LightRAG 深度比較 - Next →
petgraph vs rustworkx / rustworkx-core:Rust 與 Python 圖計算怎麼選