要不要给循环设上限?设多少?
本课一问:第 3 课的循环停不停由模型的回复决定。模型要是一直调工具、就是不收工呢?要不要给循环设一个轮数上限?设多少?
这一课先不碰:上下文越来越长导致的退出与压缩(A5);子 agent 的预算怎么和父 agent 分账(A7);按钱算的上限(A8)。
从已有出发
第 3 课最后留下的例子:遇到「说要调工具却没给调用」,OpenCode 原样重发,而且没有专门的次数限制。把那张表切到「长任务:连续 55 轮都在调工具」看各家默认值在哪里咬住:
真实序列取自本课项目 miniagent 的 trace(Qwen3-8B-MLX-4bit,录制于 2026-09-26);构造序列只用来检验谓词,标「构造事件序列 · 真谓词」。每格的判定由浏览器里的 JS 移植现算,并与生成脚本在 pin 源码上跑出的期望值比对(a01-termination.json,2026-09-26)。源码链接需联网。
只有 CoreCoder(第 50 轮)和本课项目(默认 30)被上限停下;另外 6 个(含构造的朴素谓词)一直转到第 56 轮模型自己收工。其中几家不是忘了设上限,是故意不设——下面看为什么。
先自己设计:两个朴素答案
答案 A:不设上限(learn-claude-code s01 的 while True)。模型不收工,循环就不停:烧钱、占着机器,直到有人手动中断。本课项目 6 次不设上限的运行全部自己停下,最长 8 轮 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测,样本很小:只说明这个模型在这个任务上没有跑飞,不能说明不会跑飞。 出处待补 ——但「这次没跑飞」不是兜底。
答案 B:设一个硬上限(CoreCoder max_rounds = 50,到上限返回一句「(reached maximum tool-call rounds)」)。代价是截断:
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
数据:本课项目 miniagent 在贯穿任务上的 20 次真实运行(Qwen3-8B-MLX-4bit,2026-09-26);截断后的验收结果由 gen_a01_loop_data.py 重放每次运行前 N 轮的 edit_file 并实跑 python -m unittest -v test_dateparse 得到。只是这一个模型、这一个任务上的 20 次,用来看形状,不能外推成「上限该设多少」的通用答案。
硬上限不是错,它把「死循环」换成了「静默截断」。两种风险之间没有免费的默认值,各家的选择其实是在回答三个问题:到上限时硬停还是让模型先收尾?不设上限时靠什么兜底?截断发生时留不留痕迹?下面是五种回答。
max_rounds 停下——模型在第 4 轮不知道这是最后一轮,验收没通过。把上限拖到 N 看重放:N = 6 时有 2 次由不过变过(截在一次重复修改之前,纯属碰巧),N 在 19 以下总有成功被截没,N ≥ 19 才一次也不伤。 试三件事:① 点预设 4:两个斜纹方块就是录制时自带上限 4 的运行;点一个,下方的逐轮验收条全是「·」;② 点预设 6:2 个灰方块变成了「由不过变过」——点开它们,第 6 轮验收通过、第 7 轮又没了:第 7 轮模型把同一个修改又发了一遍(A2 讲的 + 1 + 1);③ 点预设 20:只剩最右边那个 21 轮的运行被截断,而它的修复在第 19 轮就落地了,截在 20 验收照样通过。
数据:本课项目 miniagent 的真实运行(src/data/project/agent-traces.json,2026-09-26 导出;模型 Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。token 数取自服务端 usage;思考截取约 600 字、正文与工具输出约 900 字(保留头尾)。耗时受本机同时运行的其他任务影响,只能粗看;token 与轮数才可比。
小上限防住的是跑飞,换来的是在任务中途被切断:上面左栏第 4 轮,模型刚改完第一处、正准备跑测试,循环就停了,它的回复里没有任何「我还没做完」的迹象,因为它不知道自己被截断了。截断会不会留痕迹,取决于 harness:本课项目在 trace 里写 max_rounds,CoreCoder 返回一句固定的话,而用户看到的可能只是「它停了」。Hermes 在 2026-08 把默认上限改成不限,代码注释给的理由就是「caps caused silent mid-task truncation」 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条Hermes 最新版 config_defaults.py 的注释(commit c32119b12,C 报告核实);属于项目方基于生产经验的自述,没有公开数据。本课 pin 56dc01d9 上默认仍是 500,见勘误表 H2。 出处待补 。
改进:五种设计
| 设计 | 谁 | 到上限 / 预算用完时 | 靠什么兜底 | 源码 |
|---|---|---|---|---|
| 硬上限 | CoreCoder;本课项目(默认 30) | 直接停,返回固定的话(CoreCoder)或写明 max_rounds(miniagent) | 上限本身 | CoreCoder v0.4.0 agent.py:28,:54,:99 |
| 软上限 + 收尾提示 | OpenCode | 最后一步在消息末尾追加 MAX_STEPS_PROMPT(「已到最大步数,只能用文字总结」),让模型自己收尾;默认 agent.steps ?? Infinity,即不设 | 模型遵守提示 | prompt.ts:1178-1179,:1281 |
| 不设上限,靠压缩兜底 | Codex;Claude Agent SDK 的 max_turns 默认也不设 | 没有「到上限」这回事 | 源码注释:「as long as compaction works well in getting us way below the token limit, we shouldn’t worry about being in an infinite loop」 | turn.rs:441 |
| 预算 + 一次总结调用 | Hermes | 迭代预算用完,先以 budget_exhausted 退出,再发一次去掉工具的请求让模型总结,结果记为 max_iterations_reached | 父子 agent 各有独立预算(pin:500 / 50) | conversation_loop.py:1634,:1660-1669;turn_finalizer.py:95-142 |
| 交给评估器 | learn-claude-code s17;Hermes goals;Codex Stop hook | 模型想停时,另一个没有工具的模型判断「目标达成了没有」,没达成就让它继续 | 评估器自己也有次数上限 | 见岔路 B3 |
🔍 为什么 Codex 敢不设上限?
Codex 的循环里,每一轮采样结束都会检查上下文是否接近上限(turn.rs:383-439),接近就先自动压缩(run_auto_compact,:443)再继续。它的判断是:只要压缩能把 token 降下来,循环就不会因为上下文爆掉而失控;至于「模型永远不收工」,交给用户随时中断和 Stop hook。这是一个赌注,不是证明:压缩本身可能失败(A5 会看到压缩失忆的真实例子),所以 Codex 另外有 turn 级的错误出口(:546-555)。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 给「模型不收工」一个兜底,同时尽量不在任务中途静默截断 |
| ② 为什么没有标准答案? | 截断伤不伤,取决于上限落在「最后一次有效修改」之前还是之后,而这个位置每次运行都不同 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条重放的算术:上限 N 下的验收 = 前 N 轮修改写回后的测试结果;N 越过最后一次有效修改,结果才与不设上限相同(本页 20 次运行逐次算出)。 出处待补 ;各家选择反映的是对两种风险的不同估计 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条五种设计均为源码直证;动机来自代码注释(Codex :441、Hermes 最新版注释)与结构归纳。 出处待补 |
| ③ 实验上呢? | 本课项目 20 次:上限 4 截断 18 次,成功全灭;上限 20 截断 1 次,不伤成功;不设上限的 6 次都自己停下 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测 + 重放;单一模型、单一任务,不能外推出「上限该设多少」。 出处待补 |
演进地图:你在这里
- 解决了不需要任何协议:模型本来就会写命令。卡在真实基线 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停;而且它不回答「要不要强制停」。 - 第四代 上限与预算:硬上限 / 收尾提示 / 不设上限靠压缩兜底 / 预算加总结调用 CoreCoder 50 · OpenCode steps · Codex 不设 · Hermes 预算 你在这里解决了死循环有了兜底;到上限时可以返回显式原因,或让模型先收尾。卡在上限 4:2 次运行都在第 4 轮刚改完第一处时被截断,模型毫不知情;不设上限的 6 次全部自己停下(最长 8 轮)。重放:上限 6 碰巧救回 2 次(截在重复 edit 之前),而最长那次成功运行要到第 19 轮才修好。
- 解决了用户不必等整次运行结束才能纠偏。卡在插话时机本身是设计决策:同一条事件序列里,4 个生产循环把插话送进下一次请求,3 个教学 / 项目循环要等这次运行结束。
- 解决了压缩检查、提醒注入、权限中断恢复都能插在两次调用之间。卡在循环要自己维护:Codex 把它拆成四层(submission / task / turn / stream),教学蓝本只写了其中一层。
- 解决了「模型说做完了」交给另一个没有工具的模型核对。卡在评估器也会错:Hermes 给目标循环设了 20 轮上限(pin 56dc01d9 config_defaults.py:1801),注释写明是防评估器「假阴性」;每轮多一次模型调用。
一句话
先停在这里:这是里程碑 M1 要你拍板的决策:要不要 max_rounds、设多少、不设时靠什么兜底,理由要引用 T1 的轮数分布(见章首页的项目推进)。
再深一层:岔路 B3:谁来判定「真的做完了」——第五种设计,把「能不能停」交给另一个模型。
下一个缺口
循环转起来了,模型怎么说清「改哪里」? → A2 工具系统
上面那个上限 6 碰巧救回来的例子,病根不在循环:模型把同一个 edit_file 发了两遍,第二遍在已经改好的行上又加了一次 + 1,而唯一匹配检查照样放行。edit 该怎么设计,是 A2 的事。
你可能会问
- 「上限到了,能不能先让模型把话说完再停?」 能,这就是 OpenCode 的收尾提示和 Hermes 的总结调用。代价是多一次模型调用,而且模型未必照做(OpenCode 的工具仍在请求里)。
- 「按轮数设上限,还是按 token / 钱设上限?」 轮数便宜好懂,但一轮的成本差别很大(本课项目 26 次运行里,一轮请求的 prompt 从 875 到 2,618 token,一轮生成的 token 从 19 到 4,096);Claude Agent SDK 同时提供
max_turns和max_budget_usd。按钱算账是 A8 的内容。 - 「子 agent 的轮数算谁的?」 Hermes 在 pin 上给子 agent 独立的 50 轮预算(最新版 250),learn-claude-code 的子 agent 写死 30 轮(
s06_subagent/code.py:274)。A7 展开。