← 返回题目列表

如何用状态机设计可恢复的 Agent?

高频 困难 第 23 / 26 题 更新于 2026/09/17
AI Agent状态机持久化故障恢复

简化版

可恢复 Agent 应把流程状态交给确定性状态机管理,而不是让模型靠对话猜当前进度。状态机明确允许的状态、事件、转移条件和副作用;每一步先持久化意图,再幂等执行工具,最后提交结果。进程崩溃后根据检查点和操作日志恢复,遇到不确定结果先查询外部状态,不能盲目重放写操作。

详细版

典型状态可以是 CREATED → PLANNING → RUNNING → WAITING_APPROVAL → SUCCEEDED/FAILED/CANCELLED,其中 RUNNING 再按步骤保存计划版本、当前节点、输入产物和已产生副作用。模型只提出计划或动作候选,控制器校验当前状态是否允许该事件。

RUNNING --need_approval--> WAITING_APPROVAL
WAITING_APPROVAL --approved(version)--> RUNNING
RUNNING --retryable_error--> RETRY_WAIT
RUNNING --verified_complete--> SUCCEEDED

数据库更新要使用乐观锁或 compare-and-set,防止两个 worker 同时推进。工具调用携带幂等键;调用前记录 STARTED,调用后记录结果。若崩溃发生在请求发出后、结果落库前,恢复器应凭业务号查询工具端,而不是直接再调用。超时、取消、补偿和人工审批都要成为显式状态。

完整版教学

一、为什么仅靠消息历史不能恢复

消息历史能描述发生过什么,却不保证状态唯一。模型可能说“下一步准备付款”,但系统不知道付款请求是否已经发出;两个 worker 读取同一历史,也可能同时执行下一步。

状态机把合法状态和转换写进代码,使“现在能做什么”可判定。LLM 擅长生成候选动作,状态机负责生命周期、一致性和副作用边界,两者分工后才能可靠恢复。

记忆钩子:对话是叙事,状态机是事实;恢复必须依赖可提交的事实,而不是重新阅读故事后猜测。

二、先定义状态、事件和守卫条件

状态表示稳定阶段,事件表示触发原因,守卫条件决定某次转换是否允许。例如收到 approve 事件不一定能恢复任务,还要校验审批令牌绑定当前计划版本且未过期。

当前状态事件守卫条件下一状态
CREATEDstart输入完整PLANNING
RUNNINGneed_approval动作为高风险WAITING_APPROVAL
WAITING_APPROVALapprove版本匹配、令牌有效RUNNING
RUNNINGcomplete验证器通过SUCCEEDED

终态应不可逆,除非业务明确支持重新打开。模型不能从 FAILED 任意跳到 SUCCEEDED,必须创建恢复事件并重新通过验证。

三、把状态与大产物分开保存

状态记录应小而结构化,包括任务 ID、版本、当前节点、预算、等待原因和产物引用。网页全文、日志和生成文件放对象存储,通过不可变 artifact ID 关联。这样更新状态无需反复写入大对象。

{
  "task_id": "t-42",
  "state": "RUNNING",
  "version": 17,
  "current_step": "verify_report",
  "artifacts": ["artifact://t-42/report-v3"]
}

产物引用最好带内容哈希,恢复时可以确认输入没有被悄悄替换。敏感内容的访问权限也由存储层执行,而不是复制到状态 JSON。

四、用乐观锁防止并发推进

队列可能重复投递,同一任务也可能被两个 worker 同时领取。更新时使用 WHERE task_id=? AND version=17,成功后版本变为 18;只有一个 worker 能提交,另一个发现影响行数为零就重新读取。

UPDATE agent_task
SET state='RUNNING', version=18
WHERE task_id='t-42' AND version=17;

状态锁只保护控制记录,不能自动保证外部 API 不重复执行。因此还要给写工具传幂等键,并在状态中记录动作 ID。

五、解决“请求已发出但结果没落库”

最棘手的崩溃点在外部调用成功之后、状态提交之前。恢复器看到动作仍为 STARTED,无法判断外部世界是否改变。直接重试可能重复付款,直接跳过又可能漏执行。

正确设计是为动作分配稳定业务号,并要求工具支持幂等或状态查询。恢复时先用业务号查询:已成功则补记结果;明确未执行才重放;结果未知则转人工或等待对账。

假设故障概率只有 0.1%,一天执行 100 万次仍约有 1000 次落在异常路径,所以不能把它当作极端情况忽略。

六、等待也必须是一等状态

人工审批、异步工具和退避重试都可能等待几秒到几天。不能让进程阻塞持有内存,而要保存检查点,记录唤醒条件和截止时间,再由事件或定时器恢复。

WAITING_APPROVAL 要保存待审批动作哈希;RETRY_WAIT 要保存下一次运行时间和已重试次数;WAITING_TOOL 要保存外部 job ID。收到过期或重复事件时,状态机应安全忽略。

等待期间外部条件可能变化,恢复前必须重新校验价格、权限和计划版本,而不是沿用旧观察。

七、取消与补偿不是删除任务

取消表示停止未来步骤,但已发生副作用可能需要补偿。例如行程 Agent 已订酒店、尚未订机票,用户取消时要根据规则决定是否退订酒店。补偿本身也会失败,因此需要独立步骤和状态。

CANCEL_REQUESTED -> COMPENSATING -> CANCELLED
                               \-> COMPENSATION_FAILED

不要把补偿等同于数据库回滚;外部世界通常只能执行反向业务操作,且可能产生手续费。操作日志要记录原动作与补偿动作的关联。

八、用转换覆盖率和恢复演练测试

单元测试应覆盖每条合法转换和非法事件,集成测试要在工具调用前后、状态提交前后注入崩溃。恢复后验证没有重复副作用、没有跳过必要步骤,并且最终状态可解释。

指标包括任务停留时长、各状态积压、版本冲突率、重复事件数、恢复成功率和补偿失败率。若大量任务卡在 WAITING_TOOL,问题可能在回调丢失而非模型规划。

可对 1000 次执行随机注入 5% 崩溃;若恢复后完成 48 次、2 次需要人工,则恢复自动成功率为 96%,同时要确认零重复付款。

九、常见误区与追问

  • 误区:把当前步骤写进 Prompt 就是状态管理。 Prompt 不是并发安全、可事务提交的事实存储。
  • 误区:数据库事务可以覆盖外部工具调用。 普通本地事务无法原子提交第三方副作用。
  • 误区:队列只投递一次,所以不用幂等。 超时与确认丢失会产生重复投递。
  • 误区:失败后从最后一步直接重试即可。 先要判断上一次动作是否已经在外部成功。
  • 误区:取消就是把任务标记删除。 已发生的副作用可能需要可追踪补偿。
  • 追问:状态应该多细? 以恢复决策不同为界;若两个阶段的重试、权限或补偿策略不同,就应拆开。
  • 追问:模型可以决定状态转移吗? 模型可提出事件,控制器必须验证守卫条件并执行转换。
  • 追问:如何处理迟到审批? 校验当前状态、计划版本和有效期,不匹配则拒绝或重新审批。

十、加强记忆

可恢复 Agent 记住“状态、版本、幂等、检查点、补偿”:确定性状态机规定合法转换,用版本号挡住并发推进;每个外部动作有稳定幂等键,调用前后留下操作记录;等待时持久化检查点并释放进程;恢复遇到不确定结果先查询外部状态;取消时对已发生副作用执行可审计补偿。模型负责建议,状态机负责事实与承诺。