GraphRAG 有条"按问题挑材料"的隐藏路径:文档 0 提及,实测也未必更省——彻底搞懂GraphRAG第五篇

3 阅读30分钟

30 秒判断:本篇基于 graphrag 3.2.0。上一篇(《那个让用户填的社区层级参数,为什么在 GraphRAG 里根本没法用?》)说到 Global Search 有个结构性麻烦—— "这次该看多细"这个判断被外包给了用户,而用户手里只有一个失准的层号。GraphRAG 还有一条可选路径:Dynamic Community Selection。但它不是来替代层号的——层号照样要填(当候选池的预过滤上限);它换掉的是"切完就全给":按问题给每个候选社区打相关性分,相关的才收下、并继续往下看更细的子社区。我把它的算法、打分器、三组实测和一年半的功能史都核了一遍。结论:方向是对的,但它既没解决"深度"、也还不具备被直接依赖的条件——官方文档提它 0 次,打分提示词不可配置,而"打分失败"的默认值与"判定相关"的阈值恰好是同一个数。

一句话结论:它换掉的只是第 0 步(选材料),后面四步原样复用。代价是选材从"读本地表"变成"每个候选社区一次 LLM 调用"——这些调用在三组实验里各花了多少,第 1 节按 打分 / map / reduce 三段把原始数字列了出来。

适合谁读:正在用 GraphRAG 做全局问答、被"选哪一层"折磨过的同学(但先说清:换这条路也绕不开它);也适合任何想在设计里加"自适应粒度"的人——它踩到的坑很有代表性。

结构速览:0 节是它是什么(第一眼看上去很好);1 节是三组实验的数据(逐次原始数字 + 三组并排);2 节把机制拆开;3 节讲为什么会这样;4 节是成熟度信号;5 节是结论。

证据标记:✅ 实测(§1 三组实验 · §3.3 的打分与阈值重放 · §4.2 的选择过程重放)· 📄 源码(graphrag 3.2.0)· 📎 官方文档/GitHub · 🤔 推断。


0. 先说它是什么:一个看起来更聪明的选材料方式

上一篇的结论是:Global Search 让用户填"社区层级",而这个旋钮在结构上做不到它承诺的事——它不等于"只取某一层",也不保证"调大就更细"。

那有没有别的路?有一条——但它不是用来替代层号的。GraphRAG 里有个可选项,换掉的是第 0 步里的另一个动作:默认路径切完就把范围内的报告全塞给后面;这一条会先逐个社区判断"这份材料要不要" ,要就收下、并继续往下看更细的子社区。

这条路叫 Dynamic Community Selection。它有几个值得先知道的特征:

  • 默认关,得显式开(CLI 是 --dynamic-community-selection);
  • 官方文档几乎没写它——docs/query/global_search.md 里 "dynamic" 出现 0 次;
  • 它的选择逻辑一年半没动过(功能史见第 4 节)。

三个词先对上(后文反复用):社区=索引期把实体聚成的簇;社区报告=这个簇的 LLM 摘要,查询期读的就是它;层号(level) =社区树的层级,数字越小越粗、越大越细。

它看起来好在哪:

  • 默认路径是"切一刀、全给",它改成"按问题挑"——更接近人做分析时的做法;
  • 输出可以跨层,而且父子二选一 ⇒ 同一片内容不会被父报告和子报告重复代表;
  • 打分是孤立判断(一次只看一份材料 + 问题,不跟别的社区比)⇒ 天生没有上一篇里那个"批内相对分不可比"的毛病;
  • map / reduce 的成本只花在入选的材料上,而不是整个语料(打分本身另算,见第 1、3 节)。

所以第一眼看上去,它像是一次真正的改进。下面第 1 节放三组实验的原始数据(含采样口径),第 2 节把它在代码里到底改了什么拆开看。


1. 实测:三组问题的原始数据

先上实测。语料是一份 7 篇文章、32,363 token 的小库,分属几个不同主题——这样才凑得出"同域"和"跨域"两类问题:

代号文章主题原文
01判官不稳定时,先别换判官——一次 LLM-as-Judge 的完整排查实录LLM 评测掘金
02换文章不天然等于换根因:探针设计与双信号转绿判据LLM 评测掘金
03金标准写作规范可外化吗:一次盲写验证LLM 评测掘金
04单 Agent 架构演进Agent 架构掘金
05多 Agent 架构梳理Agent 架构掘金
06从 Prompt 到 Loop:LLM 应用的工程方法论工程方法论掘金
A5How we contain Claude across products(英文)AI 安全Anthropic

