论文说根层摘要只要原文的 2.6%,我实测是 126%——彻底搞懂GraphRAG第六篇

30 秒判断 :本篇基于 graphrag 3.2.0,自己跑了三组语料(另对照论文公布的两组)。GraphRAG 全局问答能省 token,靠的是"索引期把语料压成一批社区报告"。论文里这个压缩比是 2.3--2.6% ,而我自己那三组语料里有两组超过 100%(126% / 129% )------报告比原文还长。往下拆,膨胀来自三个乘数:同一片原文被很多社区各写了一遍 、每份报告有一个长度地板 、社区切得太碎。而那个地板,是提示词里一句"至少 5 条 findings"造出来的。

一句话结论 :压缩比不是方法自带的属性,它是"每根社区能分到多少源材料 "与"一份报告至少要写多长 "这两个量的比值。前者由你的语料和抽取粒度决定,后者由提示词决定------只有前者远大于后者时,这套东西才省 token。

适合谁读:打算用(或正在用)GraphRAG 做全局问答、想知道"这套配置在我的语料上划不划算"的同学。不需要读过原论文。

结构速览:1 节是先摊账;2--3 节拆膨胀的机制;4--5 节是两条容易被忽略的发现;6 节给出"什么语料能喝下默认配置"的判据。

证据标记 :✅ 实测 (本文三组语料、多层统计)· 📄 源码/提示词原文 · 📎 论文 · 🤔 推断。


0. 一张对不上的账

GraphRAG 的卖点之一,是全局问答便宜 。它的路径是:索引期把语料压成一批社区报告 ,查询期不再读原文,而是对这批准摘要做 map-reduce。论文里这条路的成本是这样写的:根层社区摘要只需要"直接对源文本做摘要"(TS)那条基线的 2--3% 。

我按论文的方法在自己的语料上建了一遍索引,然后量同一件事:

语料 源 token 根社区数 每根社区覆盖 根层报告 ÷ 原文
论文 · 播客 1,014,611 34 29,842 2.6%
论文 · 新闻 1,707,694 --- 31,049 2.3%
我们 · 同源新闻子集(51 篇) 122,387 37 3,308 38.0%
我们 · 14 篇技术笔记 108,786 47 2,315 126%
我们 · 7 篇小语料 32,363 17 1,904 129%

先说清那份英文新闻的来路 :它不是随手找的新闻,而是论文那份 News 语料的同源子集 。论文 §4 写明它的 News 数据集取自 MultiHop-RAG(Tang & Yang, 2024),类别是娱乐 / 商业 / 体育 / 技术 / 健康 / 科学,时间跨 2013-09 到 2023-12;我在这份语料上做固定种子 42 的分层抽样,抽到 51 篇 / 12.2 万 token ,约为全量(按本文 tokenizer 计 138 万 token ;论文报约 170 万,差在分块与 tokenizer 口径)的 9% 。所以"论文 · 新闻 2.3% → 我们 38.0%"这组对照,是同一批源材料缩小一个数量级,不是换成另一种语料。

同一件事,这几组语料给出 2.3% 到 129% ------差了将近 50 倍,而且方向是反的:不是"压得没那么狠",是"压完反而更大"。

这篇不讨论"谁的语料有问题",只拆机制:这个 126% 是怎么产生的。 拆完你会发现,它其实是三个乘数的必然结果,而其中两个能被你直接调。


1. 先把账摊开:不是一层的问题

很多人(包括我)一开始只看根层。但社区报告是分层生成的,深层的量和根层完全不是一个量级。同一份 10.9 万 token 的技术笔记语料:

层 社区数 报告合计 token ÷ 原文 平均每份
L0(最粗 / 根) 47 136,909 125.9% 2,913
L1 383 709,980 652.6% 1,854
L2(最细) 747 1,096,851 1008.3% 1,468
L3 8 13,003 12.0% 1,625
全层合计 1,185 1,956,743 1798.7% ---

