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 包括:
- Agent Handoff:当前 Agent 输出恢复包,另一个 Agent 接管。
- Agent as Tool:主 Agent 以工具方式调用子 Agent。
- Leader & N Workers:领导者拆分任务,Workers 并行,领导者合并和验证。
- Agent2Agent:多个对等 Agent 通过协议交换任务、证据和状态。
如果只是把一个 Agent 的 Prompt 拆成几个角色,并没有获得真正的 MultiAgent 能力。真正的边界必须包含独立状态、输入输出合同、错误语义和合并规则。
4. StopReason:循环如何知道该停
stopReason 不应该是一个随意字符串,因为它同时影响 UI、恢复、计费和评测。建议至少区分:
COMPLETED 验证器确认完成
WAITING_HUMAN 等待用户或审批决定
PAUSED_BUDGET 预算或时间耗尽,可恢复
INTERRUPTED 外部中断,状态已冻结
FAILED_RETRYABLE 可重试的暂时失败
FAILED_TERMINAL 不应自动重试的终态失败
CANCELLED 用户取消
每个 StopReason 应附带 checkpoint_id、last_safe_point、pending_actions、required_decision 和 evidence_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_id、session_id、turn_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 控制”在成功率、重复副作用和恢复时间上的差别。