模型修 Bug 总爱"顺手重写"?这篇论文把过度编辑量化成了可学习的指标

上周碰到一个特别典型的场景:让模型修一个 range(len(x) - 1) 的差一错误,本来删掉 - 1 就完事了,结果模型给你交回来一份 60 行的"重写版"——加了输入校验、dtype 强制转换、NaN 掩码、曲线重采样,测试全过,代码评审的人直接血压拉满。

新加坡国立大学(NUS)这篇新论文《When Models Edit Too Much: On the Fidelity of Minimal Code Edits》(arXiv:2609.04061)研究的就是这个现象。说实话,我看到标题的第一反应是:终于有人把这件事正经量化了。

核心摘要:大模型修代码时普遍存在"过度编辑"——功能上修对了,diff 却远超最小必要改动。作者用 400 道 BigCodeBench 题目注入受控 AST 级 corruption,让每个修复任务都有已知的最小补丁,然后发现连 GPT-5.5 这样的强模型也重度过度编辑:Pass@1 0.823 很漂亮,但编辑量是 Claude Opus 4.7 的四倍多。好消息是,一句"尽量保留原始代码"的提示词就能把平均 excess 编辑距离从 0.195 压到 0.131,Pass@1 还反涨 2.3 个点;更进一步,用带编辑惩罚的 GRPO 风格 RL 做后训练,可以让模型学到"最小编辑"这个偏好而不损伤通用编码能力。我的判断:这不是一篇刷榜论文,而是一个被忽视的评测维度的正式立项——值这个阅读时间。

论文信息

  • 标题:When Models Edit Too Much: On the Fidelity of Minimal Code Edits
  • 作者:Tongyao Zhu、Wei Hern Lim、Min-Yen Kan
  • 机构:National University of Singapore
  • 日期:2026 年 9 月 3 日
  • 链接:https://arxiv.org/abs/2609.04061
  • 代码:https://github.com/nreHieW/over-editing

🎯 问题:修对了,但改得太多了

现在主流代码基准——HumanEval、MBPP、BigCodeBench、SWE-bench——清一色只看测试过不过。这个视角在 greenfield 场景(从零写代码)没问题,但在真实工程里,大量需求是 brownfield 维护:在一份有历史包袱的代码上做局部修复。

这时候"改多少"和"对不对"同样重要。一个本来一行的修复,被模型扩成 60 行,评审成本直接暴涨,隐含的设计决策被抹掉,还可能在测试覆盖不到的地方引入回归。但对只跑测试的评测来说,这两种修复完全不可区分。

作者把这个现象叫 over-editing(过度编辑),定义很干脆:功能正确、但改动超过最小修复所需代码量的修复。

Figure 1 就是个教科书级案例:

图1:单行差一 bug 的最小修复与六个前沿模型的修复对比

图1:BigCodeBench/663 的 range(len(x) - 1) 差一错误。最小补丁只动一行(+1/−1)且通过全部 5 个测试;GPT-5.4 却删 5 行加 60 行,塞进输入验证、dtype 强转、NaN 掩码和重采样——没有任何一个测试要求这些代码。左下"Other models"给出同输入跑 5 次全过的模型的中位改动量:claude-opus-4.7 只加 3 行,gpt-5.6-terra 加 46 行。全部补丁都通过测试,但编辑保真度天差地别。

这张图有个细节我很喜欢:作者对同一输入跑了 20 个前沿模型、各 5 次补全,五次全过的模型里,中位插入行数从 2 到 60 不等。而且——新发布的模型并不一致地比前任更保守。也就是说这不是"模型变强自然会改"的事,而是各家的默认编辑风格就不一样。


🏗️ 方法:怎么把"改多了"变成一个数

要量化过度编辑,得先知道"最小编辑"长什么样。真实 bug 的最小补丁是说不清的,作者的做法很讨巧:自己造 bug