L2 这一层的报告总量,是原文的 10 倍。 而 L2 的社区有 747 个------比根层多了 16 倍。

所以第一个要问的问题不是"报告为什么长",而是"为什么会有这么多社区"。

1.1 社区数是按实体数硬拆出来的(但还有第二个终止条件)

GraphRAG 的社区发现是两段:先做一次 Leiden 划分(得到根层),然后把超过阈值的社区继续拆 ,阈值 max_cluster_size = 10(📄 config/defaults.py、graphs/hierarchical_leiden.py)。

但终止条件不止"拆到 ≤10"这一条 。论文的原话是"recursively detecting sub-communities within each detected community until reaching leaf communities that can no longer be partitioned "(📎 论文 §3.1.4)------子图再也拆不出更细的划分时就停 。所以 max_cluster_size 是目标,不是保证。

"拆不动"真的会发生吗? 会,而且很好造:喂一张 20 个节点的完全图 进去,得到的是一个 20 个实体的叶子 ------任何切分都会让模块度变差,算法就不切了。我拿官方那条 native 调用试了三张合成图(脚本 _probe_unsplittable.py):

合成图 结果
20 个节点的完全图 1 个 20 实体的叶子(拆不动)
两个 10 节点完全图 + 一条桥 2 个簇,各 10 个实体(拆得动)
8 / 12 两簇、簇间稀疏 2 个簇(12 与 8)------12 那个没继续拆(它自己是个完全图)

真实语料里也确实留下过这种"胖叶子"(脚本 _check_unsplittable.py;这里"叶子" = 没有子社区的社区):14 篇技术笔记 931 个叶子里有 2 个 >10 实体(最大 15);7 篇小语料 109 个里 2 个(最大 13);51 篇英文新闻 571 个里 18 个(最大 65,是阈值的 6 倍多) 。

不过在我们这份技术笔记语料上,"按实体数硬拆"仍然是主导机制,实测分布吻合得很好:

层 社区数 实体数中位 ≤10 实体的占比
L0 47 87 0%
L1 383 10 50%
L2 747 4 100%

所以准确说法是:层级主要由"超过 10 个实体就继续拆"决定;拆不动的子图会停在超阈值那一步,变成一颗"胖叶子" (这份语料里不到叶子的 1%,但新闻那份最大的一颗有 65 个实体)。

这条规则给出的下界是有前提的:

只要都拆得动,社区数 ≥ 实体数 ÷ 10;而每个叶子社区能分到的材料只剩一两块。

我的语料抽出了 4,919 个实体 ⇒ 拆得动的话至少 492 个叶社区(实测最深两层 755 个,量级吻合)。社区一多,报告就多------这是第一个乘数。


2. 另外两个乘数

2.1 同一片原文被写了很多遍

社区之间共享原文块 :一个文本块里往往有多个分属不同社区的实体,于是这个块会同时出现在多个社区的材料里,而每个社区都要为它写一份完整报告。

层 社区数 块被引用的总次数 去重后的块数 平均每个块被几个社区引用
L0 47 413 104 3.97
L1 383 862 103 8.37
L2 747 1,081 103 10.50

103 个块在 L2 被引用了 1,081 次------平均每块 10.5 个社区 。所以"10 倍"不是"报告啰嗦",而是"同一片原文被写了 10 遍"。

2.2 每份报告有一个长度地板

按"社区自己有多少块材料"分桶,问题更清楚(L2,747 个社区):

材料 社区数 报告 token 合计 平均每份 占 L2 报告量
1 块(约 1,200 token) 545 776,193 1,424 70.8%
2 块 132 202,474 1,534 18.5%
3--4 块 53 86,048 1,624 7.8%
5+ 块 17 32,136 1,890 2.9%

73% 的 L2 社区只有 1 块材料,它们贡献了 70.8% 的报告量。 而对照根层:只有 2 个"1 块社区"(2.5%),31 个"5+ 块社区"(71.2%)。

看最后两列:材料从 1 块涨到 5+ 块(5 倍 ),平均报告只从 1,424 涨到 1,890( +33% )。报告长度几乎不跟着材料走------它有一个下限。

