从0打造编码Agent 阅读 + 实验 ≈ 25 分钟 页内实验 ×2
A2 工具系统:模型怎么说清「改哪里」? 第 1 课

让模型按行号改,行不行?

按行号改
dateparse.py 有两处要修:第 35 行和第 41 行。第一处修复只要顺手多写 2 行,第二次按旧行号 41 改就落在了 if m: 上,而且不报任何错。整文件覆写不怕行号,却要把 44 行重写一遍(487 token 对 54 token)。锚定一段唯一的原文,位置怎么漂都跟得上。
❓

本课一问:模型读完文件、想好了改哪里,它得把「哪里」告诉 harness。最直接的说法是行号——「把第 35 行换成……」。这样行不行?

这一课先不碰:给出的原文匹配不上、或匹配到两处怎么办(第 2 课);怎么保证模型写出来的格式合法(第 3 课);外部工具怎么接进来(第 4 课);这次修改该不该被允许(A3)。

从已有出发

A1 结束时,循环已经转起来了:harness 把消息发给模型,模型回一段文字,里面写着要调哪个工具、参数是什么;harness 照着执行,把结果塞回下一次请求。这一章补的是「工具层」:模型能调哪些工具、每个工具的参数怎么设计。

从已有出发 · 累计数据流 点已学环节看它回答什么
每一轮循环都经过

全章用同一个贯穿任务:让本课项目任务仓里失败的 test_parse_date 通过。要改的文件是 dateparse.py,44 行,埋了两处 bug:

两处 bug 按顺序暴露:修掉第一处、重新跑测试,才会看到第二处。所以模型至少要改两次同一个文件。本课项目 miniagent 用本机的 Qwen3-8B 真实跑过这个任务,每一次 edit_file 调用都留在 trace 里(本章叫它 T8 语料),后面几课反复用。

先自己设计:报行号

模型已经通过 read_file 看到了整个文件,最省事的工具设计是「把第 a–b 行换成 X」:模型只要报两个数,不用把原文再抄一遍,输出最短。

问题出在「第二次」。行号是模型上一次读文件时记下的快照,而第一次修改可能已经让文件多了或少了几行。下面在真实的 dateparse.py 上现算:先照第一处修复改一次,再用旧行号 41 做第二处修复。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案按行号改:把第 a–b 行换成 X
行号寻址 vs 片段寻址 · 在真实的 dateparse.py 上现算

文件:任务仓 tasks/parse_date/repo/dateparse.py(本课项目,2026-09-26)。第一处修复的「T8 真实」一项取自项目 trace;其余三项是在同一修复后顺手多写几行的构造输入,落点由浏览器现算。「按行号」这一列假设工具是「把第 a–b 行换成 X」,模型用的是它最后一次读文件时的行号。

🎯

试三件事:① 在「T8 真实」上先猜再看——真实 trace 里第一处修复恰好是 1 行换 1 行,k = 0,旧行号碰巧还对;② 切到「多写 2 行校验」,再猜一次:按行号那一列会把第 41 行的 if m: 整行换掉,而黄线标出的第 43 行才是目标;③ 切到「多写 3 行」,看「按片段」一列:无论 k 是几,它都落在目标行上。

为什么差值刚好是 k

把「行号过期」写成一行算术。第一次修改把第 35 行换成了 n 行(n = 1 时行数不变),目标行在它下方:

第二处修改的目标,在新文件里的行号
= +
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
按行号改的工具默认 k = 0。k 只要不是 0,换掉的就是别的行;而且工具本身无法察觉,因为「第 41 行」永远存在。

这就是行号寻址的根本问题:行号描述的是位置,位置会被别的修改挪动;内容描述的是目标本身,只要它还在、还唯一,就能被找回来 。

改进:三种不用行号的格式

