模型第一次看到任务时,不可能同时知道失败原因、正确修改和最终测试结果。它需要通过行动获得新信息,而每一条新信息都会改变下一步。

读完这篇,你应该能回答:为什么一次回答不够?模型、Harness 和外层控制程序各做什么?怎样判断完成、等待或失败?

资料说明:本文依据 OpenAI 对 Codex agent loop 的公开拆解、OpenAI agent 实践指南与 Anthropic 的 Building effective agents。不同 SDK 字段会变化,图中的状态名是厂商中立的教学抽象。

一、先跟着一个任务看懂循环

1.1 跟着支付测试走完四轮

  1. 第一轮:模型先读取失败日志,发现错误与过期订单退款有关。
  2. 第二轮:它读取相关函数和调用方,确认缺少一条过期状态校验。
  3. 第三轮:它做最小修改并运行支付测试;测试仍然失败,但返回了新的边界条件。
  4. 第四轮:它根据新错误调整代码,再次运行测试。目标测试和相关回归都通过,系统才允许结束。

如果没有这个往返,模型只能根据一开始的有限信息猜答案。Loop 的价值不是“多调用几次模型”,而是让每次动作带回新事实,用新事实修正下一步。

1.2 Loop 里到底是谁在重复什么

Agent Loop 是外层程序反复执行的一段流程:让模型决定下一步,由 Harness 执行动作,把结果交还模型,再判断继续还是停止。它不是模型在内部无限思考。

  • 决定下一步:模型根据目标和当前材料,提出读取、修改、测试或结束。
  • 执行动作:Harness 检查并执行工具调用,模型本身不直接启动进程。
  • 得到观察:文件内容、测试输出和错误信息成为新的 observation,也就是“动作后看见的结果”。
  • 继续或停止:外层程序检查任务是否完成、是否需要等待、是否已经不应继续。

最小版本只需记住三种大状态:工作中等待外部信息已经结束。后文的状态机,只是为了把等待原因和结束原因记录得更精确。

二、让一次运行有证据地收敛

2.1 每一轮都要改变下一步判断

用普通话说,这一轮必须为下一轮带回变化:读文件得到新事实,运行测试得到新错误,修改代码得到新 diff。外层程序先整理这个结果,更新当前进度,再为模型准备下一轮材料。只有当证据、计划、风险或完成判断发生变化时,继续才有意义。

下面的表只是给刚才的动作加上工程名称:准备材料叫 Assemble,模型判断叫 Infer,执行叫 Execute,整理结果叫 Observe,决定继续或停止叫 Decide。它们不是另一套流程。

Agent loop 状态图,五个循环站点和 continue、final 两个出口
Observation 不一定改变外部环境,但必须改变证据状态、计划、风险或完成判断;否则下一轮大概率只是更昂贵的重复。
状态输入必须产出的新事实
Assemble目标、当前状态、最近 observation下一次 model view 与预算
Infermodel view受约束的提案或 final candidate
Execute已授权 tool calltyped result、artifact、环境变化
Observe结果与环境差异成功、失败、未知及 provenance
Decidedone criteria、风险、预算continue 或明确 exit reason

2.2 停止不只有“完成”和“没完成”

“模型返回了 final”只是一个信号,不是完整的终止协议。运行时要把它解释成多种出口:已验证完成、需要用户输入、权限阻塞、可重试错误、不可重试失败、预算耗尽、人工中止。不同出口需要不同证据和后续所有者。

Agent loop 退出关口图,区分完成、等待、阻塞、预算、失败与取消
Exit reason 是 outer loop 的输入,也是 UI、审计和恢复路径的公共事实。
出口最低证据接下来谁负责
Completeddone check 通过 + artifact/diffEvaluator 或交付流程
Waiting user缺失决策、可选项、默认影响用户
Blocked所需 capability / permission 与当前范围Harness owner / 人工审批
Retryable failure错误分类、attempt、退避条件当前 loop 或 outer loop
Terminal failure不可恢复原因、在途副作用已对账、失败 artifactOuter loop / evaluator / 人工
Budget exhausted已完成、未完成、checkpointOuter loop / 人工
Cancelled取消源、在途动作、清理状态Harness cleanup

2.3 先写清什么证据才算做完

没有 done contract,agent 会用语言流畅度代替完成。修 bug 的 done 可能是目标测试通过、相关回归通过、diff 在范围内且没有未解释副作用;研究任务的 done 可能是覆盖指定来源、每个结论有引用、冲突被标明。完成条件应在 loop 开始前进入状态,在每次 observation 后重新评估。

模型可以建议“我认为已经完成”,但确定性检查应由 harness 执行;主观质量可以由独立 grader 或人评审。自我反思有价值,却不能既当选手又当唯一裁判。