换个口径看同一件事(每个社区自己的材料 token vs 它产出的报告 token):

层 平均材料 平均报告 报告 ÷ 材料
L0 10,085 2,913 0.29
L1 2,608 1,854 0.71
L2 1,683 1,468 0.87
L3 2,100 1,625 0.77

材料从 L2 到 L0 涨了 6.0 倍 ,报告只涨 2.0 倍 (幂律指数约 0.38)。报告长度和材料量是高度次线性的------只有根层在真正压缩(0.29),L2 几乎是在"复述"(0.87)。


3. 那条地板,是提示词里一句"至少 5 条"造出来的

官方默认的社区报告提示词里有这么一句(📄 prompts/index/community_report.py,我这轮的产物与它一字不差):

DETAILED FINDINGS: A list of 5-10 key insights about the community. Each insight should have a short summary followed by multiple paragraphs of explanatory text grounded according to the grounding rules below. Be comprehensive.

要求里同时有下限 (至少 5 条)、条数上限(最多 10 条)、以及"每条要多段、要全面"。

于是当材料只有 2 个实体、1 块原文时,模型也必须凑出 5 条"多段"的发现。它凑的方式我在真实样例里看得一清二楚。举一个 L2 社区的例子(材料 1 块 / 1,200 token、只有 2 个实体):

  • 报告 951 token、5 条 findings ,其中一条是"社区结构以草案框架为中心,关系明确"------也就是把邻接表念了一遍;
  • 另一个社区的报告里有一条是"输入侧与输出侧通过关系 2166 和 2167 形成双向约束"------把图结构本身描述了一遍。

三类"凑长度"的成分:① 复述实体描述 (把实体表里的 description 换个说法);② 对图结构的元描述 ("X 是社区的核心枢纽"------零信息量);③ 引用标记 ------实测约 15% 的字符 是 [Data: Entities (...)] 这类标记(根层更夸张:26.5% )。

再叠加一句"至少 5 条",报告长度的下限就被钉死了。

一条能直接搬走的判据:

只有当每个社区的平均材料明显大于这个地板(约 1.5k token)时,报告才不膨胀。 实测:根层 8.79 块 ✓ / L1 2.25 块(临界)/ L2 1.45 块 ✗。

3.1 一个反例,证明这不是"提示词写不好"

同一份提示词下,材料有 9 块 / 10,208 token 的那个社区,只写了 1,390 token 的报告------压缩到 0.14 倍,是真正的好摘要。

也就是说:提示词没问题,是"材料太少"这种情形它没被设计来应对。


4. 一个我没想到的发现:高层报告从来没读过子报告

按直觉,分层摘要应该是"上层看下层的摘要,越往上越粗"。论文摘要里确实有这么一句: "summaries at higher levels of the hierarchy recursively incorporating lower-level summaries" 。

但论文正文(§3.1.5)给的实际规则是条件式的:

如果该社区的全部元素摘要装得下 上下文窗口,就按叶子社区的做法办 ------直接总结元素。装不下时 ,才按子社区 token 体量降序,用子社区摘要替换对应的元素摘要,直到装得下。

也就是说,"递归纳入下层摘要"只在超限那条路径上成立。

而我们这份语料走的是哪条路?我把 1,185 个社区的上下文构建过程全部离线复算了一遍:context_exceed_limit = True 的数量是 0。

全程没有发生一次"用子社区摘要替换",高层报告从来没有读过低层报告。

这意味着两层之间的粗细差异,不是靠"递归抽象"实现的 ,而只靠两件事:① 高层的材料更多;② 输出带宽固定 (5--10 条 findings,加上那个本该存在的长度上限)。而这两条都是经验值------下节说第二个为什么半残了。


5. 两个方向都被扭曲,而"上限"那半还被弄丢了

5.1 上下限都在扭

统计三层报告的 findings 条数(提示词规格是 5--10 条):

