从0打造编码Agent 导读 4 课 + 3 条岔路 + 1 节实验课 ≈ 4 小时
A1 循环:模型只会吐文字,它怎么改你的文件?什么时候停? 章导读

循环:模型只会吐文字,它怎么改你的文件?什么时候停?

语言模型只会生成文字。这一章从「让它把命令写进回复、程序抠出来执行」出发,被真实数字一步步逼到工具调用协议、循环、双重校验的停止条件和轮数上限。全部数据来自本课项目 miniagent 用本机 Qwen3-8B 的 26 次真实运行;6 个开源 harness 的终止判定被搬进浏览器,与原实现逐格比对。

主线定位

累计数据流 · 一次任务在 harness 里经过哪些环节 已学本课新增还没学
每一轮循环都经过

本章问题链

每一课只回答一个问题;上一课的失败,逼出下一课的问题。

课这一课的问题朴素方案在哪个真数字上失败一句话
第 1 课模型只会吐文字,它怎么「动手」?不给工具、正则抠命令:同一个模型 8 次,测试通过 0 次模型只提出调用,harness 负责执行
第 2 课一次调用不够,怎么让它连续干活?固定调 3 轮:20 次真实运行 3 轮内收工 0 次,重放下 6 次成功有 6 次变失败agent 的唯一状态是 messages[],每一轮都整份重发
岔路用户在它干活时插话怎么办?等运行结束再读:第 1 轮的插话要等 6 次模型调用之后才被看见插话什么时候被看见,是 harness 定的
岔路为什么不用框架自带的自动多步?OpenCode 的 streamText 不传 stopWhen,两次调用之间要做十来件事自己写循环,是为了保住「两次调用之间」这个位置
第 3 课「没有工具调用就停」够不够?v1 在 3 次运行里把「服务端丢了调用」当成写完了;重放下 3 次成功会变失败「没有工具调用」只是正常退出的一条路,生产循环要双重校验
第 4 课要不要给循环设上限?设多少?上限 4:2 次运行都截在刚改完第一处时,模型毫不知情上限是在死循环与静默截断之间取舍,没有免费的默认值
岔路谁来判定「真的做完了」?模型自己停下 21 次,验收没过 13 次停不停可以由模型说了算,做没做成要由模型之外的东西来判
实验课26 次真实运行:循环怎么转、在哪停—trace 让每一次都能逐轮查清

演进地图

模型只会吐文字,怎么动手、什么时候停:每一代都被上一代的某个真数字逼出来
  1. 解决了不需要任何协议:模型本来就会写命令。
    卡在真实基线 T1a:同一个模型、同一个任务跑 8 次,执行抠出来的命令后测试通过 0 / 8;其中 6 次抠出的 GNU 写法 sed -i 在 macOS 上直接报错,改成 BSD 写法再跑也 0 次通过——一次回复只看得见第一处 bug。
  2. 第一代 工具调用协议:模型写结构化的调用,harness 执行后回填 tools 参数 + tool_calls + role: tool
    解决了格式由模型训练与服务端解析器负责,harness 拿到的是 name + JSON 参数;执行结果以 role: tool 回填,模型能看见后果。
    卡在一次调用只能走一步:贯穿任务的真实成功运行,最少也用了 6 轮(读 → 改 → 测出第二处 → 改 → 再测)。协议之下仍然是文字:24 次原生调用的运行里,服务端解析器 7 次把 JSON 不合法的调用整条丢掉。
  3. 朴素 固定调用 3 轮 for _ in range(3)
    解决了能连续走几步了,而且一定会停。
    卡在贯穿任务的 20 次真实运行里,3 轮以内收工的 0 次;只调 3 轮,6 次成功里有 6 次变成失败(重放前 3 轮的 edit 后实跑验收)。
  4. 第二代 循环到模型不再调工具为止 CoreCoder、learn-claude-code s01
    解决了轮数由任务决定,不由写死的数字决定;状态只有一个 messages[],每轮整份重发。
    卡在「没有工具调用」不等于「做完了」:harness v1 有 3 次运行因服务端丢了调用而误停;26 次里模型自己停下 21 次,其中 13 次验收没通过。
  5. 第三代 双重校验 + 截断防护 + 可枚举的退出原因 OpenCode、Pi、Hermes;本课项目 v2
    解决了finish_reason 和消息体都看:说要调工具却没给调用、被截断、报了不认识的结束原因,都不当成「做完了」。
    卡在补救也会失败:pd-v2-nothink-s1 连续 3 次发出被丢弃的调用,提醒 2 次无效后以 dropped_tool_call 停;而且它不回答「要不要强制停」。
  6. 第四代 上限与预算:硬上限 / 收尾提示 / 不设上限靠压缩兜底 / 预算加总结调用 CoreCoder 50 · OpenCode steps · Codex 不设 · Hermes 预算
    解决了死循环有了兜底;到上限时可以返回显式原因,或让模型先收尾。
    卡在上限 4:2 次运行都在第 4 轮刚改完第一处时被截断,模型毫不知情;不设上限的 6 次全部自己停下(最长 8 轮)。重放:上限 6 碰巧救回 2 次(截在重复 edit 之前),而最长那次成功运行要到第 19 轮才修好。
  7. 岔路 双层循环:干活时插话、做完后追加 Pi steering / follow-up;Codex 任务层
    解决了用户不必等整次运行结束才能纠偏。
    卡在插话时机本身是设计决策:同一条事件序列里,4 个生产循环把插话送进下一次请求,3 个教学 / 项目循环要等这次运行结束。
  8. 岔路 不用框架的自动多步,自己驱动循环 OpenCode 调 streamText 不传 stopWhen
    解决了压缩检查、提醒注入、权限中断恢复都能插在两次调用之间。
    卡在循环要自己维护:Codex 把它拆成四层(submission / task / turn / stream),教学蓝本只写了其中一层。
  9. 岔路 独立评估器决定能不能停 learn-claude-code s17 · Hermes goals · Codex Stop hook
    解决了「模型说做完了」交给另一个没有工具的模型核对。
    卡在评估器也会错:Hermes 给目标循环设了 20 轮上限(pin 56dc01d9 config_defaults.py:1801),注释写明是防评估器「假阴性」;每轮多一次模型调用。
