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

工具定义每轮占多少?一次工具调用在网络上长什么样?

一次工具调用的往返
用本课项目的真实 trace 逐轮回放一次修 bug:6 个工具的定义每一轮都随请求重发,在第 1 轮 899 个 token 里占 685 个;第 n+1 次请求的全部文本,开头原封不动就是第 n 次请求,只在末尾多了模型写的调用和工具的结果。模型从不执行工具,它只写下调用。
❓

本课一问:第 1 课说「模型给出 old_string,harness 去文件里找」。这一来一回在网络上到底长什么样?工具的定义要发几次、占多少?

这一课先不碰:发出去的 token 怎么计费、前缀缓存怎么省钱(A8);上下文装不下时怎么压缩(A5)。

从已有出发

A1 的循环图里,「调用工具」是一个箭头。这一课把那个箭头拆开:它其实跨了四个参与者——用户、harness、模型 API、操作系统里的工具进程。

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

用的仍然是贯穿任务的真实运行 pd-truncate-s1:本课项目 miniagent 带着 6 个工具,用本机的 Qwen3-8B 修 dateparse.py,一共 7 轮,第 4、5 轮的 edit_file 报了错(第 2 课的主角)。

先自己设计:工具定义只发一次

直觉上,工具清单像「开场白」:第一轮告诉模型有哪些工具,之后模型就记住了。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案工具定义只在第一轮发一次,之后模型记得

如果第 2 轮起不再发工具定义,模型就不知道 edit_file 的参数叫什么、old_string 必须唯一——它看到的只有历史里自己写过的几次调用。

逐轮回放

一次工具调用在网络上长什么样 · 逐轮回放

数据:本课项目 miniagent 的真实运行 pd-truncate-s1(A5 预算 2400 token,朴素截断(seed 1),2026-09-26;模型 Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。token 数用该模型的 tokenizer 与 chat template 重算,逐轮与服务端 usage.prompt_tokens 核对一致;「服务端缓存」是 mlx_lm.server 自报的 cached_tokens,它的前缀缓存跨请求保留,读数请看项目 README 的说明。

🎯

试三件事:① 第 1 轮:看构成条,紫色的工具定义占了 76%;展开「模型真正读到的文本」,找到 tools 标签里那 6 行 JSON;② 翻到第 4 轮:模型写的是一段文字,harness 执行 edit_file 后回了一句报错,下一轮请求末尾新增的正是这两块;③ 打开「相邻请求 diff」逐轮翻:每一轮的新请求,开头都原封不动是上一轮的全部内容。

回放里有三个值得记住的事实,都来自这份 trace:

  1. token 数对得上:每一轮的输入 token,用项目模型的 tokenizer 和 chat template 重算,与服务端返回的 usage.prompt_tokens 逐轮完全一致 。
  2. 前缀不变:第 n+1 次请求的文本逐轮都以第 n 次请求的全部文本开头;按 token 算,相同的是前「上一轮长度 − 1」个——最后那 1 个不同,是因为模板里「轮到模型说话」那个换行,和下一轮接上的内容被切成了同一个 token。
  3. 工具调用就是文字:第 4 轮模型写下的 edit_file 调用,在模型看来是 tool_call 标签包着的一行 JSON;工具的结果,则被模板包成 tool_response,放在一条 user 消息里。
⚠
常见误解:「模型执行了工具」「工具定义只在第一轮发一次」。

模型只写下「我要调 edit_file,参数是……」这段文字;读文件、比对 old_string、写回磁盘、跑测试,全部是 harness 在本机做的。回放里每个执行箭头旁都标着 miniagent 的权限裁决(allow),那是 A3 的伏笔:因为执行在 harness 手里,拦也只能在 harness 这里拦。工具定义则每一轮都发——模型 API 没有记忆。

🔍 工具描述是 prompt 资产:谁写、写多长

工具的描述不是代码注释,而是每一轮都送进模型的 prompt。OpenCode 把内置工具的描述外置成 16 个单独的 .txt 文件,方便 review 与改写:其中 edit.txt 用项目模型的 tokenizer 数是 286 个 token。本课项目 miniagent 的 edit_file 描述连同参数定义占 152 个。描述写长了,每一轮都付钱;写短了,模型可能用错——Anthropic《Writing effective tools for AI agents》讲的就是怎么用评测来改描述(本章唯一必看)。

证据三问

问本课的回答
① 它解决什么问题?看清一次工具调用的真实成本:工具定义每轮都付,历史每轮都重发
② 为什么是这样?模型 API 无状态,每次请求必须自带全部上下文
③ 实验上呢?本课项目的真实 trace:7 轮里工具定义合计 4,795 token,占输入的 46%

演进地图:你在这里

这一课回放的 edit_file 调用,属于演进图里的「唯一片段替换」一代。

模型怎么说清「改哪里」:每一代都被上一代的某个真数字逼出来
  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 课,看第 4、5 轮那两次报错该怎么处理。
再深一层:把回放拖到最后一轮,看模型最后的总结:它说测试已经能通过——而 harness 自己跑测试的结果是没通过。这是 A1「模型说做完 ≠ 真做完」的又一个真实例子。

下一个缺口

回主线:片段锚定,匹配不上或匹配到两处怎么办? → 第 2 课

你可能会问