索引跑完是 143 个社区 / 143 份报告;查询固定用 community_level=2,所以候选池是 level ≤ 2 的 141 份报告——默认路径每次都把这 141 份全送进 map。三类问题是:

#类型问题原文真正需要哪几篇
1同域·密集这批资料里,做 LLM 判官评测时踩过哪些坑?怎么排查?01 02 03
2同域·稀疏单 Agent 和 多 Agent 架构各自解决什么问题?有什么代价?04 05
3跨域·需 5 篇把评测里"探针设计"的思路套到 Agent 架构选型上,该怎么设判据?01 02 03 04 05

这三组都是这个功能面向的全局问题——三组都算"该用它"的场景,没有哪一组"更该用它"("跨域"只是问题形状不同,不是更该赢的一组)。

采样口径:每条问题 × 每条路径各跑 3 次(合计 18 次),下面给的是每次运行的原始数字,不取均值。表的读法:打分次数 是 Dynamic 的选材环节(日志里叫 build_context,默认路径没有这一步)、map 批数 是送进 map 的批数(reduce 每轮固定 1 次);prompt token、output token 按 打分 + map + reduce 三段拆开。

另外两条读表说明:L0/L1/L2 指社区层级,L0 是最粗的根层、L2 是这次查询取到的最细一层;默认路径三次的 map 段完全相同(365,881 token、32 批),只有 reduce 段和答案长度在动。选材明细表里的"按文档"一列,是把社区归到它 text_unit 最多的那篇(社区可跨文档),只用来看入选材料的来源构成。

1.1 「同域·密集」(需要语料 01 02 03)

问题:这批资料里,做 LLM 判官评测时踩过哪些坑?怎么排查?

成本与产出(每条路径各 3 次)

次路径map 批数打分次数prompt token(打分+map+reduce)output token(打分+map+reduce)耗时(s)答案字符
1默认32—0+365,881+12,7280+45,718+4,955475,599
1Dynamic1086237,535+113,763+12,68215,364+16,385+5,729556,466
2默认32—0+365,881+12,6630+42,301+68336817
2Dynamic1081221,073+109,884+12,66613,516+17,078+5,019545,676
3默认32—0+365,881+12,7160+45,687+4,360474,983
3Dynamic1193253,895+119,469+12,57915,934+17,577+5,330586,358

Dynamic 的选材明细(每条路径各 3 次)

次被评社区(L0/L1/L2)入选社区(L0/L1/L2)打分分布(0/1/2/3/4/5)入选社区按文档
186(17/50/19)47(3/26/18)24 / 7 / 8 / 7 / 2 / 3801:29 02:9 03:5 06:3 A5:1
281(17/41/23)46(1/27/18)20 / 5 / 11 / 5 / 4 / 3601:29 02:9 03:5 06:2 A5:1
393(17/53/23)48(3/27/18)30 / 8 / 7 / 4 / 10 / 3401:29 02:9 03:5 06:3 A5:1 04:1

一句话看数:三次里 Dynamic 把送进 map 的材料从 141 份收到 46–48 份(为此打了 81–93 次分),总 prompt token 344k–386k(默认路径是 378k 上下),耗时 54–58 秒(默认 36–47 秒);入选的 46–48 个社区里,43 个属于 01/02/03。

1.2 「同域·稀疏」(需要 04 05)

问题:单 Agent 和 多 Agent 架构各自解决什么问题?有什么代价?

成本与产出(每条路径各 3 次)

次路径map 批数打分次数prompt token(打分+map+reduce)output token(打分+map+reduce)耗时(s)答案字符
1默认32—0+365,881+12,6400+34,702+3,817395,003
1Dynamic666189,483+63,121+7,81110,121+7,272+5,668427,333
2默认32—0+365,881+12,7130+33,137+5,189416,588
2Dynamic676214,085+66,757+9,51011,330+8,863+4,695456,263
3默认32—0+365,881+12,6840+34,702+3,615394,755
3Dynamic663181,743+64,501+9,6229,392+9,129+4,199415,390

Dynamic 的选材明细(每条路径各 3 次)

