为什么不用框架自带的自动多步?
本课一问:第 2 课自己写了一个 while。可很多 SDK 已经自带「自动多步」:给它工具清单和最大步数,它自己请求、执行工具、再请求,直到模型不再调工具。为什么生产 harness 还要自己写这个循环?
这一课先不碰:选哪家 SDK、要不要自建模型接入层(A8);子 agent 的编排(A7)。
从已有出发
第 2 课的循环在每一圈都做同一件事:把 messages[] 整份发出去、执行工具、追加结果。真实的 harness 在「追加结果」和「下一次发出去」之间,还要做很多别的事。
为什么这里不用贯穿任务的 trace:这一课的问题出在「循环由谁来写」,trace 里看不出来,只能读源码;贯穿任务在本课项目 miniagent 里的样子,就是下面表格里「只有第 ③ 层」的那一种。
先自己设计:把多步交给框架
最省事的做法:用 SDK 的自动多步(比如 Vercel AI SDK 的 streamText 配 stopWhen),工具的 execute 函数交给 SDK 调,一次调用就跑完整个任务。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
streamText 时不传 stopWhen(pin v1.18.32 的 packages/opencode/src 里这个参数出现 0 次),每次只让 SDK 跑一步;多步由 prompt.ts:1088 的 while (true) 自己驱动。两次模型调用之间,这个循环要做的事列在右边——交给 SDK 自动转圈,它们就没有地方做了。 试三件事:① 在右边的源码里点 ⑧:工具清单是每一步现算的——框架的自动多步通常在开头拿一次工具清单,整个过程不变;② 点 ④ 和 ⑤:压缩只能插在两次调用之间,这是 A5 的前提;③ 回想第 3 课 OpenCode 的退出条件:它是在 ② 这里、由 OpenCode 自己判的,不是 SDK 判的。
改进:循环分几层
教学蓝本只有一层循环。Codex 把它拆成了四层,每一层回答一个不同的问题:
| 层 | 在哪(pin 8cabf5a) | 它转一圈是什么 | 在这一层能做的事 |
|---|---|---|---|
| ① 提交层 | session/handlers.rs:706 submission_loop | 收到一个提交(用户消息、中断、配置变更…) | 中断、切换设置、启动新任务 |
| ② 任务层 | tasks/regular.rs:73-90 | 跑一个 turn;turn 结束后如果还有待处理的用户输入,再跑一个 | 把「做完之后的追加」接成新的 turn(岔路 B1) |
| ③ turn 层 | session/turn.rs:278-558 | 一次采样 + 执行它要的工具;这就是第 2 课的那个循环 | 退出判定、压缩、Stop hook、预算提醒 |
| ④ 流层 | session/turn.rs:2215-2740(try_run_sampling_request) | 一个流式事件 | 边收边执行工具、流错误重试(第 3 课的截断重发) |
OpenCode 的 while(true) 大致对应第 ③ 层,它的 ① ② 由 HTTP 服务与会话管理承担;本课项目 miniagent 只有第 ③ 层——一次性命令行工具,不需要其余几层。
框架的自动多步替你转圈,也就拿走了「两次调用之间」这个位置。上面那十来件事——压缩、提醒注入、按权限重算工具、退出的双重校验、截断重发——都要在那里做。OpenCode 用着同一个 SDK,仍然选择每次只让它跑一步。至于它这样选的原因,源码里没有注释说明,上面是根据代码结构的归纳 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条「不传 stopWhen、自己驱动循环」是源码直证;「为了在两次调用之间插入逻辑」是本课根据代码结构的推断(调研报告同样标为分析者推断)。 出处待补 。
🔍 那框架的自动多步什么时候合适?
任务短、工具少、不需要压缩和权限中断时,它省事而且不容易写错——比如 A0 讲的「一段代码就够」的 workflow 里的一个步骤。一旦需要在调用之间改写上下文(A5)、中途等用户批准(A3)、按步数或预算收尾(第 4 课),就得把循环拿回自己手里。OpenCode 自己也在试验另一条路:它的 v2(packages/core)把 runner 拆成了独立模块,但截至 2026-09-26 还没有启用,课程不把它当现行设计讲。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 在两次模型调用之间保留一个 harness 能插手的位置 |
| ② 为什么可能有效? | 压缩、提醒、权限、退出判定都依赖「上一轮的结果」和「下一轮要发什么」,只能放在两次调用之间 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条由循环结构推出;各家把哪些逻辑放在这个位置,是源码直证。 出处待补 |
| ③ 实验上呢? | 没有找到「框架自动多步 vs 自写循环」的对照实验;这一课的依据全部是源码 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条源码直证:OpenCode 不传 stopWhen(0 处)、Codex 四层循环(pin 行号见上表)。 出处待补 |
演进地图:你在这里
- 解决了不需要任何协议:模型本来就会写命令。卡在真实基线 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),注释写明是防评估器「假阴性」;每轮多一次模型调用。
一句话
回主线
那它什么时候停?「没有工具调用就停」够不够? → 第 3 课
你可能会问
- 「本课项目该不该换成框架的自动多步?」 不该:它在两次调用之间要做 A5 的压缩、A6 的记忆刷新和第 3 课的提醒重发,正是框架拿走的那个位置。
- 「OpenCode 用的 AI SDK 是哪一个?」 Vercel AI SDK;OpenCode 同时在试验去 SDK 化的另一套 runner(packages/core 里的 v2),截至 2026-09-26 没有启用。模型接入的三条路线(直连 SDK / 自建抽象层 / 第三方 SDK)是 A8 的内容。