让模型按行号改,行不行?
本课一问:模型读完文件、想好了改哪里,它得把「哪里」告诉 harness。最直接的说法是行号——「把第 35 行换成……」。这样行不行?
这一课先不碰:给出的原文匹配不上、或匹配到两处怎么办(第 2 课);怎么保证模型写出来的格式合法(第 3 课);外部工具怎么接进来(第 4 课);这次修改该不该被允许(A3)。
从已有出发
A1 结束时,循环已经转起来了:harness 把消息发给模型,模型回一段文字,里面写着要调哪个工具、参数是什么;harness 照着执行,把结果塞回下一次请求。这一章补的是「工具层」:模型能调哪些工具、每个工具的参数怎么设计。
全章用同一个贯穿任务:让本课项目任务仓里失败的 test_parse_date 通过。要改的文件是 dateparse.py,44 行,埋了两处 bug:
- 第 35 行:
DD/MM/YYYY分支把日和月解包反了,写成了month, day, year = …; - 第 41 行:月份名分支用
MONTHS.index()得到从 0 起的下标,少了+ 1。
两处 bug 按顺序暴露:修掉第一处、重新跑测试,才会看到第二处。所以模型至少要改两次同一个文件。本课项目 miniagent 用本机的 Qwen3-8B 真实跑过这个任务,每一次 edit_file 调用都留在 trace 里(本章叫它 T8 语料),后面几课反复用。
先自己设计:报行号
模型已经通过 read_file 看到了整个文件,最省事的工具设计是「把第 a–b 行换成 X」:模型只要报两个数,不用把原文再抄一遍,输出最短。
问题出在「第二次」。行号是模型上一次读文件时记下的快照,而第一次修改可能已经让文件多了或少了几行。下面在真实的 dateparse.py 上现算:先照第一处修复改一次,再用旧行号 41 做第二处修复。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
文件:任务仓 tasks/parse_date/repo/dateparse.py(本课项目,2026-09-26)。第一处修复的「T8 真实」一项取自项目 trace;其余三项是在同一修复后顺手多写几行的构造输入,落点由浏览器现算。「按行号」这一列假设工具是「把第 a–b 行换成 X」,模型用的是它最后一次读文件时的行号。
同样两次修改,第二次改成「找到 month = MONTHS.index(name.lower()) 这一段,换成……」。harness 在当前文件里搜这段文字:它只出现 1 次,在第 43 行。前面插了多少行都不影响,因为定位靠的是内容,不是位置。
代价是模型要把原文抄一遍,抄错一个空格就找不到——这是第 2 课的问题。
if m:——真正的目标已经挪到第 43 行。没有任何报错,错误要等测试或人工审查才会发现。 试三件事:① 在「T8 真实」上先猜再看——真实 trace 里第一处修复恰好是 1 行换 1 行,k = 0,旧行号碰巧还对;② 切到「多写 2 行校验」,再猜一次:按行号那一列会把第 41 行的 if m: 整行换掉,而黄线标出的第 43 行才是目标;③ 切到「多写 3 行」,看「按片段」一列:无论 k 是几,它都落在目标行上。
为什么差值刚好是 k
把「行号过期」写成一行算术。第一次修改把第 35 行换成了 n 行(n = 1 时行数不变),目标行在它下方:
这就是行号寻址的根本问题:行号描述的是位置,位置会被别的修改挪动;内容描述的是目标本身,只要它还在、还唯一,就能被找回来 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条算法性质:若片段在当前文件中恰好出现一次,搜索结果与此前插入或删除了多少行无关;行号寻址的结果等于「旧行号 + k」处的那一行,k ≠ 0 时必然偏离目标。 出处待补 。
改进:三种不用行号的格式
主流 harness 给出的答案都不是行号,而是下面三种之一。
| 格式 | 模型要输出什么 | 代表 | 付出的代价 |
|---|---|---|---|
| 整文件覆写 | 改完之后的整个文件 | write_file;aider 的 whole 格式 | 输出随文件长度线性增长 |
| 唯一片段替换 | 要换掉的原文 old_string + 新文本 | CoreCoder、Pi、OpenCode 的 edit;本课项目 miniagent | 原文要逐字抄对,且必须唯一(第 2 课) |
| 无行号 patch | 带上下文行和 @@ 定位的补丁 | Codex 的 apply_patch | 格式本身要合法(第 3 课),定位同样靠匹配 |
整文件覆写彻底绕开了定位:模型写出完整的新文件,harness 整个替换。它的格式也最不容易写错。aider 的公开排行榜里,同一个 Qwen2.5-Coder-32B-Instruct 用 whole 格式时「格式正确」比例 99.6%,换成 diff(search/replace 块)只有 71.6% 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条aider 仓库 polyglot_leaderboard.yml 的 percent_cases_well_formed 字段;单次运行,口径是 aider 自己的判定。 出处aider 排行榜(官方自报) ↗需联网 :
| 格式 | 条目 | 最低 | 中位数 |
|---|---|---|---|
whole | 15 | 92.9% | 99.6% |
diff-fenced | 4 | 92.4% | 98.45% |
diff | 47 | 64.4% | 94.2% |
architect | 3 | 100% | 100% |
高亮的一对是同一个模型 Qwen2.5-Coder-32B-Instruct 分别用 diff 与 whole 格式跑同一套题。数据:Aider-AI/aider 本地副本 @ cb6a152e 2025-10-04 11:14:08 +0200(aider/website/_data/polyglot_leaderboard.yml、edit_leaderboard.yml);字段 percent_cases_well_formed(线上页面列名「Correct edit format」,2025-11-20 更新,2026-09-26 核对可访问)。官方自报、单次运行;「格式正确」是 aider 自己的判定口径,不等于改对了代码。
但整文件覆写的代价是真实的。只改第 35 行这 1 行,用 write_file 要把 44 行全部写回去,参数是 487 个 token;用 edit_file 给出一行原文和一行新文本,只要 54 个,差 9.0 倍(都用项目录制模型 Qwen3-8B 的 tokenizer 计)。文件越长,差距越大;而输出是一个 token 一个 token 生成的(A1 讲过),越长越慢。
唯一片段替换是现在最常见的选择。CoreCoder 的 README 把理由写得很直白 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条这是设计理由,不是实验结论;作者没有给出「模型数错行」的频率数据。 出处CoreCoder@v0.7.0 README.md:142(作者自述) ↗需联网 :
Line numbers are a trap: the model only has to miscount by one and it quietly edits the wrong place.
它的 edit_file 要求 old_string 在文件里恰好出现一次:没找到就把文件开头回给模型让它重新定位,出现多次就要求补充上下文(corecoder/tools/edit.py:59-73)。
它不会改错位置,但有两笔账:① 成本随文件长度线性增长,上面 487 对 54 只是一个 44 行的文件;② 它会覆盖在「模型读文件」和「模型写回」之间发生的任何别的改动——另一个工具、另一个子 agent、用户自己在编辑器里的修改,都会被静默抹掉。CoreCoder v0.7.0 为同一文件的编辑加了一把进程内的锁 FILE_MUTATION_LOCK(edit.py:47,50),防的就是并发写;整文件覆写连「只改一小段」的余地都没有。
🔍 为什么不在每次修改之后重新读一遍文件,拿新行号?
可以,但要多一轮。真实 trace pd-truncate-s1 里,一次 read_file(dateparse.py) 的结果是 230 个 token,它会留在之后每一轮的请求里;再加上模型为这次读取多生成的一轮回复。更重要的是,它只是把「行号可能过期」变成了「模型必须记得每次都重读」——这是靠纪律,不是靠机制。片段锚定把正确性交给 harness 的搜索,模型不需要记住任何事。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 让模型说清「改哪里」,而且在文件被改过之后仍然说得清 |
| ② 为什么可能有效? | 内容锚点不受别处增删行的影响,行号锚点受 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条见上面的公式:行号寻址落在「旧行号 + k」,片段寻址只依赖内容是否唯一。 出处待补 ;整文件覆写不需要锚点,但输出与文件长度成正比 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条write_file 的参数就是整个新文件,token 数随文件变长;本课实数 487 对 54(Qwen3-8B tokenizer)。 出处待补 |
| ③ 实验上呢? | aider 的公开数据:格式越简单,「格式正确」越高(同一模型 whole 99.6% 对 diff 71.6%) 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条单次运行、aider 自己的判定口径;没有直接比较「行号 vs 片段」的公开实验。 出处aider 排行榜(官方自报) ↗需联网 ;「行号更容易出错」目前只有作者自述(CoreCoder README),没有找到公开的对照实验 |
演进地图:你在这里
- 解决了模型不用复述原文,只报行号,输出最短。卡在行号是「上一次读文件时」的快照:第一处修复只要多插 2 行,第二次按旧行号 41 改,就落在
if m:上(第 1 课现算);CoreCoder README 的原话是「Line numbers are a trap」。 - 解决了不需要定位,格式最不容易错:aider 同一个 Qwen2.5-Coder-32B,whole 格式合规 99.6%,diff 格式只有 71.6%。卡在只改 1 行,也要把 44 行的文件整个再写一遍:参数 487 token,片段替换只要 54(9.0 倍,项目模型 tokenizer 实数);还会覆盖别人在此期间的改动。
- 解决了按内容定位,不怕行号过期;0 处或多处都报错、文件不动,让模型自己补上下文。卡在真实 trace pd-truncate-s1:第 4 轮 old_string 在文件里 2 处、第 5 轮缩进多 4 格 0 处,两次报错多跑了两轮,花掉 8,481 token、109.5 秒;唯一也不等于对:pd-v2-unlimited-s1 第 7 轮的 old_string 恰好是已修好那一行的前半截,计数 = 1,于是
+ 1变成了+ 1 + 1。 - 解决了模型复述时的小漂移不再白费一轮:上面第 5 轮那条缩进多 4 格的 edit,OpenCode 直接接住(LineTrimmed 级)。卡在接住 ≠ 改对:多转义一层的引号,OpenCode 的 EscapeNormalized 级照样接住,写回去的是
\",模块导入即 SyntaxError;OpenCode 的 v2(packages/core)眼下只有精确匹配,「edit fuzzy parity」还列在待移植清单里。 - 解决了语法由解码器保证,不再靠 prompt 教。对照:靠 prompt 教格式时,aider 榜上 47 个 diff 格式条目的「格式正确」比例最低只有 64.4%(官方自报)。卡在语法只管格式,定位仍是 seek_sequence:它不检查唯一、取第一处——两处相同的
return datetime.date(year, month, day)会改到第 31 行 ISO 分支,test_parse_iso 跟着坏;整行比较还会把没写缩进的 new 行原样写进去,得到 IndentationError。 - 解决了一套协议接任意外部工具;同一个 harness 给 GPT 换上 apply_patch、给 Claude 用 edit。卡在接进来的每个工具每轮都要付上下文:chrome-devtools-mcp 的 30 个工具在项目模型的 chat template 里占 7,047 token,是 miniagent 自己 6 个工具(685)的 10.3 倍。
- 解决了N 次「模型 → 工具 → 模型」往返压成一段脚本。卡在省的是往返,不省判断:真实 trace 里模型每一步都要看上一步的结果才知道下一步做什么(岔路课逐轮算)。
一句话
先停在这里:记住「行号是快照,内容是锚点」,直接去第 2 课。
再深一层:去岔路课看一次 edit_file 调用在网络上的样子:工具定义每一轮都要重发,在第 1 轮请求里占 76%。
下一个缺口
片段锚定,匹配不上或匹配到两处怎么办? → 第 2 课:old_string 和文件差一个空格,该报错还是容错?
真实 trace 里这两种情况都发生了:pd-truncate-s1 第 4 轮的 old_string 在文件里有 2 处,第 5 轮的缩进多了 4 格。第 2 课逐个拆。
你可能会问
- 「工具定义每轮都发吗?一次工具调用在网络上到底长什么样?」 见岔路课 工具定义每轮占多少?:用 trace 逐轮回放,四条泳道看请求、回复、执行、结果。
- 「unified diff 也带行号,为什么补丁工具能用?」 在 GNU patch 这类工具里,
@@ -35,1 +35,1 @@的行号只是起点提示,真正定位靠上下文行:对不上时它在附近找上下文,并报告偏移了几行(offset)。Codex 的 apply_patch 干脆把行号去掉,只留上下文行和@@定位文字(第 3 课)。 - 「整文件覆写什么时候反而合适?」 新建文件,或者改动占了文件的大部分时。Pi 的 write 工具与 edit 工具并存,本课项目也同时给了
write_file与edit_file,由模型按情况选。