Ian Chou's Blog

GraphRAG 全局檢索:社區分群 + 社區摘要 + Map-Reduce 聚合(LangChain 版)

GraphRAG 全局檢索:社區分群 + 社區摘要 + Map-Reduce 聚合(LangChain 版)

當「局部 GraphRAG」只能回答跟少數 seed 強相關的問題時,全局檢索能把你的所有經驗切成主題社區(community),先在主題層檢索,再回到素材層取證,最後用 map-reduce 聚合成一個結構化總結。

為什麼需要 Global Retrieval

我們原本的 GraphRAG 是「局部檢索」路線:先對 resume_chunks 做向量檢索找到 seed materials,再抽 seed skills,然後用知識圖做 N-hop 擴展,最後用擴展技能回去二次檢索。

這套流程在回答「我有沒有做過 X」這類局部問題時很強,但對下面這種「需要跨主題聚合」的問題比較吃力:

原因很直觀:局部 GraphRAG 的起點依賴 seed 命中;廣域問題可能命中零散或偏科,導致後續擴展也會被帶偏。

所以我們加上一條「全局檢索」路徑:用圖分群(community detection)把技能圖切成多個主題社區,為每個社區生成可檢索的摘要(community summary),查詢時先檢索社區摘要,再針對命中的社區去素材庫取證,最後用 map-reduce 聚合答案。

目標架構(兩層檢索 + Map-Reduce)

query
  → retrieve community summaries(LanceDB: graph_communities)
  → for each selected community:
       retrieve evidence(LanceDB: resume_chunks)
       map:生成「本社區答案」+ evidence ids
  → reduce:聚合所有社區答案成最終總結(附 evidence ids)

關鍵差異:第一層檢索從「素材層」提升到「主題層」。你可以把 graph_communities 想成「你的經歷地圖索引」,而 resume_chunks 則是「可引用的原始素材證據」。

離線:社區分群與摘要索引(build-communities)

Step 1:從 skill-graph.json 建無向技能圖

社區偵測先用「無向圖」比較合理(社區更偏主題聚合,不是因果方向)。我們把 implies / impliedBy / relatedTo 都視為邊,組一張無向圖:

程式實作在:

Step 2:用 NetworkX 做 community detection(MVP)

NetworkX 是 Python 端 py-kb 套件的依賴(見 [pyproject.toml](file:///home/iantp/GitHub/2601-19-resume-skills/py-kb/pyproject.toml#L10-L24)),用 uv sync 安裝後即可使用。

MVP 直接用 NetworkX 的 greedy_modularity_communities,不用新增套件:

from networkx.algorithms.community import greedy_modularity_communities
communities = list(greedy_modularity_communities(g))

接著用 min_size / max_size / max_communities 做裁切,避免超小社區與超大社區(超大社區摘要會太噪)。

Step 3:為每個社區產生可檢索摘要

摘要有兩種策略:

class CommunitySummary(BaseModel):
    summary_short: str
    summary_long: str
    key_skills: list[str] = Field(default_factory=list)

實作在:

Step 4:把社區摘要寫進 LanceDB(graph_communities)

每筆 graph_communities 記錄包含:

關於向量維度:本 repo 的 resume_chunks.vector 已是 1024 維(Voyage-4),因此社區摘要也用相同 embedding 設定,避免維度 mismatch。

LanceDB 封裝在:

寫入策略上,除了清掉舊版本,也會先刪掉同 graph_version 的既有資料,避免重複 add 導致資料越疊越多:

table.delete(f"graph_version != '{graph_version}'")
table.delete(f"graph_version = '{graph_version}'")
table.add(records)

CLI:建立社區索引

cd py-kb
uv run career-kb graph build-communities --max-communities 200
uv run career-kb graph build-communities --max-communities 30 --no-use-llm

命令入口:

線上:global/hybrid 查詢(query --mode global|hybrid)

Step 1:先在社區摘要上做向量檢索

comm_hits = search_graph_communities(query, limit=top_communities)

封裝在:

Step 2:每個社區再回到 resume_chunks 取證(evidence)

對於每個命中的社區,我們會:

實作在:

此外,為了讓 --json 輸出可用,我們把 evidence record 裡的 vector 移除(不然會把 1024 維浮點陣列塞進 JSON,體積巨大):

Step 3:Map(逐社區產生子答案,附 evidence ids)

Map 用 LangChain structured output,強制模型輸出固定 schema,讓後續 reduce 更穩:

class CommunityMap(BaseModel):
    community_id: int
    map_answer: str
    evidence_ids: list[str] = Field(default_factory=list)

實作在:

Step 4:Reduce(聚合總結,主題分段 + evidence ids)

Reduce 把各社區 map 的產出整合成「整體答案」。我們在 prompt 中要求結構清晰且每段附 evidence id,確保可追溯。

實作在:

CLI:global / hybrid 查詢

cd py-kb

# 全局:先命中社區摘要,再取證 + map-reduce
uv run career-kb graph query "backend core strengths" \
  --mode global --top-communities 3 --evidence-per-community 2 --json

# 混合:global 命中社區後,對社區技能做 hops 擴展,再取證 + map-reduce
uv run career-kb graph query "observability" \
  --mode hybrid --top-communities 2 --evidence-per-community 2 --hops 1 --json

命令入口:

踩坑與修正

1) 向量維度要一致(1024)

resume_chunks.vector 在現有 DB schema 是 1024 維;因此社區摘要也必須用同樣維度的 embedding,否則 table.search(query_vector) 會在 runtime 出錯或結果異常。

embedding 設定在:

2) JSON 輸出不要帶 vector

向量欄位適合用在 DB 檢索,不適合跟著 CLI JSON 回傳。把 evidence 的 vector 去掉後,--json 的輸出體積會小很多,也更適合 downstream(例如再餵給其他流程)。

總結

這條「全局檢索」路徑補上了 GraphRAG 的另一半拼圖:

接下來如果想再提升品質,通常會從三個方向下手:

  1. 改善 community detection(更穩定或更貼近語意的分群)
  2. 強化 evidence retrieval(結合 BM25/filters/rerank)
  3. 對 reduce 做更強的結構化輸出(例如固定章節、固定引用格式)

Career Knowledge Base 是一個本地優先的履歷知識庫系統,使用 Python + LanceDB + Voyage AI + LangChain 建構。