次被评社区(L0/L1/L2)入选社区(L0/L1/L2)打分分布(0/1/2/3/4/5)入选社区按文档
166(17/29/20)26(0/11/15)27 / 18 / 13 / 7 / 0 / 104:10 06:8 05:4 A5:3 01:1
276(17/35/24)27(0/12/15)34 / 18 / 19 / 2 / 2 / 104:9 06:8 05:4 A5:3 01:3
363(17/29/17)26(0/12/14)25 / 12 / 19 / 6 / 0 / 104:10 06:7 05:4 A5:3 01:2

一句话看数:三次里 Dynamic 只把 26–27 份材料送进 map(map 只切 6 批),打分 63–76 次;总 prompt token 256k–290k,低于默认路径的 378k 上下;答案字符 5,390–7,333(默认 4,755–6,588)。入选社区里 04/05 合计 13–14 份,其余 12–14 份散在 06、A5、01 上。

1.3 「跨域」(需要 01 02 03 04 05)

问题:把评测里"探针设计"的思路套到 Agent 架构选型上,该怎么设判据?

成本与产出(每条路径各 3 次)

次路径map 批数打分次数prompt token(打分+map+reduce)output token(打分+map+reduce)耗时(s)答案字符
1默认32—0+365,881+12,7620+59,740+5,333695,788
1Dynamic17128330,906+188,849+12,77425,469+34,538+7,110957,606
2默认32—0+365,881+12,6660+57,737+4,829675,445
2Dynamic17131342,794+186,931+12,65425,154+32,781+6,253916,526
3默认32—0+365,881+12,6240+63,366+4,378664,794
3Dynamic17127324,415+194,492+12,66926,093+33,411+6,708917,463

Dynamic 的选材明细(每条路径各 3 次)

次被评社区(L0/L1/L2)入选社区(L0/L1/L2)打分分布(0/1/2/3/4/5)入选社区按文档
1128(17/66/45)79(1/39/39)20 / 55 / 28 / 15 / 9 / 101:25 A5:18 02:11 04:10 06:9 05:4 03:2
2131(17/67/47)76(1/41/34)25 / 61 / 22 / 16 / 6 / 101:27 06:14 A5:12 04:9 02:7 05:4 03:3
3127(17/66/44)83(2/42/39)15 / 66 / 28 / 12 / 4 / 201:28 A5:14 02:13 06:11 04:9 05:4 03:4

一句话看数:三次里 Dynamic 把 76–83 份材料送进 map(17 批),打分 127–131 次;总 prompt token 531k–542k,是默认路径 378k 上下的 1.4 倍左右,耗时 91–95 秒(默认 66–69 秒)。三次的入选社区里,7 篇文档全都出现了。

1.4 三组结果并排

实验路径送进 map 的材料map 批数打分次数prompt token 合计耗时
1 同域·密集默认141 份(全给)32—378k(三次几乎一样)36–47s
1 同域·密集Dynamic46–48 份10–1181–93344k–386k54–58s
2 同域·稀疏默认141 份(全给)32—378k39–41s
2 同域·稀疏Dynamic26–27 份663–76256k–290k41–45s
3 跨域默认141 份(全给)32—378k66–69s
3 跨域Dynamic76–83 份17127–131531k–542k91–95s
  • 默认路径是三组的共同基线,而且它的成本不随问题变:三组都是 141 份材料、32 批、33 次调用、prompt token 378k 上下。
  • Dynamic 三组各不相同:送进 map 的材料从 26–27 份到 76–83 份,打分从 63–76 次到 127–131 次,prompt token 从 256k–290k 到 531k–542k。与默认基线比:第 2 组更低,第 1 组基本持平,第 3 组更高。
  • 同一个问题重复三次,数字会动:默认路径的答案字符从 817 到 5,599(差 6.8 倍);Dynamic 三次分别选中 46/47/48、26/27/26、79/76/83 个社区,入选材料的来源构成与打分分布也逐次不同—— "数量稳定"不等于"选的是同一批" :三次之间入选集合的两两重叠是 94%–96% / 71%–89% / 61%–65%(见 3.3)。
  • 量级参照:默认路径一次查询要喂进 37.8 万 prompt token;作为对照,这份语料本身只有 3.2 万 token。
  • 范围声明:本文只记录成本和选材行为,没有评答案质量——上面"答案字符"只说明答案多长,不说明答得对不对;所以"更贵/更便宜"不等于"更好/更差"。

那"有没有预想的那么好"?——没有。 三组里,材料确实每次都被收窄了(141 份 → 26–83 份),但总成本没跟着一起降:第 2 组更低、第 1 组基本持平、第 3 组反而更高。收窄材料这件事它做到了,"因此更省"没有兑现(为什么会这样,见第 3 节)。