从 BigCodeBench 抽 400 个带参考解和可执行测试的问题,往参考实现里注入 1 或 2 个局部化的 AST 级 corruption(比如把切片边界改错、比较运算符取反),只保留注入后测试会挂的样本。这样每个任务的最小目标补丁就是注入操作的逆操作,ground truth 干干净净。

为什么不用 LLM 生成 bug?因为要控制 bug 的局部性——修复量超过逆操作多少才有意义,前提是逆操作本身是已知的。这个设计把评测的可解释性保住了。

基准统计值得一看:黄金修复里 50.2% 只需单 token 编辑,91.8% 至多 2 个 token,没有一个超过 2 行。也就是说,这是个"本来就该小改"的分布,任何大改动都没有借口。Bug 语义分布上,predicate/control-flow 占 43.0%,computation/value 29.9%,boundary/iteration 18.7%,API/data semantics 8.5%。

三个评估维度

Pass@1 不用解释,功能正确性基线。

Excess Levenshtein distance 是核心指标。提取函数体、去注释、token 化后算归一化编辑距离:

\[E_{Lev}(M) = d(M, C) - d(G, C)\]

其中 \(C\) 是损坏解、\(G\) 是黄金修复、\(M\) 是模型输出,\(d(\cdot)\) 是按较大函数体 token 数归一化的 token 级 Levenshtein 距离。大于 0 就说明模型改得比黄金补丁多,数值越大过度编辑越严重。

为什么不用 CodeBLEU 这类 n-gram 指标?作者给了个挺有说服力的理由:corruption 是故意局部化的,n-gram 重叠会奖励表面风格相似——一个臃肿但风格像原文的补丁反而得高分。token 编辑距离追踪的是"你是否扰动了逆转 bug 之外的实现",这才是要问的问题。

Added cognitive complexity 补编辑量抓不到的部分:一个短补丁照样可能引入额外分支和嵌套,让代码更难审。用的是 SonarSource 那套 Cognitive Complexity 度量,用 Python AST visitor 计算模型输出减去黄金修复的复杂度差值。注意这不是 Halstead 复杂度,别搞混了。

