本地 Agent 可以把一轮执行放在一个持续运行的 Loop 里:调用模型,解析工具调用,执行工具,把结果交回模型。进程存活时,这条路径很直接。
把同样的代码放进 Pod,问题就发生了变化。滚动升级、进程崩溃和租约失效都可能让执行中断。设计恢复机制之前,需要先回答:下一位执行者,究竟从哪里继续?
消息记录与执行状态
用户可以看到完整的聊天记录,说明消息被保存了。但一条工具调用消息并不能证明工具已经执行,也不能证明执行结果已经提交。
执行状态至少要能区分:下一步待调度、已经开始、结果已提交,以及外部结果未知。恢复依赖的是这些明确的状态边界,而不是简单地重新读一遍聊天记录。
例如,工具已经创建了外部任务,但 Worker 在写回结果前崩溃。新 Worker 如果只看见“没有结果”,再次创建就可能产生重复副作用。
在提交点恢复
一种设计是把执行拆成有限的步骤。每一步完成后,在同一事务里提交结果、更新执行位置,并写入下一步的 Outbox 事件。MQ 负责通知有工作可做,数据库保存可恢复的事实。
with transaction() as tx:
run = tx.lock_run(run_id)
require_active_owner(run, epoch)
tx.save_result_once(step_id, result)
tx.advance_run(run_id, next_step)
tx.append_outbox(run_id, next_step)
这里的 epoch 是执行所有权的代次。接管发生后,新执行者得到新的代次;旧执行者随后写回时,存储层必须拒绝过期代次的提交。
这个约束需要由真正执行写入的权威服务原子校验。只在 Worker 开始时检查一次,并不能挡住执行期间发生的接管。
消息确认与任务完成
长任务不必一直占着同一条未确认的 MQ 消息。更清晰的边界是:消费端把唤醒请求可靠地纳入调度状态后确认消息,后续执行由持久化状态与租约管理。
相应地,也需要补偿扫描:发现可执行但没有被及时调度的任务,或者租约已过期的任务,重新发起唤醒。消息用于低延迟推进,扫描负责找回遗漏,两者通过幂等状态转换收敛。
这不是唯一实现。关键是明确确认消息之后,哪个持久化组件继续承担恢复责任。
外部副作用的边界
数据库里的 fencing 不能撤回已经发出的外部请求。对于创建、发送、修改等动作,还需要工具端幂等键、结果查询、对账或人工处理机制。
当外部系统不支持幂等,也无法查询先前请求的结果时,超时只能说明“结果未知”。这时直接重试可能制造重复结果,应把不确定状态保留下来。
在 Agent 架构专题 中,可以沿着 Loop、领域服务和工具域,继续展开这几条边界。