主流 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 排行榜:模型按哪种 edit 格式回复,「格式正确」的比例(每个点一个模型,69 个条目)
60%70%80%90%100% wholediff-fenceddiffarchitect Gemini 2.0 Pro exp-02-05 · whole · 格式正确 100% · 两次尝试通过率 35.6% · 2025-02-25 gpt-4o-mini-2024-07-18 · whole · 格式正确 100% · 两次尝试通过率 3.6% · 2024-12-21 claude-3-5-sonnet-20241022 · diff · 格式正确 99.6% · 两次尝试通过率 51.6% · 2025-01-17 gpt-4o-2024-11-20 · diff · 格式正确 95.1% · 两次尝试通过率 18.2% · 2024-12-30 gpt-4o-2024-08-06 · diff · 格式正确 94.2% · 两次尝试通过率 23.1% · 2024-12-30 o1-2024-12-17 (high) · diff · 格式正确 91.5% · 两次尝试通过率 61.7% · 2024-12-21 DeepSeek Chat V2.5 · diff · 格式正确 92.9% · 两次尝试通过率 17.8% · 2024-12-21 claude-3-5-haiku-20241022 · diff · 格式正确 91.1% · 两次尝试通过率 28% · 2024-12-21 Qwen2.5-Coder-32B-Instruct · diff · 格式正确 71.6% · 两次尝试通过率 8% · 2024-12-22 o1-mini-2024-09-12 · whole · 格式正确 96.9% · 两次尝试通过率 32.9% · 2024-12-22 gemini-exp-1206 · whole · 格式正确 98.2% · 两次尝试通过率 38.2% · 2024-12-22 gemini-2.0-flash-exp · whole · 格式正确 100% · 两次尝试通过率 22.2% · 2024-12-22 yi-lightning · whole · 格式正确 92.9% · 两次尝试通过率 12.9% · 2024-12-23 DeepSeek Chat V3 (prev) · diff · 格式正确 98.7% · 两次尝试通过率 48.4% · 2024-12-25 Qwen2.5-Coder-32B-Instruct · whole · 格式正确 99.6% · 两次尝试通过率 16.4% · 2024-12-26 Codestral 25.01 · whole · 格式正确 100% · 两次尝试通过率 11.1% · 2025-01-13 DeepSeek R1 · diff · 格式正确 96.9% · 两次尝试通过率 56.9% · 2025-01-20 DeepSeek R1 + claude-3-5-sonnet-20241022 · architect · 格式正确 100% · 两次尝试通过率 64% · 2025-01-23 qwen-max-2025-01-25 · diff · 格式正确 90.2% · 两次尝试通过率 21.8% · 2025-01-28 o3-mini (medium) · diff · 格式正确 95.1% · 两次尝试通过率 53.8% · 2025-01-31 o3-mini (high) · diff · 格式正确 93.3% · 两次尝试通过率 60.4% · 2025-01-31 gemini-2.0-flash-thinking-exp-01-21 · diff · 格式正确 77.8% · 两次尝试通过率 18.2% · 2025-01-21 chatgpt-4o-latest (2025-02-15) · diff · 格式正确 93.3% · 两次尝试通过率 27.1% · 2025-02-15 claude-3-7-sonnet-20250219 (no thinking) · diff · 格式正确 93.3% · 两次尝试通过率 60.4% · 2025-02-24 claude-3-7-sonnet-20250219 (32k thinking tokens) · diff · 格式正确 97.8% · 两次尝试通过率 64.9% · 2025-02-24 gpt-4.5-preview · diff · 格式正确 97.3% · 两次尝试通过率 44.9% · 2025-02-27 QwQ-32B · diff · 格式正确 67.6% · 两次尝试通过率 20.9% · 2025-03-06 QwQ-32B + Qwen 2.5 Coder Instruct · architect · 格式正确 100% · 两次尝试通过率 26.2% · 2025-03-07 command-a-03-2025-quality · whole · 格式正确 99.6% · 两次尝试通过率 12% · 2025-03-14 gemma-3-27b-it · whole · 格式正确 100% · 两次尝试通过率 4.9% · 2025-03-15 DeepSeek V3 (0324) · diff · 格式正确 99.6% · 两次尝试通过率 55.1% · 2025-03-24 Gemini 2.5 Pro Preview 03-25 · diff-fenced · 格式正确 92.4% · 两次尝试通过率 72.9% · 2025-04-12 chatgpt-4o-latest (2025-03-29) · diff · 格式正确 64.4% · 两次尝试通过率 45.3% · 2025-03-29 Quasar Alpha · diff · 格式正确 98.2% · 两次尝试通过率 54.7% · 2025-04-04 Llama 4 Maverick · whole · 格式正确 99.1% · 两次尝试通过率 15.6% · 2025-04-06 Grok 3 Beta · diff · 格式正确 99.6% · 两次尝试通过率 53.3% · 2025-04-10 Grok 3 Mini Beta (low) · whole · 格式正确 100% · 两次尝试通过率 34.7% · 2025-04-10 Grok 3 Mini Beta (high) · whole · 格式正确 99.6% · 两次尝试通过率 49.3% · 2025-04-10 Optimus Alpha · diff · 格式正确 97.3% · 两次尝试通过率 52.9% · 2025-04-10 gpt-4.1 · diff · 格式正确 98.2% · 两次尝试通过率 52.4% · 2025-04-14 gpt-4.1-mini · diff · 格式正确 92.4% · 两次尝试通过率 32.4% · 2025-04-14 gpt-4.1-nano · whole · 格式正确 94.2% · 两次尝试通过率 8.9% · 2025-04-14 o4-mini (high) · diff · 格式正确 90.7% · 两次尝试通过率 72% · 2025-04-16 openhands-lm-32b-v0.1 · whole · 格式正确 95.1% · 两次尝试通过率 10.2% · 2025-04-19 gemini-2.5-flash-preview-04-17 (default) · diff · 格式正确 85.3% · 两次尝试通过率 47.1% · 2025-04-20 Gemini 2.5 Pro Preview 05-06 · diff-fenced · 格式正确 97.3% · 两次尝试通过率 76.9% · 2025-05-07 Qwen3 32B · diff · 格式正确 83.6% · 两次尝试通过率 40% · 2025-05-08 Qwen3 235B A22B diff, no think, Alibaba API · diff · 格式正确 92.9% · 两次尝试通过率 59.6% · 2025-05-09 claude-sonnet-4-20250514 (no thinking) · diff · 格式正确 98.2% · 两次尝试通过率 56.4% · 2025-05-24 claude-sonnet-4-20250514 (32k thinking) · diff · 格式正确 97.3% · 两次尝试通过率 61.3% · 2025-05-24 claude-opus-4-20250514 (no think) · diff · 格式正确 98.7% · 两次尝试通过率 70.7% · 2025-05-25 claude-opus-4-20250514 (32k thinking) · diff · 格式正确 97.3% · 两次尝试通过率 72% · 2025-05-25 gemini-2.5-flash-preview-05-20 (no think) · diff · 格式正确 93.8% · 两次尝试通过率 44% · 2025-05-26 gemini-2.5-flash-preview-05-20 (24k think) · diff · 格式正确 95.6% · 两次尝试通过率 55.1% · 2025-05-25 gemini-2.5-pro-preview-06-05 (default think) · diff-fenced · 格式正确 100% · 两次尝试通过率 79.1% · 2025-06-06 gemini-2.5-pro-preview-06-05 (32k think) · diff-fenced · 格式正确 99.6% · 两次尝试通过率 83.1% · 2025-06-06 DeepSeek R1 (0528) · diff · 格式正确 94.6% · 两次尝试通过率 71.4% · 2025-06-06 o3 (high) · diff · 格式正确 94.7% · 两次尝试通过率 81.3% · 2025-06-25 o3 · diff · 格式正确 93.8% · 两次尝试通过率 76.9% · 2025-06-25 o3 (high) + gpt-4.1 · architect · 格式正确 100% · 两次尝试通过率 78.2% · 2025-06-27 o3-pro (high) · diff · 格式正确 97.8% · 两次尝试通过率 84.9% · 2025-06-28 grok-4 (high) · diff · 格式正确 97.3% · 两次尝试通过率 79.6% · 2025-07-11 Kimi K2 · diff · 格式正确 92.9% · 两次尝试通过率 59.1% · 2025-07-17 gpt-oss-120b (high) · diff · 格式正确 79.1% · 两次尝试通过率 41.8% · 2025-08-06 gpt-5 (high) · diff · 格式正确 91.6% · 两次尝试通过率 88% · 2025-08-23 gpt-5 (medium) · diff · 格式正确 88.4% · 两次尝试通过率 86.7% · 2025-08-25 gpt-5 (low) · diff · 格式正确 86.7% · 两次尝试通过率 81.3% · 2025-08-25 DeepSeek-V3.2-Exp (Reasoner) · diff · 格式正确 97.3% · 两次尝试通过率 74.2% · 2025-10-03 DeepSeek-V3.2-Exp (Chat) · diff · 格式正确 98.2% · 两次尝试通过率 70.2% · 2025-10-03 71.6%99.6%
格式条目最低中位数
whole1592.9%99.6%
diff-fenced492.4%98.45%
diff4764.4%94.2%
architect3100%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 把理由写得很直白 :

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 的搜索,模型不需要记住任何事。

