从0打造编码Agent 导读 导读 ≈ 15 分钟
A2 工具系统:模型怎么说清「改哪里」? 章导读

工具系统:模型怎么说清「改哪里」?

循环转起来之后,模型得把「改哪里」说清楚。这一章从「报行号」出发,被真实数字一步步逼到唯一片段替换、容错匹配、语法约束的 patch,再看外部工具经 MCP 接进来时每一轮要付多少上下文。五个真实 edit 引擎被搬进浏览器,跑的是本课项目 trace 里的真实 edit。
本章一问:模型怎么说清「改哪里」?

主线定位

🧭

上游(A1 循环):模型只会吐文字;harness 把文字里的工具调用切出来执行,结果放回下一轮,没有工具调用就停。
本章(A2 工具系统):工具怎么定义、模型怎么把「改哪里」说清楚、说错了 harness 怎么办、外部工具怎么接进来。
下游(A3 权限):工具能改文件、能跑命令、能连外部系统——它要执行 rm -rf 时,谁来拦?

下面是累计数据流,本章点亮「工具层」。点亮的节点可以点开,看它回答什么问题、在贯穿任务的 trace 里长出哪一段。

累计数据流 · 一次任务在 harness 里经过哪些环节 已学本课新增还没学
每一轮循环都经过

本章问题链

每一课只回答一个问题,上一课留下的缺口就是下一课的问题。

  1. 让模型按行号改,行不行?——第一处修复多写 2 行,第二次按旧行号就改到了 if m:;整文件覆写不怕行号,却要多花约 9 倍的输出。
  2. old_string 和文件差一个空格,该报错还是容错?——只查存在会改错位置,严格唯一会多花轮次,容错只管「找」不管「写」。
  3. 能不能从源头保证 patch 格式合法?——把 19 行语法交给解码器;它只保证语法,不保证定位。
  4. 外部工具怎么接进来?MCP 放在哪一层?——MCP 在 harness 与工具之间;接进来的每个工具每轮都要付上下文。
  5. 实验课:五个真引擎跑真实 edit 语料

演进地图

本章的「现代做法」是被一连串真实数字逼出来的。每一课都会用这张图标出「你在这里」。

模型怎么说清「改哪里」:每一代都被上一代的某个真数字逼出来
  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。

贯穿任务

全章都在同一个任务上展开:让本课项目任务仓里失败的 test_parse_date 通过。要改的 dateparse.py 有两处 bug(第 35 行日月顺序、第 41 行月份少 + 1),而第 31 行与第 36 行有一模一样的一行 return datetime.date(year, month, day)——片段锚定的「唯一性」问题就从这里来。

本课项目 miniagent 用本机的 Qwen3-8B 真实跑过这个任务:到 2026-09-26 为止,trace 里一共 33 次 edit_file,去重后 14 条(本章叫它 T8 语料)。第 2 课与实验课逐条用它们;第 4 课的外部工具用不上这个任务,换成三个真实 MCP server,课内写明了原因。

三层交互

层这一章里是什么在哪一课
概念动画行号过期(先改一处、再按旧行号改第二处,逐步看落点);MCP 一次往返的三条泳道步进;一次工具调用的逐轮回放第 1 课、第 4 课、岔路 B1
算法模拟器(真算法、小数据)五个 edit 引擎的浏览器移植(learn-claude-code、CoreCoder、Pi、OpenCode 9 级 replacer、Codex seek_sequence),6 个预置坑 + 压轴新漂移,old_string 可改、实时重算;apply_patch 19 行语法的逐字前缀检查器;OpenCode 按模型换工具集的规则第 2 课、第 3 课、第 4 课
真模型 / 真数据T8 真实 edit × 五个引擎,每格附「写回任务仓实跑测试」的结果;项目 trace 的逐轮请求(token 与服务端 usage 核对一致);本机实跑三个 MCP server 的真实工具清单与一次真实 JSON-RPC 往返;aider 公开排行榜实验课、岔路 B1、第 4 课、第 1/3 课

每个移植的算法都配了 fixture:期望输出由原仓库代码实跑得到(OpenCode 与 Pi 用 Node 跑 TypeScript 原文,CoreCoder 与 learn-claude-code 用 Python 加载原函数,Codex 用 rustc 编译 Rust 原文件),页面加载时逐条比对,组件右上角显示一致的条数。

项目推进:里程碑 M2 · search-replace 的 edit_file

对应 04-实践项目/MILESTONES.md 的 A2 一节。代码在 miniagent/tools.py(tool_edit_file 在第 182–207 行),测试在 tests/test_tools.py。本课项目 v0.5.0 已经实现了唯一匹配版本。

