一次调用不够,怎么让它连续干活?
本课一问:上一课的协议让模型能动一步:写下一个调用,harness 执行、回填。可修一个 bug 至少要「跑测试 → 读文件 → 改 → 再跑测试」四步,第二处 bug 还藏在第一处后面。一次调用不够,怎么让它连续干活?
这一课先不碰:「不再调工具」是不是可靠的停止信号(第 3 课);要不要设上限、设多少(第 4 课);用户在它干活时插话(岔路 B1);框架自带的多步(岔路 B2)。
从已有出发
第 1 课的结论:模型只写下调用,harness 执行并把结果作为一条 role: tool 消息追加;下一次请求带着它发出去,模型就「看见」了结果。要走下一步,只要再请求一次。
贯穿任务还是修 test_parse_date。真实运行里模型要做的事大致是:列目录、读 dateparse.py、改第一处、跑测试(这时第二处才暴露)、改第二处、再跑测试、写总结。
先自己设计:固定调 3 轮
最直接的写法:把「请求 → 执行 → 回填」重复固定的次数,比如 3 次(for _ in range(3))。它一定会停,也能连着走几步。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
数据:本课项目 miniagent 在贯穿任务上的 20 次真实运行(Qwen3-8B-MLX-4bit,2026-09-26);截断后的验收结果由 gen_a01_loop_data.py 重放每次运行前 N 轮的 edit_file 并实跑 python -m unittest -v test_dateparse 得到。只是这一个模型、这一个任务上的 20 次,用来看形状,不能外推成「上限该设多少」的通用答案。
问题在于轮数是任务决定的,不是写代码的人能提前知道的:同一个任务、同一个模型,自己停下的真实运行从 5 轮到 21 轮都有(另有 2 次被录制时设的上限截断)。所以把判断交给模型:模型还在调工具,就接着转;模型给出一段不再调工具的回复,就停。轮数由这一次任务自己长出来。
试三件事:① 默认「只调 3 轮」:所有方块都在红线右边,全部会被截断;② 把 N 拖到 7:只剩 7 轮以上的运行被截断,「由过变不过」只剩 1 次;③ 点那个最右边的绿方块(21 轮,pd-v2-layered-s1):它要到第 19 轮修好的那一刻,截在更早的任何地方验收都过不了。
改进:循环到模型不再调工具为止
Anthropic 的官方文档把这个循环写成五段:接收(提示词、system prompt、工具定义、此前的对话)→ 评估并回复(模型回一段文字、一个或多个工具调用,或两者都有)→ 执行工具(harness 跑每个调用、收集结果)→ 重复(后两段反复,每转一圈叫一个 turn)→ 返回(模型给出不含工具调用的回复时,循环结束) 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条五段循环是官方文档的描述(深度调研 3-0 满票)。「turn」是 Claude SDK 的记账术语,不是跨厂商的通用概念(第 3 课)。 出处Claude Agent SDK · How the agent loop works(官方,2026-09-26 核对) ↗需联网 。
教学蓝本里,整个循环就是一个 while(或 for)加一个判断。CoreCoder 的最小形态:
learn-claude-code 的 s01 更短:while True,没有上限,没有 tool_use 块就 return(s01_agent_loop/code.py:87-117)。本课项目 miniagent 的 loop.py 也是同一个形状,下面就用它录下的一次真实运行,一步一步走:
数据:本课项目 miniagent 的真实运行 pd-v2-unlimited-s3(harness v2 · A1 无轮数上限(seed 3),录制于 2026-09-26;模型 Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。每条消息的 token 用该模型的 tokenizer 与 chat template 逐条归属(a01-loop-data.json),7 轮请求的合计与服务端 usage.prompt_tokens 逐轮一致。左栏代码是 04-实践项目/miniagent/loop.py 在 2026-09-26 17:01 的快照,只显示与循环有关的行。
试三件事:① 每到「回复」这一步先猜模型会调哪个工具——第 4 轮它会跑测试,第 5 轮修第二处(它是看了第 4 轮的测试输出才知道的);② 看每一次「请求」的括号:括住的永远是从第一条开始的全部,不是新增的那几条;③ 点一个很高的绿色块(第 2 轮 read_file 的结果),看它的原文:这 400 多个 token 之后每一轮都会再发一遍。
同一次运行,换一种画法:
数据:本课项目 miniagent 的真实运行(agent-traces.json,Qwen3-8B-MLX-4bit,2026-09-26)。节点与次数全部由 trace 的 rounds[].tool_calls 现数。
展开图是「这一次」:模型 7 次、工具 6 次、一个出口。切到「折成环」,同名节点合并、边上标次数,就只剩「模型 ⇄ 工具」一个环——所谓 agent loop,就是这个环。环丢掉了顺序;要知道第 5 轮为什么改第二处,得回到展开图、看第 4 轮的测试输出。
极小真数字:每轮整份重发,账怎么算
模型 API 不记得上一次请求:每次请求都必须自带全部上下文。所以第 n 次请求 = 工具定义 + 此前的全部消息 + 模板补的几个标记,而各轮请求的总和比对话本身长得多:
模型 API 是无状态的:请求体里没有「上一轮」的引用,模型能用的只有这一次发来的 token。上面的括号每一轮都从第一条括起。服务端的前缀缓存(prefix cache)能让重复的开头算得更快、更便宜,但它是服务端的计算复用,请求里照样是整份——本课项目的服务端从第 2 轮起报告的缓存命中,中位数占 prompt 的 91%,可 prompt_tokens 一个都没少。
🔍 为什么 miniagent 用 while True 而 CoreCoder 用 for range(50)?
这正是第 4 课的问题。本课项目的 LoopConfig.max_rounds 默认 30、可设为 None(不设上限),里程碑 M1 要你拍板;CoreCoder 默认 50;learn-claude-code 的 s01 与 Claude Agent SDK 的 max_turns 默认都不设上限(官方文档表格:Max turns · Default: No limit)。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 让模型根据上一步的结果决定下一步,轮数由任务决定而不是写死 |
| ② 为什么是这样? | 模型 API 无状态,只能把完整历史随每次请求发出 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条请求体里没有指向上一轮的引用;本页每一轮的 prompt_tokens 都能由「工具定义 + 此前全部消息 + 模板标记」逐条加出来,与服务端 usage 一致(a01-loop-data.json:21 次运行、129 轮全部相等)。 出处待补 ;停止交给「模型不再调工具」这个信号,是设计选择 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条官方五段循环(满票)。它是否足够,见第 3 课。 出处Claude Agent SDK 文档 ↗需联网 |
| ③ 实验上呢? | 贯穿任务 20 次真实运行,自己停下的从 5 轮到 21 轮;固定 3 轮会让 6 次成功里的 6 次失败 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测 + 重放:截在第 3 轮、把前 3 轮真实执行的 edit 写回任务仓副本后跑验收(gen_a01_loop_data.py 的 cap_replay,N = 实际轮数时与录制结果 20/20 一致)。 出处待补 |
演进地图:你在这里
- 解决了不需要任何协议:模型本来就会写命令。卡在真实基线 T1a:同一个模型、同一个任务跑 8 次,执行抠出来的命令后测试通过 0 / 8;其中 6 次抠出的 GNU 写法
sed -i在 macOS 上直接报错,改成 BSD 写法再跑也 0 次通过——一次回复只看得见第一处 bug。 - 解决了格式由模型训练与服务端解析器负责,harness 拿到的是 name + JSON 参数;执行结果以
role: tool回填,模型能看见后果。卡在一次调用只能走一步:贯穿任务的真实成功运行,最少也用了 6 轮(读 → 改 → 测出第二处 → 改 → 再测)。协议之下仍然是文字:24 次原生调用的运行里,服务端解析器 7 次把 JSON 不合法的调用整条丢掉。 - 解决了能连续走几步了,而且一定会停。卡在贯穿任务的 20 次真实运行里,3 轮以内收工的 0 次;只调 3 轮,6 次成功里有 6 次变成失败(重放前 3 轮的 edit 后实跑验收)。
- 解决了轮数由任务决定,不由写死的数字决定;状态只有一个 messages[],每轮整份重发。卡在「没有工具调用」不等于「做完了」:harness v1 有 3 次运行因服务端丢了调用而误停;26 次里模型自己停下 21 次,其中 13 次验收没通过。
- 解决了finish_reason 和消息体都看:说要调工具却没给调用、被截断、报了不认识的结束原因,都不当成「做完了」。卡在补救也会失败:pd-v2-nothink-s1 连续 3 次发出被丢弃的调用,提醒 2 次无效后以
dropped_tool_call停;而且它不回答「要不要强制停」。 - 解决了死循环有了兜底;到上限时可以返回显式原因,或让模型先收尾。卡在上限 4:2 次运行都在第 4 轮刚改完第一处时被截断,模型毫不知情;不设上限的 6 次全部自己停下(最长 8 轮)。重放:上限 6 碰巧救回 2 次(截在重复 edit 之前),而最长那次成功运行要到第 19 轮才修好。
- 解决了用户不必等整次运行结束才能纠偏。卡在插话时机本身是设计决策:同一条事件序列里,4 个生产循环把插话送进下一次请求,3 个教学 / 项目循环要等这次运行结束。
- 解决了压缩检查、提醒注入、权限中断恢复都能插在两次调用之间。卡在循环要自己维护:Codex 把它拆成四层(submission / task / turn / stream),教学蓝本只写了其中一层。
- 解决了「模型说做完了」交给另一个没有工具的模型核对。卡在评估器也会错:Hermes 给目标循环设了 20 轮上限(pin 56dc01d9 config_defaults.py:1801),注释写明是防评估器「假阴性」;每轮多一次模型调用。
一句话
先停在这里:记住那道每轮都从第一条括起的括号。
再深一层:两条岔路——用户在它干活时插话怎么办?(循环里还有第二种输入)、为什么不用框架自带的自动多步?(生产 harness 为什么坚持自己写这个 while)。
下一个缺口
那它什么时候停?「没有工具调用就停」够不够? → 第 3 课
这一课的停止条件只有一个:回复里没有工具调用。可真实运行里,有的回复是被截断的、有的是服务端把调用丢了——它们也「没有工具调用」。