第 03 章 · 主循环、状态与停止条件
本章目标:把 while 循环理解为一个状态机。它不只是“重复调用模型”,而是在每轮之后更新事实、预算和终止理由。
1. 循环为什么是 Agent 的心跳
聊天模型一次只能根据当前输入给出一次输出。编码任务的路径通常无法提前知道:要先跑测试,看到错误后再决定读哪个文件,修改后还要重新验证。循环把“未知的下一步”交给模型,同时让真实观察逐轮进入上下文。
最小循环看起来很短:
for turn in range(max_turns):
response = call_model(build_request(history))
calls = extract_tool_calls(response)
if not calls:
return final_text(response)
for call in calls:
result = execute(call)
history.append(result)
短代码隐藏了大量产品决定:一轮包含多少个工具调用;并行调用怎样排序;工具失败是否继续;用户中途打断时保存什么;模型返回空文本怎样处理;达到上限后是失败还是部分完成。
2. 用状态机而不是用“感觉”停止
一个成熟循环至少维护:
- 当前 turn 编号与最大轮数;
- 任务状态:running、waiting_approval、completed、failed、interrupted;
- 已执行工具及参数摘要;
- 时间、Token、费用和错误预算;
- 完成证据与尚未解决的问题。
停止理由应当成为结构化数据。completed 表示验收条件已经满足;max_turns 表示预算用完但目标未必完成;blocked 表示缺少权限或用户信息;interrupted 表示人类主动停止。它们不能都被界面显示成“Agent 已完成”。
3. 观察怎样改变下一轮
工具结果不是附录,而是下一轮决策的事实来源。好的 observation 会保留退出码、关键错误、受影响路径和是否截断;坏的 observation 可能包含十万行日志,或者只写“失败”而没有原因。
以修复测试为例:
- 第一轮模型请求 run_shell;
- Executor 返回 exit_code=1 和断言位置;
- Runtime 把失败结果加入 history;
- 第二轮模型读取相关函数;
- 修改后再次运行测试;
- exit_code=0 成为完成证据。
这里不存在固定的“第二步一定读 app.py”。真正固定的是每轮都必须把新事实带回状态机。
4. 防止循环原地打转
最大轮数只是最后保险。更早的检测包括:
- 相同工具和相同参数连续调用;
- 错误信息没有变化;
- 上下文每轮增长但没有新增证据;
- 模型反复修改和撤销同一位置;
- 工具一直返回权限拒绝。
可以为最近调用计算指纹。如果同一指纹连续出现两到三次,Runtime 注入一条观察:“你正在重复相同行为,请重新检查假设”,或者直接进入 blocked。关键不是惩罚模型,而是把“没有取得信息增益”变成系统能识别的状态。
5. 人类怎样进入循环
人类不是只在开始和结束出现。审批会让状态变成 waiting_approval;补充需求会形成 steer 事件;中断会取消正在运行的模型或工具;恢复会从持久化状态继续。Runtime 必须区分“用户的新要求”和“工具返回的数据”,否则模型可能把控制命令当成普通文本。
安全的中断通常分两层:先请求任务停止,不再启动新工具;再终止仍在运行的子进程。恢复时要说明哪些动作已经完成,避免再次执行有副作用的调用。
6. 代码落点与检查
在当前实现中,run_agent 承担循环,history 承担短期状态,max_turns 是最基础的预算。下一步扩展时,不要把所有字段塞成零散局部变量,可以引入 AgentState 数据类,让每次状态变化都能记录。
失败案例
API 请求超时后直接重试并不总是安全。模型请求可能已经成功返回工具调用,只是客户端没有收到;如果工具具有付款、发送或删除副作用,盲目重放会产生双重执行。
完成本章后,打开实验 01,逐步观察 user、assistant 和 tool 消息怎样增长。再切换“修复测试”场景,找出失败从终点变成下一轮输入的那一步。