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 应用的工程方法论 | 工程方法论 | 掘金 |
| A5 | How 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,728 | 0+45,718+4,955 | 47 | 5,599 |
| 1 | Dynamic | 10 | 86 | 237,535+113,763+12,682 | 15,364+16,385+5,729 | 55 | 6,466 |
| 2 | 默认 | 32 | — | 0+365,881+12,663 | 0+42,301+683 | 36 | 817 |
| 2 | Dynamic | 10 | 81 | 221,073+109,884+12,666 | 13,516+17,078+5,019 | 54 | 5,676 |
| 3 | 默认 | 32 | — | 0+365,881+12,716 | 0+45,687+4,360 | 47 | 4,983 |
| 3 | Dynamic | 11 | 93 | 253,895+119,469+12,579 | 15,934+17,577+5,330 | 58 | 6,358 |
Dynamic 的选材明细(每条路径各 3 次)
| 次 | 被评社区(L0/L1/L2) | 入选社区(L0/L1/L2) | 打分分布(0/1/2/3/4/5) | 入选社区按文档 |
|---|---|---|---|---|
| 1 | 86(17/50/19) | 47(3/26/18) | 24 / 7 / 8 / 7 / 2 / 38 | 01:29 02:9 03:5 06:3 A5:1 |
| 2 | 81(17/41/23) | 46(1/27/18) | 20 / 5 / 11 / 5 / 4 / 36 | 01:29 02:9 03:5 06:2 A5:1 |
| 3 | 93(17/53/23) | 48(3/27/18) | 30 / 8 / 7 / 4 / 10 / 34 | 01: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,640 | 0+34,702+3,817 | 39 | 5,003 |
| 1 | Dynamic | 6 | 66 | 189,483+63,121+7,811 | 10,121+7,272+5,668 | 42 | 7,333 |
| 2 | 默认 | 32 | — | 0+365,881+12,713 | 0+33,137+5,189 | 41 | 6,588 |
| 2 | Dynamic | 6 | 76 | 214,085+66,757+9,510 | 11,330+8,863+4,695 | 45 | 6,263 |
| 3 | 默认 | 32 | — | 0+365,881+12,684 | 0+34,702+3,615 | 39 | 4,755 |
| 3 | Dynamic | 6 | 63 | 181,743+64,501+9,622 | 9,392+9,129+4,199 | 41 | 5,390 |
Dynamic 的选材明细(每条路径各 3 次)
| 次 | 被评社区(L0/L1/L2) | 入选社区(L0/L1/L2) | 打分分布(0/1/2/3/4/5) | 入选社区按文档 |
|---|---|---|---|---|
| 1 | 66(17/29/20) | 26(0/11/15) | 27 / 18 / 13 / 7 / 0 / 1 | 04:10 06:8 05:4 A5:3 01:1 |
| 2 | 76(17/35/24) | 27(0/12/15) | 34 / 18 / 19 / 2 / 2 / 1 | 04:9 06:8 05:4 A5:3 01:3 |
| 3 | 63(17/29/17) | 26(0/12/14) | 25 / 12 / 19 / 6 / 0 / 1 | 04: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,762 | 0+59,740+5,333 | 69 | 5,788 |
| 1 | Dynamic | 17 | 128 | 330,906+188,849+12,774 | 25,469+34,538+7,110 | 95 | 7,606 |
| 2 | 默认 | 32 | — | 0+365,881+12,666 | 0+57,737+4,829 | 67 | 5,445 |
| 2 | Dynamic | 17 | 131 | 342,794+186,931+12,654 | 25,154+32,781+6,253 | 91 | 6,526 |
| 3 | 默认 | 32 | — | 0+365,881+12,624 | 0+63,366+4,378 | 66 | 4,794 |
| 3 | Dynamic | 17 | 127 | 324,415+194,492+12,669 | 26,093+33,411+6,708 | 91 | 7,463 |
Dynamic 的选材明细(每条路径各 3 次)
| 次 | 被评社区(L0/L1/L2) | 入选社区(L0/L1/L2) | 打分分布(0/1/2/3/4/5) | 入选社区按文档 |
|---|---|---|---|---|
| 1 | 128(17/66/45) | 79(1/39/39) | 20 / 55 / 28 / 15 / 9 / 1 | 01:25 A5:18 02:11 04:10 06:9 05:4 03:2 |
| 2 | 131(17/67/47) | 76(1/41/34) | 25 / 61 / 22 / 16 / 6 / 1 | 01:27 06:14 A5:12 04:9 02:7 05:4 03:3 |
| 3 | 127(17/66/44) | 83(2/42/39) | 15 / 66 / 28 / 12 / 4 / 2 | 01: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 同域·密集 | Dynamic | 46–48 份 | 10–11 | 81–93 | 344k–386k | 54–58s |
| 2 同域·稀疏 | 默认 | 141 份(全给) | 32 | — | 378k | 39–41s |
| 2 同域·稀疏 | Dynamic | 26–27 份 | 6 | 63–76 | 256k–290k | 41–45s |
| 3 跨域 | 默认 | 141 份(全给) | 32 | — | 378k | 66–69s |
| 3 跨域 | Dynamic | 76–83 份 | 17 | 127–131 | 531k–542k | 91–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 从结果里剔除
队列 = 下一轮要评的子社区
直到队列空
三个直接推论:
- 输出是一组可能跨层的社区,不是一个层;
keep_parent=False做的恰好是"子相关就丢父"——和上一篇讲的默认路径里那件"父报告本该被子报告替换、却没替换成"的失效逻辑是同一个语义,这里反而是生效的;- 选出来的报告之后仍走同一套装配(打散、切批、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:
| 查询 | 被评社区 | 入选社区 | 打分 token | map token | 合计 | 默认路径 |
|---|---|---|---|---|---|---|
| 同域·密集 | 81–93 | 46–48 | 221k–254k | 110k–119k | 344k–386k | 378k |
| 同域·稀疏 | 63–76 | 26–27 | 182k–214k | 63k–67k | 256k–290k | 378k |
| 跨域 | 127–131 | 76–83 | 324k–343k | 187k–194k | 532k–542k | 378k |
把 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–141 | 342k–378k | 344k–386k | −3.7% |
| 同域·稀疏 | 89–103 | 241k–277k | 256k–290k | −29% |
| 跨域 | 207–210 | 550k–558k | 532k–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/48 | 92% · 71–89% · 26/27/26 | 80% · 61–65% · 79/76/83 |
| 2 | 95% · 89–93% · 42/42/41 | 86% · 53–74% · 12/16/17 | 67% · 19–29% · 19/17/15 |
| 3 | 93% · 78–84% · 36/32/34 | 86% · 0–20% · 2/2/4 | 80% · 0–22% · 5/2/6 |
| 4 | 92% · 76–87% · 29/27/31 | 99% · 100% · 1/1/1 | 91% · 100% · 0/0/0(选空) |
("判定一致" = 同一个社区跨三次运行"rating ≥ 阈值"是否一致;"集合重叠" = 三次入选集合的两两 Jaccard。第 4 行那两个 100% 是选空或只剩一份带来的假稳定,别读成"更稳"。)
三条读法:
- 默认那一档是四个阈值里最稳的:集合重叠 61%–96%,判定一致率 80%–97%——能用。
- 往上调只会更不稳:第 3 组从六成重叠掉到两成,再往上直接选空 ⇒ 官方那 5 档里,只有"0 与非 0"这一条线是可重复的。挑社区其实用不着 5 档、2 档就够——而且代码已经是这么用的(阈值 1 的含义就是"只要不是 0 分就要")。
- 这是典型的"接口承诺"与"实际能力"错位:接口给 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–93 | 6%–9% |
| 同域·稀疏 | 63–76 | 19%–27% |
| 跨域 | 127–131 | 43%–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 已经钉住的三个坑
- 有社区没有报告 ⇒ 剪枝到那里就断。 我们另一份语料(14 篇技术笔记)上,64 个社区只生成了 52 份报告(缺 12 个)。翻索引日志,同一个 run 里正好 12 条
No report found for community,名单与缺失清单一个不差。而DynamicCommunitySelection只把"有报告"的社区放进层级表,遇到没报告的子社区只在 debug 日志里跳过——代码里还留着一句TODO check why some sub_communities are NOT in report_df。 - 根层空 ⇒ 构造就炸。 构造函数直接取
self.levels["0"]:如果索引里根层一个社区都没有报告,这里会 KeyError。同一份语料里,根层 13 个社区只有 12 份报告,刚好没踩上。(📎 上游 issue #2382 报的就是这个;对应的修复 PR #2413 已关闭、未合入。) level是字符串。 数据模型是str、读表时转字符串、算法里也用self.levels[str(level)]⇒ 配置和日志里的"2"与2不能混着理解。
另外一条不算坑但值得知道:算法回传的统计只有 llm_calls / prompt_tokens / output_tokens,逐社区的评分明细 ratings 只进 debug 日志,不上浮给调用方——想拿它做分析得自己接日志。
4.2 本次没走到的三条代码路径(自测时要盯)
这三条都在代码里,但这次 18 次查询里一次都没触发。列出来是因为它们决定的是"失败时会发生什么":
- 兜底几乎不会触发。 代码里有一条"保底召回":某一轮队列空、且至今一个相关社区都没有时,把下一层整层塞进队列再评一轮。9 次 Dynamic 运行里它一次都没触发——它等价于要求根层 17 个社区全部得 0 分,而实测根层通过数是 6–16 个。(📄
query/structured_search/global_search/community_context.py) num_repeats > 1时是多数投票:取票数最多的那个值(并列取小),不是取平均——"重复几次取均值"是想当然。本次跑的都是默认num_repeats=1,这条没走到。- JSON 解析失败 ⇒ 默认给 1 分:代码注释写着 "so the report is kept",而默认阈值正是 1 ⇒ 一次失败的调用等于"判定相关"(既收下它、又继续下探它的子社区)。本次 9 次运行的日志里 JSON 修复/报错命中 0 次;但默认路径的失败是"丢一批"(安静地少一块),这里的失败是"多花一轮 + 引入噪声"(安静地变贵、变脏)。
5. 小结:三句话
- 方向是对的,但没有走到底:它把"层号切一刀、范围内全给"换成了"逐个社区做相关性检查"——这一步是对的。但它没有"够了就停"的中止条件:判据是"相不相关"而不是"够不够回答",而"该钻多深"的上界仍然由用户填的层号框定(第 2 节那张表最后一行)⇒ 深度这件事它没碰(3.2)。
- 代价是实打实的,而且事先就能算出来:选材多了一个花钱的环节,而打分一次 ≈ 塞一份报告进 map 一次(都是 2.3k–2.9k token)⇒ 成本 ≈ 单价 ×(被评 + 入选)。三组里只有"同域·稀疏"压到了候选池的一半以下(token −29%),跨域那组反而 +41% ——而系统没有任何"值不值得走这条路"的预判(3.1)。
- 它现在还不具备被直接依赖的条件:文档 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 那几行就行)——我只有一组语料,很想看看"砍到候选池一半以下才划算"这条判据在别处成不成立。