现在有一类系统很流行:不动模型权重,让一个 proposer 模型反复改写 agent 外围的代码——记忆、编排、评测逻辑——每轮生成一个新候选,跑分,留下好的。这类 "harness self-evolution" 循环的标准接口设计有个朴素的假设:把所有历史候选、分数、轨迹都挂载给 proposer,给它最大的编辑自由,它就会去读证据、探索各种改法、把分数的上涨变成能力的上涨。
这三个等号,每一个都未必成立。
循环本身长这样:
每轮 t:
1. proposer 面对一个文件系统: 所有历史候选的源码、分数、轨迹都挂载在里面
2. 它自己决定读哪些(也可以不读)、从哪个候选出发(也可以从头写)
3. 产出一个新的候选 harness 模块
4. 外层循环评测、归档, 进入下一轮
全程冻结: 模型权重、执行器
全程放开: 读什么、改什么、怎么组织 agent
我们做的不是提出新方法,而是给这个循环装上探针:每一轮,记录 proposer 打开过哪些文件、写出的候选是什么组织形态、分数的提升由哪些任务构成。测量把循环拆成三个通道——evidence(它读了什么)、action(它改了什么)、outcome(它得到了什么)。结论可以一句话说完:这三个通道各有一道窄门,名义上的访问和实际兑现的行为之间,处处是缺口。

图 1 · 总览。A:证据通道——挂载的历史目录随轮次增长,实际读取的集合几乎不动,覆盖率 82% → 14%。B:动作通道——编辑集中在预算、hook、输出契约上,拓扑结构(Topology)和 Ensemble 几乎无人触碰。C:结果通道——训练集上的增益在 held-out 上分解:契约修复(绿)留下来了,任务绑定的技巧(红)掉了回去。
这篇博客按通道讲我们发现的问题和结论。完整实验细节在论文仓库里。
一、证据通道:挂上去的不等于读到的
标准接口把全部历史迭代目录挂载给 proposer。我们跑了 21 组不加限制的实验(MemGPT 式记忆 harness,内层模型 Qwen3-8B,LoCoMo 和 LongMemEval 两个 benchmark,30 轮迭代),记录每个文件的读取路径。
第一个发现:可见历史在涨,检查量不涨。 挂载的目录从平均每轮 2.2 个涨到 23 个,翻了十倍;proposer 每轮实际打开的文件数始终停在 18 个左右,覆盖率从 82% 一路掉到 14%。我们排除了 prompt 截断和工具调用预算缩水的可能——读文件调用数、工具输出字节数在早晚期窗口里都是平的。不是看不到,是没去读。
第二个发现:读的全是最近几轮。 一个近期迭代被翻开的概率是旧迭代的 4 到 7 倍。唯一例外是全局分数表:95% 的后期轮次都会打开它,尽管解释这些分数的诊断轨迹躺在旁边没人碰。proposer 留下了结果,扔掉了原因。

图 2 · A:覆盖率随挂载档案增大而衰减(紫:Codex,绿:Kimi)。B:21 组无限制实验里「第 t 轮读到第 i 轮」的概率热力图——读的行为贴着对角线(近期迭代),不断变大的三角形内部无人光顾。C:Random-3 干预下的同一张图,灰色格子是当轮未挂载的;只要旧迭代被挂进三个槽位,它就会被读,与离对角线多远无关。
到这里有个解释上的歧义:proposer 是真的觉得旧证据没价值,还是旧证据在竞争一个有限的检查预算时输了?这两种解释在基线行为下无法区分——它们产生同一个观测:一个没被读的目录。
区分的方法是做一个干预:随机把可见目录限制到三个(Random-3,用确定性哈希选,不带分数也不带新旧标签)。结果旧迭代的翻阅率从 6–9% 跳到 81%。随机选择是这个实验的灵魂:它没有告诉 proposer「这个旧目录值得读」,只是把竞争者拿走了。旧证据照样被读。
所以结论是:旧证据不是因为旧而被拒绝的,它是在有限检查预算的竞争中出局的。 Recency 是分配的结果,不是偏好。
顺带一提,这个干预只改变了注意的分配,没有带来分数提升——几个曝光策略的冠军分数在统计上分不开(LoCoMo p=.39,LongMemEval p=.48)。读得更多不等于做得更好,这是另一回事。
二、动作通道:够得着的不等于会改的
第二个实验换了一个基底:一个 vendored 的 harness 优化器,候选是自由形式的 .mjs agent 模块,所有节点跑同一个模型,四个领域(数学 RealMath、检索 BCPlus、交互环境 AppWorld、代码修复 SWE-Pro)。我们关心的问题是:proposer 的编辑分布在多大的组织形态空间里?
基线答案:贴着种子 harness 的形态走。 低成本的 hook 在 8/8(RealMath)和 7/8(SWE-Pro)的领域-proposer 组合里被采纳;而结构性的改造几乎不发生。最刺眼的一个对照:Codex 在四十次尝试里明确读过节点调用 API,但没有产出过一个多节点候选;Kimi 读同一份源码,有时就会重构。信息不缺,缺的是别的东西。
是「不知道可以这么改」,还是「知道但第一次下手的成本太高」?我们用 frozen-state replay 来分离这两种解释:取 12 个冻结的运行状态(3 个轮次 × 2 个 proposer × 2 个领域),在三种 proposal contract 下各采样 4 个提案。机制本身在三种条件下完全可用,hook 照样能触发,多节点照样能跑——变的只有提示文本:
Bare : 删掉所有关于分解、hook、并行结构的提示
Name-Only : Bare + 只列出六个可优化维度的名字, 不给实现示意
Afford : Name-Only + 每个维度附上机制示意 (implementation scaffold)
三种 contract 的实际差异做过字节级比对:除了 scope 句、节点选项里的 ", hooks"、以及那份维度目录之外,逐字节相同。

