同一个任务跑了很多次,循环每次怎么转、在哪一轮、因为什么停?
这一课做什么:前面每一课只取一两次运行讲一个点。这一课把本课项目的 26 次真实运行全部摊开(合计 517,684 token;验收通过 9 次;停止原因:no_tool_calls 21 次、max_rounds 2 次、length 2 次、dropped_tool_call 1 次),用四个视图去读它们。所有数字都来自 trace,没有一个是手写的。
实验一:轮数分布与上限
数据:本课项目 miniagent 在贯穿任务上的 20 次真实运行(Qwen3-8B-MLX-4bit,2026-09-26);截断后的验收结果由 gen_a01_loop_data.py 重放每次运行前 N 轮的 edit_file 并实跑 python -m unittest -v test_dateparse 得到。只是这一个模型、这一个任务上的 20 次,用来看形状,不能外推成「上限该设多少」的通用答案。
试三件事:① 找出「上限 N 下由过变不过」降到 0 的最小 N,再找「由不过变过」出现的 N——想想为什么截得早反而救了一次;② 点任一方块,下面的回放会跟到那次运行的第 N 轮(实验二);③ 把 N 拖到最右:所有方块都不再被截断,这就是「不设上限」时这 20 次的真实结果。
实验二:逐轮回放任意一次运行
数据:本课项目 miniagent 的真实运行(src/data/project/agent-traces.json,2026-09-26 导出;模型 Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。token 数取自服务端 usage;思考截取约 600 字、正文与工具输出约 900 字(保留头尾)。耗时受本机同时运行的其他任务影响,只能粗看;token 与轮数才可比。
试三件事:① 选 pd-promptjson-s1(提示词约定 JSON 的那次):第 5、6、8、9 轮都是写满 4096 token 的截断回复,看 harness 每次追加的紫色提醒;② 选 pd-v2-firstmatch-s1:看「权限裁决」那一行,第 2、6 轮的测试命令被规则拒绝(A3 的伏笔),模型之后怎么绕;③ 选 pd-v2-unlimited-s1,翻到第 7 轮:同一个 edit_file 又发了一遍,验收因此没过(A2 的伏笔)。
实验三:并排对照
同一个 harness、只差一个变量的两次运行,按轮对齐:
数据:本课项目 miniagent 的真实运行(src/data/project/agent-traces.json,2026-09-26 导出;模型 Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。token 数取自服务端 usage;思考截取约 600 字、正文与工具输出约 900 字(保留头尾)。耗时受本机同时运行的其他任务影响,只能粗看;token 与轮数才可比。
数据:本课项目 miniagent 的真实运行(src/data/project/agent-traces.json,2026-09-26 导出;模型 Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。token 数取自服务端 usage;思考截取约 600 字、正文与工具输出约 900 字(保留头尾)。耗时受本机同时运行的其他任务影响,只能粗看;token 与轮数才可比。
实验四:结构与判定
数据:本课项目 miniagent 的真实运行(agent-traces.json,Qwen3-8B-MLX-4bit,2026-09-26)。节点与次数全部由 trace 的 rounds[].tool_calls 现数。
真实序列取自本课项目 miniagent 的 trace(Qwen3-8B-MLX-4bit,录制于 2026-09-26);构造序列只用来检验谓词,标「构造事件序列 · 真谓词」。每格的判定由浏览器里的 JS 移植现算,并与生成脚本在 pin 源码上跑出的期望值比对(a01-termination.json,2026-09-26)。源码链接需联网。
最后,换一次运行,把 messages[] 的增长再走一遍——这次是一次失败的运行,看它在哪一步走偏:
数据:本课项目 miniagent 的真实运行 pd-v2-unlimited-s1(harness v2 · A1 无轮数上限(seed 1),录制于 2026-09-26;模型 Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。每条消息的 token 用该模型的 tokenizer 与 chat template 逐条归属(a01-loop-data.json),8 轮请求的合计与服务端 usage.prompt_tokens 逐轮一致。左栏代码是 04-实践项目/miniagent/loop.py 在 2026-09-26 17:01 的快照,只显示与循环有关的行。
别从这些数字读出「这个模型修 bug 的成功率」。单一模型(4bit 量化的 8B)、单一推理服务、一个主任务、每种配置一到三次 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:用来看循环的形状与失败的样子;频率与比例不能外推。 出处待补 。它们能说明的是:截断、丢弃、误停、重复修改这些现象都会在真实运行里发生,而且 trace 足够让你逐轮找到它们。
自己动手
- 在
04-实践项目里多跑几次(或换一个模型):.venv/bin/python -m miniagent run --task parse_date --run-id my-run --max-rounds 0; - 用项目的导出脚本更新
src/data/project/agent-traces.json,再运行src/data/precomputed/gen_a01_loop_data.py与gen_a01_termination.py(需要 bun); - 刷新本页:新运行会出现在下拉框和方块图里;TerminationSim 右上角的一致性检查会重新覆盖全部序列。
演进地图
这一课的四个视图,覆盖了演进图的主线四代。
- 解决了不需要任何协议:模型本来就会写命令。卡在真实基线 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),注释写明是防评估器「假阴性」;每轮多一次模型调用。