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

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

资料说明:本文保留厂商中立的支付测试教学流程,并用 2026 年 9 月 20 日核实的 OpenAI Codex 固定源码快照说明实际控制流。图中状态、done check 和每轮 checkpoint 是建议的控制器设计;Codex 的 turn 完成事件不等同于业务任务已通过验收。

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

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 五步循环与继续、停止分支,停止不等于通过任务验收
Observation 不一定改变外部环境,但必须更新已知事实、计划、风险或完成判断;否则下一轮大概率只是更昂贵的重复。
状态输入必须产出的新事实
Assemble目标、当前状态、最近 observation下一次模型输入与预算
Infer模型输入符合限制的提案或 final candidate
Execute已授权 tool calltyped result、artifact、环境变化
Observe结果与环境差异成功、失败或未知,以及结果来源
Decidedone criteria、风险、预算continue 或明确 exit reason

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

“模型返回了 final”只是一个信号,不能直接代表任务验收成功。建议控制器明确区分:已验证完成、需要用户输入、权限阻塞、可重试错误、不可重试失败、预算耗尽或人工中止。每种结果要保存的内容和接手方都不同。

七种建议退出结果:已验证完成、等待用户、权限阻塞、可重试失败、终止失败、预算耗尽与已取消
建议的任务结果分类:供 outer loop、UI、审计和恢复共同读取;这些标签不是 Codex 事件枚举的逐项对应。
出口至少要保存什么接下来谁处理
Completeddone check 通过 + artifact/diffEvaluator 或交付流程
Waiting user缺失决策、可选项、默认影响用户
Blocked所需 capability / permission 与当前范围Harness 的权限组件 / 人工审批
Retryable failure错误分类、attempt、退避条件当前 loop 或 outer loop
Terminal failure不可恢复原因、在途副作用已核对、失败 artifactOuter loop / evaluator / 人工
Budget exhausted已完成、未完成、checkpointOuter loop / 人工
Cancelled取消源、在途动作、清理状态Harness cleanup

Codex 的实际停止判断比“收到 final”更具体。run_turn把模型是否需要后续采样与待处理输入合并:工具结果还要交回模型,或队列里还有输入,就可能继续;达到上下文阈值时还可能先整理上下文。即便模型没有新工具调用,服务端 ResponseEvent::Completed中的 end_turn: false 也会要求后续采样。没有后续输入时,Stop hook 仍可提供继续工作的指令。这里判断的是是否再调用模型,源码没有因此自动证明支付测试和回归都通过。

终止事件也要看字段。任务执行器发出的 TurnComplete可以携带 error;中止另走 TurnAborted。所以 turn 已结束、没有终止错误、业务任务已验证完成应分别判断。若只收到“测试进程仍在运行”的 observation,就算模型随后结束本轮,也没有拿到测试通过的证据。

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

没有明确的完成条件,agent 会用语言流畅度代替完成。修 bug 的 done 可能是目标测试通过、相关回归通过、diff 在范围内且没有未解释副作用;研究任务的 done 可能是覆盖指定来源、每个结论有引用、冲突被标明。在自己设计的控制器中,应把完成条件在 loop 开始前写入状态,并在每次 observation 后重新检查;通用 Agent 客户端不会自动知道所有仓库的验收标准。

模型可以建议“我认为已经完成”,但确定性检查应由 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 失败后再试,必须改变一个条件

重试只有在输入、环境、策略或时间发生变化时才有意义。暂时性连接故障可以等待后重试同一个请求;已确定的测试断言失败,则需要新的诊断或代码变化。不能把两者当成同一种“再试一次”。把错误分成 transient、invalid input、permission、not found、conflict、invariant violation 与 unknown,才能选择正确恢复。

错误恢复手册:按暂时错误、输入无效、权限不足、状态冲突、不变量破坏与状态未知选择动作
恢复策略属于错误类别,而不是属于“失败”这个大桶。图中是建议动作;重试之后仍需检查结果,不能保证成功。
错误类合理变化不要做
Transient退避、抖动、有限重试立即高速重放
Invalid input重新读取 schema、修参数原参数重试
Permission请求明确授权或换只读路径绕过权限检查
Conflict / stale刷新状态、重新规划覆盖新事实
Invariant failure回滚、缩小修改、升级继续叠加改动
Unknown保存 artifact、停止或隔离探测无限自我解释

Codex 把模型连接恢复放在 handle_response_stream_error:错误先决定能否重试和等待多久,常规路径受 provider 的 stream retry 配置约束,并可能切换传输方式。另有受功能开关控制的连接重试分支,条件满足时不受这项次数上限约束。因此不能把“有限重试”写成所有配置下的实现保证。run_sampling_request重试前会重新读取当前历史;这不是要求把已经运行过的支付测试或外部退款原样重放。模型连接重试、工具执行重试与业务副作用对账必须分别设计。

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

到这里,初学者已经可以用“证据变化、完成条件、退出原因、预算和重试变化”评审一次运行。下面把这些判断展开成一套可持久化控制器的设计示例,不是 Codex 内部状态结构的复刻。

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

先不用字段名,沿支付任务看这套建议设计怎样更新状态:

  1. 本轮开始时,Runtime 读取任务目标、上一条测试失败和剩余预算。
  2. 装配器把这些事实放进当前模型请求,模型提出一次有 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 run.state != "running":
        checkpoint(run)
        break

    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 本身不会触发任务完成。checkpoint 失败应停止推进并上报错误;它不能把外部副作用与本地状态写入变成原子事务。

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

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

Agent loop 负责让一次 run 靠真实结果逐步接近完成。下一篇把视角拉长:当任务需要多次 run、定时触发、队列选工和失败接力时,如何设计 outer loop。

官方资料