图 3 · A:三种 contract 下的提案采纳率。只给名字(Name-only)几乎不动;给实现示意(Scaffold)把结构形态、hook、输出契约全部推高。B:12 个冻结状态的配对对比,空心是 Name-only,实心带脚手架。每一个有效状态都向脚手架移动。
结果干净利落:只给名字,结构性采纳和 Bare 分不开(4 正 1 负,p=.375);给了实现示意,全部八个有效状态都向结构性采纳移动(8 正 0 负,p=.008)。结合基线里低成本 hook 被 readily 采纳的事实,答案指向首改成本(first-edit cost),而不是认知缺口。
还有一个耐人寻味的角落:Ensemble 算子在 contract 里自始至终有定义、有示意,但在 80 个正式候选里只出现了一次,在全部 143 个 replay 提案里一次都没有。proposer 既不采纳也不拒绝它,它就待在实现分布之外。我们能确认的只是「没发生」,原因无从谈起。
三、结果通道:分数涨了不等于能力涨了
最后一个通道问的是:champion 比种子高出来的那些分,到底是什么?
我们把每一个「种子失败、champion 翻盘」的任务拿出来(四个领域共 48 个任务、78 条失败 rollout),逐条给种子的失败归类:
A · 机械性, 提交层面 : 正确答案已经在 transcript 里推出来了,
但根本没提交 / 超长在提交前被截断 /
装进了 grader 拒收的容器格式
B · 机械性, 资源层面 : 推导还在正常推进, 撞上 maxTurns / token
预算 / 工具调用死循环, 被掐断
C · 真错误 : 通过正常提交路径, 交出错误的值或计划

图 4 · A:失败归因,一个格子一条失败 rollout。深蓝 A、浅蓝 B、灰 C,百分比是机械性占比:RealMath 60%、BCPlus 84%,而 AppWorld 和 SWE-Pro 全是真错误。B:每个冠军的 held-out 增益。蓝色冠军带的是契约修复机制,迁移成立;橙色的任务绑定技巧与种子分不开。
在训练收益最大的两个领域里,机械性失败占绝对多数:BCPlus 翻盘任务的 16/19,RealMath 的 26/43。种子把答案推出来了,只是丢在了提交门口。多机械?看 grader 的 arity 闸门——提交列表和 gold 列表长度不一致就直接拒收,连等价判断都走不到。RealMath 有 12 条种子 rollout 死在这里。我们写了一个与模型无关的通用归一化:
# 把提交的分部答案折叠进 gold 的容器类型, 没有模型参与
gold 是 Tuple -> 提交的分部做 Tuple join
gold 是 FiniteSet -> 每个分部单独包一层 Tuple 再入集
其他情况 -> 原样不动
12 条里 8 条直接通过纯符号对象比较,11 条通过官方 grader;唯一被拒的那条值本来就是错的——归一化不放行坏答案。把这些修复折回去,种子自己在训练集上就从 .473 涨到 .547。
接着是 held-out 检验:冠军在新任务上还能不能赢种子?结果分成两类。评测契约的修复会迁移——强制提交、容器归一化、rescue 节点、预算护栏,在 held-out 的 RealMath(+.11,p=.0003)和 BCPlus(+.06,p=.0001)上依然显著,因为每个新任务都被同一份评测契约打分。任务绑定的技巧不迁移——AppWorld 的三个训练冠军(训练分 .933 / .844 / .844)到了 held-out 全部回落(.79 / .75 / .73),和种子分不开;45 个训练任务不够让选择保持诚实。没有任何一个冠军在契约修复之外拿出可迁移的收益。
SWE-Pro 还顺便暴露了噪声地板:结构迥异的冠军在两个方向上翻动同一小撮任务。在那里,小幅度的分数差异根本标识不出稳定的改进。
最后一个把三个通道串起来的观察:160 个候选里 151 个都动了 maxTurns——预算轴被彻底打满。编辑集中在最便宜的维度上,而这些编辑恰好修复契约层面的损失,贪心的 accept 步骤又恰好 reliably 保留这类收益。于是一条大幅上扬的训练曲线,可以和零可迁移能力增益共存。
四、一个形状,三个机制
三道窄门长得像一个形状——名义访问远大于实际兑现——但机制是三个不同的东西:
- 证据通道是检查预算的竞争:不给旧证据贴任何标签,只减少竞争,它就被读了;
- 动作通道是首改成本:给名字没用,给实现脚手架才有用;
- 结果通道是奖励的构成:契约修复大、稳、一致,贪心的 accept 天然偏爱它,而剩下的分数差异沉在噪声地板以下。
所以标准的 harness self-evolution 是一个局部优化器:它通过有界的检查预算从不断膨胀的档案里采样,通过 proposal contract 构造编辑,通过一个复合分数决定保留什么。在这些预算下,它围着种子 harness 做局部搜索,产出的是有用但有界的评测契约修复。分数更高,本身并不证明能力天花板更高。
对做这类系统的人,三条设计义务:
- 挂载一个工件不等于供给一份证据。 要报告的是 read set,不是 mounted set。
- 命名一个可编辑维度不等于脚手架一次编辑。 要报告的是实现分布,不是 contract 文本。
- 分数上涨不等于能力上涨。 要报告的是 held-out 迁移和失败归因,不是训练曲线。
一些诚实的限制:证据和动作的干预跑在不同基底上,综合靠的是共享的资源分配结构而非同一个操纵;失败归因是 transcript 逐条判的,有 LLM 辅助,只有容器子集是机器验证的;冠军是单种子,非配对的分数比较功效有限。我们把 audit ledger 和全部 instrument 代码放在仓库里,欢迎来挑错。