状态机设计实战:从 INIT 到 FAILED 的可恢复执行流(含幂等与重试策略)
专栏第 4 篇:把“能跑”的 Agent,升级为“出错也能稳住”的工程系统。
一、先给标题与结构(可直接用于 CSDN)
标题建议:
《状态机设计实战:从 INIT 到 FAILED 的可恢复执行流(含幂等与重试策略)》
文章结构:
- 问题描述:没有状态机为什么会“偶发失控”
- 状态模型:最小可用状态集合与流转规则
- 核心实现:状态机伪代码 + 事件驱动
- 关键机制:幂等、超时、重试、降级
- 验证方法:如何证明“真的可恢复”
- 常见报错与排查
- 总结与下一篇预告
二、问题描述:为什么第 4 篇必须讲状态机
前 3 篇我们已经完成:
- 确立单 Agent 路线;
- 明确最小可用架构;
- 搭好可运行脚手架。
但一到真实请求,常见问题会集中爆发:
- 工具超时后,流程卡死;
- 重试触发重复执行,导致脏数据;
- 同一请求多次进入同一步骤,结果不一致;
- 故障时只能“看日志猜”,无法确定系统当前处于什么阶段。
本质原因:执行流缺少明确状态边界。
三、最小可用状态模型
3.1 状态定义
建议先用 5 个状态(足够覆盖 MVP):
INIT:请求进入,完成参数校验与上下文装载PLAN_READY:任务规划完成,可执行TOOL_RUNNING:工具执行阶段(可多步)ANSWER_READY:答案完成,可返回FAILED:执行失败,等待重试或降级
3.2 状态流转主路径
INIT -> PLAN_READY -> TOOL_RUNNING -> ANSWER_READY
异常路径(任一步可能):
任意状态 -> FAILED
可恢复路径:
FAILED -> TOOL_RUNNING(可重试场景)FAILED -> ANSWER_READY(降级兜底场景)
四、状态流转规则
每条状态迁移都要满足 3 个约束:
-
前置条件(Guard)
例如:INIT -> PLAN_READY前,必须完成 query 校验。 -
动作(Action)
迁移时触发的执行动作,如生成计划、调用工具、写状态。 -
后置记录(Audit)
每次迁移必须记录日志:request_id / from / to / reason / latency
五、核心伪代码:可恢复执行流
from enum import Enum
class State(str, Enum):
INIT = "INIT"
PLAN_READY = "PLAN_READY"
TOOL_RUNNING = "TOOL_RUNNING"
ANSWER_READY = "ANSWER_READY"
FAILED = "FAILED"
class TransitionError(Exception):
pass
ALLOWED = {
State.INIT: {State.PLAN_READY, State.FAILED},
State.PLAN_READY: {State.TOOL_RUNNING, State.FAILED},
State.TOOL_RUNNING: {State.ANSWER_READY, State.FAILED},
State.FAILED: {State.TOOL_RUNNING, State.ANSWER_READY}, # 重试/降级
State.ANSWER_READY: set()
}
def transit(ctx, to_state: State, reason: str):
frm = ctx.state
if to_state not in ALLOWED[frm]:
raise TransitionError(f"invalid transition: {frm} -> {to_state}")
ctx.state = to_state
log_transition(
request_id=ctx.request_id,
from_state=frm,
to_state=to_state,
reason=reason
)
def run(ctx):
try:
transit(ctx, State.PLAN_READY, "request validated")
plan = build_plan(ctx.query)
ctx.plan = plan
transit(ctx, State.TOOL_RUNNING, "plan created")
tool_outputs = []
for step in plan.steps:
out = run_step_with_guard(ctx, step) # 幂等+超时+重试
tool_outputs.append(out)
ctx.answer = summarize(tool_outputs)
transit(ctx, State.ANSWER_READY, "answer generated")
return success(ctx.answer)
except Exception as e:
transit(ctx, State.FAILED, f"error: {type(e).__name__}")
fallback = degrade(ctx, e) # 兜底回答
transit(ctx, State.ANSWER_READY, "fallback response")
return fail_with_fallback(fallback)
六、4 个关键机制:状态机要“稳”,必须配齐
6.1 幂等键(防重复执行)
建议:idem_key = request_id + step_id
- 若该 key 已有执行结果,直接返回缓存;
- 防止重试或网络抖动导致重复调用外部工具。
6.2 超时控制(防卡死)
- 每个工具调用必须配置 timeout;
- 超时不等于系统失败,应进入
FAILED分支处理。
6.3 重试策略(防瞬时故障)
- 建议最多 2 次;
- 使用指数退避(如 200ms -> 500ms);
- 达到上限后进入降级逻辑。
6.4 降级兜底(防不可用)
- 工具不可达时返回“部分结果 + 明确说明”;
- 保证用户收到可用响应,而不是 500 崩溃。
七、验证清单:如何证明状态机“真可恢复”
上线前至少跑这 3 类测试:
-
正常链路
期望:状态按主路径流转,最终到ANSWER_READY -
异常链路(工具超时)
期望:进入FAILED,触发重试或降级,最终仍可返回 -
重复请求链路(同 request_id)
期望:命中幂等缓存,不重复执行外部步骤
建议监控指标:
- 状态迁移成功率
FAILED占比- 平均恢复耗时
- 重试命中率
- 幂等命中率
八、常见报错与排查
-
报错:invalid transition
- 原因:状态图定义不完整或调用顺序错误
- 处理:统一通过
transit()修改状态,禁止“手改 state”
-
现象:同一步骤被执行多次
- 原因:幂等键缺失
- 处理:落地
request_id + step_id级别缓存
-
现象:FAILED 后直接中断无返回
- 原因:缺少降级出口
- 处理:补
FAILED -> ANSWER_READY的 fallback 路径
-
现象:线上问题难复现
- 原因:没记录迁移日志
- 处理:补齐 from/to/reason/latency/request_id 字段
九、本篇总结
状态机不是“多写几行枚举”,而是 Agent 工程可控性的核心。
一句话:没有状态机,你是在“碰运气跑流程”;有状态机,你是在“可验证地执行系统”。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)