编码模型做得太多了
摘要
一篇博客文章探讨“过度编辑”问题:编码大语言模型在修复简单错误时改写了过多代码,提出衡量指标与训练方法以鼓励最小化、忠实于原意的编辑。
暂无内容
查看缓存全文
缓存时间: 2026/04/22 18:03
# 编程模型做得太多了
来源:https://nrehiew.github.io/blog/minimal_editing/
本文代码见此处(https://github.com/nreHieW/fyp)。
AI 辅助编程已成常态,借助 Cursor、GitHub Copilot、Claude Code、Codex 等工具,我们越来越让模型直接改动代码。过去一年如果你用过其中任何一款,大概率经历过类似场景:让模型修一个简单的 bug(可能是差一错误,或某个运算符写错)。结果 bug 是修了,可半个函数被重写,多出一个 helper 函数,原本好好的变量名被换掉,还加了新的输入校验,diff 大得吓人。
我把这种现象称为**过度编辑(Over-Editing)**:模型倾向于把无需改动的代码也一并重写。问题远比看上去严重——代码审查已是瓶颈,审查者需要知道改了什么、为何改、是否安全。模型哪怕改得正确,只要把整个函数推翻,审查难度就会陡增,因为代码已面目全非。
本文将探究:现有大模型是否普遍过度编辑?能否训练出更“忠实”的编辑器?
## 过度编辑
过度编辑问题(https://nrehiew.github.io/blog/minimal_editing/images/overediting.png)
图 1:典型过度编辑案例。GPT-5.4(高推理强度)为修复差一错误,把整段函数重写,而正确做法只是把 `range(len(x) - 1)` 改成 `range(len(x))`。
过度编辑指模型为解决当前问题,改动了超出最小必要范围的代码。严格定义:若模型输出功能正确,但结构与最小修复所需相比偏离过多,即属过度编辑。
图 1 的例子很直观:bug 是 `range()` 里的差一错误,只需改一行。GPT-5.4(高推理强度)却重写整个函数:显式 `None` 检查、`np.asarray` 转 `dtype=float`、有限值掩码、数组大小校验、改 `curve_fit` 签名、替换绘图逻辑……虽然测试通过,diff 巨大,且无一处是用户要的。
可以把工作模式分为两类:绿地(green-field,从 0 新建)与棕地(brown-field,在既有代码库上迭代)。棕地场景下,现有代码已被团队理解并有意写成当前模样,模型的任务仅是修问题,别碰无关部分。
常见建议是“多写测试,测试过了就行”。然而过度编辑是棕地失败:与正确性失败不同,它完全逃过测试。模型生成更多代码,工程师就要审更多;过度编辑让审查更难,逻辑更复杂,行数更多,代码质量悄然下滑。
## 量化过度编辑
研究过度编辑,首先需要一份“最小改动”明确的代码编辑数据集。与大多数基准用另一 LLM 挖坑不同,我们**程序性地**在 BigCodeBench(https://arxiv.org/abs/2406.15877)的 400 道题上制造 bug:翻转比较符(`<`→`<=`)、`+` 变 `-`、布尔值取反(`True`→`False`)等。¹ 每道被污染的题仍语法正确,且能触发对应测试失败,因此“真相”修复就是腐蚀的逆操作,天然最小。我们不仅看模型是否修对,还看**额外改了多少**。
### 指标
多数编程基准用 Pass@1 衡量正确性。但 Pass@1 只必要不充分:模型可以全对 yet 把每个函数重写。我们需要能捕捉“多余改动”的指标。
**Token 级 Levenshtein 距离**
与字符级不同,先用 Python tokenizer 把代码拆成原子语法单元(`def`、`add`、`(`、`a`、`,`、`b`、`)`、`:`、`return`、`a`、`+`、`b`),再算 Levenshtein。
示例:
```
def add(a, b): def someotherfunctionname(a, b):
return a + b return a + b
```
字符级距离 19,token 级距离仅 1(`someotherfunctionname` 算一个 token)。再按总 token 数归一化,使不同长度函数可比。
我们不直接比较模型输出与真相,而是都把**被污染的输入**当基准:设 C 为污染解,G 为真相,M 为模型输出。最小真实编辑距离 D_true = d(G, C),模型编辑距离 D_model = d(M, C),定义相对补丁分:
S(M) = D_model − D_true
越接近 0 表示模型补丁越接近最小修复。
**新增认知复杂度**
认知复杂度(Cognitive Complexity,改良自圈复杂度)衡量代码理解难度,惩罚嵌套、递归、混合逻辑运算符、不直观控制流。例如直线代码无分支最易读。
示例:
```
def process(items):
result = []
for item in items: # +1
if item > 0: # +2(循环内嵌套)
if item % 2 == 0: # +3(两层嵌套)
result.append(item)
return result
# 认知复杂度:6
```
我们的腐蚀只改值不改结构,因此正确修复应使认知复杂度增加 0。模型输出若增加,即属多余。我们也禁止负值(无谓简化同样不受欢迎)。
## 模型会过度编辑吗?
会,哪怕最前沿的也会。
| 模型 | Pass@1 ↑ | 归一化 Levenshtein ↓ | 新增认知复杂度 ↓ |
|----|----|----|----|
| **推理模型** |
| GPT-5.4 | 0.723 | 0.395 | 2.313 |
| Claude Opus 4.6 | **0.912** | **0.060** | 0.200 |
| Gemini 3.1 Pro Preview | 0.858 | 0.145 | 0.501 |
| GLM 5 High | 0.859 | 0.099 | 0.320 |
| Qwen 3.6 Plus | 0.858 | 0.145 | **0.048** |
| Kimi 2.5 | 0.835 | 0.151 | 0.770 |
| DeepSeek R1 | 0.820 | 0.232 | 0.673 |
| DeepSeek Chat V3.1 | 0.795 | 0.232 | 0.694 |
| GPT-5 High | 0.713 | 0.438 | 3.832 |
| **非推理模型** |
| GPT-5.4 | 0.770 | 0.327 | 1.563 |
| Claude Opus 4.6 | 0.900 | **0.079** | 0.313 |
| Gemini 3.1 Pro Preview | 0.860 | 0.129 | 0.358 |
| GLM 5 | 0.840 | 0.097 | **0.235** |
| Qwen 3.6 Plus | **0.870** | 0.106 | 0.605 |
| Kimi 2.5 | 0.770 | 0.140 | 0.687 |
| DeepSeek V3 | 0.800 | 0.201 | 0.803 |
| DeepSeek Chat V3.1 | 0.802 | 0.235 | 1.223 |
| GPT-5 Minimal | 0.738 | 0.397 | 2.877 |
表 1:推理/非推理模型对比。同模型出现两次表示可切换模式。各指标最优加粗。
最新前沿模型中,GPT-5.4 过度编辑最严重:推理模式 Levenshtein 0.39,非推理 0.33,新增认知复杂度 2.31/1.56;然而 Pass@1 仅 0.723/0.770,正确性也垫底。Claude Opus 4.6 取得最高 Pass@1(推理 0.912,非推理 0.900),同时 diff 最小:Levenshtein 0.06/0.08,新增认知复杂度 0.20/0.31。Gemini 3.1 Pro Preview 紧随其后,开源权重里 GLM 5 最保守。
## 加提示有用吗?
不少论文发现 LLM 失败模式,却未先问“直接告诉它行不行”。我把提示加上一句“**重要:请尽可能保留原代码及其逻辑**”,再看效果。
推理 vs 非推理(https://nrehiew.github.io/blog/minimal_editing/images/reasoning_vs_non.png)
图 2:显式要求最小编辑后,Pass@1 与 Levenshtein 距离变化。颜色区分是否开启推理。
显式提示后,所有模型 Levenshtein 距离下降,除 DeepSeek R1/V3 外 Pass@1 也提升。一种解释是:最小编辑约束缩小了修复搜索空间,反而更容易命中正确补丁。推理模型下降更显著,说明其指令遵循能力更强。
## 推理=想太多=改太多?
推理模型通常更擅长编程,Pass@1 也更高。但看“编辑风格”,结论不同。
推理 vs 非推理(https://nrehiew.github.io/blog/minimal_editing/images/comparison_plots.png)
图 3:推理/非推理模型 Levenshtein 距离对比。仅统计两者都做对的样本,以排除正确性偏差。
图 3 把同一家族的推理/非推理配对,仅看两者都做对的样本。默认提示(上)下,多数推理模型比非推理更爱大改:DeepSeek V3、GPT-5、GPT-5.4、Gemini 3.1 Pro Preview、Qwen 3.6 Plus、Kimi 2.5 的推理柱均更高。推理模型常把“修 bug”想成“顺手重构”,结果更臃肿。例外是 Claude Opus 4.6,推理版反而更克制。
一旦显式要求保留原代码(下),推理模型全面反超:Levenshtein 距离普遍低于非推理,几乎全线领先。Claude Opus 4.6(推理)降至最低。GPT-5 与 GPT-5.4 的推理版也大幅下降,不过 GPT-5.4 非推理仍略胜一筹。
结论:推理模型**默认**爱过度编辑,因其“多想”常把代码“优化”一番;但同样因推理强,一旦给出约束,它们最能照办。通用→显式提示的落差,推理模型普遍更大,说明过度编辑并非能力硬伤,而是默认行为可被覆盖。
## 训练
自然要问:能否**训练**出更忠实的编辑器?实验以 Qwen3 4B 2507 Instruct 为基座,先用 0-shot 与 8-shot 加显式提示当基线,其余方法在**无提示**的通用场景评测。
### 设置
先用同样腐蚀策略在 DeepCoder(https://www.together.ai/blog/deepcoder)上造合成训练集;再用基座模型自蒸馏:每题生成 8 条解,留正确者并按 Levenshtein 距离排序,取最小编辑样本,类似 Context Distillation(https://arxiv.org/abs/2209.15189)。
评估四种方法:
- **SFT**:直接在程序腐蚀数据集上做监督微调。
- **rSFT**:自蒸馏数据里每题取 Levenshtein 最小的 3 条做拒绝采样 SFT。
- **DPO**:用同题最大/最小 Levenshtein 样本做偏好优化。
- **RL**:强化学习,奖励 = 归一化 Levenshtein 奖励 + 正确性惩罚:
```
r = r_edit + 0.1 # 通过测试
r = -0.2 # 未通过
```
### 有效吗?
| 模型 | Pass@1 ↑ | 归一化 Levenshtein ↓ | 新增 CC ↓ |
|----|----|----|----|
| 基线(0-shot) | 0.735 | 0.169 | 0.731 |
| 基线(8-shot) | 0.775 | 0.115 | 0.479 |
| SFT | **0.932** | **0.002** | **0.000** |
| rSFT | 0.782 | 0.100 | 0.435 |
| DPO | 0.752 | 0.021 | 0.113 |
| RL | 0.802 | 0.046 | 0.112 |
表 2:各微调方法在**同类型腐蚀**的域内测试集结果。
第一次跑,SFT 好得离谱:模型似乎完美学会任务。我怀疑它只是记住腐蚀逆操作,而非学到通用“最小编辑”。于是用**完全不同腐蚀方式**重新造训练集与测试集,验证其泛化能力。
相似文章
本地编码智能体现在不错,但得盯着用
作者认为本地编码智能体对小型任务很有用,但需要持续监督以防止错误和范围蔓延,描述了小修复、测试和手动差异检查的迭代工作流程。
- -危险地跳过阅读代码 – olano.dev
文章认为,随着组织采用大语言模型进行代码生成,工程实践必须从审查生成的代码转向关注规格说明和测试,同时需要组织层面支持新流程。
代码审查变得昂贵,重写变得廉价
LLMs 通过生成过度设计的代码,使代码审查变得更加昂贵,但重写现在变得廉价,从而将开发者的工作转向更多的前期规划和迭代简化。
提示优化为何有效,为何有时无效:基于因果启发的编辑级分析
本文对自动化提示优化进行了基于因果启发的分析,涵盖多种框架、大语言模型和任务,识别出特定编辑类型(如复杂度增加型、元指令型)根据任务特征具有系统的负面或正面效应,从而解释了泛化失败的原因。
编码代理是否带来了新的审查问题?
本文讨论了虽然编码代理能够有效生成代码,但它们却在审查和信任变更方面引入了新的瓶颈,质疑代理是减少了审查工作量还是转移了审查工作量。