指标靠不靠谱?作者请了 3 名有 5–10 年经验的开发者盲审 100 对通过测试的补丁,判断哪个"更好审、更忠实"。Excess Levenshtein 与人类多数意见的一致率是 94.8%(可审查性,Cohen's κ=0.897)和 96.9%(忠实性,κ=0.939)。认知复杂度的一致性就一般般(κ 只有 0.4 左右),作者也如实写了——这个指标是辅助位。另外对 100 个高 excess 的通过修复做盲审,82.3% 的确定案例里确实存在真正不必要的编辑。指标不全是噪声,这是可以放心的。


🧪 实验一:前沿模型全都在过度编辑

RQ1 的答案简单粗暴:是的,全都改多了,而且过度编辑不是能力弱的症状

50 个模型-prompt 设置的主评测里,正确性和编辑保真度明显分离。两个对照很能说明问题:Claude Opus 4.7 拿到最佳非推理 trade-off——高正确率配上小编辑;而 GPT-5.5 High 推理档 Pass@1 冲到 0.823,excess distance 却是 Opus 4.7 的四倍多。

我的第一反应是:这其实暴露了各家后训练数据的倾向。模型默认的修复策略更像"交一份健壮的解"——这是从"从零写代码"的评测文化里继承来的习惯——而不是"恢复一个最小局部补丁"。评测没问过这个问题,模型自然不会答。


🧪 实验二:一句话提示词就能治大半

这是全文性价比最高的发现。把 prompt 从"Fix and complete my function."换成追加一句"…but keep as much of the original code as possible",其他什么都不动:

指标 通用 prompt 加保留指令后 变化
Excess Levenshtein 0.195 0.131 显著下降(signed-rank p<10⁻⁴)
Added cognitive complexity 降 26.6%(p<10⁻⁴)
Pass@1 升 2.3 个点(95% CI [+1.49, +3.05])

50 个设置全部左移,其中 40 个 Pass@1 还提升了。逐模型看更有意思:GPT-5.5 High 的 excess 从 0.299 几乎腰斩到 0.159;而本来就很忠实的 Opus 4.7 几乎不动——提示词纠正的是那些"默认就爱大改"的模型。开源小模型消融里也一样:平均 Pass@1 从 0.788 升到 0.828,excess 从 0.176 降到 0.121。

作者的解读我很认同:这句提示词切换了模型的任务框架。通用 prompt 下,模型把现有实现当"待改进的参考";加上保留子句后,它把原始代码当"要保留的证据"。改的不是能力,是任务理解。

等等,那推理能力和模型规模呢?会不会模型想得更久、长得更大就自然改得少了?

RQ3 的答案:都不是。推理效应是模型特异性的——GPT-5.5 开了推理之后 added cognitive complexity 反而大幅上升,DeepSeek V3.2 在保留指令下推理后两个指标都变差。规模也不单调:Qwen2.5-Coder-Instruct 从 0.5B 到 32B,Pass@1 稳定涨,但 excess distance 从 14B 的 0.108 回升到 32B 的 0.127。想靠 scaling 等这个问题自己消失,不现实。


🧪 实验三:哪些 bug 最容易被"顺手重写"

RQ4 的分 bug 类型分析(Table 1)藏着全文我最喜欢的一个观察:

Corruption 类型 示例 Pass@1 Excess Lev. Added CC
Slice bounds x[a:b] 0.874 0.353 1.25
List indexing list[i] 0.780 0.244 1.14
Comparison ops. x < y 0.799 0.210 0.78
Sort order reverse=True 0.709 0.239 0.80
Conditional inv. if cond: 0.778 0.207 1.46
Edge-case guards if not list: 0.791 0.205 0.94

看第一行:切片边界错误的 Pass@1 最高(0.874,最容易修),excess 也最大(0.353,改得最多)。过度编辑不是难度效应。真正触发它的是歧义性——一个单 token 的边界错误,在模型眼里看起来像是"缺失前置条件、不安全索引、不稳定控制流"的证据,于是它开始加护栏。

对 530 个高 excess 通过修复做多标签分类后,这个画像更清楚了:

模式 占比 含义
Defensive generalization 64.2% 加宽泛的校验、检查、fallback
Data-flow rewrite 63.2% 整个任务重新求解而非局部打补丁
Contract drift 34.7% 改动输出、副作用或 mutation 行为
Feature accretion 23.6% 加绘图、报告类"润色"
Dependency fallback 3.2% 加备用资源或硬编码数据

64.2% 的过度编辑是"防御性泛化"。说到底,模型表现得像被要求交付生产级健壮代码,而评测衡量的是恢复原意。这是修复粒度的错配,也是任务框架的错配——那一句提示词之所以有效,正是因为它纠正的恰是这一点。逻辑闭环了。


🔬 实验四:最小编辑能不能直接学进模型里

提示词治标,能不能治本?作者用 Qwen3-4B-Instruct-2507 做后训练实验,训练数据来自 DeepCoder 的代码注入 1–10 个 corruption,4141 个训练样本、400 个测试实例,另备 20 种域外(OOD)corruption 测泛化。对比四种方法:SFT(在程序化生成的最小修复上训)、rSFT(8 个候选里留通过者中编辑最小的 3 个)、DPO(偏好最小编辑胜过最大编辑),以及 RL——每题采 K=16 个修复,用 GRPO 风格目标,奖励是:

\[r(M)=\lambda_{exec}-\lambda_{edit}\, e(M)\]

其中 \(\lambda_{exec}=0.1\)\(\lambda_{edit}=1.0\)\(e(M)\) 就是 excess 编辑距离;修挂了的给 −0.2。这个奖励设计有个狠的地方:如果编辑过大(excess > 0.3),奖励比一个错误修复还低。改太多比修错更亏。

主结果(Table 3,Qwen3-4B):

方法 In-domain Pass@1 ↑ In-domain Excess ↓ OOD Pass@1 ↑ OOD Excess ↓ LCB 变化
SFT 0.932 0.002 0.458 −0.008 −14.9
rSFT 0.782 0.100 0.780 0.107 −6.9
DPO 0.752 0.021 0.787 0.092 −4.6
RL 0.802 0.046 0.782 0.050 涨 0.6 分

这张表的信息量很大,拆开说。

SFT 在 in-domain 上几乎"解决"了问题(0.932),但 OOD Pass@1 直接崩到 0.458——它把见过的 corruption 模式背下来了。那漂亮的 in-domain 小编辑指标只描述还过得去的子集,不构成可用的修复策略。这个对比对工程实践是个警告:拿合成 bug 的最小修复做 SFT,很可能训出一个只会修自己见过的 bug 的模型

DPO 的 OOD Pass@1 略高(0.787),但 RL 用几乎相同的正确率给出明显更小的补丁(0.050 vs 0.092)和更低的复杂度,拿下最佳 trade-off。而且看 LiveCodeBench v6——检查最小编辑训练有没有把通用编码能力练废——SFT 掉了 14.9 个点,rSFT 和 DPO 也在回退,只有 RL 持平略涨(32.6% → 33.2%)。这跟最近"SFT 记忆、RL 泛化"的一系列发现完全对得上。

两个消融也值得记住。LoRA 消融(Table 4)显示 Pass@1 在 rank 16 就饱和了,但编辑保真度一路提升到 rank 64——rank 64 的 LoRA 几乎追平全参 RL 的 excess(0.051 vs 0.050),added CC 甚至更好。这说明最小编辑在很大程度上是个可学习的偏好,而不是新的编码技能——一个小适配器就能捕获,不用大动基座模型。奖励消融(Table 5)则显示只奖正确性会得到"能工作但臃肿"的编辑,加入 cognitive complexity 项反而变差,完整奖励最平衡。

还有两个泛化证据:RL 扩展到 Qwen3 4B–14B,excess 随训练稳定下降且 Pass@1 保持;迁移到 Defects4J 的真实 Java 单方法 bug(模型从没见过 Java 训练数据),14B RL 版保持通过率的同时把 excess 从 0.105 压到 0.074、原始 token 编辑量从 40.3 降到 34.5。学到的偏好是真偏好,不是对 Python 合成 bug 的过拟合。


💡 我的判断

这篇论文最值钱的地方不是某个具体数字,而是把编辑保真度立成了一个独立的评测轴。在它之前,"模型修 bug 时顺手重写了半个函数"这件事在所有榜单上都是隐形的。现在它可测量了——而且作者顺手证明了它可缓解、可学习。

对工程实践的直接启发有三条。第一,如果你的产品里有代码修复 agent,prompt 里加上"keep as much of the original code as possible"这类保留指令是零成本收益——降编辑量的同时 Pass@1 还涨。第二,做代码修复的评测时,别再只报 Pass@1 了,加一个编辑距离维度的成本很低。第三,想训自己的修复模型,SFT 在合成最小补丁上的路线基本是陷阱,RL 加编辑惩罚才是正解——而且 LoRA rank 64 就够用。

也要泼两盆冷水。主评测是函数级 Python 合成 bug,比真实仓库级 issue 简单得多,作者自己也承认;Defects4J 上 4B 模型 7% 的修复率说明小模型离实用还很远。另外认知复杂度这个指标和人类判断的一致性一般(κ≈0.4),当作辅助信号看就好。人类标注规模也偏小——3 人 100 对,结论方向可信,精度别太当真。

至于批判性视角:用注入 bug 的逆操作当"最小编辑"的 ground truth,这个设定干净但有点理想化——真实世界里"最小"经常是有争议的,多个合理修复路径并存时这套指标会偏严格。不过作为第一根标尺,够用了。

总的来说,这是那种"问题一直都在,只是没人正式命名"的论文。过度编辑是默认行为,不是能力上限——这句话我觉得会成为代码 agent 评测的一个新共识。


觉得有启发的话,欢迎点赞、在看、转发。跟进最新AI前沿,关注我