层 份数 中位 恰好 10 条 恰好 5 条 低于 5 条 最高
L0(根) 47 9 19 份 = 40.4% 2 份 0 11
L1 383 8 28 份 = 7.3% 31 份 6 份 10
L2(最碎) 743 7 10 份 172 份 = 23.1% 20 份 10

根层完整分布:{5: 2, 7: 2, 8: 10, 9: 13, 10: 19, 11: 1}------大头挤在 8--10,40% 正好顶到 10,还有一个写到 11(越出提示词给的范围)。

两头都不是"材料需要多少就写多少" :材料最多的根层被截断在上限 (想说的更多但写不完),材料最少的 L2 被撑到下限(没东西可说也得凑 5 条)。而且上下限都是软约束------L2 有 20 份连 5 条都没写够。

5.2 长度上限被 prompt-tune 丢掉了

官方默认的报告提示词里有这么一句(出现 2 处):

Limit the total report length to {max_report_length} words.

而 prompt-tune 会整篇重写提示词 。我实测了它产出的三份报告提示词:max_report_length 出现 0 次 ------占位符只剩 {input_text}。运行时 .format() 不会报错(多余的关键字参数被忽略),所以既没有长度上限,也没有任何提示。

实测结果:最长的一份根层报告 4,169 token,远超"2000 词"的原意。

所以这批报告的实际形态是:下限由"至少 5 条"撑着,上限无人管。


6. 那什么语料能喝下这套默认配置

把上面所有东西合成一条判据:

根层报告总量 < 原文 ⟺ 根社区数 × 平均报告长度 < 源 token

而"平均报告长度"有个地板(在我这份语料上约 1.4k token),所以真正的主控量是"每根社区能分到多少源材料" 。它又可以被拆成两个因子的乘积:

每根社区材料 = (源 token ÷ 实体数) × (每根社区平均实体数) = 抽取粒度 × 聚类粒度

拿实测数据验一下(两组都对得上,误差约 12%,来自块之间的重叠):

语料 token/实体 每根社区实体数 相乘 实测每根材料
51 篇英文新闻 39.5 74.2 2,931 3,308
14 篇技术笔记 22.1 93.3 2,062 2,315

于是这几组语料的对照就有了解释:

语料 每根社区覆盖 根层报告 ÷ 原文
论文 · 播客 29,842 2.6% ✓
论文 · 新闻 31,049 2.3% ✓
我们 · 同源新闻子集(51 篇 / 12.2 万 token,论文那份的 9%) 3,308 38.0%
我们 · 14 篇技术笔记(10.9 万 token) 2,315 126%
我们 · 7 篇小语料(3.2 万 token) 1,904 129%

两个可以立刻用的结论:

  1. 适合默认配置的语料是"长、松散、叙事型" (新闻 / 播客 / 会议记录)------每个社区能分到的材料多;不适合的是"精炼、概念密度高"的(技术笔记 / 论文 / 规格文档)------实体密、社区多、每社区材料少,必然撞地板。
  2. 换语料类型能改善,但改变不了量级。 我把语料换成同规模的英文新闻(就是上面那份论文同源子集)后,每根社区材料从 2,315 涨到 3,308(+43%),根层压比从 126% 降到 38%(改善 3.3 倍)------但离论文的 2.6% 还差 15 倍 。原因是根社区数几乎不随语料量涨(论文 34 个根 / 101 万 token,我们 37 个根 / 12.2 万 token),要让每根社区材料涨到 3 万 token,得把语料喂到约 110 万 token。

⚠️ 一个陷阱:这条判据里没有"语料总量"这一项。它看起来像主控变量,其实只是背景------真正起作用的是上面那个"抽取粒度 × 聚类粒度"。12 万 token 这个量级,换什么语料类型都进不了论文那个区间。