上面就是这三组实验的全部数据。第 2 节把它的机制拆开(代码里它到底改了什么),第 3 节讲原因。


2. 把机制拆开:它只改了第 0 步

先把位置放准:Global Search 一共五步(选材料 → 装配上下文 → map → reduce → 返回)。Dynamic 只替换第 0 步,第 1–4 步一个字节都没改。

两条路的第 0 步,核心差别只有"材料怎么选":

对比项默认路径Dynamic
材料怎么定用户填层号 N ⇒ 取 level <= N 的全部报告,切完就全给、不再筛从根层开始,逐社区按问题打分,只收过筛的,并沿过筛的分支继续下探
输出形状固定范围的报告集合一组可能跨层的社区,父子通常二选一
成本纯本地(读表)每个候选社区一次 LLM 调用(× num_repeats)
深度由谁框定用户填的层号(level <= N 一刀切)也还是用户——老的 community_level 在这条路上当预过滤 cap(读表时仍按 level <= N 砍一刀,只是跳过 roll-up);dynamic_search_max_level(默认 2)只在兜底路径上当上限

注意最后一行:Dynamic 并没有解决"深度"这件事——它换掉的是"要不要筛、凭什么筛";而"凭什么筛"用的是"相不相关",不是"够不够回答"(详见 3.2)。

算法本身很短(📄 query/structured_search/global_search/community_context.py):

起点 = level 0 里"有报告"的社区
循环:
  并发给队列里每个社区打分(0–5)
    ≥ threshold(阈值,默认 1,等于"不是 0 分就要")⇒ 收进结果 + 把它的 children 入队
    keep_parent=False(子相关就丢父)⇒ 把这个社区的 parent 从结果里剔除
  队列 = 下一轮要评的子社区
直到队列空

三个直接推论:

  1. 输出是一组可能跨层的社区,不是一个层;
  2. keep_parent=False 做的恰好是"子相关就丢父"——和上一篇讲的默认路径里那件"父报告本该被子报告替换、却没替换成"的失效逻辑是同一个语义,这里反而是生效的;
  3. 选出来的报告之后仍走同一套装配(打散、切批、map、reduce),所以那些损耗原样继承。

3. 为什么会这样

三组实测的成本走向,其实在动手之前就能预计——原因只有两个(3.1、3.2),其余都是实现层面的问题(3.3)。

3.1 原因一:多了一个"花钱的环节",而系统从不判断"这钱值不值得花"

结构上多了一步。 默认路径的第 0 步是读本地表,不花钱;这条路要在选材阶段逐个社区打分,而每评一份都要把一整份报告喂进模型(use_summary=False)——"评一份"和"用一份"几乎是同一笔钱(2,554–2,885 vs 2,343–2,489 token/份)。

于是有没有收益,只取决于一件事:省下的 map 消耗能不能盖过新增的打分消耗。同一次运行里,prompt token 分成三段(打分/map/reduce),9 次 Dynamic 运行里打分占 61%–74% ——大头在打分,不在 map:

查询被评社区入选社区打分 tokenmap token合计默认路径
同域·密集81–9346–48221k–254k110k–119k344k–386k378k
同域·稀疏63–7626–27182k–214k63k–67k256k–290k378k
跨域127–13176–83324k–343k187k–194k532k–542k378k

把 token 换算成单价,会看到一个不好看的对称:

  • 打分一份:2,554–2,885 token(= 报告本体均价 1,661 + 约 950 token 的固定开销,也就是提示词模板本身);
  • 把一份报告放进 map:2,343–2,489 token(默认路径 365,881 ÷ 141 份 = 2,595)。

于是整条路的成本可以压成一句算术(拿它去拟合 9 次 Dynamic 运行,误差 ±6%):

Dynamic ≈ 2.6k ×(被评 + 入选) vs 默认 ≈ 2.6k × 候选池大小(141)

查询被评+入选预测实测相对默认
同域·密集127–141342k–378k344k–386k−3.7%
同域·稀疏89–103241k–277k256k–290k−29%
跨域207–210550k–558k532k–542k+41%

判据只有一条:把"被评 + 入选"压到候选池的一半以下,这条路才划算。 三组里只有"同域·稀疏"做到了——这不是运气问题,是这个结构必然出现的分界。

