Agent 运行时状态机设计:把规划、执行、等待和恢复拆成可验证状态
生产 Agent 不能只有一段 while 循环。本文用显式状态机拆分规划、工具调用、等待确认、恢复和终止状态,并给出转移约束、持久化字段与测试方法。
本文要点
- 主题聚焦:大模型、AI Agent、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
很多 Agent 原型把所有逻辑塞进一个循环:读取消息,调用模型,判断是否调用工具,再把结果塞回上下文。演示时它很顺畅,线上一旦遇到超时、用户确认、队列重试或进程重启,就很难回答一个基本问题:这次任务究竟处于什么状态,下一步是否允许执行。
更可靠的做法是把 Agent 当作状态驱动的工作流,而不是一段持续运行的提示词。状态机不是为了让图画得漂亮,而是为了让每一次副作用都有进入条件、退出条件和持久化证据。最小状态集合通常包括 RECEIVED、PLANNING、AWAITING_APPROVAL、EXECUTING_TOOL、OBSERVING_RESULT、RETRY_WAIT、COMPLETED 与 FAILED。不同业务可以扩展,但不要让“任意状态都能跳到任意状态”。
先定义状态契约,而不是先写节点
每个状态需要回答四件事:输入从哪里来、允许产生什么副作用、下一状态由谁决定、哪些字段必须落库。以 AWAITING_APPROVAL 为例,输入应是一个已经归一化的执行计划和风险摘要;这个状态不允许发起写操作;它只能由用户确认、用户拒绝或过期定时器推动;落库字段至少有计划版本、审批人、审批范围、过期时间和请求摘要哈希。
下面是一个精简的 TypeScript 表达。重点不是枚举本身,而是把转移限制在单一函数中:
type RunState = "RECEIVED" | "PLANNING" | "AWAITING_APPROVAL" | "EXECUTING_TOOL" | "OBSERVING_RESULT" | "COMPLETED" | "FAILED";
const allowed: Record<RunState, RunState[]> = {
RECEIVED: ["PLANNING", "FAILED"],
PLANNING: ["AWAITING_APPROVAL", "EXECUTING_TOOL", "FAILED"],
AWAITING_APPROVAL: ["EXECUTING_TOOL", "FAILED"],
EXECUTING_TOOL: ["OBSERVING_RESULT", "FAILED"],
OBSERVING_RESULT: ["PLANNING", "COMPLETED", "FAILED"],
COMPLETED: [],
FAILED: [],
};
function transition(run: { state: RunState }, next: RunState) {
if (!allowed[run.state].includes(next)) throw new Error(`illegal transition: ${run.state} -> ${next}`);
return { ...run, state: next };
}
不要把模型返回的文本直接当作状态。模型只能提出候选动作,确定性运行时负责校验参数、权限、预算和当前状态。这样即使模型误把“稍后重试”说成“立即执行”,执行器仍会拒绝非法转移。
给副作用一个可追踪边界
一个状态转移与一次外部调用并不总是一一对应。比如 EXECUTING_TOOL 可以创建一个带幂等键的工作项,真正的 HTTP 请求由 worker 执行;OBSERVING_RESULT 再消费结果事件。这样进程在请求后崩溃时,恢复逻辑不会盲目重复写入,而是先用幂等键查询已知结果。
建议对每次运行保存 runId、stateVersion、planHash、toolCallId、idempotencyKey、lastErrorCode 和 updatedAt。stateVersion 用于乐观并发控制:更新时要求版本匹配,避免两个 worker 同时把一次运行推进到不同分支。审计日志引用这些 ID,而不是复制完整提示词或敏感工具参数。
对于需要用户等待的场景,状态机必须有时间语义。审批过期后应进入可解释的终态或回到重新规划,而不是让一条任务永久挂在队列中。超时不是普通错误消息,它是状态转移的输入。把超时、取消和人工接管作为一等事件设计,运维才能统计卡住的原因。
用场景测试验证转移,而非只测提示词
测试应该覆盖每条危险边:未审批不能进入写工具、工具超时不能直接标记完成、同一结果事件重复到达不会重复结算、已完成任务不能被旧 worker 再次推进。可以把转移表当成测试数据,逐项断言允许和拒绝的组合。对于恢复测试,在每个副作用后故意中断进程,再从数据库快照重放,确认最终状态和外部影响都只出现一次。
上线前可做一次小型验证:创建一条需要审批的写操作,确认它在没有确认事件时停在 AWAITING_APPROVAL;发送确认后模拟工具超时;重启 worker 并重新投递同一结果事件。若最终只保留一个外部变更、状态有完整时间线且错误可解释,状态机边界才算可用。
何时不要过度建模
只读、无副作用且能在一次请求内完成的问答,不必强行拆成十个状态。状态机带来存储、监控和迁移成本。判断标准是:任务能否跨进程、跨用户交互或跨外部系统。如果答案是能,就要显式建模;如果不能,简单的请求级处理往往更清晰。
延伸阅读
Related