7. 小结:三句话

  1. 报告比原文长,不是因为模型啰嗦 ,而是三个乘数叠出来的:社区被硬拆到碎 (>10 实体就继续拆)+ 同一片原文被 10 个社区各写一遍 + 每份报告有约 1.4k token 的长度地板。
  2. 地板是提示词里"至少 5 条 findings、每条多段、要全面"造出来的 ,而本该平衡它的长度上限被 prompt-tune 静默丢掉了。所以现在的形态是"下限有人管、上限没人管"。
  3. 能不能省 token,取决于"每根社区分到的材料 ÷ 报告地板"这个比值 ------而它 = 抽取粒度 × 聚类粒度 ÷ 地板。先算这个比值,再决定要不要用这套方案,比跑完一遍再发现报告比原文还长要便宜得多。

下一篇:那 2.6% 到底是在什么条件下成立的

这篇量的是"报告有多长"。但还有一个更靠前的问题:论文那个 2.6%,是在什么前提下测出来的? 我做了个对照实验------同一份语料,只把抽取提示词从"6 个域的通用新闻"换成"技术 + 体育"两个域,根层压比就从 38% 变到 27% 。也就是说,这个数字更像是"你目标的函数",而不是"你的语料的函数"。下一篇把这条链讲完,包括它对"图到底值不值得用"意味着什么。


附:核对方式

  • 版本:graphrag 3.2.0(Python 3.11),DeepSeek deepseek-chat 作补全模型,索引分块用默认配置(1200 token / 100 重叠)
  • 语料:① 14 篇中文技术笔记(108,786 token);② 7 篇小语料(32,363 token);③ 论文 News 语料的同源子集:51 篇英文新闻(122,387 token,固定种子 42 分层抽样;全量按本文 tokenizer 计 1,381,153 token,取到约 9%)
  • 论文与数据集(📎):原论文 From Local to Global §4 数据集说明(其 News 数据集取自 Tang & Yang, 2024);MultiHop-RAG(Tang, Y. & Yang, Y., 2024)
  • 主要代码/提示词位置(📄):prompts/index/community_report.py(findings 规格与长度上限)、index/operations/summarize_communities/graph_context/context_builder.py(上下文构建与超限判断)、config/defaults.py(ClusterGraphDefaults.max_cluster_size = 10、CommunityReportDefaults)
  • 本地统计脚本(都在开源仓库):_explain_l2_blowup.py(块级复用与分桶)、_l2_size_mix.py(按材料块数分桶)、_level_cost_table.py(分层成本)、_l2_findings.py(findings 分布)、_diag_reports.py(上下文超限复算)、_compare_roots.py(跨语料根社区对照);1.1 的两个:_probe_unsplittable.py(合成图验证"拆不动")、_check_unsplittable.py(三份语料里数"胖叶子")
  • 本文的完整实验记录与代码:github.com/lipeidonggz...

如果你的语料上也算过"根层报告 ÷ 原文"这个比值,欢迎贴出来------我手上三组都在 100% 以上,很想看看有没有人跑进论文那个区间,以及需要多长的语料。

相关推荐
李溪白2 小时前
Agent 自主规划 —— 怎么让 Agent 自己拆解任务、动态调整策略
agent
李溪白2 小时前
LangGraph 高级用法:状态管理、条件路由、人工介入的完整案例
agent
柒和远方2 小时前
DocResearch 项目面试:把每个模块讲明白,而不是背术语
python·llm·agent
拍手笑沙鸥3 小时前
系列4之「超体」工具调用与 Function Calling:给智能体装上"双手"
aigc·agent
Luhui_Dev3 小时前
数学问题在 Agent 实践中的难点
人工智能·数学·agent
柒和远方3 小时前
DocResearch 项目理解:从一条命令到一份有出处的报告
python·llm·agent
Maiko Star3 小时前
* LangChain Agent智能体详解(上):创建调用、工具绑定与系统提示词
python·langchain·agent
七夜zippoe3 小时前
Agent 自主决策机制:什么时候该检索、什么时候该直接回答
人工智能·ai·agent·检索·自主决策
沉默王二3 小时前
再见了 WebUI,DeepSeek 桌面版真不错。
openai·agent·deepseek