数字口径:本课项目 miniagent 的真实运行(Qwen3-8B-MLX-4bit,2026-09-26;单一模型、单一任务,只说明现象会发生,不估计频率);「截在第 N 轮」的验收结果是重放那次运行前 N 轮的 edit 后实跑测试得到的;插话一行来自 8 个终止谓词在同一条构造事件序列上的判定(a01-termination.json)。

贯穿任务:修好 test_parse_date

全课的贯穿任务是项目 04-实践项目/tasks/parse_date:让失败的 test_parse_date 通过,测试文件不许改。dateparse.py 里埋了两处 bug,按顺序暴露:修掉第一处、重跑测试,才看得见第二处——这逼着 agent 走「跑测试 → 改 → 再跑」的多轮循环。

这一章给贯穿任务的 trace 立起骨架:messages[] 每轮怎么长、每轮的 finish_reason、总轮数与停止原因。后面每一章都在这副骨架上加一层(A2 的 edit 裁决、A3 的权限、A5 的上下文……)。本章用到的真实数据:

三层交互

层本章里的组件在哪一课
概念动画一次工具调用的协议逐步展开(请求体、回复、执行、回填,可切到「模型实际读到的文本」);Pi 两层 while 与两个队列的逐步动画(事件顺序由原文件实跑得到)第 1 课、岔路 B1
算法模拟器(真算法、小数据)8 个终止谓词(朴素谓词 + CoreCoder、learn-claude-code、Pi、OpenCode、Codex、Hermes + 本课项目)× 10 条事件序列,逐步揭示、每格链到 pin 源码行;页面加载时 80 组判定与期望值逐格比对第 3 课、第 4 课、岔路 B1、实验课
真数据无工具基线 T1a;messages[] 逐轮增长(Python Tutor 式,块高 = 真实 token);轮数分布 × 上限(重放验收);26 次运行的逐轮回放与并排对照;折成环的聚合图;中途插话与中断的真实录制 T1b第 1、2、4 课、实验课、岔路 B1

期望值的来源分三种,组件里每一行都标着:CoreCoder v0.4.0、learn-claude-code s01、Pi 与本课项目是原文件实跑(Python 加载原模块、bun 执行原 TypeScript;假模型按剧本回复,工具只记录不执行);OpenCode 与 Hermes 的判定条件从 pin 源码逐字切出来求值;Codex 是 Rust、本机没有编译器,按源码逻辑复刻。生成脚本:src/data/precomputed/gen_a01_termination.py 与 .ts。