把三个“检查”放回不同时间尺度就不会混淆:Agent Loop 看到本次测试通过后,判断“这次运行可以退出”;Outer Loop 读取测试、CI 或 review 证据,判断“这张长期任务卡可以结束”;Evals 则把许多任务重复运行,比较“这个系统版本是否稳定变好”。它们依次检查一次 run、一张 work item 和一版系统。

2.4 为什么不能让 Loop 无限运行

Loop 至少管理时间、model calls、tool calls、token、外部 API、并发与风险预算。仅设最大迭代数会把不同成本的动作视为相同。更好的控制器在每轮前估计下一动作的价值与成本,并为验证预留预算。

  • 接近上限时缩小搜索范围、减少并行或请求人工选择,而不是突然截断。
  • 为最终验证保留独立预算,防止“改完了但没钱测试”。
  • 高风险写动作受独立权限门约束,不能用更多 token 换取授权。
  • 预算耗尽必须生成 checkpoint 和 exit reason,不能伪装成完成。

2.5 失败后再试,必须改变一个条件

重试只有在输入、环境、策略或时间发生变化时才有意义。相同参数、相同错误、相同 context 的连续调用是 loop bug。把错误分成 transient、invalid input、permission、not found、conflict、invariant violation 与 unknown,才能选择正确恢复。

Agent loop 失败恢复图,按暂时错误、非法输入、权限、冲突和不变量失败选择不同动作
恢复策略属于错误类别,而不是属于“失败”这个大桶。
错误类合理变化不要做
Transient退避、抖动、有限重试立即高速重放
Invalid input重新读取 schema、修参数原参数重试
Permission请求明确授权或换只读路径绕过 gate
Conflict / stale刷新状态、重新规划覆盖新事实
Invariant failure回滚、缩小修改、升级继续叠加改动
Unknown保存 artifact、停止或隔离探测无限自我解释

三、进阶:把三种状态展开成可恢复的状态机

到这里,初学者已经可以用“证据变化、完成条件、退出原因、预算和重试变化”评审一次运行。真正实现控制器时,才需要把这些判断固化成可持久化状态。

while (toolCalls.length) 只描述了语法条件,没有描述工程状态。至少要区分 running、waiting approval、waiting user、blocked、completed、failed、cancelled 和 budget exhausted。状态决定谁可以唤醒 run、哪些资源仍有效、outer loop 之后能否重试。

先不用字段名,沿支付任务看一轮状态怎样变化:

  1. 本轮开始时,Runtime 读取任务目标、上一条测试失败和剩余预算。
  2. 装配器把这些事实变成当前 model view,模型提出一次有 call id 的下一步动作。
  3. Harness 校验并运行工具,得到新的 observation;模型的提案本身不直接改 run state。
  4. 控制器把 observation 写回状态,更新进度、预算、重复失败计数和可能的退出原因。
  5. 状态 checkpoint 成功后,控制器才开始下一轮;若 done check 通过、需要审批或已无有效变化,就进入相应出口。
{
  "run_id": "run_18",
  "state": "running",
  "iteration": 7,
  "goal": { "id": "fix-payment-test", "done_check": "test://payment" },
  "last_observation": { "kind": "test_failure", "ref": "log://9f2" },
  "budgets": { "tool_calls_left": 12, "time_s_left": 480 },
  "progress": { "changed_files": 2, "same_failure_count": 1 }
}
形状级示例:Loop state 不等同于 messages;它是运行时可以确定性判断和恢复的控制状态。
while run.state == "running":
    view = assemble_context(run)
    proposal = model.infer(view)
    observation = harness.dispatch(proposal, run)
    run = transition(run, observation)

    if done_check(run):
        run.state = "completed"
    elif repeated_without_change(run):
        run.state = "blocked"
    checkpoint(run)
最小控制器:policy.ask 会把 running 变成 waiting_approval;approval_granted 再唤醒 running;只有测试 observation 满足 done check,状态才进入 completed。模型输出 final 本身不会触发完成。

四、复盘:一次健康的运行长什么样

评审点健康信号危险信号
Progress每轮有新证据或状态差异相同调用与错误重复
Done运行前定义、运行后验证依赖 final 语气
Exit机读 reason + checkpoint只有成功/失败布尔值
Budget分项、动态、预留验证只设 max iterations
Recovery按错误类型改变策略任何错误都 retry
Human在决策与 authority 边界介入每步审批或永不介入

Agent loop 负责让一次 run 有证据地收敛。下一篇把视角拉长:当任务需要多次 run、定时触发、队列选工和失败接力时,如何设计 outer loop

官方资料