Agent Loop 与 Runtime:让模型从回答问题变成完成任务

1. 第一性原理:任务完成需要一个闭环

模型本身只能生成候选动作,不能天然观察真实世界,也不能保证动作已经生效。因此 Agent 的最小闭环是:

读取目标与状态
  → 装配上下文
  → Model Call
  → 解析一个或多个 Tool Call
  → 执行工具并记录 Observation
  → 更新状态并验证
  → 继续、暂停或结束

这就是 ReAct 的工程化版本。ReAct 的思想是让推理和行动交替发生;Runtime 的职责是把交替过程变成可取消、可恢复、可观测、可评测的状态机。

2. LoopSession、LoopTurn 与 LoopControl

LoopSession 是任务级容器,至少保存 session_id、目标版本、当前状态、预算、权限、检查点、证据和轨迹位置。它跨越多个用户 Turn,不能简单等同于聊天历史。

LoopTurn 是一次用户输入驱动的运行单元。一个 Turn 通常包含一次 Model Call 以及紧随其后的 Tool Call 集合。模型可能一次返回多个相互独立的工具动作,所以 Tool Call 应支持 Batch,但 Batch 内每个动作都要有独立的参数校验、权限判断、超时和结果。

Turn {
  turn_id
  input_event
  context_snapshot
  model_call
  tool_calls[]
  observations[]
  verification
  stop_reason
}

LoopControl 是外部控制面,不是模型输出的一部分。它至少包含:

  • Steer:用户改变当前方向,保留任务身份和已验证事实,但要求重新规划。
  • Followup:当前任务结束后追加一个后续要求,不能误当成当前 Turn 的普通文本。
  • Interrupt:立即停止可中断动作,冻结状态并等待处理。
  • Cancel:终止任务;若工具已经产生外部副作用,必须进入补偿或人工确认流程。

3. PlanAct、CodeAct 和 MultiAgent 到底改变了什么

三者都源自 ReAct Loop,差别是“谁组织下一步行动”。

PlanAct 把行动分成计划和执行两个阶段。优点是目标和依赖更清楚;缺点是计划会过期,所以每完成一个关键节点都需要验证,并允许 Replanner 修改剩余计划。

CodeAct 让模型写一段代码作为行动。它减少多次模型往返,适合批量处理,但代码拥有更强副作用,必须限制文件、网络、进程和凭据,并要求执行结果结构化返回。

MultiAgent 把一个问题拆成多个循环。常见 Pattern 包括:

  1. Agent Handoff:当前 Agent 输出恢复包,另一个 Agent 接管。
  2. Agent as Tool:主 Agent 以工具方式调用子 Agent。
  3. Leader & N Workers:领导者拆分任务,Workers 并行,领导者合并和验证。
  4. Agent2Agent:多个对等 Agent 通过协议交换任务、证据和状态。

如果只是把一个 Agent 的 Prompt 拆成几个角色,并没有获得真正的 MultiAgent 能力。真正的边界必须包含独立状态、输入输出合同、错误语义和合并规则。

4. StopReason:循环如何知道该停

stopReason 不应该是一个随意字符串,因为它同时影响 UI、恢复、计费和评测。建议至少区分:

COMPLETED              验证器确认完成
WAITING_HUMAN          等待用户或审批决定
PAUSED_BUDGET          预算或时间耗尽,可恢复
INTERRUPTED            外部中断,状态已冻结
FAILED_RETRYABLE       可重试的暂时失败
FAILED_TERMINAL        不应自动重试的终态失败
CANCELLED              用户取消

每个 StopReason 应附带 checkpoint_idlast_safe_pointpending_actionsrequired_decisionevidence_refs。这样恢复时不会把“暂停”误当成“完成”,也不会从头重放已经成功的副作用。

5. HITL 的三个干预点

人机协同不是在最后弹一个“确认”按钮,而是 Runtime 的控制面。Middleware 可以在三个位置干预:

  • PreTurn:模型调用前检查目标版本、上下文、预算、敏感数据和当前权限。
  • MidTurn:模型已经提出 Tool Call 后,执行前检查风险;高风险动作可以暂停等待审批。
  • PostTurn:工具执行后检查结果、验证证据和是否允许继续下一轮。

HITL 有两种基本模式。Stop-Resume 会保存检查点并结束当前执行,用户作出决定后新请求恢复;适合跨进程、跨设备和长时间等待。Hang-Wait 会保留任务并在服务端等待决定;交互简单,但会占用租约、连接或调度资源。生产系统通常使用 Stop-Resume,把等待变成持久化状态而不是挂住 Worker。

6. Middleware、Hook 与上下文控制

Middleware 适合改变或拒绝请求,例如脱敏、预算检查、权限检查、上下文重建和错误归一化;Hook 更适合观察或触发副作用,例如记录 Trace、更新指标、发通知。两者都必须有明确顺序和异常策略。

一个可靠的执行顺序可以是:

PreTurn: 读取任务合同 → 检查权限 → 装配 Context → 检查预算
Model:   调用模型 → 校验结构化输出
MidTurn: 逐个 Tool Call 做策略检查 → 执行 → 记录 Observation
PostTurn: 验证结果 → 更新 Memory/Trace → 决定 Continue 或 Stop

不要把所有逻辑塞进模型 Prompt。模型可以提出意图,但 Middleware 才是系统最终的权限边界。

7. SSE 与 Stream:展示过程不等于保存状态

SSE 是服务端到浏览器的单向事件通道,适合把模型增量文本、Tool 开始/结束、验证结果和状态变化推给 UI。它不保证事件不丢,也不等于数据库事务。

生产事件应有 event_idsession_idturn_id、单调序列号、事件类型、时间戳、payload 和 schema_version。客户端断线后带上 last_event_id 重连,服务端从事件日志补发;事件处理必须幂等。

Stream 描述传输过程,State 描述事实,Trajectory 描述完整轨迹。三者不能混为一谈:流可以丢,状态要可恢复,轨迹要可评测。

8. 多 Turn 上下文压缩

压缩的目标不是让 Token 越少越好,而是保留完成任务所需的约束、决定、证据和未完成动作。可以分三级:

  • L1:删除重复日志、折叠连续输出、压缩 Tool response。
  • L2:将历史 Turn 转为结构化摘要,保留目标、决定、失败、证据和待办。
  • L3:把大对象、代码 diff 和轨迹外置,只在需要时按引用重载。

触发点也有三种:PreTurn 预算不够时压缩,MidTurn 工具返回大结果后局部压缩,PostTurn 完成一轮后更新长期摘要。压缩后要运行一致性检查,确认目标版本、禁止事项、已验证事实和待办没有丢失。

9. 最小伪代码

while session.can_continue():
    control = session.consume_control_events()
    if control.interrupt:
        return pause(CHECKPOINT, INTERRUPTED)

    context = context_builder.build(session)
    decision = model.call(context)
    calls = validate_tool_calls(decision)

    observations = []
    for call in calls:
        policy.require(call, session.permissions)
        observations.append(tool_executor.run_idempotently(call))

    session.append_turn(decision, observations)
    verdict = verifier.check(session)
    if verdict.completed:
        return finish(COMPLETED, verdict.evidence)
    if verdict.needs_human:
        return pause(CHECKPOINT, WAITING_HUMAN)

return pause(CHECKPOINT, PAUSED_BUDGET)

10. 学习者练习

先实现单 Agent,再人为注入三类故障:Tool 超时、用户中断、SSE 断线。然后增加一个 Replanner 和一个 Verifier,比较“只依赖模型”与“有 Runtime 控制”在成功率、重复副作用和恢复时间上的差别。