AI 改文件的三种流派:为什么换个编辑工具,同一个模型就变笨了

📄 本文综合改写自 Paul Pham 发布在 DEV Community 的《Deep Dive into Editing Tools in AI Coding Harnesses》,并补充了 Codex V4A 补丁格式的技术分析资料,原文链接见文末。
同一个模型,在 Claude Code 里改代码顺顺当当,换到另一个 harness 上就开始频繁报错,错误信息还都长一个样:「String to replace was not found」。
把模型换掉再试,换两三个,还是一样。
但这不是模型变笨了。问题出在更下面一层:Agent 究竟是怎么把「我要改这里」这个意图,变成磁盘上真实发生的变动。
这一层有个不太出名的名字,叫编辑原语(edit primitive)。它平时完全不露面,可一旦它不合格,再强的模型也会在「把 print("hello") 改成 print("hello world")」这种级别的活上翻车。
一、模型其实碰不到你的文件#
先说一个容易被忘记的事实:大语言模型不能直接操作文件系统。
它唯一的输出通道是文本。所以它必须先把改动「描述」出来,再由外面的 harness 解析这段描述,落到真实文件上。描述格式设计得好不好,直接决定了落地的成功率。
这件事有两个无法回避的设计张力。
第一,行号不可信。模型会幻觉行号,尤其是在前面几轮编辑已经让内容整体位移之后。任何依赖绝对行号的格式,早晚都会崩。
第二,整文件重写太贵。为了改一行代码而吐出整个文件,token 成本高,出错概率也不低,文件越大越明显。
所以主流 harness 最后都收敛到了同一条路:用周围代码作为锚点,而不是用行号定位。方向一致,但格式细节上分岔得很厉害。而那些分岔的地方,恰好就是失败的来源。
二、三条路线,三种代价#
路线一:精确字符串替换#
Claude Code 走的是最直白的一条。它的结构化 Edit 工具接受一对参数:old_string 和 new_string。harness 拿着 old_string 在文件里找,找到唯一匹配就替换。
好处是心智模型极简。没有格式要学,模型不需要背任何语法,读起来也一目了然。
代价藏在「唯一」和「逐字符」这两个词里。锚点必须在文件中唯一出现,而且必须完全一致。多一个空格、缩进从四个空格变成 tab、行尾多一个尾随空格、文件被 formatter 重排过一次,匹配立刻失败。
这类失败在 Claude 自家模型上相对少见,因为这套 harness 是围着它们调优的,模型很熟悉这个工作流。可一旦换成通过 OpenRouter 接进来的其他模型,尤其是参数量较小的开源模型,失败率就明显上来了。
路线二:上下文补丁#
Codex 走了另一条路。它既不用行号,也不用一整段精确字符串,而是描述一个补丁:
*** Begin Patch
*** Update File: app.py
@@ def greet():
print("hello")
- print("old line")
+ print("new line")
*** End Patch
三行前缀承载了全部语义:空格开头表示上下文行,必须和现有代码匹配;减号表示要删除的行;加号表示要新增的行。@@ 后面可以跟一个锚点字符串,比如类名或者函数签名,用来在文件里定位位置。
真正的差别在匹配算法。Codex 的渐进模糊匹配会按顺序尝试三种策略:先精确匹配,再忽略行尾差异匹配,最后忽略所有空白差异匹配。这三层基本覆盖了「模型记忆里的代码和你硬盘上的文件在尾随空白上不一致」这类日常摩擦。
还有一个容易被忽略的设计:一个补丁信封里可以同时做新增文件、更新文件、删除文件、重命名文件。多文件重构不需要拆成很多次往返,模型一次就能把整个改动意图表达完整。
路线三:把「想改什么」和「怎么改」拆开#
Cursor 的做法是把编辑拆成两个阶段:先用 LLM 画一个 sketch,负责表达意图;再交给一个专门的 Apply 模型负责集成到真实文件里。意图生成和机械落地各归各管,各自做自己擅长的事。
还有不少有意思的变体。Aider 用 search/replace 块配合 difflib 的分层模糊匹配,匹配不上时还会给出「你是不是想找这一段」的提示。RooCode 用的是从预估区域出发、由中间向外扩散的模糊匹配,并且会保留相对缩进结构。
一张对比表#
| Agent | 格式 | 锚定策略 | 特点 |
|---|---|---|---|
| Codex | *** Begin Patch 信封加 @@ 上下文头 | 上下文行匹配,逐级模糊 | 一次补丁可跨文件、可重命名;模型原生训练过这套语法 |
| Claude Code | 结构化 Edit 工具,old_string 与 new_string | 精确字符串匹配,且必须唯一 | 心智模型简单,没有格式学习成本 |
| Aider | Search/Replace 块 | 基于 difflib 的分层模糊匹配 | 匹配失败时给「你是不是想找」提示 |
| RooCode | Search/Replace 块 | 从预估区域由中间向外模糊匹配 | 保留相对缩进 |
| Cursor | LLM sketch 转专用 Apply 模型 | 两段式,先生成后集成 | 意图与机械操作分离 |
三、真正的敌人是陈旧的文件状态#
如果把上面这些失败统统归因为「模型不听话」,那是诊断错了方向。
绝大多数编辑失败有一个共同根因:模型脑子里的文件,和磁盘上的文件,不是同一个版本。
模型记得的是版本 A。而在它组织补丁的这段时间里,文件可能已经变成了版本 B:
- 保存时触发的 formatter 重排了缩进
- linter 的 autofix 改掉了引号风格
- pre-commit hook 跑了一轮自己的改动
- 编辑器自动保存把你手动的微调写了回去
- 另一个 agent,或者同一个 agent 的另一个会话,动了同一个文件
- 上一轮编辑只应用了一半就中断了
这时候锚点写得再准也没用,因为那是拿版本 A 的坐标去版本 B 的地图上找路。找不到是必然的,不是意外。
这也顺手解释了一个常见现象:为什么「重试一次」经常就好了。因为重试之前 agent 通常会重新读一遍文件,坐标被刷新了。
四、七个真实的失败模式#
| 现象 | 底层原因 | 缓解方向 |
|---|---|---|
| 报「找不到要替换的字符串」 | 锚点不唯一,或内容已被格式化改动 | 锚点带上唯一的邻近上下文;改之前重读文件 |
| 匹配成功率随缩进风格波动 | 前导缩进不在模糊容忍范围内,tab 与空格不互通 | 统一 .editorconfig 和 formatter 设置 |
| 同一段代码出现多次,改错了地方 | 锚点缺少区分度 | 用 @@ 链式上下文锚定到类名或函数签名 |
| 错误信息模糊,模型越改越乱 | 失败原因没有回传给模型 | 错误信息里带上失败锚点的原样文本 |
| 文件处于半成品状态 | 补丁部分应用,缺少原子性 | 明确全有或全无,或按文件粒度报告成功与失败 |
| 补丁被直接拒绝 | 用了绝对路径 | 路径只能是相对路径,这本身也是防目录穿越的安全约束 |
| 一次改一大片,全部失败 | 补丁粒度太粗 | 拆成多个小 hunk,逐个验证 |
Codex 这里有一个具体的坑值得单独记一下:它的模糊匹配能容忍行尾空白差异,但前导缩进不在容忍范围之内。一个模型以为文件用四空格缩进、实际却用 tab 的 Python 文件,上下文匹配会直接失败,而且失败信息往往看不出缩进才是问题。
还有一个实现边界。Warp 团队发现 V4A 解析器在单个文件段内部处理多个 @@ 上下文操作时行为不正确。如果你打算自己写 harness,这个用例必须单独测一遍。
五、格式不只是格式,它必须被模型内化#
V4A 最值得琢磨的地方,是 OpenAI 在这个语法上投入了大量训练算力。
2025 年 4 月 GPT-4.1 发布时,配套的提示指南就明确记录了这个格式,并提到在补丁生成上做了「可观的训练投入」。这笔投入一路延续到后续每一个 Codex 模型。所以对 Codex 来说,V4A 不是一个外挂的约定,而是刻进模型行为里的东西。
这个区别有一个非常干净的证据。
Codex CLI v0.80.0 里有一个改动,移除了向系统提示追加 apply_patch 说明的逻辑。结果 Azure 上托管的 GPT-4.1 部署直接坏了,因为那些模型没有把这套格式内化进系统行为,说明一撤走就什么都不会了。同一份指令,放在原生 Codex 模型上可以省略,放到别处就必须手动注入。
结论很直白:编辑原语的有效性等于格式设计乘以模型的熟悉程度。缺了任何一半,工具都会在纸面上成立、在实测里失败。
六、把工具当自变量,而不是把模型当自变量#
回到开头那个场景。反复换模型解决不了问题,因为被反复调的那个旋钮不是瓶颈。
原文作者有一个很实在的观察链条。他最早在 Claude Code 里偶尔看到编辑失败,当时觉得正常。后来通过 OpenRouter 接入别的模型,失败明显变多。再后来他用 Pi coding agent 配开源权重模型,原因终于彻底暴露:Pi 默认的 edit_tool 依赖精确文本替换,oldText 必须和当前文件完全一致,差一个字符就报「Could not find edits[0]」。
他的反应不是换模型,而是自己给 Pi 补了一个 apply_patch 工具,让它也能服务非 GPT 系列的模型。
他的总结可以直接抄下来当原则用:一个强模型配上脆弱的编辑工具,照样会在简单改动上失败;一个更好的 harness,能让更弱的模型可靠得多。有时候改进 Agent 不需要更好的模型,只需要更好的工具。
我自己搭 Agent 环境时也踩过同一条线。用来改文件的 patch 工具内置了九种模糊匹配策略,第一次看到时我觉得这是过度设计,一个替换动作为什么要这么多兜底。等到文件里开始同时出现 CRLF 换行、超长无换行的段落、以及缩进风格不统一的区块,才明白这九种策略就是「基本不失败」和「经常失败」之间的那段距离。
七、一份实践清单#
如果这些失败正在消耗你的时间,下面这些是我认为性价比最高的动作。
给使用者:
- 改之前先读,读完立刻改。中间不要插入会被格式化或者触发 hook 的动作。
- 锚点要带唯一性。同一段代码如果在文件里出现多次,把函数签名或类名一起锚进去。
- 把格式化推迟到改动落地之后。别让 formatter 在编辑窗口的中间运行。
- 一次任务只改一件事。把无关重构混进同一个补丁,等于让失败范围变得不可控。
- 失败后不要连续重试。先看文件现在的实际内容,再决定锚点怎么写。
- 让错误信息可操作。「Error: Invalid Context: @@ def fib(n):」这种把失败锚点原样报出来的信息,模型下一轮就能自纠。只说一句「编辑失败」,会把它推进死循环。
给自建 harness 的人:
- 路径校验:拒绝绝对路径和
..穿越。 - 备份策略:打补丁前做快照,或者直接交给 git stash。
- 原子性决策:选全有或全无,或者按文件粒度报告成功与失败。不要留半成品文件给下一个环节。
- 错误信息设计:这是最便宜也最被低估的一项。回传失败锚点,等于把一次失败转化成一次可学习的信号。
结语#
Agent 的能力通常被简化成一个模型排行榜上的分数。可真实的编码循环是这样的:模型产生意图,harness 把意图翻译成文件变动,测试给出反馈,模型再修正意图。
编辑原语正好卡在第一环和第二环的接缝上。它不会出现在任何榜单里,却决定了这个循环能不能闭合。接缝漏了,再强的模型也会从那里漏出去。
Codex 的 V4A 给人的启示不是「这个格式最好」,而是「格式值得被认真设计,甚至值得为它专门训练模型」。如果你的 Agent 在某些任务上莫名其妙地显得笨,先别急着换模型。看一眼它是怎么改文件的,答案很可能就在那里。
参考来源
- Paul Pham, Deep Dive into Editing Tools in AI Coding Harnesses, DEV Community, 2026-09-17 · 原文
- Daniel Vaughan, The V4A Diff Format: How Codex CLI’s apply_patch Actually Edits Your Code, Codex Knowledge Base · 原文
- OpenAI Codex 仓库中 apply_patch 工具说明 · GitHub
- Codex issue #14046,Azure 托管 GPT-4.1 的 apply_patch 失效 · GitHub