一次代码审查修订失败的上下文分析:Kimi K3 与 LLVM PR #220979
本页目录
摘要
Kimi K3 在一个长会话中为 LLVM 提交了 PR #220979。收到审稿意见后,K3 在原会话中给出了三个修订方案,均未采纳审稿意见;然而,GPT-5.6 Sol 在新会话中处理同一任务,一次即给出了与最终合并版本逻辑等价的修订。这一反差既可能源于模型能力的差距,也可能源于原会话的上下文。为区分二者,我在 K3 收到审稿意见的决策点截断原会话,对会话记录施加单变量编辑(删除自动压缩、补入函数代码、替换压缩摘要中的一句、更换任务说明、在续跑消息末尾追加审稿意图),并从同一决策点重复采样,K3 共 10 个条件、36 次有效运行;另以 GPT-5.6 Sol 补做两个条件,各 5 次;各条件的预测均在开跑前写定。不带历史时,K3 有 5/5 将检查并入已有循环,而原样续跑仅为 1/5;删除自动压缩后为 3/5;补入函数代码(0/3)、替换摘要中的问题框架句(1/3)和更换任务说明(0/3)均无改善;追加一句完整的审稿意图后为 3/3,其前半句或后半句单独给出时也均为 3/3;相比之下,只提醒重读审稿意见为 0/3。Sol 在 fresh 条件下为 5/5;接续同一份压缩后的 K3 历史(去掉 K3 的系统提示与思考)时同样为 5/5。结果表明,原会话中的失败不能简单归因于能力不足或信息缺失,一句无歧义的意图陈述即可改变 K3 的选择;推理记录提示,失败主要发生在带历史时对审稿意见的理解与取舍上。
1. 引言
LLVM 寄存器合并器(RegisterCoalescer)中的一个断言失败,由 Kimi K3 在一个长会话中完成复现、定位与修复,补丁提交为 PR #220979。审稿人要求不新增独立的扫描,而是扩展已有的检查(原文见 2.2 节)。然而,K3 在原会话中给出的三个修订方案均仍为独立扫描,未被采用;而当我在新会话中将同一任务交由 GPT-5.6 Sol 处理时,Sol 一次给出了与最终合并版本逻辑等价的修订。
对此最直接的解释是两个模型存在能力差距。本文检验另一种解释:K3 具备落实该意见的能力,但原会话的上下文阻碍了这一点;若该解释成立,则进一步确定上下文中起作用的部分。方法参照 Anthropic 对网络安全事件的对齐评估(An alignment assessment of recent cybersecurity incidents):在事件记录中定位关键决策点,将每个假设落实为一处具体的记录编辑,从决策点重新采样并比较行为比率,且不采信模型对自身行为的解释。
分工如下:补丁由 K3 编写,合并版本的修订来自 Sol。研究问题、实验条件与每组预测由我确定,实验中的三处转向也由我提出。其一,H-a 的前提原是“K3 决策时看不到预检循环”;我回查原会话后发现,K3 在压缩前不仅读过这段循环,还以它为先例另写了扫描,信息缺口假设因此不成立(4.3 节)。其二,弱线索无效后,我没有继续拆分上下文,而是直接改用明确的意图陈述,带历史的 K3 由此从 1/5 恢复至 3/3(4.5 节)。其三,我质疑分析所依据的关键词片段,发现其仅覆盖约 6% 的内容;补读全文后,“14 次失败中 10 次想到正确做法”的结论被更正为三类(5.1 节)。Claude 协助编写运行脚本、执行实验和整理分析,它在分析中的疏漏列在附录 B。
本文的主要贡献如下:
- 在一个真实的 coding agent 失败案例中分离了能力与上下文的影响。方法参照参考研究,在决策点截断原会话,做单变量的记录编辑并重采样(第 3 节):同一个 K3 不带历史时为 5/5,带历史原样续跑为 1/5;一句明确的审稿意图即可恢复并入(合计 9/9),只提醒重想则为 0/3(第 4 节)。
- 对失败运行推理记录的逐次通读:失败并非源于误读代码,而是分为决策失败、没找到可行写法与误读审稿意图三类;不带历史的运行遇到相同的障碍,但将审稿意见第一句视为硬约束(第 5 节)。
- 两条可检验的 harness 设计建议,以及由该决策点整理成的可复用评测用例(第 7 节)。
2. 背景
概括而言,第一版补丁为修复缺陷新增了一个循环,审稿人认为扩展函数中已有的循环即可覆盖同一情形。本节其余部分为面向编译器读者的技术细节。
2.1 缺陷
自 f44079d 起,x86 指令选择会生成 %27.sub_8bit = MOV8rm ... 形式的子寄存器写。该写操作不带 undef 标记,因此会隐式读取整个寄存器的旧值,以保留其余位。
RegisterCoalescer::removeCopyByCommutingDef 为删除一条 COPY,会将其前的 OR 换向(commute),改写显式操作数,再从活跃区间中删除旧定义。然而 partial def 仍在隐式读取该旧值,重试合并时 JoinVals::analyzeValue 随即断言失败:Instruction is reading nonexistent value。
该函数原有一段换向前的预检循环(约第 922 行):它遍历 use_nodbg_operands,遇到读取当前值号 AValNo 且绑定(tied)到 def 的 use 时放弃换向。partial def 属于 def 操作数,不在该循环的遍历范围内。
2.2 第一版补丁、审稿意见与合并版本
第一版补丁新增了一个专门扫描 def 的循环:
for (const MachineOperand &DefMO : MRI->def_operands(IntA.reg())) {
if (!DefMO.getSubReg() || DefMO.isUndef())
continue;
SlotIndex ReadIdx =
LIS->getInstructionIndex(*DefMO.getParent()).getRegSlot();
if (IntA.Query(ReadIdx).valueIn() == AValNo)
return {false, false};
}审稿人 arsenm 针对这段新增的循环留下了以下意见:
I don't think you should need a new scan over defs. Most code should be checking readsReg, not getSubReg / isUndef directly.
MachineOperand::readsReg() 的定义为 !isUndef() && !isInternalRead() && (isUse() || getSubReg()),即不带 undef 的子寄存器 def 本身即计为读取。因此,合并版本不再单独扫描 def,而是将已有的预检循环扩展至全部操作数:
for (MachineOperand &MO : MRI->use_nodbg_operands(IntA.reg())) {
// Make sure all reads of AValNo can be rewritten to the new register.
for (MachineOperand &MO : MRI->reg_nodbg_operands(IntA.reg())) {
if (!MO.readsReg())
continue;
...
if (UseMI->isRegTiedToDefOperand(OpNo))
// Partial defs and tied uses can't be rewritten independently.
if (MO.isDef() || UseMI->isRegTiedToDefOperand(OpNo))
return {false, false};审稿意见的两句分别对应这两处改动:前一句要求不新增扫描,后一句要求以 readsReg() 取代手写的 getSubReg() / isUndef() 判断。
2.3 原会话的配置
原会话运行在 Kimi Work 的 k3-agent 上,上下文窗口为 256k。会话进行至约 21 万 token 时触发自动压缩,数百条消息被压缩为一份交接摘要;第一版补丁写于压缩之后,审稿意见在其后到达。
3. 方法
图 1 概括了实验设计。
3.1 决策点与重采样
决策点设在原会话中 K3 取回审稿意见之后、开始思考之前,即 K3 首次接触审稿意见且尚未形成方案的位置。原会话在该点截断,按各条件编辑后以 kimi-code/k3 续跑,保留全部思考;续跑消息为一句统一的继续指令,各条件的补充句附在其后。
正式实验前,先验证了记录编辑的有效性:续跑时上下文依据会话记录重建,修改记录即可生效,按原有结构插入的工具调用与结果会被模型视为真实结果。
不带历史的对照条件(fresh)仅提供一份冻结的任务说明,内容包括 issue、根因、第一版补丁与审稿意见原文。两个 Sol 条件由 GPT-5.6 Sol 运行:Sol fresh 与 K3 的 fresh 使用同一份任务说明;Sol 接 K3 历史与原样续跑使用同一份历史记录与续跑消息,二者的差别见 3.2 节。
样本量方面,fresh、原样续跑与不压缩各 5 次,其中后两组先各运行 3 次,再依预先设定的补样规则增至 5 次;其余 K3 条件各 3 次,Sol 两组各 5 次。
3.2 条件与预注册预测
K3 的每个条件只改动一个变量(表 1)。各条件按顺序设计,后续条件在前序结果得出之后确定,而非在实验开始前统一设定。本文所称“预注册”,指每个条件的预测均在该条件开跑前写定,开跑后不再修改。
表 1 实验条件、编辑内容与预注册预测
| 条件 | 历史 | 编辑 | 预测 |
|---|---|---|---|
| fresh | 无 | 只给任务说明 | — |
| 原样续跑 | 压缩后 | 不做编辑 | — |
| 不压缩 | 未压缩 | 删除压缩事件,保留压缩前全文 | 多于 1/5 |
| 换任务说明 | 压缩后 | 续跑消息换成 fresh 的任务说明 | — |
| H-a | 压缩后 | 决策点插入一次读整个函数的工具调用与结果 | ≥2/3(放弃独立扫描) |
| 摘要框架句替换 | 压缩后 | 把摘要中描述 bug 框架的一句换成纯事实陈述 | ≥2/3 |
| 澄清审稿意图 | 压缩后 | 续跑消息末尾补一句完整的审稿意图 | 3/3 |
| 只给否定 | 压缩后 | 只补审稿意图的前半句 | 3/3 |
| 只提醒重想 | 压缩后 | 只提醒重读第一句,不给答案 | 3/3 |
| 只给方向 | 压缩后 | 只补审稿意图的后半句 | 3/3 |
| Sol fresh | 无 | 只给任务说明,由 Sol 运行 | 5/5 |
| Sol 接 K3 历史 | 压缩后,去掉 K3 的系统提示与思考 | 由 Sol 续跑,续跑消息与原样续跑相同 | 5/5 |
“—”表示对照条件,开跑前未设预测。“历史”一列指续跑时带入的原会话记录:“压缩后”为自动压缩产生的交接摘要及其后的消息,“未压缩”为删除压缩事件后保留的全部消息。文中“带历史”均指带入原会话记录,包括压缩后与未压缩两种,与 fresh 的“不带历史”相对,并非特指未压缩的全文。
四个续跑补充句的原文分别是:
- 澄清审稿意图:“补充:arsenm 的意思是不要新增独立的扫描,而是在函数里已有的检查上扩展。”
- 只给否定:“补充:arsenm 的意思是不要新增独立的扫描。”
- 只提醒重想:“补充:先想清楚 arsenm 第一句话到底想让我们怎么改。”
- 只给方向:“补充:arsenm 的意思是在函数里已有的检查上扩展。”
只给否定与只给方向两组分别检验澄清句的前后两半。
H-a 插入的是应用第一版补丁后的 removeCopyByCommutingDef 全文,不单独标注预检循环,以免构成方向提示。摘要框架句替换仅改动压缩摘要中的一句,详见 4.4 节。
Sol fresh 用于检验原始对话中 Sol 的成功是否稳定;Sol 接 K3 历史用于观察同一份压缩后的历史换由另一个模型续跑时的行为。后者与原样续跑之间不只相差一个变量:除模型外,harness、系统提示与工具集均不同,且不含 K3 的思考,因此不属于单变量编辑。
3.3 结果度量
文中“并入”指修订方案放弃独立扫描、改为扩展已有的预检循环,无论该方案已写入代码还是仅在回复中提出。各条件的计数均以此为准,逐次运行的结果见附录 A 的表 5。
代码质量以盲评衡量。评分器为 Claude Sonnet,仅读取匿名化的最终代码,按结构 3 分、落实审稿意见 3 分、正确性 3 分、注释 1 分评定十分制,每次运行评 3 次并取中位数。另以两个 MIR 回归测试作为硬锚点,测试失败时正确性记为 0;第一版补丁同样能通过这两个测试,因此它们只能排除崩溃,无法区分不同写法。
第一版补丁本身已修复该缺陷。本研究衡量的是对审稿意见的采纳,而非一般意义上的功能正确性。盲评 10 分中有 6 分来自结构与落实审稿意见,因此盲评分数与“并入”高度相关,不能作为独立的质量证据。盲评分数主要由写法决定:并入的代码为 9–10 分,保留或另起独立扫描为 7–8 分;仅提出方案而未修改代码的运行按第一版补丁评分,为 3–4 分。唯一的例外是一次在行为测试中崩溃的运行。分数分布见图 3。
为评估小样本下的差异,本文对部分比较报告双侧 Fisher 精确检验的 p 值。这些比较为事后选定,未做多重比较校正,仅作描述性参考。图 2 中的比例另附 Wilson 95% 置信区间。
3.4 阅读推理记录
推理记录由 Claude 抽取并通读,范围为各次运行自续跑开始至形成方案的设计阶段。未并入的 17 次运行中有 14 次、不带历史的 5 次中有 4 次经全文通读,其余仅做了关键词检索。思考记录未必反映行为的真实原因,因此本文未向模型追问其想法,结论仅依据行为与代码。Sol 的推理内容加密保存,无法阅读,因此对 Sol 只分析其最终方案。
3.5 对参考研究做法的借鉴
表 2 列出参考研究的做法及其在本文中的对应。
表 2 参考研究的做法与本文的对应
| 参考研究的做法 | 本文的对应 |
|---|---|
| 弱线索无效时,升级为无歧义的明确陈述 | 先做最强的意图陈述(澄清组),再沿同一方向逐档减弱 |
| 编辑插入的位置本身是一个变量,越近越有效 | 所有补充句都放在最近一轮,决策点固定不变 |
| 原轨迹中的一次好行为可能只是运气,需要重采样看比率 | 原始对话中 Sol 的那次成功只记为动机,不作为证据;Sol 在 fresh 条件下另行重采样 5 次,所有条件都重采样 |
| 单独看证据和在原上下文中看证据,判断可能截然不同 | 用 fresh 对照与带历史续跑做比较 |
| 不采信模型的自我解释 | 观测指标只用行为与代码,不追问模型 |
4. 结果
表 3 列出各条件的计数及预测的检验结果。
表 3 各条件的预测与结果
| 条件 | N | 预测 | 结果 |
|---|---|---|---|
| fresh | 5 | — | 5/5 |
| 原样续跑 | 5 | — | 1/5 |
| 不压缩 | 5 | 多于 1/5 | 3/5 |
| 换任务说明 | 3 | — | 0/3 |
| H-a | 3 | ≥2/3(放弃独立扫描) | 0/3 |
| 摘要框架句替换 | 3 | ≥2/3 | 1/3 |
| 澄清审稿意图 | 3 | 3/3 | 3/3 |
| 只给否定 | 3 | 3/3 | 3/3 |
| 只提醒重想 | 3 | 3/3 | 0/3 |
| 只给方向 | 3 | 3/3 | 3/3 |
| Sol fresh | 5 | 5/5 | 5/5 |
| Sol 接 K3 历史 | 5 | 5/5 | 5/5 |
两个 Sol 条件的结果见 4.6 节。
图 3 给出各次运行的盲评分数。
4.1 不带历史时,K3 能落实这条审稿意见
fresh 组 5 次均并入已有循环。写法上,一次与合并版本逻辑等价,三次仅对 def 使用了 readsReg()。与原样续跑相比,5/5 对 1/5,p ≈ 0.048;该对照还混有模型配置的差异(见第 8 节)。
4.2 删除自动压缩使并入次数增加,但没有达到不带历史的水平
主假设为:压缩摘要中“补丁已写好、已验证、已获认可”一类的表述,使 K3 过度相信第一版设计。
结果方向与预测一致:不压缩组为 3/5,原样续跑为 1/5,差异不显著(p ≈ 0.52)。我此前进一步预期不压缩组会接近 fresh,乃至写出合并版本,该预期未实现:三次并入均仅对 def 使用了 readsReg()。
换任务说明组为 0/3:两次保留扫描,一次换成另一个扫描。
不压缩组中有一次运行读到了我本地仓库中的最终修法,但其代码修改均发生在此之前;该结果予以保留,剔除后为 2/4。
4.3 决策时补入函数代码没有带来提升
H-a 检验信息缺口假设:预检循环仅在压缩前出现,K3 决策时无法看到。
H-a 组为 0/3。其中两次删除了第一版扫描,但均换成另一个独立扫描,一次遍历活跃区间的段,一次遍历值号;另一次保留扫描。预测是否成立取决于标准:按预测所用的“放弃独立扫描”为 2/3,按并入为 0/3。
然而,事后检索原会话发现,该假设的前提并不成立。压缩前,K3 已读过整个函数,并在思考中将这段循环视为与自身方案最相似的先例,有意使新的 def 扫描与之对称。它也考虑过 readsReg(),但得出了错误结论:
defs don't 'read' unless... a subreg def without undef IMPLIES reading the other lanes — there's no readsReg flag on it
续跑时同样如此:原样续跑组 5 次均在续跑开头自行读到了预检循环,并查阅了 readsReg() 的定义。因此在续跑条件下,H-a 注入的是 K3 本就会读到的信息。审稿意见的两句恰好分别对应 K3 当时的两个判断:一是必须另写扫描,二是 readsReg() 不覆盖 def。
4.4 替换摘要中的问题框架句没有带来提升
原样续跑组的 4 次失败运行均基于同一理由作出决定:use 列表无法反映这笔隐式读取,而这正是缺陷的根源,因此必须另行扫描 def。例如其中一次起草的回复:
The existing pre-check scans use operands only; the hazard comes from a def operand (subreg non-undef implicit read), which is invisible in use lists — that's the root cause of the bug. So the def scan is needed, but I switched it to readsReg().
压缩摘要描述该缺陷时使用的正是同一框架:
但它的 use 改写循环只遍历显式操作数(约 951-1000 行),看不见这笔隐式读;
相反,摘要中“已验证”“已获认可”一类的定稿表述未被任何一次运行引用。因此我假设,起作用的是摘要给出的问题框架,而非其传递的确定性。
编辑方法是将该句替换为“而那条 partial def 仍在隐式读取 %27 换向前的旧值(值 4);”。替换句保留缺陷机制,不提及预检循环,不提示方向,长度相近。
结果为 1/3,预测(≥2/3)不成立,与原样续跑的 1/5 相近。该组一次未并入的运行在思考中 47 次使用“use 列表看不见”的表述,该表述系其由“缺陷存在”自行推得。
4.5 明确陈述审稿意图足以恢复并入,只提醒重想则不能
澄清组仅在续跑消息末尾补充一句审稿意图,不提供实现细节,结果为 3/3,预测成立。其中一次与合并版本同型。
随后,沿同一方向逐档降低陈述强度:
- 只给否定组为 3/3,预测成立。
- 只提醒重想组为 0/3,预测(3/3)不成立。
- 只给方向组为 3/3,预测成立。三次均仅对 def 使用了
readsReg()。
澄清句的两半单独给出时,结果均为 3/3。这一结果仅表明任一半句单独即足以产生效果,不能说明其作用机制。将澄清、只给否定、只给方向三组合计,9/9 对原样续跑的 1/5,p ≈ 0.005;对只提醒重想的 0/3,p ≈ 0.005。
只提醒重想组三次均未并入:两次改用其他做法,一次遍历值号,一次检查区间段终点的指令;另一次保留扫描。三次均将审稿意见第一句重新解读为“信息已在区间或现有数据结构中,无需扫描”。
其中一次曾写出正确方向:
So "no new scan over defs" could mean: scan reg_operands (or extend the existing use-loops to reg loops) with
MO.readsReg() && MO.getParent() != ....
但随后的推理转向了其他方向:
Hmm wait — actually! Maybe his point is architectural in a different direction
此后它未再评估该方案,最终建议保留 def 扫描,仅在其前增加 hasSubRanges() 的提前返回判断。
4.6 Sol 在 fresh 与接续 K3 历史时均能并入
Sol fresh 组的预测为 5/5,结果为 5/5,五次的写法均与合并版本逻辑等价。K3 的 fresh 组同为 5/5,差别在写法:K3 多仅对 def 使用了 readsReg()。
Sol 接 K3 历史组的预测同为 5/5,结果为 5/5,预测成立。该组与原样续跑并非单变量对照:Sol 看到的是另一个模型的历史,不含 K3 的思考,系统提示与工具集也不同(3.2 节)。因此,它与原样续跑 1/5 之间的差别不能单独归因于模型(见 6.3 节)。
5. 推理记录分析
本节为对同一批数据的观察性分析,用于解释第 4 节的结果,不构成独立证据。
5.1 失败分为三类
判定标准为:是否想到将预检循环改为遍历全部操作数,并在循环内区分 def 与 use。据此,经通读的 14 次失败运行分为三类(表 4):
表 4 通读的 14 次失败运行的分类
| 类别 | 次数 | 典型表现 |
|---|---|---|
| 决策失败:想到正确结构却不用 | 5 | 以清晰优先、改动太大、“I don't think”只是保留语气、留给审稿人选、下一轮再说等理由放弃;其中 2 次写出了完整代码 |
| 没找到可行写法 | 6 | 以为并入就要对所有操作数用同一个条件,发现会拦下普通 use,于是退回只扫 def |
| 误读审稿意图 | 3 | 把第一句理解为“别扫 def,改用活跃区间里的信息” |
逐句核对表明,失败运行对预检循环与 readsReg() 本身的描述均正确,失败并非源于对代码的误读。三类失败的性质不同。第一类为决策失败:正确结构已写出,却因审稿策略或先前结论被放弃。第二类中多数运行已理解审稿意见要求并入已有循环,障碍在于实现方式,所缺的是在循环内按 def 与 use 分支这一步(见 5.2 节);其中仅个别运行误解了意图,例如一次 H-a 运行认为遍历全部操作数的集合更大,审稿人不可能是此意:“That's a superset — but my def_operands scan is smaller. So "no new scan" can't mean that.”第三类为误读审稿意图,只提醒重想组的 3 次亦属此类(见 4.5 节)。决策失败确实存在,但仅占通读样本的 5/14。
5.2 不带历史的运行遇到同样的障碍,但把第一句当作硬约束
fresh 组的 5 次运行都曾遇到“922 行的循环只遍历 use”这一障碍。其中一次也遇到了“对所有操作数使用 readsReg() 会拦下普通 use”的问题,另一次同样出现过审稿策略类的考虑:
The "I don't think you should need a new scan over defs" is a "I don't think" — a suggestion/question.
区别在于后续处理:5 次均将第一句视为必须满足的约束,明确否定了“保留扫描、仅改用 readsReg()”的方案。例如:
But that still has a "new scan over defs", which the reviewer questions.
它们的解法是重新界定已有检查所拦截的对象。同一次运行写道:
So the existing check is a specific case of "a read of AValNo that cannot be rewritten to NewReg". A partial def reading AValNo also cannot be rewritten.
未找到可行写法的 6 次所缺的,正是在循环内按 def 与 use 分支这一步。
5.3 带历史的失败运行沿用先前结论,或援引审稿策略
第一类理由是沿用先前结论。“看不见,所以必须扫描”是 K3 在压缩前自行得出的结论,经由摘要带入续跑;不压缩组的原始历史中同样包含这一推理。fresh 的输入仅有补丁与审稿意见,“看不见”只是阅读代码时注意到的一个事实。一次摘要框架句替换组的运行进一步推断并入与只扫 def 等价:
Scanning reg_operands with
(MO.isDef() && MO.readsReg())== scanning def_operands with readsReg (same set, since def_operands = reg_operands filtered by isDef). So either formulation is equivalent; def_operands is cleaner.
第二类理由是审稿策略。一次不压缩组的运行写出了完整的合并代码,随后写道:
This is arguably the "no new scan" ideal. But it rewrites arsenm's existing loop — bigger diff, higher review friction. My recommendation: minimal variant (readsReg filter, keep separate loop), mention in reply that the check complements the tied-use guard right below and can be merged into one reg_operands walk if he prefers. Give him the choice in the reply — reviewers love that.
另一次摘要框架句替换组的运行同样写出了正确代码,最终选择暂缓改动:
If he then says "fold it into the existing loop" or "use interval valnos", we do that next round. That's a normal review dialogue — you don't have to preempt everything.
不带历史的运行中均未出现此类理由。
6. 讨论
6.1 编辑实验支持什么
按证据强度,第 4 节的结果支持以下判断:
- 较强:失败不能简单归因于能力不足。不带历史的 K3 为 5/5,原样续跑为 1/5(p ≈ 0.048)。
- 较强:一句明确的意图陈述足以让带历史的 K3 并入。完整的意图句及其前后两半单独给出均为 3/3,合计 9/9;只提醒重想为 0/3。
- 较弱:自动压缩可能起了部分作用。删除压缩后为 3/5,与原样续跑的差异不显著(p ≈ 0.52)。
- 否定性结果:信息缺口与摘要框架句都不是主因。补入函数代码(0/3)和替换框架句(1/3)均无提升,且续跑中的 K3 本来就会自己读到那段代码。
第二点与参考研究的发现方向一致:弱线索无效时,须改用无歧义的陈述才能改变行为。
两项不成立的预测同样具有信息量。框架句替换未见效,说明该句并非框架的唯一来源,K3 会自行推出同一框架。只提醒重想未见效,说明问题不在于是否重新审视审稿意见,而在于重新审视后得出的解读。
6.2 一个尚未检验的解释
综合第 4 节与第 5 节:带历史的 K3 面对存在歧义的意见时,倾向于将歧义朝“保留现有设计、回复解释”的方向消解;不带历史的 K3 面对同一意见,则将其视为硬约束。
我最初认为这是能力问题,或 K3 不擅长处理不确定的任务。fresh 组面对同样存在歧义的意见却为 5/5,表明 K3 至少在部分条件下能给出正确修订,失败不能简单归因于能力不足。但 fresh 与带历史的续跑在上下文长度、信息组织与任务描述上均不同,这些差异尚未完全分离。结合 fresh 组的推理,我目前的解释是:这更可能源于训练中习得的偏好、模式匹配与策略,在长历史中表现为坚持自身先前的结论,或套用“最小改动”的审稿策略。
6.3 关于 Sol
我认同 Sol 的写法,源于一项当时未告知任何模型的先验。此前我在 #202599 里尝试修复一个相关的 RegisterCoalescer 问题,该尝试最终被 bababuck 的 #204039 取代,后者仅在已有逻辑上修改了数行;#202599 最终仅以回归测试的形式合并。我当时仔细研读过 #204039 的写法,由此形成了“保持现有结构、在已有检查上扩展”的先验。
原始对话中 Sol 的成功仅有一次。fresh 条件下的重采样表明,Sol 在该任务上能稳定给出正确修订:Sol 与 K3 在 fresh 下同为 5/5,差别只在写法(4.6 节)。接续 K3 历史时,Sol 仍为 5/5,说明去掉 K3 思考后的这份历史不足以使 Sol 放弃并入;但与原样续跑的 1/5 相比,差别可能来自模型,也可能来自被去掉的 K3 思考,本文无法区分。
7. 对 Coding Agent 设计的启示
以下是由本文结果引出的几点推测,均未经检验,能否起效仍需在同一决策点上重采样验证。
把审稿意见改写成约束。一句明确的意图陈述即可使带历史的 K3 由 1/5 恢复至 3/3,这提示它缺少的或许不是方案,而是对“哪些要求必须满足”的确认。让 agent 在设计前先列出意见中的硬约束,也许能起到类似的作用;但只提醒重想的 0/3 表明,agent 自己的复述未必正确。
用不带历史的子 agent 提出修订。不带历史时 K3 为 5/5,因此可以设想由一个不带会话历史的子 agent 提出修订、主 agent 负责整合。不过主 agent 仍可能以先前的结论否定子 agent 的方案(5.3 节),实际效果尚不确定。
把决策点作为评测用例。本文的决策点可以作为一个源自真实 trace 的评测用例:固定历史前缀与续跑消息,测量并入率;三类失败也可能对应若干可自动检查的信号。单个用例的代表性有限,能否推广到同类场景尚待验证。
8. 局限
每个条件仅运行 3–5 次,p 值仅作描述性参考,未做多重比较校正;各条件按顺序设计,后续条件参考了前序结果,但预测均在开跑前写定。这主要影响证据较弱的判断(6.1 节):不压缩组 3/5 与原样续跑 1/5 的差异无法与抽样波动区分;fresh 5/5 对原样续跑 1/5、意图陈述各组合计 9/9 对只提醒重想 0/3 的差距较大,受影响较小。
主指标与计分规则均在查看部分结果之后确定。盲评分数与“并入”高度相关,反映的主要是审稿意见是否被采纳,而非独立的代码质量;两个回归测试也无法区分不同写法。
实验以 1M 窗口的 kimi-code/k3 续跑,原会话则是 256k 窗口的 k3-agent,不压缩条件因此只在实验中成立;续跑均为一次性作答,不包含原会话之后的多轮反馈。这些差异在各条件间相同,限制的是结论向原会话推广的程度,而非条件之间的比较。隔离也不完全:联网仅由 prompt 约束,一次运行读到了最终修法(见 4.2 节),隔离克隆的日期、分支名与提交作者也留有人为痕迹。
Sol 接 K3 历史并非单变量编辑,其结果只能作为对照观察;Sol 的推理内容加密,无法像 K3 一样分析。原始对话中的 Sol 输入包含我口述的“K3 的修改还是不对”,与 fresh 条件并不完全对等;实验中的 Sol fresh 组则与 K3 的 fresh 输入完全相同。
9. 后续工作
后续工作首先针对上述局限中影响结论解释的部分。6.3 节无法区分 Sol 接 K3 历史时的差别来自模型还是来自被去掉的 K3 思考;让 K3 以同样去掉思考的历史续跑,可以分离这两个因素。隔离方面,带历史的 K3 能推断出我本地真实仓库的位置,后续实验需使用独立的系统账户或虚拟机。度量方面,现有测试无法区分 fresh 中出现的“readsReg() 仅用于 def”这类不完全落实,需要增加覆盖“undef 且 tied 的 use”的测试。
其次是检验第 7 节的设计建议,优先检验意见复述:该方法无需人工补充说明,若验证有效即可直接纳入 harness。为此,我计划整理并公开续跑、记录编辑与评分工具,以便在更多会话中识别外部反馈与 agent 先前结论发生冲突的决策点,将单个用例扩展为评测集。
附录 A:各次运行结果
表 5 各次运行的结果与盲评分数
| 运行 | 条件 | 方案 | 盲评 |
|---|---|---|---|
| r005 | fresh | 并入 | 9 |
| r008 | fresh | 并入 | 10 |
| r009 | fresh | 并入 | 9 |
| r010 | fresh | 并入 | 9 |
| r011 | fresh | 并入 | 4 |
| r012 | 原样续跑 | 保留扫描 | 4 |
| r015 | 原样续跑 | 保留扫描 | 4 |
| r018 | 原样续跑 | 保留扫描 | 7 |
| r021 | 原样续跑 | 保留扫描 | 4 |
| r022 | 原样续跑 | 并入 | 4 |
| r013 | 不压缩 | 并入 | 9 |
| r019 | 不压缩 | 并入 | 9 |
| r023 | 不压缩 | 并入 | 9 |
| r016 | 不压缩 | 保留扫描 | 4 |
| r024 | 不压缩 | 保留扫描 | 3 |
| r014 | 换任务说明 | 保留扫描 | 7 |
| r017 | 换任务说明 | 换成另一个扫描 | 7 |
| r020 | 换任务说明 | 保留扫描 | 7 |
| r025 | H-a | 保留扫描 | 4 |
| r026 | H-a | 换成另一个扫描 | 8 |
| r027 | H-a | 换成另一个扫描(测试崩溃) | 3 |
| r034 | 摘要框架句替换 | 保留扫描 | 7 |
| r035 | 摘要框架句替换 | 并入 | 9 |
| r036 | 摘要框架句替换 | 保留扫描 | 3 |
| r037 | 澄清审稿意图 | 并入 | 9 |
| r038 | 澄清审稿意图 | 并入 | 10 |
| r039 | 澄清审稿意图 | 并入 | 4 |
| r044 | 只给否定 | 并入 | 4 |
| r045 | 只给否定 | 并入 | 9 |
| r046 | 只给否定 | 并入 | 9 |
| r047 | 只提醒重想 | 其他做法 | 4 |
| r048 | 只提醒重想 | 保留扫描 | 4 |
| r049 | 只提醒重想 | 其他做法 | 3 |
| r050 | 只给方向 | 并入 | 9 |
| r051 | 只给方向 | 并入 | 9 |
| r052 | 只给方向 | 并入 | 9 |
| r007 | Sol(fresh) | 并入 | 10 |
| r053 | Sol(fresh) | 并入 | 10 |
| r054 | Sol(fresh) | 并入 | 10 |
| r055 | Sol(fresh) | 并入 | 10 |
| r056 | Sol(fresh) | 并入 | 10 |
| r057 | Sol 接 K3 历史 | 并入 | 4 |
| r058 | Sol 接 K3 历史 | 并入 | 10 |
| r059 | Sol 接 K3 历史 | 并入 | 4 |
| r060 | Sol 接 K3 历史 | 并入 | 10 |
| r061 | Sol 接 K3 历史 | 并入 | 3 |
盲评按最终代码评分;仅提出方案而未修改代码的运行按第一版补丁评分,故部分并入的运行为 3–4 分。
附录 B:分析过程中的更正
以下三处疏漏曾改变对失败原因的解释。
- 设计 H-a 前没有检索原会话。Claude 的分析仅确认预检循环的代码在压缩前出现过,未发现 K3 当时将其视为先例并仿照另写扫描。我在不知情的情况下批准了设计并定下预测。此后流程改为:设计任何插入或删除类编辑之前,先检索原会话中的全部相关内容。
- 计数口径。Claude 曾报告原样续跑组“并入 0/5”,漏计了一次仅在回复中提出并入的运行,经我追问后更正为 1/5。早期“放弃独立扫描”的标签还将“换成另一个扫描”计入其中。此后以逐运行结果表作为唯一来源。
- 失败分类。Claude 最初仅依据约 6% 的关键词片段得出“14 次失败中 10 次明确想到正确做法”,补读全文后更正为 5 / 6 / 3。
附录 C:实验基础设施
续跑。命令为 kimi -S <session_id> -p ... --output-format stream-json,续跑消息原文为“继续。(注意:只使用本地仓库,不要联网。)”。可在某次工具结果之后截断续跑,但续跑会追加一条新的用户消息并重新注入权限提示,并非无缝接续同一轮。原会话由 Kimi Work 创建,其模型别名 k3-agent 在 CLI 中不可用,改为 kimi-code/k3 后方可续跑。续跑时沿用记录中的系统提示,哈希与原会话一致;13 个核心工具相同,另多出 Kimi Work 的 7 个工具,缺少 2 个。
运行。每次运行先建立独立克隆,应用并提交第一版补丁,校验文件哈希,再作为独立的后台进程启动。启动器自行计时,超时后终止整棵进程树,随后收集 diff、会话记录、token 用量与网络审计。时间上限为 30 分钟,达到上限或中途中断的运行以终止时的状态计;正常结束与超时均计入样本,CLI 报错(如额度用尽)不计入,并补跑同一条件。部分条件为节省额度共用同一克隆路径运行,使历史前缀命中缓存;为防止样本彼此重复,预先规定若后续运行的第一段思考与首次运行逐字相同即终止该组,该规则未被触发。
隔离。Codex 在 Windows 上的沙箱并不禁止联网,K3 一侧也无网络限制,因此联网仅由 prompt 约束,并依赖事后审计;审计显示所有运行均未联网。部分运行在形成方案之后访问过运行目录之外的文件,除一次读到最终修法外(见 4.2 节),均未接触答案。实验中的数次越界访问表明,对带历史的 agent 而言,路径与用户名本身即构成线索,按目录隔离并不充分;已完成的运行目录移出运行区后,仍可被全盘搜索找到。后续实验应将完成的运行移至另一磁盘或打包存档,更稳妥的做法是使用独立的系统账户或虚拟机。
评分。盲评时,同一批运行混合后随机编号;评分器为 Claude Sonnet 子 agent,无法看到条件,且非被测模型。另设复核规则:总分 ≥ 8 但测试失败,或总分 ≤ 4 但测试通过且已放弃独立扫描时,转交人工复核。
Sol 续跑。Sol 接 K3 历史组取原样续跑组 K3 在决策点实际收到的请求,去掉 K3 的系统提示与思考后转为 Codex 会话,再发送与原样续跑组相同的续跑消息。转换后的历史保留了 K3 的工具名。
实验数据截至 2026 年 10 月 13 日。