证据三问

问本课的回答
① 它解决什么问题?让模型说清「改哪里」,而且在文件被改过之后仍然说得清
② 为什么可能有效?内容锚点不受别处增删行的影响,行号锚点受 ;整文件覆写不需要锚点,但输出与文件长度成正比
③ 实验上呢?aider 的公开数据:格式越简单,「格式正确」越高(同一模型 whole 99.6% 对 diff 71.6%) ;「行号更容易出错」目前只有作者自述(CoreCoder README),没有找到公开的对照实验

演进地图:你在这里

模型怎么说清「改哪里」:每一代都被上一代的某个真数字逼出来
  1. 朴素 按行号改 「把第 a–b 行换成……」 你在这里
    解决了模型不用复述原文,只报行号,输出最短。
    卡在行号是「上一次读文件时」的快照:第一处修复只要多插 2 行,第二次按旧行号 41 改,就落在 if m: 上(第 1 课现算);CoreCoder README 的原话是「Line numbers are a trap」。
  2. 第一代 整文件覆写 write_file;aider 的 whole 格式
    解决了不需要定位,格式最不容易错:aider 同一个 Qwen2.5-Coder-32B,whole 格式合规 99.6%,diff 格式只有 71.6%。
    卡在只改 1 行,也要把 44 行的文件整个再写一遍:参数 487 token,片段替换只要 54(9.0 倍,项目模型 tokenizer 实数);还会覆盖别人在此期间的改动。
  3. 第二代 唯一片段替换(search-replace) CoreCoder、本课项目 miniagent
    解决了按内容定位,不怕行号过期;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。
  4. 解决了模型复述时的小漂移不再白费一轮:上面第 5 轮那条缩进多 4 格的 edit,OpenCode 直接接住(LineTrimmed 级)。
    卡在接住 ≠ 改对:多转义一层的引号,OpenCode 的 EscapeNormalized 级照样接住,写回去的是 \",模块导入即 SyntaxError;OpenCode 的 v2(packages/core)眼下只有精确匹配,「edit fuzzy parity」还列在待移植清单里。
  5. 第四代 语法约束解码的 patch Codex apply_patch(19 行 Lark 语法);strict 工具
    解决了语法由解码器保证,不再靠 prompt 教。对照:靠 prompt 教格式时,aider 榜上 47 个 diff 格式条目的「格式正确」比例最低只有 64.4%(官方自报)。
    卡在语法只管格式,定位仍是 seek_sequence:它不检查唯一、取第一处——两处相同的 return datetime.date(year, month, day) 会改到第 31 行 ISO 分支,test_parse_iso 跟着坏;整行比较还会把没写缩进的 new 行原样写进去,得到 IndentationError。
  6. 工具从哪来 工具集随模型变,外部工具经 MCP 接入 OpenCode registry.ts;MCP 2026-07-28
    解决了一套协议接任意外部工具;同一个 harness 给 GPT 换上 apply_patch、给 Claude 用 edit。
    卡在接进来的每个工具每轮都要付上下文:chrome-devtools-mcp 的 30 个工具在项目模型的 chat template 里占 7,047 token,是 miniagent 自己 6 个工具(685)的 10.3 倍。
  7. 岔路 Code Mode:让模型写脚本调工具 Codex exec / wait;OpenCode codemode
    解决了N 次「模型 → 工具 → 模型」往返压成一段脚本。
    卡在省的是往返,不省判断:真实 trace 里模型每一步都要看上一步的结果才知道下一步做什么(岔路课逐轮算)。
数字口径:token 数用项目录制模型 Qwen3-8B 的 tokenizer 与 chat template 算(与服务端 usage 逐轮核对一致);裁决与测试结果是原仓库代码实跑(a02-edit-fixtures.json,2026-09-26);trace 来自本课项目 miniagent 的真实运行(2026-09-26);aider 数字来自 aider 仓库的 polyglot_leaderboard.yml(官方自报);MCP 工具数来自本机实跑 tools/list。

一句话

📌
行号在第一次编辑后就会过期,锚定片段不会。
🚦

先停在这里:记住「行号是快照,内容是锚点」,直接去第 2 课。
再深一层:去岔路课看一次 edit_file 调用在网络上的样子:工具定义每一轮都要重发,在第 1 轮请求里占 76%。

下一个缺口

片段锚定,匹配不上或匹配到两处怎么办? → 第 2 课:old_string 和文件差一个空格,该报错还是容错?

真实 trace 里这两种情况都发生了:pd-truncate-s1 第 4 轮的 old_string 在文件里有 2 处,第 5 轮的缩进多了 4 格。第 2 课逐个拆。

你可能会问