你要拍板的决策

决策选项这一章里的依据
edit 格式走哪条路整文件覆写 / 唯一片段替换 / 容错降级 / 语法约束的 patch第 1 课(行号与整文件的账)、第 3 课(约束解码只管格式)
唯一匹配失败时报错还是降级CoreCoder 与 miniagent 报错;Pi 归一化后再找;OpenCode 9 级降级加护栏第 2 课与实验课:T8 里被严格唯一拒绝的真实 edit、容错接住后测试的结果
bash 超时多少、输出截多长本项目默认 30 秒、8000 字符(保留头尾)MILESTONES.md;A5 讲输出怎么占上下文

验收标准(全部可测)

  1. .venv/bin/python -m unittest tests.test_tools 全部通过,至少覆盖:唯一匹配成功并返回 diff;0 处匹配报错、文件不变,且错误信息引导模型补充上下文;多处匹配报出次数、文件不变;空白必须精确匹配;越界路径被拒;bash 超时被杀。
  2. 若选择降级:至少实现 LineTrimmed 与 IndentationFlexible 两级,并且「歧义」用例与「匹配范围远大于 old_string」用例必须拒绝(照 OpenCode 的比例护栏)。
  3. 与页面 fixture 一致:src/data/precomputed/a02-edit-fixtures.json 的 miniagent 一列由项目代码实跑生成,全部 26 个输入的裁决与结果 SHA-256 必须与页面上的浏览器移植一致(实验课右上角)。
  4. CRLF 与 BOM 文件改完后保持原换行与 BOM。当前 v0.5.0 不满足这一条:fixture 的「CRLF 文件」用例里,改一行后整份文件的换行都被改成了 \n(结果与原文件从第 1 行起 44 行不同),原因是 Python 的 read_text / write_text 做了换行转换;Pi 与 OpenCode 的做法见第 2 课。
  5. 在真实 trace 里至少找到一次 edit_file 成功的 diff:jq 'select(.type=="tool_call" and .name=="edit_file")' traces/*.jsonl。

坑与带走

本章拦截的误解

误解纠正在哪一课
整文件覆写最安全不会改错位置,但输出随文件长度增长(44 行的文件 487 对 54 token),还会覆盖并发改动第 1 课
模糊匹配越多越好容错只管「找」不管「写」:多转义的引号、弯引号被接住后原样写回,模块导入即语法错误第 2 课
唯一匹配就不会改错唯一只说明只有一处;真实 trace 里 old_string 恰好是已修好那行的前半截,+ 1 变成了 + 1 + 1第 2 课
约束解码让模型更会改代码它只保证语法;定位仍靠 seek_sequence,两处相同的行照样改到第一处第 3 课
模型执行了工具;工具定义只发一次模型只写下调用,执行在 harness;工具定义每一轮都发,第 1 轮占 76%岔路 B1
MCP 是让模型调用模型的协议MCP 连的是 harness 与工具、资源;Sampling 已在 2026-07-28 版废弃,官方建议直接对接模型 API第 4 课
废弃 = 已经移除;Codex 也能当 MCP server废弃窗口至少 12 个月;codex mcp-server 已于 2026-09-05 删除第 4 课
Code Mode 更安全它关住的是脚本,脚本调用的工具照样有全部能力岔路 B2

常见的坑(写代码时)

症状后果怎么办
行号寻址,第二次编辑落错位置静默改错,测试可能仍失败改用唯一片段锚定(第 1 课)
只查存在、改第一处重复片段时改错位置,还报「成功」先数出现次数,>1 就报错(第 2 课)
模糊匹配没有护栏一次替换吃掉大片无关代码比例护栏 + 歧义拒绝(第 2 课)
CRLF / BOM 让精确匹配失败,或改一行换掉全文换行明明一样却报 not found;diff 里每一行都变了在归一化空间里匹配,按原格式写回(Pi edit.ts:344-350)
全量加载所有 MCP 工具每轮前缀膨胀;中途增删工具让缓存失效deferred + tool search(第 4 课)

带走这些句子

  1. 行号在第一次编辑后就会过期,锚定片段不会。
  2. 容错与改错是同一把刀的两面,护栏决定它往哪边切。
  3. 能用解码约束保证的格式,就别靠 prompt 教;约束只管格式,不管定位。
  4. MCP 是接工具的协议,不是接模型的协议;接进来的每个工具每轮都要付上下文。
  5. 模型从不执行工具,它只写下调用;工具定义每一轮都随请求重发。
  6. Code Mode 省的是往返次数,不省判断,也不自动更安全。
  7. 同一条 edit,五个引擎给出三种裁决;只有跑过测试才知道谁对。

深入这一章

按你愿意花的时间选。每条都实际打开核对过(2026-09-26);站外链接需联网。

🎮 想更直观 10–30 分钟

先看各家怎么落地 edit,再回来看五张引擎卡
16 min英文需联网 2025-04-26
Codex、Aider、OpenHands、RooCode、Cursor 怎样接收、应用 AI 生成的 edit,以及应用失败时怎样一级级降级。
对应第 1、2 课。第 2 课的「精确 → 忽略空白 → 相似度」阶梯在这里有各家的横向对照;Cursor 部分基于公开访谈,不是源码,读时注意。
10 min英文需联网 页面 2025-11-20 更新
225 道练习题上各模型的通过率,以及每个模型用哪种 edit 格式、「格式正确」的比例(展开行可见 Percent cases well formed)。
对应第 1、3 课。本章那张格式分布图用的就是它的数据文件;看完会明白为什么「格式」本身值得单独一课。

🔬 想吃透原理 1–3 小时

唯一必看:Anthropic《Writing effective tools for AI agents》。其余按课挑
长文英文需联网 2025-09-11
工具该怎么设计、描述怎么写、怎样用评测(甚至让 agent 自己)改进工具。
对应岔路 B1 与第 2 课。本章反复出现「工具描述与报错信息每轮都进 prompt」;这篇把它讲成了可重复的工程流程,是本章所有取舍的出发点。
长文英文需联网 2025-11-04
工具定义与中间结果如何撑爆上下文,以及改成「写代码调工具」后的量级:150,000 → 2,000 token。
对应第 4 课与岔路 B2。官方自报的示例场景,拿来当量级参考线,不当评测。
20:31英文需联网 2024-04-02
SWE-agent 作者之一讲 agent-computer interface(ACI)的设计怎样决定了 SWE-bench 分数,包括编辑命令的设计。
对应第 1、2 课。「ACI」一词出自 SWE-agent 论文;偏早,但「工具接口决定成绩」的论证至今成立。
27:15英文需联网 Mastra 频道 · 2026-03-31
Pi 作者讲为什么在用过 Claude Code、Codex、OpenCode 等之后自己写一个极简 agent,以及刻意不做的东西。
对应第 2 课的 Pi 引擎。作者本人口述的设计取舍,听完再看 Pi 的四个默认工具与归一化匹配。
1 h 58 min英文需联网 11 节视频
按课时从零写 MCP server 与 client,连到 Claude。
对应第 4 课。想系统动手时看;课程早于 2026-07-28 版,握手部分以本章为准。

📄 想读原文 按需

规范与源码都钉在具体版本上
规范需联网
无状态化(去掉 initialize 握手、_meta 自带版本与能力、server/discover)、废弃 Sampling/Roots/Logging、tools/list 稳定排序。第 4 课的四条都出自这里。
规范提案需联网
经 MCP Resources 分发 SKILL.md 的扩展,2026-09-13 合并为 Final;SDK 实现仍在进行中。第 4 课提到,A6 展开。
源码需联网
19 行 Lark 语法原文,第 3 课逐行讲过;最新版移到 core/assets/tools/,内容相同。
官方文档需联网
自定义工具可带 lark 或 regex 文法,采样由 LLGuidance 约束。第 3 课的机制出处。
官方文档需联网
strict: true 用约束采样保证工具参数符合 JSON Schema,以及数量限制。第 3 课。
官方文档需联网
whole、diff、diff-fenced、udiff、editor-diff / editor-whole 各自的样子。第 1 课。
论文需联网
ACI 的出处:为 agent 专门设计的编辑、搜索、查看命令显著提升了 SWE-bench 表现。本地 PDF 待统一下载。
🚫

我替你排除了什么:Jimmy Song《MCP 协议架构》(中文,2026-08-14 更新;本想作「想直观」的中文资源,核对后发现它的 _meta 键名没有 io.modelcontextprotocol/ 前缀、三项废弃功能的迁移去向也与官方规范不一致,暂不收)· MCP Inspector 作「想直观」资源(它是调试工具,要本地跑 npx,适合在项目里用)· analysis_claude_code(逆向的是 Claude Code v1.0.33,版本太旧)· agent-prompt-observatory(小项目,只借鉴它的可视化方式)· 任何「Aider 实测证明某种 edit 格式更好」的二手转述(本轮没找到一手实验)· 「Anthropic 提出 ACI」的说法(ACI 出自 SWE-agent 论文)。