codegraph explore 输出预算分配机制详解:诊断仪表、按分比例分配与"预留必花"渲染设计(CG-4 到 CG-21)
本文聚焦 codegraph 仓库中 codegraph_explore 工具的输出预算分配体系:固定字节信封如何在多个候选文件间切分、切分决策为何长期不可观测、以及从 CG-4 诊断、CG-10 相关性打分、CG-12 按分比例分配,到 CG-15/CG-21"预留必须被花掉"的完整演进过程。读完你将掌握 EXPLORE_ALLOCATION 各常量的含义与校准依据、allocateExploreBudget 的分配算法、渲染循环中 buy 规则与 carry-forward 的实现细节,以及 CODEGRAPH_EXPLORE_DEBUG 诊断与回归探针的实际用法。
固定信封:问题的起点
codegraph_explore 的输出被一个固定的字节信封约束:总字符上限 getExploreOutputBudget().maxOutputChars,并且被硬顶在 25K,以保证宿主 Agent 永远不会把结果外置成文件再 Read 回来。至于这个信封在文件之间如何切分,是由 handleExplore 中一条长长的门控、分层与上限链条决定的——在 CG-4 落地之前,这条链条完全不可观测:你只能读一个 explore 响应然后靠猜,而无法说出"这个文件占了 16%,那个占了 20%"。
分层预算本身由索引文件数决定,当前实现见 getExploreOutputBudget:
| 文件数区间 | maxOutputChars |
defaultMaxFiles |
maxCharsPerFile |
|---|---|---|---|
| < 150 | 13,000 | 4 | 3,800 |
| < 500 | 18,000 | 5 | 3,800 |
| < 5,000 | 24,000 | 8 | 6,500 |
| < 15,000 | 24,000 | 8 | 7,000 |
| ≥ 15,000 | 24,000 | 8 | 7,000 |
大仓库不再放大单次响应,而是通过 getExploreBudget 增加允许的调用次数(1~5 次)。18,000 与 24,000 这两个档位的上限都刻意压在宿主内联工具结果上限(约 25K)之下——超过就会触发"写文件 + Read 回读",正是这个工具要防止的失败模式。
CG-4:让分配可观测的诊断仪表
设置 CODEGRAPH_EXPLORE_DEBUG 后,每一次 codegraph_explore 调用(MCP 工具或 codegraph explore CLI)都会输出一份报告,取值决定输出位置:
| 取值 | 输出目标 |
|---|---|
1 / true / on / yes / stderr |
stderr 上的人类可读表格 |
json |
stderr 上的一份 pretty-printed JSON 报告 |
| 其他任意值 | 视为路径——每行一份 JSON(JSONL)追加 |
未设置 / 0 / false / off / no / 空 |
关闭 |
该模块是 src/mcp/explore-diagnostics.ts。每个文件报告:相关性分数、图(RWR)质量、去重后的查询词命中数、排序标志(named / entry / central / spine / low-value / generated)、渲染模式、被分配的源码字节数、实际交付的字节数、两者占比,以及是否被裁剪;对于最终没有渲染的文件,报告原因(max-files、cliff、budget-whole-file、budget-clusters、unreadable、no-ranges)。总量部分覆盖信封全貌(delivered vs allocated vs maxOutputChars vs 硬顶)、源码/元文本拆分、每个筛选阶段的文件漏斗,以及所应用的阈值(分数下限、相关性门)。
它默认关闭,且关闭时输出逐字节一致——它随产品二进制一起发布,任何让响应偏移一个字节的诊断都会使开启它时所做的一切 A/B 测量失效。ExploreDiagnostics.start() 在环境变量未设置时返回 null,因此所有调用点都是 diag?. 空操作,由 tests/explore-diagnostics.test.ts 钉住这一约束。
两个信封数字被刻意分开:
- allocated(分配)——渲染循环在最终硬顶截断之前选择发出的量,是分配器自己的决策,预算工作围绕的正是它;
- delivered(交付)——Agent 实际收到的量。
两者恰好在硬顶截断时出现分叉。把二者混为一谈,尾文件被丢弃就悄无声息了。
基线(2026-08-03,本仓库 main)
codegraph explore "how does explore allocate its output budget across files" --path .
469 个文件被索引 → small 档位(maxOutputChars 18,000,maxCharsPerFile 3,800,defaultMaxFiles 5)。信封:18,000 预算下交付 23,196 / 分配 23,193——超支 29%,之所以没出事,只是因为有 25K 硬顶在它之上兜底。
| # | share | bytes | score | graph | hits | flags | render | file |
|---|---|---|---|---|---|---|---|---|
| 4 | 21.2% | 4,928 | 10 | 0.125 | 1 | entry | whole | scripts/agent-eval/offload-eval-hook.mjs |
| 5 | 20.1% | 4,665 | 10 | 0.125 | 1 | entry | whole | scripts/agent-eval/offload-eval-metrics.mjs |
| 3 | 19.8% | 4,585 | 22 | 0.125 | 1 | entry central | whole | scripts/agent-eval/parse-session.mjs |
| 1 | 15.8% | 3,659 | 54 | 0.322 | 4 | entry central | clusters* | src/mcp/tools.ts |
| 2 | 15.0% | 3,479 | 34 | 0.082 | 2 | entry | clusters* | src/index.ts |
* 被裁剪。排名进入但从未渲染:scripts/agent-eval/offload-eval-cost.mjs(#6)与 src/resolution/lru-cache.ts(#7),均被 maxFiles 切掉。
文件漏斗:17 个分组 → 10 个过分数下限(≥3)→ 10 个过低价值过滤 → 7 个过相关性门(graph ≥ 0.0193,为最大值 0.3215 的 6%)→ 5 个进入输出。
基线揭示的两件事
相关性分数不驱动分配。 真正回答该查询的 src/mcp/tools.ts 拥有 5.4 倍的相关性分数、2.6 倍的图质量与 4 倍的词命中,得到的份额却小于每一个 .mjs 脚本。三个 agent-eval 脚本合计吃掉信封的 61%;答案文件只拿 16%。
机制在于两条分配路径都由文件大小而非相关性决定:小文件能过 WHOLE_FILE_MAX_LINES/WHOLE_FILE_MAX_CHARS 从而整文件发出,大文件落入簇选择并在 maxCharsPerFile 处被裁剪。于是弱相关的 130 行脚本拿到自己 100% 的内容,强相关的 5,000 行文件只拿到 3,800 字符。排序本身是对的(tools.ts 排 #1)但毫无收益——排名对文件拿到多少字节没有任何影响。
信封被超订。 18,000 的预算下分配出 23,193,说明每文件上限无法组合成总量上限;总量只能靠 25K 硬顶静默丢弃整个尾部段落来执行。换一个略不同的索引状态(多一个候选文件),同样的查询分配到 27,518 字符,硬顶丢掉了 7,678 字符的段落——响应中最大的一次分配——唯一的痕迹是结尾的截断提示。
这就是该 epic 其余部分要补的缺口:相关性比例分配加相对悬崖(CG-12),建立在不奖励偶然名字碰撞的打分之上(CG-10,已落地,见下节)。
CG-10:相关性打分——决定"什么进入响应"
CG-10 改变的是什么进入响应,先于字节怎么切。四个杠杆全部是乘法性的,可以无顺序意外地叠加。
1. 符号类型加权(Kind weighting)
一个符号到达所属文件的方式分层(named seed +50、查询命中 +10、与命中相邻 +3、外围 +1)说明的是它怎么来的;RELEVANCE_KIND_WEIGHT 回答的是这个匹配是不是证据。函数/类型类权重 1.0,成员约 0.5,constant/variable/parameter 仅 0.15–0.35——一个局部变量叫 explore 在被其他证据佐证之前只是名字碰撞。
孤立检查(Isolation):对弱类型符号的前两档,"有没有人在用它?"就是佐证——图中不存在任何使用边(contains 被排除——词法嵌套不是使用)时降到 0.08。成本有界:只有权重能撑起整个文件的档位的弱类型符号才付出探针代价,且子图自身边在多数情况下免费回答。实测无延迟变化(210 vs 211 ms/call,n=12 交错测量)。
外围上限(Peripheral cap):距任何命中 ≥2 跳的节点现在累积进一个上限为 5 的独立桶。没有限制时每个这样的节点都加一个平白 +1,文件越大反而越"相关"——parse-session.mjs 靠一个偶然常量和十两个无关符号拿到了 22 分。体积不是证据。
2. 相对分数下限(Relative score floor)
score >= 3 的绝对下限在任何头部文件得分 50+ 的仓库里都会放进噪声。新下限是 clamp(topScore × 0.2, 1, 10),当前实现中 SCORE_FLOOR_MAX = 10(src/mcp/tools.ts),计算见 src/mcp/tools.ts:
- 相对——弥散型问题没有文件主导,所有候选都贴近顶部,整个梯度存活;精确型问题则切掉尾部;
- 上限 10——对一个函数符号的一次直接查询命中。单次满强度命中从不偶然,因此别处的任何集中度都不应排除它。没有这个上限时,一个 named-seed 密集文件曾把下限推到 21,丢掉了一个 Agent 按类名点名的文件(类以
+10进入而非+50——named seed 是函数符号); - 回填(backfill)——存活文件少于 3 个时,被下限切掉的最好者回到池中,但只从有真实证据(≥绝对下限)的文件中回补;如果什么都没剩,回填放弃该要求:gather 明明找到了候选却回答"未找到相关代码",只会把 Agent 直接送回去 grep。
3. 生成文件状态进分数,而非平局裁决
rankPenalty(file) 对生成文件同时把相关性分数和图质量乘以 0.3(低价值文件乘 0.5),当前实现见 src/mcp/tools.ts。只作用于分数的话修不了 #1500:生成 CRUD 的图质量比手写用例更大,而比较器里图质量先于分数。该惩罚自归一化——全生成代码库中所有文件一起缩放,相对排名不受影响——且永不硬排除:按名字问生成 API 时,named-seed 档仍会把它排到第一。
4. excludeLowValueFiles——已死的配置
任务要求重新考虑的那个按档位标志是死配置:声明在 ExploreOutputBudget 上并按档位设置,却没有任何地方读取。后续变更早已让 test/spec/icon/i18n 排除在所有档位无条件生效,该标志被删除(源码注释见 src/mcp/tools.ts)。
实质性缺口在检测器而非门控:isLowValue 原来匹配 /\/(tests?|__tests?__|spec)\//,锚定在前导斜杠上,因此仓库根目录的 test/ 目录——express、cobra 以及 npm 和 Go 的大部分项目——永远匹配不上。Express 的"how does express route a request to a handler?"把信封的 59% 花在三个测试文件上,而 lib/application.js 被裁剪。现在检测器同时锚定 ^ 与 /(src/mcp/tools.ts),同一查询返回 lib/application.js + lib/response.js,没有任何测试文件。
两个关联变更:过滤现在运行在分数下限之前,并且用整个 gather 而非下限之后的集合来判断"还有没有其他候选"(在下限之后判断,正是下限的最低保留量把测试文件作为"梯度"拉回来的途径);以及穿过过滤 ≥2 个非测试候选逃生门存活下来的低价值文件,通过 rankPenalty 降权而非保留满强度。
实测效果
同一索引、确定性测量(CODEGRAPH_EXPLORE_DEBUG 诊断,两臂同一构建体系,基线 bd86ad2):
| 仓库 · 查询 | 之前 | 之后 |
|---|---|---|
| 本仓库 · self-query 夹具 | 72% 给 eval 脚本,tools.ts 18.5% |
脚本 0%,tools.ts #1 |
本仓库 · handleExplore buildFlowFromNamedSymbols … |
82% 给 eval 脚本 | tools.ts 48% + index.ts 32% |
| 本仓库 · "how is error handling done" | 58% 给 eval 脚本,tools.ts 交付 0 |
transport/tools/cobol/api |
| 本仓库 · "what languages does codegraph support" | 63% 给 scripts/add-lang/* |
grammars/index/cli |
| 本仓库 · "main components of the indexing pipeline" | — | 逐字节一致 |
| payroll-go 夹具 | 生成文件 57.4%,答案 25.6% | 答案 61.5%,生成 23.5% |
| express · route a request | 59% 给 test/* |
application.js + response.js |
| cobra · 3 个查询 | — | 逐字节一致 |
两个逐字节一致的行是对照组:答案本已集中的地方,新下限只是更早、更便宜地剪掉同一条尾部,得到同样的响应。
已知的薄输出案例。 Express 的"how does the app object get created and what does it expose"从 4 个文件(头部是一个 examples/ 文件,占 38%)缩到单独的 lib/express.js,2.6 KB 对着 13 KB 预算。lib/application.js 只靠一个未使用的文件级 var app 匹配上——在符号层面与 eval 脚本里未使用的 const explore 无法区分;express 把 API 面建模为赋给该对象的属性,图中没有这些边。这是抽取覆盖面问题,不是排序问题。回填曾被尝试并拒绝:节点数平局让名额给了 examples/route-middleware,占信封 48%。薄而精确胜过噪声填充——错误文件并不能替 Agent 省掉它本来要在其上填充的那次后续调用。
CG-12:按分比例分配——决定"字节怎么切"
CG-10 修好了什么进入响应。CG-12 修的是进入的字节如何切分——在此之前这根本没有被真正决定过。每个准入文件都被同一个平的 maxCharsPerFile 封顶,整文件规则又给任何小于 maxCharsPerFile × 3 的文件全部内容。所以信封跟着文件大小走:
- self-query:
memory-budget.ts(分数 18)以 5,672 字符整文件发出,占 51.2%;tools.ts(分数 41,4 倍图质量,3 倍词命中——它字面上就放着分配器)在 3,800 处被裁剪,只拿 32.9%。 - payroll-go:两个生成 CRUD 文件各以约 4.5 KB 整文件发出,还消耗了档位 4 个文件名额中的 2 个,于是
BuildPayslip——问题中手写的"计算"一半——排 #6 从未渲染。
模型
allocateExploreBudget(src/mcp/tools.ts)在排序之后、任何渲染之前运行一次。它为每个文件预留信封的一份;渲染循环随后花的是预留,而不是与排在前面的文件争夺剩余。
- 权重 =
score × worth × (spine ? 2 : 1)。worth是rankPenalty被第二次应用:排序回答"这个文件是不是关于查询的",分配回答"这些字节能否教会 Agent 任何东西"。生成 CRUD 可以合法地获得排名——它在每个领域词上名字碰撞,且体量大、自引用密集,在比较器优先的结构键上得分——但它的字节仍是机械样板。第二次惩罚才是真正把它沉下去的东西。 - 相对悬崖在头部权重的 15% 处(
CLIFF_FRACTION),自身上限为SCORE_FLOOR_MAX(10)。低于它的文件拿到零源码——只有路径、符号和行号,约 100 字符而非约 4,500,并且不占maxFiles名额,名额让给挣得字节数的文件。正是这次名额移交让BuildPayslip进入了响应。上限和比例同样重要:一个 500 分的神文件否则会把悬崖推到 75,让分数下限刚放进来的所有同级文件噤声。 - 先保底再切分。 每个准入文件先拿
MIN_CHARS(700——足以容纳一个完整方法);剩余部分按权重切分。保底让弥散型调查问题仍返回梯度;剩余部分让精确型问题集中。 - 安全阀而非每文件上限:任何文件不超过信封的 70%(
MAX_SHARE)。平的每文件上限退位——比例切分本身已按权重份额限制了文件。
当前常量全集见 EXPLORE_ALLOCATION:
| 常量 | 值 | 作用 |
|---|---|---|
CLIFF_FRACTION |
0.15 | 低于头权重 15% 的文件零源码 |
CLIFF_MAX |
SCORE_FLOOR_MAX(10) |
悬崖阈值上限,保护单次满强度命中 |
MIN_CHARS |
700 | 每文件保底预留 |
MAX_SHARE |
0.7 | 单文件安全阀 |
FILE_OVERHEAD |
200 | 每文件的 markdown 开销(头 + 围栏),切分前从池里扣除 |
SPINE_WEIGHT_BOOST |
2 | 流程脊柱文件权重加倍且豁免悬崖 |
WHOLE_FILE_GRACE_FRACTION / MAX |
0.15 / 800 | 整文件规则的"薄片"宽限 |
WHOLE_FILE_BUY_FRACTION |
0.6 | 预留已买下文件大部分时整文件发出(CG-21) |
WHOLE_FILE_BUY_OVERSHOOT_FRACTION |
0.15 | buy 规则的共享超支池 |
预留随后治理所有渲染路径——整文件、簇、聚焦/骨架——在此之前整文件分支比簇分支慷慨 3 倍,正是这 3 倍摆动按文件大小决定了切分。
让预留真正"咬合"需要两个支撑性变更:
- 超大簇按成员收缩。 簇是若干完整符号区间的合并,在符号密集的文件里所有符号会合并成一个横跨整个文件的 blob(cycle.go 的 209 行
Service)。旧规则无论如何大都整块拿走排名第一的簇,于是单簇文件直接无视预算——比分配多拿约 40%,排在它下面的文件随后因空间不足被丢。新收缩按重要度丢弃完整成员,函数体永远不会被切半。 - 到达顺序的停止点被移除。
budget-90pct与!fileNecessary && totalChars > maxOutputChars检查按到达顺序丢文件:排名靠前的文件花光信封,之后的一切都在一个自己无权参与制定的上限处被切掉。现在只剩绝对硬顶停止。
实测效果(CG-12)
| 仓库 · 查询 | 之前(CG-10 后) | 之后 |
|---|---|---|
| 本仓库 · self-query 夹具 | tools.ts 32.9%,memory-budget.ts 51.2% |
tools.ts 60.6%,memory-budget 17.2% |
| payroll-go 夹具 | 答案 61.5%,生成 23.5%,BuildPayslip 缺席 |
答案 78.7%,生成 0%,BuildPayslip 被交付 |
| express · route a request | 2 文件,头部 43.1% | 1 文件,头部 82.3%(response.js 触悬崖——0 词命中) |
| express · app registers middleware | 1 文件,71.6% | 逐字节一致 |
| cobra · parse flags and execute | 2 文件,command.go 40.1% |
2 文件,command.go 77.2% |
| cobra · 弥散 "main components" | 3 文件,头部 48.1% | 3 文件,头部 50.0%——梯度保持 |
| gin · request reaches a handler | 3 文件,头部 ginS/gins.go 48.8% |
3 文件,头部 routergroup.go 53.8% |
| gin · 弥散 "what it provides" | 3 文件,头部 recovery.go 43.0% |
4 文件,头部 context.go 33.3% |
两个弥散行是矫枉过正的对照组:文件数保持(3→3,3→4),调查型问题仍然拿到梯度。两个 gin 行还把头部文件换成了更贴切的一个——routergroup.go 胜过于薄壳的 ginS 单例包装,context.go 胜过 recovery.go——因为集中由权重而非"哪个文件碰巧小"决定。
"此前未裁剪的文件不会变得被裁剪"的例外。 memory-budget.ts 此前以 5,672 未裁剪整文件发出,现在在 3.1 KB 预留内走簇路径。这是该 epic 对自身 bug 的诊断而非回归:它 18 分对 tools.ts 的 58 分,拿更大切片纯粹因为小到能整文件发出。保证在它本应成立的地方成立——没有文件因为更紧的上限丢失字节;丢字节的是比例切分认定被超供的文件。
CG-6:回归夹具——把失败模式钉死
两个夹具把这个失败模式钉死,让它无法静默复发。它们是以失败为目的写成的——这正是它们的用途。CG-10 关闭了两者的排序半边,CG-12 关闭了字节切分半边;现在两者都通过,是活的回归。下表引用的数字是CG-10 之前的基线;现状见上面两张"实测效果"表。
夹具声明在 scripts/agent-eval/allocation-fixtures.json,由 scripts/agent-eval/probe-allocation.mjs 驱动——它通过 JSONL 旁路驱动 CG-4 诊断(因此测量的是产品分配器而非重新推导),把渲染文件分组为 answer 对 incidental,并检查声明的份额阈值。需要一次当前 npm run build;任何断言失败时退出码 1。
node scripts/agent-eval/probe-allocation.mjs # 两个夹具
node scripts/agent-eval/probe-allocation.mjs payroll-go # 单个
node scripts/agent-eval/probe-allocation.mjs --json # 机器可读
1. payroll-go——报告者的形状
tests/fixtures/payroll-go/ 是一个合成 Go 服务:生成 FKIT CRUD 旁立手写 payroll 用例,从 HTTP 路由进入。完整描述见该目录 README。要点:
- 生成文件用普通名字并携带
// Code generated ... DO NOT EDIT.——对纯路径检测不可见,这正是它成为 #1500 夹具而非.pb.go夹具的原因——旁边还有payrollpb/*.pb.go覆盖可路径检测的通道。 - 刻意碰撞:
BuildPayslip、Upsert、Store各存在两次(生成与手写),生成层在每个查询词上名字碰撞。 cycle.go(227 行)位于整文件窗口之上从而被裁剪;生成文件在窗口之下从而整文件发出。
查询——一个架构问题,没有点名任何回答符号:"how does payroll cycle create and calculate payslips?"
| allocated | delivered | |
|---|---|---|
| 手写 | 48.4% | 25.6%(全部是领域类型) |
| 生成 CRUD | 39.9% | 57.4% |
cycle.go 拿到最大的单份切片(7,052 字符,30.6%)却交付零——19,500 硬顶丢掉它的整个段落。payslip_builder.go(排名 #8)从未渲染。于是 runPayrollCycleAll、手写的 BuildPayslip 和真正的 Upsert 从未到达 Agent,而到达的每个字节描述的都是 CRUD 或类型。
该夹具是无交互的(hermetic):探针把树复制到临时目录并每次重新索引,同一构建下两次运行逐字节一致(已验证)。tests/explore-allocation-1500.test.ts 在 vitest 中运行同样的断言。
CG-10 之后生成文件排 #3/#4 而非 #1/#2,cycle.go 交付 38.9%(此前交付为零),runPayrollCycleAll + 真正的 s.store.Upsert(ctx, slip) 到达 Agent。这些断言现在是活的回归。仍保持 it.fails 的是 payslip_builder.go:它排 #6,档位 maxFiles 是 4,渲染循环仍按文件大小花钱——那是 CG-12 的工作。
刻意留下的未修发现: runPayrollCycleAll 在 *payslipstore.Store 上调用 s.store.Upsert,但图把这条边解析到生成的 internal/gen/fkit/payroll/store.go 的 Store.Upsert。两个都定义 Store.Upsert 的包之间同名方法解析选错了接收者。它在分配上游——错误的边把生成 store 拉进子图。CG-10 缓解了症状(生成 store 在分数和图质量上都被罚,不再挤掉真正的),但解析 bug 本身属于同名方法解析工作(见 samename-method-resolution-1079)。
2. self-query——没有生成代码的同款 bug
上述基线提升为夹具:本仓库,"how does explore allocate its output budget across files"。scripts/agent-eval/*.mjs 偶然提到 explore 和 BUDGET——它们是 eval 脚手架而非分配器——且小到能整文件发出,而 src/mcp/tools.ts 大到会被裁剪。
493 个索引文件(small 档)下:脚本语料占交付信封 71.8%(分配 79.4%),对照 tools.ts 的 18.5%——尽管 tools.ts 分数 46 对 10、2.3 倍图质量、3 倍去重词命中。
该夹具读取本仓库的活索引,因此与 payroll-go 不同,其精确数字随仓库变化而漂移。断言因此是相对的(答案组对偶然组、最大交付文件),从不用固定百分比。两件事需要知道:
- <500 文件档边界很近。 本仓库索引 493 个文件(含新夹具);跨过 500 会把
maxOutputChars从 18,000 翻到 24,000、maxFiles5→8、maxCharsPerFile3,800→6,500,上表所有数字都会移动。跨过之后应重新定基线,而不是把漂移当回归。 - 添加
payroll-go夹具本身把计数从 472 推到 493。它的 Go 文件不匹配该查询的任何词,因此只改变档位算术。
复现
查询探索的是本仓库,所以对 src/mcp/tools.ts 的未提交编辑会改变结果——索引会拾取它们、分数会移动(同一查询在 CG-4 工作树上依同步状态报告 tools.ts 13–19%)。在干净树上测量:从 main 恢复 src/mcp/tools.ts、删除 src/mcp/explore-diagnostics.ts、codegraph sync,然后运行构建出的 dist/ 二进制(其中仍带着仪表)。测完恢复。
CG-14:把分配锁死——覆盖设计
分配变更是一个函数加三个渲染循环边界,而它回归的每一种方式都是静默的:没有异常,响应只是变得没用一点,Agent 回退到 Read。因此覆盖围绕"如果移除这个杠杆,某个测试会变红吗?"来构建,而不是围绕行覆盖率。
覆盖分布
| 文件 | 负责 |
|---|---|
| tests/explore-proportional-allocation.test.ts | allocateExploreBudget 隔离测试——切分、悬崖、档位不变量、信封安全、脊柱加权、退化输入 |
| tests/explore-allocation-e2e.test.ts | 同样行为穿过真实渲染循环与真实索引项目——self-query 夹具形状、退化结果集、弥散查询对照 |
| tests/explore-allocation-1500.test.ts | 报告者的 Go 形状(CG-6 夹具 1)加硬顶压力用例 |
| scripts/agent-eval/probe-allocation.mjs | 对构建出的 dist/ 跑两个 CG-6 夹具,含读取本仓库自身索引的活 self-query 臂 |
最后两者的分法是刻意的:self-query 夹具读移动目标(本仓库),精确数字随树漂移,它应该出带到带外——在那里漂移是一个重新定基线的数字,而不是一个变红的套件。npm test 拥有它的合成镜像:同样的三个角色、同样的大小不对称,固定不变。
两个边界不是同一个边界
值得说一次,因为混淆二者的测试看起来对、并为错误的原因通过:
maxOutputChars约束的是预留。sum(allowances) <= pool <= maxOutputChars,在每个档位、每种候选形状下精确成立。- 硬顶——
min(maxOutputChars × 1.5, 25000)(src/mcp/tools.ts)——约束的是响应。 渲染循环被允许有界超支(整文件宽限、超大首簇),所以响应合法地超过信封。payroll-go正是这样:13,000 信封、19,500 硬顶下交付 19.3K。
只有 25K 是绝对的。超过它宿主会把结果写文件让 Agent Read 回来——正是这个工具存在要防止的失败。
变异测试,而不只是绿灯
每个杠杆曾被逐一从 src/mcp/tools.ts 移除并重新跑套件。每个杠杆都至少被一个失败测试覆盖——没有红色测试的杠杆是可以被意外删除的杠杆:
| 变异 | 变红的测试 |
|---|---|
渲染循环回退到 CG-12 之前(fileBudget = maxCharsPerFile,整文件边界 maxCharsPerFile * 3) |
5 e2e + 2 payroll |
| 比例切分替换为均分 | 4 unit |
禁用悬崖(cliffAt = 0) |
7 unit + 3 payroll |
| 移除脊柱加权与悬崖豁免 | 3 unit |
移除 MIN_CHARS 保底 |
2 unit |
移除 MAX_SHARE 上限 |
4 unit |
第一行最关键:它在合成夹具上逐字复现 #1500,一个相关性只有一半的文件纯靠体积拿更大份额。
| file | score | CG-12 前 | CG-12 |
|---|---|---|---|
src/mcp/allocator.ts |
77.5 | 4,843(39.7%) | 9,335(80.1%) |
src/util/budget-math.ts |
36.0 | 6,079(49.8%) | 1,037(8.9%) |
覆盖发现的两个缺陷
两者都是靠写不变量而非读代码找到的:
- 取整后的份额可以超过池。 对每个文件的比例切片做
Math.round让预留总和最多超过pool每文件半个字符——小,但把"预留装进信封"从精确变成了假。现在两项都向下取整(src/mcp/tools.ts)。 - 非有限分数产生 NaN 预留。
Infinity权重让每个份额变成Infinity / Infinity。管线中分数是有限和,实际不可达,但失败形态是把 NaN 交给渲染循环。weightOf现在失败安全到 0(src/mcp/tools.ts)。
校准 vs 不变量
EXPLORE_ALLOCATION 被导出以便测试读取。不变量测试(信封安全、档位单调性、保底)引用常量,在任何取值下成立;一个测试钉住字面量,因此重新调整常量是一个可见的决策——说"重跑探针",而不是对夹具静默重新校准。
CG-15:Agent A/B 的发现——预留可以花不掉
Agent A/B(完整记录见 docs/benchmarks/explore-allocation-ab-1500.md)在两个中型仓库上通过——client-go 和 excalidraw 每次运行 Read 均为 0,excalidraw 34s → 24s 中位且少一次 explore 调用,占基线信封 10.5% 的生成 clientsets 从每个新运行中消失——但在小型对照组 express 上 3 次运行中 1 次失败:对 lib/utils.js 的 4 次 Read,而基线只对另一个文件做了 1 次 Read。
这不是 Agent 方差。确定性回放该运行自己的查询:
lib/utils.js(5,293 B,272 行) |
基线 | CG-12 |
|---|---|---|
| 交付 | 6,380(46.1%)整文件 | 583(7.7%)簇桩 |
| 源码信封(预算 13,000) | 13,849 | 9,241 |
诊断显示分配器是对的,渲染循环不是:
allocation 12,398 reserved of 12,400 pool · nothing cliffed
# deliv% bytes reserved score flags render file
1 5.7% 583 3,870 56.0 named entry central clusters lib/utils.js
utils.js 是排名第一的文件,预留 3,870 字符——只花了 583。整文件边界(现见 src/mcp/tools.ts)是 allowance + min(GRACE_MAX, allowance * GRACE_FRACTION) = 3,870 + 580 = 4,450,低于文件 5,293 字节,于是整文件渲染被拒;回退簇渲染只有三个匹配符号可用,发出 583 字符。预留的其余 3,287 字符没有被再分配——丢失了,这就是响应在预算不变的情况下缩水三分之一的全部原因。
这正是 CG-12 自己的验收标准("此前未裁剪的文件不会变得被裁剪")的失败。CG-14 记录过一个实例作为文档化例外(memory-budget.ts);这是同一缺陷的野外版本,代价是一次 Agent 往返。它不是系统性的:两个中型仓库上渲染循环饱和([over budget] [TRUNCATED],23,600 池预留了 23,599),没有可丢失的东西。它需要这样一个文件:预留落在自身尺寸之下、匹配符号集又薄——最可能出现在小仓库,那里每文件预留最小。
两个候选修复
放宽信封明确不在其中——那是 iter2 已证明无效的旋钮,且这里的字节本来就已为该文件预留。
- 让足够大的预留买下整文件。 当
allowance >= k * fileSize(某个k < 1,utils.js 处在 0.73)时整文件渲染,让有界超支被硬顶容差吸收。改动小,匹配观察到的形状。 - 再分配缺口。 渲染循环知道文件实际尺寸后,把花不掉的字节交给下一排名文件。更强的不变量——池被花完——并直接修复缩小的信封症状。
两者可组合;单独 (1) 就足以覆盖此案例。无论哪个落地,都需要 express 形状作为无交互夹具——匹配符号少、尺寸刚好高于预留的中型头名文件——因为现有套件中没有这个形状,这正是它被放行的原因。
CG-21:花掉预留——两个修复都落地
两个修复都落地了,因为它们覆盖同一失败的不同半边,且单独都不充分。统一规则:预留是渲染循环必须兑现的承诺,而不是它可以悄悄少用的上限。
1. 已买下文件大部分的预留,买下剩余部分
WHOLE_FILE_BUY_FRACTION(0.6,src/mcp/tools.ts)。既有宽限按薄片校准——只救基本上放得下的文件。在"放得下"与"几倍于预留"之间有个洞,lib/utils.js(预留/尺寸 = 0.73)正落在洞里。所以测试不是文件是否放得进预留,而是预留是否已经买下了文件的大部分:超过 0.6 时循环至多额外支付三分之二的预留,而不是失去整件事——花在一个已经挣得这些字节的文件上。
资金是容易做错的部分。 资格测试是比例,因此凡是有多个文件贴近它的地方,它们全部合格——N 个独立超支会把响应膨胀到渲染顶丢掉最后者。按各文件自身余量出资被尝试并在 payroll 夹具上测量:三个文件整文件发出,payslip_builder.go——问题问的、计算工资单的那个文件——被完全丢弃,好让三个更高排名的文件各自发出最后的薄片。被丢的段落严格差于被簇化的段落。
所以 buy 规则是两个独立测试,分开正是全部设计:
| 读取 | 回答 | |
|---|---|---|
| 资格(merit) | 文件自己的 reserved |
这个文件的相关性是否挣得了它的大部分自身? |
| 资金(funding) | 一个共享超支池(WHOLE_FILE_BUY_OVERSHOOT_FRACTION,信封的 15%) |
字节是否存在,且它下面每个预留仍可支付? |
资格读 reserved 而非带进位后的 allowance,因此借来的宽裕永远无法把弱文件提升到整文件。资金对照分配器承诺的测量而非 renderCeiling——顶在信封之上 50%,谁欠谁什么它一无所知,从它出资 buy 只是把缺口移到循环最后到达的文件头上。owedBelow 项让它成为位移防护而非尺寸上限,且自限:每次 buy 增大 sourceSpent,池不会被花两次。当前实现(src/mcp/tools.ts):
const buysWhole = fileContent.length <= graceBound
|| (reserved >= fileContent.length * EXPLORE_ALLOCATION.WHOLE_FILE_BUY_FRACTION
&& sourceSpent + fileContent.length + owedBelow <= sourceCeiling
&& fileContent.length <= fundedHeadroom);
2. 文件花不掉的,交给下面的文件
低于 buy 比例时缺口是真实的——文件几倍于预留,簇渲染才是对的——但字节仍不能蒸发。渲染循环维护两个运行总量(src/mcp/tools.ts):迄今所有承诺的(reservedSoFar)与迄今所有发出的(sourceSpent),其差值是下一个文件可以加到自己预留上的宽裕(src/mcp/tools.ts)。
用两个总量而非一个穿过循环十几个 continue 的 spent 变量,因此任何出口路径都漏不掉记账——不可读、磁盘漂移、因顶跳过、薄匹配集。它是对称的:超支的 buy 让 sourceSpent 跑过 reservedSoFar,抑制宽裕直到之后的少支覆盖债务,于是池双向守恒,没有文件被切到低于承诺值。宽裕按排名顺序流动(单遍循环中唯一还能支付的文件),并被 MAX_SHARE 夹住,以免少支的头名把整个响应交给弱尾部文件。
实测效果(CG-21)
Express 复现场,同一索引、同一 13,000 预算:
lib/utils.js(5,293 B,272 行) |
基线 | CG-12 | CG-21 |
|---|---|---|---|
| 交付 | 6,380(46.1%)整文件 | 583(7.7%)桩 | 6,268(39.3%)整文件 |
| 源码信封 | 13,849 | 9,241 | 14,505 |
以及 self-query——epic 自身验收标准尚未满足的地方:
| 文件 | CG-12 前 | CG-12 | CG-21 |
|---|---|---|---|
src/mcp/tools.ts(分数 58) |
32.9% | 60.6% | 52.6%(10,945,簇) |
src/resolution/memory-budget.ts(分数 18) |
51.2% 整文件 | 17.2% 簇 | 27.3% 整文件(5,672) |
memory-budget.ts 例外被解决而非重新辩护。 CG-14 把它记录为"此前未裁剪的文件不会变得被裁剪"的文档化例外;它的预留/尺寸比是 0.73——与 express utils.js 同一个窗口——因此 buy 规则覆盖它。答案文件仍然赢下信封,并且没有以前整文件发出的东西被裁剪——这是 CG-12 验收标准两半首次同时成立。
tests/explore-allocation-e2e.test.ts 中的合成镜像刻意不动:其辅助处在比例约 0.17,远低于 buy 比例,因此仍走簇——正确地。这种分歧正是比例的意义:memory-budget.ts 被薄片超供,合成辅助被倍数超供。
覆盖
两个新的无交互夹具,每个杠杆一个,因为现有套件两者都看不到:payroll 与 self-query 夹具都饱和([over budget] [TRUNCATED],23,600 池预留 23,599),而饱和的响应没有可丢失的未花预留。
| 夹具 | 形状 | 防护 |
|---|---|---|
| "低于文件尺寸的预留仍买下文件" | 中型头名文件、薄匹配集、尺寸刚好高于预留且在宽限边界之外 | buy 规则 |
| "无法花掉的预留流向下一文件" | 头名文件大到买不起(比例 0.24)少支;稠密的第二名吸收 | carry-forward |
每个夹具带一个 fixture shape 块断言它依赖的窗口(0.6 × size <= reserved < size、宽限边界之外、220 行整文件上限之内)。它们是承重的而非脚手架:一旦目标漂移小到宽限可覆盖,每个门控都会空真通过——正是这个缺陷躲藏的方式。
变异测试,方法与 CG-14 相同:
| 变异 | 变红的测试 |
|---|---|
移除 buy 分支(buysWhole = fileContent.length <= graceBound) |
3(新 buy 夹具) |
移除 carry-forward(allowance = reserved) |
2(新 carry-forward 夹具) |
移除资金守卫(… && true) |
4——含 payroll 的 payslip_builder.go 被丢 |
初稿踩过的两个陷阱,都让测试在缺陷存在时通过:
- 脊柱上限遮蔽每文件断言。
SPINE_CEILING本就允许流程路径簇达到其 allowance 的 1.5 倍,所以没有任何 carry-forward "delivered > reserved" 也为真。carry-forward 夹具刻意建在无脊柱上并断言 1.1 倍余量——变异下实测 9,297 对 7,479。 - "花完整个池"不是不变量。 小于预留的文件合法地少支(夹具的
response.ts:预留 5,292 交付 1,635)。断言是每文件的——delivered >= min(reservation, 文件尺寸)——express 的utils.js违反的正是它,池求和断言表达不出来。
第三个条件——由评审而非测试发现
buy 还必须装得进渲染顶。整文件分支拒绝中途切函数,因此超过 renderCeiling 的整文件渲染被整体跳过——意味着一次被资金池批准却被顶拒绝的 buy,用一个簇段落换了没有段落。这正是资金池存在要拒绝的那笔交易,从另一条路进来。
它只在 24K 档位可达,因此两个新夹具都看不到:
| 档位 | 信封 | renderCeiling = min(1.5x, 25000) - 600 |
资金线 = reservedTotal + 0.15x |
|---|---|---|---|
| small | 13,000 | 18,900 | 约 14,350——不可交叉 |
| medium/large | 24,000 | 24,400 | 饱和时约 27,200——交叉约 2.8K |
所以 buysWhole 还带着 totalChars + size + FILE_OVERHEAD <= renderCeiling(现实现中对照 fundedHeadroom,src/mcp/tools.ts);不满足则落入簇路径,后者由 headroom 限界、总能渲染出点什么。宽限分支刻意未动——离预留只差一个薄片的文件若仍装不下,是真正处在完整响应末尾,该行为早于本 epic。已在全部三个 A/B 仓库验证惰性(excalidraw 与 client-go 每仓库 3 个查询逐字节一致,express 复现场不变)。
Agent A/B(CG-15 的门槛,重跑)
epic 的门槛是 CG-22 而非本节。CG-22 在 CG-15 的完全相同设置下(RUNS=3、全新克隆、基线以 SHA 49c11fc 钉住)重跑了它,四项标准全部通过——12 个新臂运行中 Read 全部为 0,而基线在 3 次 express 运行中全部有读;确定性复现器在两个构建同一会话重测的情况下成立(lib/utils.js 两者都是 6,380 B 整文件,信封 13,849 → 14,913)。见 docs/benchmarks/explore-allocation-ab-1500.md § CG-22。
该节记录的两个反证——excalidraw 的答案份额低于其基线、client-go +1.5s 中位——是"已归因但未解决",不是被隐藏。n=6 每臂的 express 与 excalidraw 全量记录同样在 docs/benchmarks/explore-allocation-ab-1500.md("Re-run after CG-21" 节)。四项标准全部通过:
- 15 个新臂运行中 Read 全为 0。 把缺陷路由到这里的 express 回归(对
lib/utils.js的 4 次 Read)在 6 次尝试中未复现——而基线在 6 次中 4 次有读,对照组现在胜过了它先前输掉的那一臂。中位 24.5s → 21.5s。 - client-go——报告者的形状——答案份额保持 92.7–96.2%,基线运行在 53.8%。
- Excalidraw 约 8s 的中位差不可归因于该变更。 Explore 自身延迟 374 ms 对 372 ms(n=5,同查询同索引);确定性响应相差 +2% 且一个逐字节一致;而未改动的
main构建自己的中位在 CG-15 会话与本会话之间从 34s 移到 26.5s——与差值同量级。该仓库上 Agent 墙钟在此样本量下是噪声主导的(宿主模型思考占主导,而非工具延迟)。
核心不变量小结
从 CG-4 到 CG-21,整条工作线收敛为几条可从源码逐条核对的不变量:
sum(allowances) <= pool <= maxOutputChars——预留精确装进信封,pool = maxOutputChars - FILE_OVERHEAD × 文件数(src/mcp/tools.ts);hardCeiling = min(maxOutputChars × 1.5, 25000)约束响应,有界超支合法,25K 之上绝不外置(src/mcp/tools.ts);- 悬崖以下 = 指针而非字节:约 100 字符的路径 + 符号 + 行号,且不占
maxFiles名额; - 预留是承诺:每文件
delivered >= min(reservation, 文件尺寸),未花掉的预留按排名顺序向下流动,双向守恒; - 诊断关闭时逐字节一致:
ExploreDiagnostics.start()未设环境变量返回null,所有调用点是diag?.空操作(src/mcp/explore-diagnostics.ts,钉死于 tests/explore-diagnostics.test.ts)。
这些不变量与杠杆一一对应到测试:杠杆移除必变红,常量重调必可见,夹具形状断言防止门控空真通过——这是"既有可复制的实测数字,又有源码级原理支撑"的完整闭环。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00