但系统不做这个判断。 代码里没有任何"这一趟值不值得走"的预判:它不问"这个问题潜在相关的社区有多少",也没有"关联面太宽就退回默认路径"的分支。唯一的兜底是"根层一个相关社区都没有"时才整层全评——那是保底召回,不是成本回退,而且几乎不触发(见 4.2)。

可以怎么改(我的建议) :在根层先做一次便宜的预判——只读 L0 的报告摘要(十几份),估一下"可能通过的子树有多大(含所有子孙社区)";如果估算规模接近或超过候选池本身,就直接退回默认路径。这一步的代价是十几份摘要,却能挡掉最亏的那几组。

3.2 原因二:没有"够了就停"的中止条件

打分问的是"这份材料相关吗",不是"这些材料够不够回答"。所以遍历的规则是"相关就继续往下"——没有任何中止条件,直到把相关的分支走到底、或者撞上候选池的上界。

数据上是看得见这一点的:问题越"哪儿都相关",被评社区就越多——跨域那组三次评了 127 / 131 / 127 个,几乎把 141 份的池子评了一遍;而它其实只是问"这套探针思路怎么挪到 Agent 选型上"。

而且"该钻多深"这件事仍然由用户填。 候选池的上界是用户给的层号:community_level=N 把池子砍到 level ≤ N(这次 141 份);dynamic_search_max_level(默认 2)只在那条几乎不触发的兜底路径上当上限(见 4.2)。

可以怎么改:把判据从"相关"换成"够了没有"——手里这批材料能不能给问题一个像样的回答;不够就继续往下,够了就停。那样"深度"才真正由系统决定,用户也不必再填层号。

3.3 其余都是实现问题:影响"选得对不对",不影响上面那笔成本账

下面这几条我都核过,但性质是"思路有了、没仔细验证"——它们决定的是选出来的东西可不可靠,不是花多少钱。换句话说:上面那两个原因是"成本为什么是这样",下面是"效果为什么不够好"。

① 5 档只在形式上,实际能用的只有 2 档。 默认阈值 1 的含义就是"只要不是 0 分就要",于是 1/2/3/4/5 的差别在选材阶段完全消失(只在 rank 排序那一侧还留一点作用)。把第 1 节那三次记录下来的 rating 按不同阈值重放一遍(脚本 _threshold_check.py,不调模型):

阈值1 同域·密集2 同域·稀疏3 跨域
1(默认)判定一致 97% · 集合重叠 94–96% · 入选 47/46/4892% · 71–89% · 26/27/2680% · 61–65% · 79/76/83
295% · 89–93% · 42/42/4186% · 53–74% · 12/16/1767% · 19–29% · 19/17/15
393% · 78–84% · 36/32/3486% · 0–20% · 2/2/480% · 0–22% · 5/2/6
492% · 76–87% · 29/27/3199% · 100% · 1/1/191% · 100% · 0/0/0(选空)

("判定一致" = 同一个社区跨三次运行"rating ≥ 阈值"是否一致;"集合重叠" = 三次入选集合的两两 Jaccard。第 4 行那两个 100% 是选空或只剩一份带来的假稳定,别读成"更稳"。)

三条读法:

  1. 默认那一档是四个阈值里最稳的:集合重叠 61%–96%,判定一致率 80%–97%——能用。
  2. 往上调只会更不稳:第 3 组从六成重叠掉到两成,再往上直接选空 ⇒ 官方那 5 档里,只有"0 与非 0"这一条线是可重复的。挑社区其实用不着 5 档、2 档就够——而且代码已经是这么用的(阈值 1 的含义就是"只要不是 0 分就要")。
  3. 这是典型的"接口承诺"与"实际能力"错位:接口给 5 档、还暴露 rank,但只有两档经得起重复。要么接受二元判定(别指望阈值和 rank 能给你更细的控制),要么自己先把打分标准定下来(提示词不可配、理由不返回、分值只进 debug 日志——这三样官方一样都没给)。

② 打分的理由被系统丢掉了。 打分函数只返回 rating、不返回 reason(reason 只进 debug 日志)⇒ 你无法从系统里拿到"为什么剪掉了它",想审计只能自己接日志。另做的一次重放(脚本 _probe_rating.py)里,模型的理由其实讲得通:对明显不相关的社区给 0 分并写"……与问题主题无直接关联,需用户自行类比迁移";对部分相关的给 2–3 分并指出"只覆盖了某一个侧面"。

