GraphRAG 到底解决了普通 RAG 的什么问题:从“切片检索“到“结构化知识“
GraphRAG 到底解决了普通 RAG 的什么问题:从"切片检索"到"结构化知识"
"普通 RAG 回答的是『哪段话提到了它』,GraphRAG 回答的是『这些材料整体在讲什么』——这是两类问题。"
🔥 摘要:RAG 落地久了会撞上一堵墙:单条问题答得不错,归纳型问题一塌糊涂。问「张三和李四什么关系」它能答,问「这 200 份文档讲了哪几个主题」它就只会把相似度最高的几段拼起来给你。根因不在模型,而在知识的组织形式:一堆扁平的文本切片里,没有"谁和谁有关系"这层信息。GraphRAG 的解法是——离线阶段用大模型把文档抽成实体-关系图,再做社区发现并生成社区摘要,在线阶段按问题类型走不同路径检索。本文拆清它解决的三类硬伤、索引流水线的六个环节、Local/Global/DRIFT 三种查询模式怎么选、以及最关键的:它的成本到底贵在哪、什么场景根本不该上。最后给一份
networkx就能跑的迷你实现。
🎯 阅读收益:① 说清普通 RAG 的三类结构性短板及其成因;② 看懂 GraphRAG 索引流水线的每一步在做什么、产出什么;③ 拿到 Local / Global / DRIFT 三种模式的选型对照表;④ 理解 GraphRAG 的成本模型与适用边界,避免为一个不需要图的场景付冤枉钱;⑤ 得到一份不依赖任何大模型 API、本地就能跑通的迷你实现。
⚠️ 说明:GraphRAG 目前存在多个实现(以微软开源项目为代表,各家细节不同),本文讲的是这一类方法的共性原理,不绑定某个具体版本,命令与参数请以你所用项目的官方文档为准。文中示例代码为教学用简化实现,用规则/统计替代了真实流程中的大模型调用,目的是让你在本地零成本跑通全流程。
一、普通 RAG 的三类硬伤
先把问题钉死。普通 RAG 的标准流程是:切块 → 向量化 → 存库 → 提问时按相似度召回 Top-K → 拼进上下文 → 生成。这套流程在局部事实型问题上非常强,但有三类问题它结构性答不了。
硬伤一:全局性 / 归纳型问题
问:"这 200 份年报里,公司提到的风险因素可以归为哪几类?"
Top-K 召回的本质是找与问句最相似的几个片段。而"归为哪几类"这个答案,并不存在于任何一个片段里——它是对全部文档做归纳之后才产生的结论。你召回的 5 个片段,最多只能反映这 5 个片段提到的风险,漏掉的一概不知。
硬伤二:多跳推理链断裂
问:"A 公司的供应商的控股方是谁?"
这需要三步:A → 供应商 B → B 的控股方 C。而「A 的供应商是 B」和「B 的控股方是 C」这两条事实,很可能躺在相距很远的两个文档块里。Top-K 按与整个问句的相似度排序,问句里出现的强信号是 A,于是召回的几乎都是讲 A 的片段——第二步就断了。
硬伤三:实体歧义与指代碎片化
同一家公司,在文档 A 里叫「XX 科技」,文档 B 里叫「XX 科技有限公司」,文档 C 里只写「该公司」。向量检索对这三种写法给出的是三个不同的语义位置,没有任何机制告诉系统它们是同一个东西。
根因一句话
普通 RAG 把知识存成了"一堆互不相识的文本片段",缺的是结构。

图里左右两侧的差别,本质上是检索的对象不同:
- 普通 RAG 检索的是文本片段,片段之间没有连接关系;
- GraphRAG 检索的是图上的结构 + 社区摘要,实体之间有显式的边,主题之间有一层级关系。
二、GraphRAG 的两条腿:离线建图,在线查询
GraphRAG 和常见 RAG 最大的工程差异是:它把大量工作前移到了离线阶段。在线查询反而变得很轻。
2.1 离线索引流水线

六个环节逐一拆开:
| 环节 | 做什么 | 关键产物 | 工程要点 |
|---|---|---|---|
| ① 文档切分 | 按语义块切分,保留来源位置 | 带 doc_id / chunk_id 的文本块 |
块不要太大,抽取实体的精度会下降;建议 300~600 token |
| ② 实体关系抽取 | 让 LLM 从每个块里抽出实体与关系三元组 | (主体, 关系, 客体) + 描述 + 来源引用 |
成本主要来源。要用结构化输出约束格式,并做去重归一化 |
| ③ 图构建 | 同名/同义实体合并成节点,关系成边 | 无向/有向加权图 | 边权重通常取"该关系被提到过的次数";实体描述做聚合 |
| ④ 社区发现 | 用 Leiden 等算法把图切成层级化社区 | 多层级的社区划分 | 层级很关键:上层社区对应大主题,下层对应子话题 |
| ⑤ 社区摘要 | 为每个社区生成一份可读的报告 | 社区报告(含关键实体与结论) | 第二成本来源。应对大社区做分片再汇总,避免超长上下文 |
| ⑥ 双层索引 | 实体/关系与社区报告分别建索引 | 向量索引 + 图结构 | 在线阶段两条路都要能查 |

