工具定义每轮占多少?一次工具调用在网络上长什么样?
本课一问:第 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 课的主角)。
先自己设计:工具定义只发一次
直觉上,工具清单像「开场白」:第一轮告诉模型有哪些工具,之后模型就记住了。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
如果第 2 轮起不再发工具定义,模型就不知道 edit_file 的参数叫什么、old_string 必须唯一——它看到的只有历史里自己写过的几次调用。
每一轮都发。代价在上面的数字里;省钱的办法不是少发,而是让「每轮都一样的开头」被服务端缓存起来,下一轮直接复用(A8 讲计费)。为此,开头必须一字不变——下面的回放里可以直接验证。
逐轮回放
数据:本课项目 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:
- token 数对得上:每一轮的输入 token,用项目模型的 tokenizer 和 chat template 重算,与服务端返回的
usage.prompt_tokens逐轮完全一致 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:transformers 的 apply_chat_template(enable_thinking 与录制时一致)对 trace 里的消息重算,7 轮全部与服务端 usage 相等。 出处待补 。 - 前缀不变:第 n+1 次请求的文本逐轮都以第 n 次请求的全部文本开头;按 token 算,相同的是前「上一轮长度 − 1」个——最后那 1 个不同,是因为模板里「轮到模型说话」那个换行,和下一轮接上的内容被切成了同一个 token。
- 工具调用就是文字:第 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 无状态,每次请求必须自带全部上下文 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条请求体里没有「上一轮」的引用;模型能用的只有这次请求里的 token。前缀缓存是服务端的计算复用,不改变这一点(A8)。 出处待补 |
| ③ 实验上呢? | 本课项目的真实 trace:7 轮里工具定义合计 4,795 token,占输入的 46% 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:单一模型、单一任务;工具多、轮数多时比例会变。 出处待补 |
演进地图:你在这里
这一课回放的 edit_file 调用,属于演进图里的「唯一片段替换」一代。
- 解决了模型不用复述原文,只报行号,输出最短。卡在行号是「上一次读文件时」的快照:第一处修复只要多插 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 课,看第 4、5 轮那两次报错该怎么处理。
再深一层:把回放拖到最后一轮,看模型最后的总结:它说测试已经能通过——而 harness 自己跑测试的结果是没通过。这是 A1「模型说做完 ≠ 真做完」的又一个真实例子。
下一个缺口
回主线:片段锚定,匹配不上或匹配到两处怎么办? → 第 2 课
你可能会问
- 「服务端返回的 cached_tokens 为什么和算出来的前缀长度不完全一样?」 本机的 mlx_lm.server 有跨请求的前缀缓存,还会把上一轮自己生成的 token 一起缓存;前几轮它报的数比「上一轮请求长度」大。项目 README 专门提醒了这一点。A8 用云端 API 的 usage 讲计费。
- 「别的 harness 发的请求也这样吗?」 结构一样(工具区 + 历史,前缀不变);模板、工具数和描述长度各不相同。录制 Claude Code、Codex 等真实 harness 的原始请求需要代理工具(如 claude-tap),本章没有做,列在项目线的待录清单里。