第一阶段 · 最小 Agent 透明内核第 3 / 30 章
本章目录

第 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. 用状态机而不是用“感觉”停止

一个成熟循环至少维护:

停止理由应当成为结构化数据。completed 表示验收条件已经满足;max_turns 表示预算用完但目标未必完成;blocked 表示缺少权限或用户信息;interrupted 表示人类主动停止。它们不能都被界面显示成“Agent 已完成”。

3. 观察怎样改变下一轮

工具结果不是附录,而是下一轮决策的事实来源。好的 observation 会保留退出码、关键错误、受影响路径和是否截断;坏的 observation 可能包含十万行日志,或者只写“失败”而没有原因。

以修复测试为例:

  1. 第一轮模型请求 run_shell;
  2. Executor 返回 exit_code=1 和断言位置;
  3. Runtime 把失败结果加入 history;
  4. 第二轮模型读取相关函数;
  5. 修改后再次运行测试;
  6. 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 消息怎样增长。再切换“修复测试”场景,找出失败从终点变成下一轮输入的那一步。