2.2 为什么"社区"这一步是灵魂
如果只做实体关系图,你拿到的是一张很大很杂乱的网,直接检索依然不知道从哪下手。
社区发现做的事,是把这张网自动聚成一团团主题,而且通常是层级化的:
- 第 0 层(最细):几十个小社区,每个对应一个具体子话题;
- 第 1 层:若干个中社区,对应几个大主题;
- 第 2 层(最粗):少数几个大社区,对应材料的整体轮廓。
有了这个层级,"这堆材料讲了什么"这个问题就有了直接的检索对象——去查最上层的社区摘要就行。这正是普通 RAG 唯一缺失的东西。
三、三种查询模式:同一张图,三种走法![在这里插入图片描述]()
| 模式 | 检索路径 | 典型问题 | 开销 | 用错的症状 |
|---|---|---|---|---|
| Local | 定位问题中的实体 → 取该实体的邻居子图 → 回到原始文本取证据 | 「A 和 B 是什么关系?」 | 低 | 拿它问全局问题 → 答不全 |
| Global | 取指定层级的全部社区摘要 → 分片 map 打分/归纳 → reduce 汇总 | 「这批材料的核心主题有哪些?」 | 高 | 拿它问单点事实 → 又慢又贵还不精准 |
| DRIFT | 先用社区摘要定方向 → 沿实体关系逐跳扩展 → 边查边判断是否继续 | 「多跳推理 + 需要可追溯的推理链」 | 中 | 拿它问简单问题 → 多绕一圈 |
3.1 Global 的 map-reduce 为什么要这么设计
Global 模式要处理的是"全部社区摘要",这个量经常远超上下文窗口。做法是:
- Shuffle + Map:把社区摘要随机打散成若干批次,每批独立喂给模型,让它针对问题输出"要点 + 该要点的重要性评分";
- Reduce:把所有批次输出的要点按评分排序、截断,再让模型汇总成最终答案。
这个设计的精髓在于:每一份摘要都被独立看过一遍,而不像 Top-K 那样只看了最相似的几份。这正是它能回答归纳型问题的原因。
3.2 一份简单的选型口诀
- 问题里有明确的实体名 → 先试 Local;
- 问题里出现「有哪些 / 总体 / 趋势 / 归类 / 主要」→ Global;
- 问题需要拐好几个弯、或者你要给用户看推理过程 → DRIFT;
- 拿不准 → 用 DRIFT 兜底,它是三者里最"折中"的。
四、成本账:它贵在哪,什么场景根本不该上
这是聊 GraphRAG 时最该说清楚、也最常被跳过的一节。
4.1 成本都花在哪
| 成本项 | 发生阶段 | 说明 |
|---|---|---|
| 实体关系抽取 | 离线 | 每个 chunk 一次(或多次)LLM 调用。最主要的成本来源,与文档量成正比 |
| 实体描述聚合 | 离线 | 每个实体一次调用,实体数通常远小于 chunk 数 |
| 社区摘要生成 | 离线 | 社区数 × 层级数。大社区需要分片 map-reduce,调用次数进一步放大 |
| Global 在线查询 | 在线 | 每次请求要处理全部社区摘要,可能是几十次 LLM 调用 |
| 存储 | 全程 | 图结构 + 向量索引 + 社区报告,比纯 RAG 高一档 |
一句话概括:GraphRAG 把成本从"在线"搬到了"离线",但总量上来了。
4.2 该上 GraphRAG 的信号
- 文档量大(几十到几千篇),且问题经常跨文档;
- 业务方常问归纳型问题("有哪些""总体来看""怎么归类");
- 需要多跳推理,而且要给用户看推理依据;
- 语料里实体歧义严重(简称、指代、别名泛滥);
- 知识相对稳定,不是每小时都在变。
4.3 不该上 GraphRAG 的信号
- 语料小、问题以"查某个具体字段/条款"为主 → 普通 RAG 甚至直接查库更合适;
- 知识更新极频繁 → 每次全量重建图的成本吃不消,需要先把增量更新方案想清楚;
- QPS 高且预算紧 → Global 模式单次请求成本是普通 RAG 的一个数量级以上;
- 只是觉得"普通 RAG 效果不好想试试" → 先确认是不是切分和检索的问题,那两步的优化成本低得多。
一个判断捷径:拿 20 个真实业务问题做测试,如果其中超过 1/3 是归纳型或多跳型,再考虑 GraphRAG;否则先把普通 RAG 的切分、混合检索、重排做扎实,性价比高得多。
五、可运行代码:用 networkx 手搓一个迷你 GraphRAG
真实 GraphRAG 依赖大量 LLM 调用,本地跑不起。下面这份实现用规则抽取 + 统计摘要替代 LLM,让你可以零 API 成本把完整流程跑通,看清楚每一步到底在干什么。
5.1 环境准备
pip install networkx
5.2 完整代码
# -*- coding: utf-8 -*-
"""
mini_graphrag.py —— 最小可运行的 GraphRAG 流程演示
真实流程中「实体关系抽取」和「社区摘要」由 LLM 完成,
这里用规则与统计替代,目的是零成本跑通全流程、看清结构。
运行:python mini_graphrag.py
"""
import re
from collections import defaultdict
import networkx as nx
# ---------- 1. 语料:假装这是你的文档库 ----------
DOCS = {
"d1": "云帆科技是澜江集团的控股子公司,主营工业视觉检测设备。",
"d2": "澜江集团收购了明远物流,明远物流承担云帆科技的产品运输。",
"d3": "云帆科技与清源大学共建机器视觉联合实验室,清源大学提供算法支持。",
"d4": "星海资本投资了云帆科技,星海资本同时投资了明远物流。",
"d5": "清源大学实验室发布新的缺陷检测算法,云帆科技采用了该算法。",
"d6": "澜江集团的年度营收增长主要来自工业视觉检测设备业务。",
}
# 真实场景由 LLM 抽取;这里用词表模拟
ENTITIES = ["云帆科技", "澜江集团", "明远物流", "清源大学", "星海资本",
"工业视觉检测设备", "缺陷检测算法"]
# ---------- 2. 实体关系抽取(规则版:同句共现即建关系) ----------
def extract_triples(docs, entities):
triples = []
for doc_id, text in docs.items():
hit = [e for e in entities if e in text]
for i in range(len(hit)):
for j in range(i + 1, len(hit)):
triples.append((hit[i], hit[j], doc_id))
return triples
# ---------- 3. 图构建:实体合并 + 边加权重 ----------
def build_graph(triples):
G = nx.Graph()
evidence = defaultdict(set) # 边的证据来源
for a, b, doc_id in triples:
if G.has_edge(a, b):
G[a][b]["weight"] += 1
else:
G.add_edge(a, b, weight=1)
evidence[frozenset((a, b))].add(doc_id)
return G, evidence
# ---------- 4. 社区发现(真实场景用 Leiden,这里用 Louvain) ----------
def detect_communities(G):
try:
return list(nx.community.louvain_communities(G, seed=42))
except Exception: # 老版本 networkx 兜底
return list(nx.community.greedy_modularity_communities(G))
# ---------- 5. 社区摘要(真实场景由 LLM 生成可读报告) ----------
def summarize_communities(G, communities, evidence):
reports = []
for idx, comm in enumerate(communities):
sub = G.subgraph(comm)
core = sorted(sub.degree(weight="weight"), key=lambda x: -x[1])
core_names = [n for n, _ in core[:3]]
src = set()
for u, v in sub.edges():
src |= evidence[frozenset((u, v))]
reports.append({
"id": idx,
"members": sorted(comm),
"core": core_names,
"sources": sorted(src),
# 真实场景这里是一段 LLM 生成的自然语言摘要
"text": "本社区围绕 {} 展开,涉及实体:{},证据文档:{}".format(
"、".join(core_names), "、".join(sorted(comm)), "、".join(sorted(src))),
})
return reports
# ---------- 6. Local 查询:实体 → 邻居子图 → 原始证据 ----------
def local_query(G, evidence, entity, docs):
if entity not in G:
return "图中未找到实体:%s" % entity
lines = ["【Local 查询】与「%s」直接相关的实体:" % entity]
for nb in sorted(G.neighbors(entity),
key=lambda n: -G[entity][n]["weight"]):
w = G[entity][nb]["weight"]
srcs = sorted(evidence[frozenset((entity, nb))])
lines.append(" - %s(共现 %d 次,来源:%s)" % (nb, w, "、".join(srcs)))
for s in srcs:
lines.append(" 证据[%s]:%s" % (s, docs[s]))
return "\n".join(lines)
# ---------- 7. Global 查询:扫全部社区摘要 → 打分 → 汇总 ----------
def global_query(question, reports, top_k=3):
kw = set(re.findall(r"[\u4e00-\u9fa5]{2,}", question))
scored = []
for r in reports:
score = sum(r["text"].count(w) for w in kw)
scored.append((score, r))
scored.sort(key=lambda x: -x[0])
picked = [r for _, r in scored[:top_k]]
lines = ["【Global 查询】命中社区:"]
for r in picked:
lines.append(" - 社区#%d:%s" % (r["id"], r["text"]))
# 真实场景:把上面这段上下文喂给 LLM 做 map-reduce 汇总
return "\n".join(lines)
if __name__ == "__main__":
triples = extract_triples(DOCS, ENTITIES)
G, evidence = build_graph(triples)
comms = detect_communities(G)
reports = summarize_communities(G, comms, evidence)
print("=" * 60)
print("图规模:%d 个实体 / %d 条关系;划分为 %d 个社区"
% (G.number_of_nodes(), G.number_of_edges(), len(comms)))
print("=" * 60)
print("\n" + local_query(G, evidence, "云帆科技", DOCS))
print("\n" + global_query("这批材料主要涉及哪些机构和业务方向?", reports))
5.3 怎么把它换成真家伙
| 示例里的简化 | 换成真实实现 |
|---|---|
| 固定实体词表 | 让 LLM 从每个 chunk 抽取实体,要求输出 JSON/结构化三元组 |
| 同句共现即建关系 | 让 LLM 同时输出关系类型(控股/投资/供应/合作…),边才有语义 |
| Louvain 社区发现 | 换成 Leiden,并保留多层级的社区划分 |
| 字符串拼接的"摘要" | 让 LLM 为每个社区生成报告,超长时分片 map-reduce |
| 关键词打分的检索 | 向量检索 + 图遍历混合;Global 用 LLM 打分 |
print 输出 |
把拼好的上下文喂给 LLM 生成最终答案,并附上来源编号 |
跑通这份代码之后,再去看成熟的 GraphRAG 实现,你会发现所有模块都能对上号——区别只是"谁来做抽取和摘要"。
六、踩坑清单
- 把 GraphRAG 当"RAG 加速器"用。它解决的是"答不了",不是"答得慢"。纯粹为了提升已有问答的准确率而上它,大概率失望。
- 低估实体抽取的质量影响。图是下游一切的基础,抽取抽歪了,社区和摘要全跟着歪。这一步值得花最多时间调 prompt 和做人工抽检。
- 不做实体归一化。同一个实体出现三个节点,图被割裂,社区发现立刻失效。别名表 + 相似度合并必须做。
- 社区层级选错。Global 查询时用最细层,上下文爆掉;用最粗层,答案空洞。要按问题粒度动态选层。
- 没设计增量更新。语料一变就要全量重建,成本直接失控。上线前必须有"新增文档只更新局部子图"的方案。
- 忽略 Global 的在线成本。本地测试时几十个社区无所谓,真上线每次请求都是几十次 LLM 调用,账单会教做人。
- 只存图、丢原文。答案是有了,但没有可点击的出处,用户不敢信。证据引用必须一路带到最终输出。
- 社区摘要超过上下文。大社区的报告本身就超长,必须实现分片 map-reduce,否则会静默截断、丢失信息。
七、下一步:能练手,也能接单
- 跑通上面的迷你实现,换成自己的语料(比如某领域的公开报告集),观察社区划分是否合理。
- 做一次 A/B 对照:同一批 20 个问题(10 个事实型 + 10 个归纳型),分别用普通 RAG 和 GraphRAG 跑,把答案质量与成本一起列成表。这张表本身就是一篇很有说服力的内容。
- 实现增量更新:新增 10 篇文档,只重建受影响的子图与社区,对比全量重建的成本差。
- 做一个"图谱可视化"前端:把实体关系图用 PyVis/ECharts 画出来,支持点选实体看证据——这是交付给客户时最加分的一环。
结语
GraphRAG 不是"更好的 RAG",它是换了个问题在答。
- 普通 RAG 问的是:哪段文字最像这个问题?
- GraphRAG 问的是:这件事,在整个知识网络里处在什么位置?
当你的用户开始问"有哪些""总体来看""它们之间什么关系",而你的系统只能堆几段相似文本时,缺的就不是更好的向量模型,而是结构。
但请务必记住它的代价:成本从在线搬到了离线,且总量上来了。上之前,先用 20 个真实问题做一次判断——如果归纳型问题不到三分之一,先把普通 RAG 的切分、混合检索和重排做扎实,那才是性价比最高的一步。
你手上的业务问题里,归纳型和多跳型问题占比大概多少?欢迎在评论区说说你的场景,一起判断该不该上图。

浙公网安备 33010602011771号