
本文以一张 LightRAG 查询流程图为线索,把 GraphRAG 这一类方案的检索链路逐环拆开,重点解释三件事:图谱在检索中到底承担什么角色、为什么必须把问题拆成两个层次、以及为什么找到实体之后还必须回头去拿原文。

设知识库被切分为文本块(chunk)集合:
给定用户问题 q ,嵌入模型 E(\cdot) 把文本映射到 \mathbb{R}^d 。朴素 RAG 的检索就是一次 Top-k 近邻搜索:
其中相似度通常取余弦相似度:
然后把 \mathcal{R}(q) 拼进提示词交给大模型生成。整条链路只有一次相似度计算,简单、快、便宜。
第一类:需要跨文档聚合。
"我们所有微服务里,哪些依赖了那个即将下线的旧版认证组件?"
答案分散在十几份文档里,每份只提到一个服务。任何单个 chunk 与问题的相似度都不高,Top-k 只能捞到最"像"的那几个,剩下的全部丢失。
第二类:需要关系推理。
"支付超时和上个季度那次数据库迁移有关系吗?"
知识库里有一段讲支付服务的告警,有一段讲迁移方案,但没有任何一段话同时提到这两件事。它们之间的联系是"支付服务依赖订单库、订单库是迁移对象"这样一条两跳路径——这条路径不存在于任何一个 chunk 里,它存在于文档之间。
第三类:主题级归纳。
"这批事故报告整体反映出什么系统性问题?"
这需要对全库做归纳,而不是找到某几段。Top-k 天然做不到。
朴素 RAG 的失效可以归结为一句话:
\text{sim}(E(q), E(c)) 度量的是"这段文字读起来像不像在回答这个问题",而不是"这段文字是否是推出答案所必需的一环"。当答案需要 c_i 与 c_j 联合成立、而两者各自的相似度都排不进 Top-k 时,检索就在第一步失败了——后面的模型再强也救不回来,因为它根本没拿到料。
切分策略、重排模型、更大的 k ,都只能在"语义邻近"这个维度上做优化,动不了根因。要动根因,得换一种索引结构。
GraphRAG 这一类方案的共同做法是:在索引期就用大模型把文本里的实体和关系抽出来,显式地存成一张图。原本散落在文档之间、只存在于读者脑中的关联,被物化成了可以遍历的边。
V 是实体集合,R 是关系类型集合,每条边是一个三元组 e = (v_i, r, v_j) 。
关键在于,图里每个元素都挂着两样东西:
元素 | 描述文本 | 来源集合 |
|---|---|---|
实体 |
|
|
关系 |
|
|
S_v 和 S_e 就是流程图里反复出现的 source_id:记录"这个实体/这条关系是从哪几个原文块里抽出来的"。
请记住这个 S 。它是整套方案能不能落地的关键,第四节会专门讲。
同样是"建图再检索",业界主要分成两派:
微软 GraphRAG 的路线:建完图之后跑社区检测(Leiden 算法),把图划分成层次化社区,再让大模型为每个社区生成摘要。全局查询时遍历这些社区摘要做 map-reduce 汇总。
LightRAG 的路线:不做社区检测,改为给实体和关系分别建立向量索引,查询时用双层关键词分别命中,再做图扩展。
本文这张流程图属于后者。理解它的价值在于:它是成本可控、增量友好的那一派,对个人和中小团队更现实。
这是整套设计里最巧妙的一步,也是最容易被误解的一步。
把知识库想象成一堆卷宗,你是接手案子的侦探。你会同时做两件不同性质的事:
只做第一件事,你会得到一堆孤立事实,拼不出案情;只做第二件事,你有了大方向却拿不出证据。必须两条线同时走,最后合并。
用一次大模型调用,把问题拆成两组关键词:
以前面那个问题为例:
"支付超时和上个季度那次数据库迁移有关系吗?"
抽取结果 | |
|---|---|
K_{\text{low}} | 支付服务、超时告警、数据库迁移、订单库 |
| 故障归因、系统依赖关系、变更影响范围 |
注意 K_{\text{high}} 里那些词在原文里可能一次都没出现过。它们不是用来做字面匹配的,而是用来在关系向量库里找"语义上属于这类关联"的边。这正是它能发现"支付服务→订单库→迁移项目"这条路径的原因。
📌 此处插入流程图
下面对着图从上到下走一遍。
用户问题→关键词抽取提示词 + 系统提示词→关键词抽取 LLM→低层关键词 K_low+高层关键词 K_high
这一步是整条链路的唯一分叉点,也是最脆弱的一环:如果关键词抽错,后面所有检索都在错误的方向上做精确计算。
工程上有两个要点:其一,输出必须是结构化的(强制 JSON),否则解析失败会直接击穿整条链路;其二,提示词里应当给出低层与高层的判别标准和示例,否则模型很容易把两类词混在一起——实践中最常见的错误是把所有词都塞进低层。
对应图的左侧分支,由 K_{\text{low}} 驱动。
第一步,实体库匹配。 把低层关键词向量化,在实体向量库里取 Top-k :
第二步,图谱扩展。 只拿到命中的实体是不够的——答案往往在邻居身上。所以沿边向外走一跳:
N(v) 是 v 的邻居集合。这一步是 GraphRAG 相对朴素 RAG 的本质增益所在:朴素 RAG 只能看到"像"的东西,而这里能看到"连着"的东西。
⚠️ 但它同时是成本失控的源头。扩展后的规模上界为:
\bar{d} 是平均度数。真实知识图谱的度数分布是长尾的——总有几个 hub 节点(比如"用户""系统""公司"这类被反复提及的泛化实体)度数高达成百上千。一旦命中一个 hub,\tilde{V}_q 直接爆炸。必须做度数剪枝或按边权截断,这不是优化项而是必需项。
对应图的右侧分支,由 K_{\text{high}} 驱动。结构对称,但起点是边而不是点:
命中关系之后,反向把两端的实体及其邻居拉进来:
为什么要单独为关系建一套向量索引? 因为"关系的语义"和"实体的语义"不是一回事。"A 依赖 B"这条边的描述文本,讲的是一种依赖模式;而实体 A 的描述讲的是 A 是什么。当用户问的是"有没有关系""是什么导致了"这类问题,他真正要匹配的是前者。把两者混在一个索引里,关系语义会被实体描述淹没。
这是图中央那个灰色虚线框,也是全文最重要的一节。
到这里,两条路径都拿到了一批实体和关系。一个自然的想法是:直接把这些实体描述和关系描述拼成上下文交给模型不就行了?
不行。 原因是:d_v 和 d_e 是索引期由大模型对原文压缩得到的产物。压缩必然丢信息——具体的数字、时间、配置项、人物原话、条件限定,全都可能在归纳过程中被抹掉。如果只给模型看图谱描述,它会基于一份"二手摘要"作答,而且无法察觉自己丢了什么。
所以必须走这条回溯链路:
第一步,按 source_id 收集候选池。 把命中的实体和关系各自的来源合并:
第二步,合并去重。 同一个 chunk 常被多个实体引用,必须去重,否则重复内容会挤占上下文预算。
第三步,用原问题向量做二次精排。 这里有一个极容易写错的细节:
注意这一步用的是完整原问题 q 的向量,而不是关键词的向量。
为什么?因为关键词是抽象和裁剪后的产物,用它去筛原文会持续放大第一步的抽取偏差;而此时候选池 \hat{C} 已经被图谱大幅收窄了——从全库几万个 chunk 降到几十个——在这么小的集合上,用信息最完整的原问题做精排,精度最高。
这个设计可以概括为一句话:用图谱做召回,用原问题做精排。 图谱负责"该看哪几页",原问题负责"这几页里哪段最贴题"。两者分工不同,用错向量就失去了各自的优势。
每条路径最终产出一份结构化上下文,由两层构成:
这个双层结构不是冗余,而是刻意的分工:
两路上下文合并后(图中的 hybrid 模式),送入回答生成模板,产出最终答案。
值得注意的是:整条链路上,大模型只被调用了两次——一次关键词抽取,一次答案生成。中间所有工作都是向量检索和图遍历,是廉价的确定性计算。这是这套方案成本可控的根本原因。
def graph_rag_query(q: str, mode: str = "hybrid") -> str:
# ① 唯一的分叉点:双层关键词抽取(LLM 调用 1/2)
kw = extract_keywords(q) # -> {"low": [...], "high": [...]}
contexts = []
# ② 本地路径:偏实体
if mode in ("local", "hybrid"):
ents = vec_entities.search(kw["low"], top_k=K)
ents, rels = expand_neighbors(ents, max_degree=DEGREE_CAP) # 必须剪枝
contexts.append(build_ctx(q, ents, rels))
# ③ 全局路径:偏关系
if mode in ("global", "hybrid"):
rels = vec_relations.search(kw["high"], top_k=K)
ents, rels = expand_from_edges(rels, max_degree=DEGREE_CAP)
contexts.append(build_ctx(q, ents, rels))
# ④ 合并两路上下文并生成(LLM 调用 2/2)
return generate(q, merge(contexts))
def build_ctx(q: str, ents, rels) -> Context:
"""图中央那个灰框:召回用图谱,精排用原问题"""
# 按 source_id 收集候选池
pool = {sid for x in list(ents) + list(rels) for sid in x.source_ids}
chunks = dedup(fetch_chunks(pool))
# ⚠️ 关键:此处用原问题 q 的向量,不是关键词的向量
top_chunks = rank_by_similarity(query=q, candidates=chunks, top_m=5)
return Context(
graph_layer=describe(ents, rels), # 结构
text_layer=top_chunks, # 证据
)任何方案的取舍都写在成本结构里。
环节 | 朴素 RAG | 社区摘要路线 | 双层检索路线(本文) |
|---|---|---|---|
索引期 LLM 调用 | 无 | 实体关系抽取 + 每个社区生成摘要 | 实体关系抽取 |
查询期 LLM 调用 | 1 次(生成) | 1 次生成 + 社区摘要 map-reduce | 2 次(抽取 + 生成) |
查询期检索开销 | 一次向量搜索 | 遍历社区摘要 | 两次向量搜索 + 图遍历 |
增量更新 | 只需新增向量 | 社区结构可能需重算 | 局部更新图与索引 |
主题级归纳能力 | 弱 | 强 | 中 |
多跳关系推理 | 弱 | 强 | 强 |
一个粗略的量级判断:设全库 n 个 chunk、| V | 个实体,双层检索的查询期复杂度约为
而社区摘要路线的全局查询需要触及社区摘要集合 \mathcal{S} ,token 消耗随 | \mathcal{S} | 线性增长。这是两者成本差异的结构性来源。
⚠️ 具体的 token 数与延迟数据请以各方案论文与官方基准为准,不同数据集差异很大,本文只做结构性对比。
值得上 GraphRAG 的场景:
不值得的场景:
一条实用判据: 先用朴素 RAG 跑一批真实问题,把失败案例分类。如果失败集中在"需要跨文档/多跳"这一类,上 GraphRAG 有明确收益;如果失败集中在"切分不当""检索到了但答错了",那问题不在索引结构上,建图救不了你。
1. 实体抽取质量决定整个系统的上限。 图建歪了,后面所有检索都在歪的结构上做精确计算。抽取环节的提示词、实体类型定义、消歧规则,值得投入的精力远超大多数人的预期。
2. Hub 节点会让图扩展爆炸。 前面公式里那个 \bar{d} 在长尾分布下毫无参考价值。必须设度数上限、按边权排序截断,或者在索引期就把"用户""系统"这类泛化实体过滤掉。
3. 同一实体的描述会不断累积。 同一个实体在几十份文档里被提到,描述会越拼越长直到塞不进上下文。需要定期做描述合并与摘要,而合并本身又是一次有损压缩——这是个需要主动管理的权衡,不是可以放任的细节。
4. 别忘了 source_id 是可信度的锚。 它不只用来回溯原文,还是做引用溯源的基础。答案里能标出"依据来自哪份文档哪一段",可信度会有质变。这个能力几乎是免费的,不要浪费。
5. 评估必须分类做,不能只看总分。 GraphRAG 的收益高度集中在特定问题类型上。如果只看一个笼统的准确率,多跳能力的提升会被单点查询上的性能退化抵消掉,你会得出"上了图谱没什么用"的错误结论。至少要把测试集按"单点事实 / 跨文档聚合 / 多跳关系 / 主题归纳"分成四类,分别看提升。这是最容易被跳过、也最容易导致错误决策的一步。
回到开头那张流程图,整套设计可以压缩成三句话:
而它最值得学的地方,恰恰是那个最不起眼的设计:明明已经有了实体描述,却仍然坚持回头去取原文。 这背后是一个很朴素的工程判断——任何经过大模型压缩的中间产物都不该被当作最终依据,因为你无法知道它丢了什么。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。