项目推进:里程碑 M1 · 最小循环

对应 04-实践项目/MILESTONES.md 的「A1 · Agent Loop」。代码在 miniagent/loop.py,测试在 tests/test_loop.py;本课项目 v0.5.0 已经实现。代码 AI 来写,决策你来做。

🧭
你要拍板的决策
  1. 要不要 max_rounds,设多少?不设时靠什么兜底?理由必须引用 T1 的轮数分布(第 4 课的方块图),写进 DECISIONS.md。项目现在:默认 30、可设 None;另有墙钟上限 1200 秒。
  2. 回复被截断、又没有工具调用,算不算做完了?项目现在:不算,提醒 2 次,仍截断才以 length 停(第 3 课)。
  3. 服务端说要调工具、却没给调用,怎么办?重发、停、还是换成提示词约定 JSON 自己解析?项目 v2:提醒重发最多 2 次,真实运行里一次也没救回来(第 3 课)。
  4. 「模型说做完了」和「真的做完了」是不是一回事?项目现在:停止由模型决定,成败由 harness 跑测试验收;要不要把验收结果喂回模型,留到 A9(岔路 B3)。
  5. system prompt 要不要写死「完成前必须重跑测试」?用 v1 / v2 两组 trace 的数字判断这条规则值不值(岔路 B3)。

验收标准(全部可测;括号里是本章开发时在项目里核对的现状):

坑与带走

本章拦截的误解
误解纠正在哪一课
模型执行了命令 / 调用了工具模型只写下调用;执行、出错处理、回填全在 harness第 1 课
每轮只发新增的消息,模型记得前面的API 无状态,每轮整份重发;7 轮请求合计是对话长度的 5.1 倍第 2 课
插话只能等这次做完检查点放在哪一层循环是设计决策;4 个生产循环一轮之内就送进去岔路 B1
用了框架的 maxSteps 就等于写好了 harness它拿走了「两次调用之间」这个位置岔路 B2
stop_reason / finish_reason 可以直接信它和消息体会不一致:空调用配 tool_calls、截断配 length、带调用报 stop第 3 课
turn / 轮 是跨厂商的通用计数各家口径不同:Claude SDK 只数工具往返,本课项目提醒重发也算一轮第 3 课
上限越小越安全截断发生在任务中途、模型不知情;上限 4 把两次运行都截在刚改完第一处第 4 课
模型说做完了就是做完了自己停下 21 次,验收没过 13 次岔路 B3
写代码时的坑
症状后果怎么办
只看 finish_reason 判停带着调用报 stop 时,调用被扔掉同时看消息体里有没有调用(第 3 课)
只看消息体判停服务端丢了调用、回复被截断,都被当成「写完了」finish 不是正常收尾就走补救路径,补救有上限、退出写明原因
截断的回复里照常执行工具调用残缺参数悄悄改错文件整批判失败,让模型重发(Pi 的做法)
中断后 assistant 的 tool_calls 没有对应结果下一次请求被 API 拒绝给没回复的调用回填 [interrupted](CoreCoder agent.py:90-94,:132-146);本课项目区分「执行中被中断」与「未执行」,真实录制见岔路 B1
硬上限到了静默返回半途而废却像完成了显式返回原因,或先让模型收尾(第 4 课)
把 turn 当通用概念跨实现比较数字不可比先说清各家的计数口径

带走这些句子(各课「一句话」):

  1. 模型只提出调用,harness 负责执行。
  2. agent 的唯一状态是 messages[],每一轮都整份重发。
  3. 插话什么时候被模型看见,是 harness 定的:纠偏在下一次请求前插进去,追加等它做完再说。
  4. 框架的自动多步替你转圈,也就拿走了「两次调用之间」这个位置;生产 harness 自己写循环,就是为了在那里做事。
  5. 「没有工具调用」只是正常退出的一条路,生产循环要双重校验。
  6. 上限是在「死循环」与「静默截断」之间取舍,没有免费的默认值。
  7. 停不停可以由模型说了算,做没做成要由模型之外的东西来判。

深入这一章

按你愿意花的时间选。每条都在 2026-09-26 实际打开核对过;站外链接需联网。

🎮 想更直观 10–30 分钟