③ 分值本身在晃,而且只有边界那一档稳。 同一社区跨三次运行:分值精确一致的只有 41%–73%(平均差 0.33–0.83 分),另一次重放里出现过 1→5、2→0;而"≥1 分"这个布尔量的判定一致率是 80%–97%,入选集合两两重叠 61%–96% ⇒ 稳在"选了几个",抖在"选了谁" 。原因也是可以看见的——分值只有一个绝对刻度(孤立的绝对判断,不跟别的社区比),于是大量社区恰好压在阈值线上:

问题类型被评社区数恰好 1 分的占比
同域·密集81–936%–9%
同域·稀疏63–7619%–27%
跨域127–13143%–52%

边界上挤的人越多,抖一下就进出——跨域那组三次只有六成重叠(上面那张阈值表里也是它最差),来源就是这半池子"恰好 1 分"。

④ 零碎的两条:阈值与"解析失败默认值"同值(一次失败的调用等于"判定相关")、打分提示词不可配——都在 4.2。


4. 它现在能不能用:把信号摆在一起看

我没有用"我觉得它不成熟"来下判断,而是把能查到的信号列出来(📎 GitHub PR / changelog / 仓库文件):

信号事实
文档docs/query/global_search.md 里 "dynamic" 出现 0 次;仓库里有一份 example notebook,但没有进导航
入口CLI 开关默认关;graphrag init 生成的 settings.yaml 里没有那五行配置,要调得手写
发布定级功能合入的 PR(#1396,2024-11-12)发布定级是 patch: ——仓库用 release-please 管发布,patch: 意味着它连"新功能"的发布说明都进不去
维护强度2024-11-12 合入 → 之后只有两次小 bug 修(#1450 / #1591)→ 再往后 5 次改动全是重构,选择逻辑一年半没动
测试唯一对应的单测是 tests/unit/query/context_builder/dynamic_community_selection.py——没有 test_ 前缀,而 CI 跑的是 pytest ./tests/unit ⇒ 按默认收集规则它根本不会被执行
已知缺陷根层无社区会 KeyError(issue #2382),修复 PR #2413 已关闭、未合入
可配置性打分提示词不可配;官方"按领域调提示词"的承诺只覆盖索引侧(map/reduce/knowledge 三项)

这些信号合起来说明的不是"这个功能不能用",而是:它没有被当成需要维护承诺的功能对待。你要用它,得自己拥有三样东西——打分提示词、阈值语义、打分稳定性。

反方向的信号(公平起见) :它有 CLI、配置、Python API 三个正式入口,有示例 notebook,也有(虽然收集不到的)单测文件,PR #1396 的描述写得也认真。所以准确的说法不是"没做完就扔了",而是——它作为一个可选路径合了进去,官方没为它背书。

4.1 已经钉住的三个坑

  1. 有社区没有报告 ⇒ 剪枝到那里就断。 我们另一份语料(14 篇技术笔记)上,64 个社区只生成了 52 份报告(缺 12 个)。翻索引日志,同一个 run 里正好 12 条 No report found for community,名单与缺失清单一个不差。而 DynamicCommunitySelection 只把"有报告"的社区放进层级表,遇到没报告的子社区只在 debug 日志里跳过——代码里还留着一句 TODO check why some sub_communities are NOT in report_df。
  2. 根层空 ⇒ 构造就炸。 构造函数直接取 self.levels["0"]:如果索引里根层一个社区都没有报告,这里会 KeyError。同一份语料里,根层 13 个社区只有 12 份报告,刚好没踩上。(📎 上游 issue #2382 报的就是这个;对应的修复 PR #2413 已关闭、未合入。)
  3. level 是字符串。 数据模型是 str、读表时转字符串、算法里也用 self.levels[str(level)] ⇒ 配置和日志里的 "2" 与 2 不能混着理解。

另外一条不算坑但值得知道:算法回传的统计只有 llm_calls / prompt_tokens / output_tokens,逐社区的评分明细 ratings 只进 debug 日志,不上浮给调用方——想拿它做分析得自己接日志。

4.2 本次没走到的三条代码路径(自测时要盯)

这三条都在代码里,但这次 18 次查询里一次都没触发。列出来是因为它们决定的是"失败时会发生什么":

  1. 兜底几乎不会触发。 代码里有一条"保底召回":某一轮队列空、且至今一个相关社区都没有时,把下一层整层塞进队列再评一轮。9 次 Dynamic 运行里它一次都没触发——它等价于要求根层 17 个社区全部得 0 分,而实测根层通过数是 6–16 个。(📄 query/structured_search/global_search/community_context.py)
  2. num_repeats > 1 时是多数投票:取票数最多的那个值(并列取小),不是取平均——"重复几次取均值"是想当然。本次跑的都是默认 num_repeats=1,这条没走到。
  3. JSON 解析失败 ⇒ 默认给 1 分:代码注释写着 "so the report is kept",而默认阈值正是 1 ⇒ 一次失败的调用等于"判定相关"(既收下它、又继续下探它的子社区)。本次 9 次运行的日志里 JSON 修复/报错命中 0 次;但默认路径的失败是"丢一批"(安静地少一块),这里的失败是"多花一轮 + 引入噪声"(安静地变贵、变脏)。

5. 小结:三句话

  1. 方向是对的,但没有走到底:它把"层号切一刀、范围内全给"换成了"逐个社区做相关性检查"——这一步是对的。但它没有"够了就停"的中止条件:判据是"相不相关"而不是"够不够回答",而"该钻多深"的上界仍然由用户填的层号框定(第 2 节那张表最后一行)⇒ 深度这件事它没碰(3.2)。
  2. 代价是实打实的,而且事先就能算出来:选材多了一个花钱的环节,而打分一次 ≈ 塞一份报告进 map 一次(都是 2.3k–2.9k token)⇒ 成本 ≈ 单价 ×(被评 + 入选)。三组里只有"同域·稀疏"压到了候选池的一半以下(token −29%),跨域那组反而 +41% ——而系统没有任何"值不值得走这条路"的预判(3.1)。
  3. 它现在还不具备被直接依赖的条件:文档 0 提及、提示词不可配、5 档里只有 2 档可重复(3.3) ,而"打分失败"的默认值与"判定相关"的阈值恰好是同一个数(见 4.2)。要用它,先准备好自己那三样东西。

如果现在就要用,按这个顺序做:① 先用你自己的语料量出"被评 + 入选"这个数(跑一次、看日志里的 build_context 调用数);② 把候选池压到必要范围(community_level 是这条路上唯一的闸门);③ 接上 debug 日志把逐社区打分落盘——否则你连它剪掉了谁都看不见;④ 别指望调阈值能救抖动(3.3)。


下一篇:报告为什么比原文还长

Dynamic 这条路处理的是"查询期怎么挑材料"。但还有个更靠前的问题我从没认真算过账:那批社区报告本身,压缩率到底是多少? 论文说根层摘要只需要源文本的 2–3%——而我实测出来的数字是 126% 。下一篇(《论文说根层摘要只要原文的 2.6%,我实测是 126%》)拆这个:报告为什么会比原文还长,以及那批报告的"长度地板"是怎么被生成出来的。


附:核对方式

  • 版本:graphrag 3.2.0(Python 3.11);代码按官方 main 核对
  • 主要代码位置(📄):query/structured_search/global_search/community_context.py(剪枝与打分)、query/context_builder/rate_relevancy.py(RATE_QUERY)、query/factory.py(五项 dynamic_search_* 装配)、query/indexer_adapters.py(读报告)、config/defaults.py
  • 功能史与 issue:PR #1219(2024-09-25 首提,未合入)、PR #1396(2024-11-12 合入)、issue #2382 与 PR #2413(已关闭)
  • 本地实验:小语料 7 篇 / 32,363 token / 143 社区;三组问题 × 两条路径各跑 3 次(共 18 次),文中报的是均值与区间
  • 第 1 节三张数据表的生成脚本:_export_three_tables.py;采样与汇总:_run_ab_sampled.py、_analyze_sampled.py;选择过程重放:_replay_selection.py;稳定性统计(打分精确一致率、判定一致率、入选集合重叠):_stability_check.py;阈值扫描(阈值 1/2/3/4 各重放一遍):_threshold_check.py;逐次明细(含被评/入选数、打分分布、分段 token、兜底是否触发)在 selection-digest-0928.json
  • 本文的完整实验记录与代码:github.com/lipeidonggz…

如果你也跑过 Dynamic,欢迎把跨域场景的 token 变化贴到评论区(想知道"被评社区数"怎么取:接上 debug 日志,看 dynamic community selection 那几行就行)——我只有一组语料,很想看看"砍到候选池一半以下才划算"这条判据在别处成不成立。