第 4 章 · 四种查询:怎么在图和报告上问答
这章讲什么: 索引产物(图 + 社区报告 + 向量库)建好后,GraphRAG 提供四种检索问答模式,各有擅长的问题类型。本章逐个讲它们取什么料、怎么组上下文、怎么调 LLM 作答。所有模式共用一个模式:
ContextBuilder组上下文 →Search编排 LLM。入口都在packages/graphrag/graphrag/api/query.py。
4.1 四选一:先建立地图
| 模式 | 取料来源 | 擅长的问题 | LLM 调用量 |
|---|---|---|---|
global | 所有社区报告 | 全局主题归纳("整体在讲什么") | 高(map 每批 + 1 次 reduce) |
local | 种子实体的邻域(实体/关系/原文块/报告) | 具体实体("关于 X 说了什么") | 低(≈1 次) |
drift | 社区报告定调 + 图上多轮下钻 | 兼顾全局与局部的复杂问题 | 中高(多轮迭代 ) |
basic | 纯文本块向量检索 | 局部事实(退化传统 RAG) | 低 |
下面按"最能体现 GraphRAG 特色"的顺序讲:global → local → drift → basic。
4.2 global:map-reduce 读遍社区报告
要解决的问题: "这批文档的主要主题是什么"——答案要综合全体,没法靠检索几块。
思路: 既然索引期已经把全体浓缩成一批社区报告,那就把报告分批发给 LLM,每批各自基于报告答一版并给"重要性打分"(map),再把各批的要点按分数汇总成终答(reduce)。这是经典的 MapReduce 套到 LLM 上。
怎么读这张图:左边把社区报告切成若干批并行 map,中间过滤+按分数排序,右边 reduce 成一段答案。
社区报告 ──切成批──▶ [批1] [批2] ... [批N]
│map │map │map (并行, semaphore 限并发)
▼ ▼ ▼
每批产出 points=[{answer, score(0-100)}, ...]
└────────┬─────────┘
汇总所有 points → 丢掉 score==0 → 按 score 降序
│ 截到 max_data_tokens
▼
reduce (1 次 LLM, 流式) ──▶ 终答
真实编排在 GlobalSearch(packages/graphrag/graphrag/query/structured_search/global_search/search.py):
- map:
_map_response_single_batch对每批用MAP_SYSTEM_PROMPT、强制 JSON 模式,让 LLM 吐points(每点带description+score)。asyncio.gather并发跑,self.semaphore控并发上限。 - reduce:
_reduce_response把所有点摊平,过滤掉 score==0 的点、按 score 降序、累加到max_data_tokens为止截断,拼成report_data塞进REDUCE_SYSTEM_PROMPT流式生成终答。 - 兜底诚实:如果所有点都 score==0(数据里没有相关信息)且没开
allow_general_knowledge,直接返回罐头答案NO_DATA_ANSWER("我不知道"),而不是硬编——避免幻觉。这段的告警注 释明确解释了权衡。
动态社区选择(省钱的关键开关): 默认要读某一层的所有报告,很贵。开 dynamic_community_selection 后,DynamicCommunitySelection.select(packages/graphrag/graphrag/query/context_builder/dynamic_community_selection.py)会从 level 0 顶层报告开始,用 LLM 给每个报告的"与问题相关性"打分(rate_relevancy),评分≥阈值才展开它的子社区继续下钻、否则剪枝。相当于沿社区树做"相关性引导的 BFS",只读相关分支,大幅省 token。
4.3 local:从种子实体扩邻域
要解决的问题: "关于 Martin Smith 都说了什么"——答案集中在某几个实体周围。
思路三步: (1) 把问题 embedding 一下,在实体向量库里找语义最近的种子实体;(2) 沿图收集这些实体的关系、邻居、相关原文块、以及覆盖它们的社区报告;(3) 按预算配比把这些料塞进单个上下文窗口,一次 LLM 作答。
第一步"问题→种子实体"是 map_query_to_entities(packages/graphrag/graphrag/query/context_builder/entity_extraction.py):
# 示意,非源码。对应 map_query_to_entities
search_results = vectorstore.similarity_search_by_text(
text=query,
text_embedder=lambda t: embedder.embedding(input=[t]).first_embedding,
k=k * oversample_scaler) # 超采样, 为后面剔除 exclude 留余量
# 命中的向量 id → 还原成 Entity 对象;query 为空时退化为"按 rank 取 top-k"
第二步+第三步在 LocalSearchMixedContext.build_context(packages/graphrag/graphrag/query/structured_search/local_search/mixed_context.py):它是"混合"上下文——把社区报告、实体表、关系表、协变量、原文块几类料按比例拼进预算。几个关键旋钮:
| 参数 | 作用 |
|---|---|
max_context_tokens | 总预算(默认 8000) |
text_unit_prop | 分给"原文块"的比例(默认 0.5) |
community_prop | 分给"社区报告"的比例(默认 0.25) |
top_k_mapped_entities | 取多少个种子实体(默认 10) |
top_k_relationships | 每个实体取多少条关系(默认 10) |
作答在 LocalSearch.search(.../local_search/search.py):拿 build_context 的结果填 LOCAL_SEARCH_SYSTEM_PROMPT,流式生成。因为只调一次 LLM,local 比 global 便宜得多,接近传统 RAG 的成本,但多了"图邻域"这层结 构化上下文。
4.4 drift:社区定调 + 图上迭代下钻
要解决的问题: 既需要全局视角、又要钻到局部细节的复杂问题——global 太粗、local 太窄。
思路: 先用社区报告给问题"定个调"并生成一批追问(primer),再把每个追问当一次 local search 去执行、执行中又冒出新追问,如此迭代若干轮、逐步在图上游走下钻,最后把整棵"问答树"归约成终答。DRIFT = Dynamic Reasoning and Inference with Flexible Traversal。
primer: 用社区报告为 query 生成
{intermediate_answer, score, follow_up_queries[]}
│ 初始 action 入 QueryState 图
▼
┌──── 主循环 (epoch < n_depth) ────┐
│ 从状态里挑排名 靠前的未完成追问 │
│ 取前 drift_k_followups 个 │
│ 每个 → 跑一次 local search │
│ local 又产出新的 follow-up 追问 │─┐
│ 写回 QueryState │ │ 累积成问答树
└────────────────────────────────────┘ │
│ 循环结束 │
▼ ◀┘
reduce: 把整棵树的答案归约成一段终答
代码分工:
- primer(
.../drift_search/primer.py):PrimerQueryProcessor.expand_query用了 HyDE 思路——secrets.choice(reports)随机挑一份社区报告当模板,让 LLM 造一个"假想答案",再用假想答案(而非原问题)去 embedding 检索,缓解"问题和文档措辞不一致"。DRIFT_PRIMER_PROMPT要求输出中间答案 + 评分 + 至少 5 个 follow-up。 - 主循环(
.../drift_search/search.py的DRIFTSearch.search):while epochs < config.n_depth,每轮rank_incomplete_actions挑追问、截前drift_k_followups个、_search_step并发跑 local search,结果连同新追问写回QueryState。 - local 复用:drift 内部直接复用
LocalSearch,只是多传drift_query/k_followups,走 local system prompt 的一个分支(见LocalSearch.search里if "drift_query" in kwargs)。 - reduce:
_reduce_response把QueryState.serialize出的整棵树答案归约成终答。
精华: drift 把"全局定调"(社区报告)和"局部深挖"(local search)用一棵动态生长的追问树缝在一起——既不像 global 那样只停在报告层,也不像 local 那样只看一跳邻域。
4.5 basic:退化成传统 RAG
basic(.../basic_search/)最简单:把文本块 embedding 存向量库,提问时 similarity_search_by_text 找最相似的几块,拼进 BASIC_SEARCH_SYSTEM_PROMPT 作答(basic_context.py(BasicSearchContext.build_context))。它不碰图、不碰社区报告,就是标准向量 RAG——留作最省钱的兜底,也方便和"图带来的增益"做对比。
4.6 一句话怎么选
问"整体/主题/有哪些" → global (可加 dynamic 省钱)
问"某个具体人/物/事" → local
复杂、要边全局边深挖 → drift
就想要个便宜的普通 RAG 兜底 → basic
4.7 代码地图
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 四种查询对外入口 | packages/graphrag/graphrag/api/query.py | global_search,local_search,drift_search,basic_search |
| global map-reduce | .../query/structured_search/global_search/search.py | GlobalSearch,_map_response_single_batch,_reduce_response |
| 动态社区选择(相关性 BFS) | .../query/context_builder/dynamic_community_selection.py | DynamicCommunitySelection.select |
| 报告相关性打分 | .../query/context_builder/rate_relevancy.py | rate_relevancy |
| 问题→种子实体 | .../query/context_builder/entity_extraction.py | map_query_to_entities |
| local 混合上下文 | .../query/structured_search/local_search/mixed_context.py | LocalSearchMixedContext.build_context |
| local 作答 | .../query/structured_search/local_search/search.py | LocalSearch.search |
| drift 主循环 | .../query/structured_search/drift_search/search.py | DRIFTSearch.search |
| drift primer(HyDE 扩展) | .../query/structured_search/drift_search/primer.py | PrimerQueryProcessor.expand_query,DRIFTPrimer |
| drift 状态树 | .../query/structured_search/drift_search/state.py | QueryState |
| basic 向量检索 | .../query/structured_search/basic_search/basic_context.py | BasicSearchContext |