先看别人怎么把循环画出来,再回来玩本章的组件
15 min中文需联网 作者本人
本课蓝本之一的第一课:Learn / Simulate / Code 三个标签页,用轮播动画演示 messages[]、tool_use 和 stop_reason。
对应第 1、2 课。它的 Simulate 用的是脚本化的示例数据,只能当第一层直觉;本章的 messages[] 组件是同一个思路换成了真实 trace。
10 min英文需联网 官方文档
生产可观测性工具怎么画 agent 轨迹:聚合视图把循环画成带次数的环,展开视图按执行顺序排开。
对应第 2 课的「展开 / 折成环」。本章那个组件的两种画法就出自这里。
30 min中文需联网 官方课程,有中文版
Thought-Action-Observation 循环、工具调用的格式、「模型只输出文本,由外部去执行」的直观讲法。
对应第 1 课。中文、配图多,适合先建立全貌;它讲的是通用 agent,不专讲编码 agent。

🔬 想吃透原理 1–3 小时

两条唯一必看按语言分:英文 Thorsten Ball,中文 labuladong
1 h英文需联网 2025-04-15 · 不到 400 行 Go
从一个只会聊天的程序开始,依次加上 read_file、list_files、edit_file 三个工具,得到一个能改代码的 agent:「an LLM, a loop, and enough tokens」。
对应第 1、2 课。每一步都能跑、每一步只多一点代码,正好是本章前两课的顺序;读完会发现生产 harness 的核心也就这么长,多出来的都是第 3、4 课的边界情况。
1 h中文需联网 系列文章,2026-09 仍在更新
把 chatbot 的 if 改成 while:约 80 行、3 个工具的最小 agent,再逐篇往上加。
对应第 2 课。中文里讲循环讲得最直接的一篇;和本章一样强调「状态就是消息列表」。
60:34英文需联网 2026-02-19 · 讲义 missing.csail.mit.edu
从「模型是给定 prompt 的条件分布」讲到 harness 把输出解释成工具调用、回填结果;顺带讲上下文管理和 YOLO 模式的隔离。
对应第 1、2 课,并预告 A3–A5。大学课程的系统性,一集覆盖整门课的骨架。
1:08:43英文需联网 2024-09-17 · ReAct 一作
「想 → 做 → 看结果」的循环从哪里来:ReAct 的动机与演化。
对应第 2 课。理论根;讲的是研究脉络,不讲工程细节。
21:52英文需联网 2026-03-14
Codex agent loop 的作者谈 harness 工程。
对应岔路 B2 与第 4 课。Codex 为什么是四层循环、为什么不设上限,听写它的人怎么说;访谈口述,细节以源码为准。

📄 想读原文 按需

官方文档、论文与 pin 源码
官方文档需联网
五段循环(receive → evaluate → execute → repeat → return)、turn 的定义、max_turns 与 max_budget_usd(默认都不设)、结果子类型与 stop_reason。第 2、3、4 课的官方口径,2026-09-26 核对。
论文需联网
把推理与行动交替写进同一条生成轨迹的奠基论文;agent 循环的学术出处。本地 PDF 待统一下载。
源码需联网
v0.4.0 的整个 agent 循环(净 117 行):无工具调用即返回、上限 50、中断回填。第 2、3、4 课逐行读过。
源码需联网
runLoop 全文:四条件退出、maxSteps 与收尾提示、两次调用之间的十来件事。第 3、4 课与岔路 B2。
源码需联网
第三层循环:needs_follow_up、Stop hook、自动压缩与那句「不必担心无限循环」的注释。最新版行号已全变(勘误表 X9),讲义只用 pin 行号。
🚫

我替你排除了什么:OpenAI《Unrolling the Codex agent loop》(2026-01-23,官方一手,本想放进「想读原文」;本轮 curl 返回 403、正文无法直读核对,按纪律不收,想看可以自己打开)· Geoffrey Huntley「how to build a coding agent」工作坊(只有 22 秒音频加代码,没有完整录像)· Thorsten Ball 播客《Agents At Work 4》(访谈闲聊,没讲机制;他的文字版已入选)· claude-trace 原 npm 包(维护已转手,功能被 claude-tap 覆盖)· 只画 happy path 的循环示意图与速成视频(终止条件、失败分支才是这一章的本体)· Lilian Weng《LLM Powered Autonomous Agents》(2023,写于原生工具调用普及之前,组件划分偏学术)· 任何「CoreCoder 约 150 行 while 循环」的二手说法(净 117 行,且说的是 v0.4.0 的整个 agent.py)。