背景问题:Agent 从 Demo 变成生产系统后,失败点会突然变多
很多大模型 Agent 的早期 Demo 都像一段同步脚本:接收用户目标,调用模型,选择工具,执行工具,再把结果返回。这个结构在演示环境里足够清晰,但一旦进入生产,就会遇到几个稳定性问题。
第一,任务可能很长。 一个代码修复 Agent 可能需要读仓库、改文件、跑测试、根据失败日志再改一轮;一个运营 Agent 可能需要查数据、生成计划、等待人工确认、再执行外部系统动作。中间任何一步超时、进程重启、容器迁移或外部 API 抖动,都可能让任务状态丢失。
第二,Agent 会执行副作用。 发送邮件、提交 PR、修改数据库、调用支付接口、触发工单流转,这些动作不能简单地”失败就重试”。如果没有幂等键、执行日志和状态边界,重试可能造成重复提交、重复扣费、重复通知或状态错乱。
第三,人工介入不是异常,而是生产流程的一部分。 高风险工具调用、低置信度输出、合规文本、财务类操作、代码合并前的审批,都需要让 Agent 暂停,并在人工确认后继续。这里的关键不是”弹出一个确认框”,而是确认前后的状态要可恢复、可审计、可回放。
因此,生产级 Agent 不能只依赖一次性上下文和内存变量,而应该设计成持久化工作流:每个关键步骤都有状态,每次外部副作用都有记录,每个可中断点都能恢复,每次失败都有明确的重试和补偿策略。
注意:本文与”Agent Memory”不同。 Memory 解决 Agent 记住什么;Durable Workflow 解决 Agent 执行到哪里、哪些副作用已经发生、失败后从哪里继续。
核心原理:把 Agent 执行拆成可恢复的状态机
1. 用状态机替代隐式调用链
普通 Agent 代码常见写法是:
result = await agent.run(user_task)
这会隐藏很多生产问题:当前执行到了哪一步?已经调用过哪些工具?哪个工具的结果可以复用?人工审批前后如何恢复?失败时应该重跑哪一段?
更稳妥的方式是把任务拆成一组明确节点:
receive_task → plan → retrieve_context → draft_action
→ human_approval? → execute_tool → verify_result → finalize
每个节点都有输入、输出、状态写入和错误处理。这样做的价值是:任务不再是一个黑盒调用,而是一条可检查、可恢复的执行轨迹。
Temporal 的 Workflow 文档把工作流描述为由代码定义的一系列步骤,并强调 Workflow Execution 会产生命令和事件,事件历史是恢复状态的依据。外部 API、数据库查询、LLM 调用和文件 I/O 应放在 Activity 中,其结果会记录下来,重放时复用而不是重新执行。
LangGraph 的 persistence 文档也采用相近思路:通过 checkpointer 保存线程级图状态,用 store 保存跨线程的应用数据。它明确把 checkpointer 用于 conversation continuity、human-in-the-loop、time travel 和 fault tolerance。
2. 检查点不是聊天记录,而是恢复游标
很多团队会把 Agent 的全部历史消息存进数据库,然后认为”已经持久化”。这只解决了一部分问题。
聊天记录回答的是:模型之前看过什么。 检查点回答的是:流程执行到了哪里、每个节点的状态是什么、下次恢复时应该从哪个安全边界继续。
一个生产可用的检查点至少应该包含:
| 字段 | 说明 |
|---|---|
workflow_id | 业务任务 ID,例如工单、PR、审批单或批处理任务 ID |
thread_id / run_id | 一次 Agent 执行实例的恢复指针 |
current_node | 当前或下一步节点 |
state_snapshot | 结构化状态,不应只存自然语言消息 |
tool_call_log | 工具调用参数、结果、错误、幂等键 |
approval_state | 人工审批状态、审批人、审批时间、审批意见 |
version | 工作流定义版本、提示词版本、工具版本 |
OpenAI Agents SDK 的 Sessions 文档提供了会话历史管理机制,支持 SQLite、Redis、SQLAlchemy、MongoDB、Dapr、EncryptedSession 等实现。这个机制适合维护 Agent 的对话连续性,但在长任务工作流里,还需要额外记录节点状态、审批状态、副作用日志和工作流版本。
3. 中断必须是可恢复中断,而不是阻塞线程
人审节点最容易被做错。错误做法是让服务端请求一直等待人工确认,或者把审批状态只存在进程内存里。一旦服务重启,审批上下文就丢失。
LangGraph 的 interrupt 文档给出了更合理的模式:interrupt() 可以在图节点中暂停执行,保存当前 graph state,并等待外部输入;恢复时使用相同的 thread ID 和 Command(resume=...) 继续。thread ID 是持久化游标,复用它会恢复同一个 checkpoint,使用新值则开始新线程。
生产实现时,可以把人审中断看成一个明确状态:
{
"workflow_id": "ticket-20260705-001",
"status": "waiting_approval",
"current_node": "approve_tool_call",
"approval": {
"type": "send_email",
"risk_level": "medium",
"payload_digest": "sha256:...",
"requested_at": "2026-07-05T03:02:02-04:00"
}
}
当审批通过时,不是重新发起一个全新 Agent,而是用相同的恢复指针继续执行。这样才能保证审批的是同一个计划、同一份工具参数和同一个风险上下文。
工程落地:一条长任务 Agent 的生产骨架
状态设计
建议先把 Agent 状态分为四层:
| 层次 | 内容 | 示例 |
|---|---|---|
| 业务状态 | 任务目标、用户输入、目标资源、业务 ID、权限上下文 | task_id, user_id, resource_uri |
| 执行状态 | 当前节点、已完成节点、失败节点、重试次数、超时信息 | current_node, retry_count, route_next |
| 模型状态 | prompt 版本、模型版本、工具列表、上下文摘要、输出草稿 | prompt_version, model_name, draft_output |
| 副作用状态 | 工具调用 ID、幂等键、外部返回 ID、补偿动作、审批记录 | tool_call_id, idempotency_key, compensation |
不要把这些全部压进一段自然语言 summary。 自然语言摘要适合减少 Token,但不适合作为唯一恢复依据。恢复依据应以结构化字段为主,摘要只能作为辅助上下文。
节点边界
节点边界要按”失败后能否安全重跑”来划分,而不是按代码函数大小来划分。
可安全重跑的节点:生成计划、读取上下文、计算草稿、校验输出、生成摘要。这些节点即使重复执行,一般也不会直接改变外部世界。
需要严格控制的节点:发送消息、提交代码、写数据库、调用支付、发起审批、修改配置。这些节点必须记录幂等键、请求摘要、外部返回结果和补偿策略。
重试与超时
LangGraph fault tolerance 文档把失败处理拆成 retries、timeouts 和 error handling 三类机制。节点失败时可以按异常类型和 backoff 设置重试;timeout 可以限制单次节点尝试耗时;只有重试耗尽后才进入 error handler。
生产实现不要只写”失败重试 3 次”。更合理的是按节点类型分层:
| 节点类型 | 重试策略 |
|---|---|
| 模型生成节点 | 允许短重试,但要控制总 Token 和总耗时 |
| 只读工具节点 | 允许指数退避重试 |
| 写入型工具节点 | 只有具备幂等键和可查询确认接口时才允许自动重试 |
| 人审节点 | 不按普通超时失败处理,进入 waiting 状态并设置业务 SLA |
| 汇总节点 | 失败可重跑,但应固定输入快照,避免恢复后摘要依据变化 |
副作用幂等
Agent 工作流的核心风险不是模型答错,而是”执行错了还无法恢复”。因此,每个副作用动作都要有唯一幂等键。
一个简单策略是:
idempotency_key = hash(
workflow_id + node_name + tool_name +
normalized_payload + workflow_version
)
写入工具前先查本地 tool_call_log:
- 如果同一个键已经成功执行 → 直接复用结果
- 如果处于 pending → 查询外部系统确认
- 如果失败 → 按错误类型决定重试、补偿或人工处理
外部系统不支持幂等键时,要降低自动化级别。可以采用”先生成待执行记录,再由人工或可靠后台任务消费”的模式,不要让 LLM Agent 直接重复写入关键系统。
人审恢复
人审不要只审自然语言解释,还要审结构化内容:
- 提交 PR 前:展示目标分支、变更文件、测试结果、风险摘要
- 发送邮件前:展示收件人、主题、正文摘要、附件清单
- 修改数据库前:展示 SQL、影响行数估计、回滚方案
审批通过后,恢复时要校验三件事:
- 审批的 payload digest 与当前待执行 payload 是否一致
- 工作流版本和工具版本是否仍兼容
- 审批是否仍在有效期内
如果任一条件不满足,工作流应回到重新计划或重新审批,而不是继续执行旧计划。
一个简化的实现结构
下面是一个不绑定具体框架的伪代码,用来表达核心结构:
class AgentWorkflowState(TypedDict):
workflow_id: str
workflow_version: str
current_node: str
task: dict
plan: dict | None
draft_action: dict | None
approval: dict | None
tool_results: list[dict]
errors: list[dict]
async def run_workflow(state: AgentWorkflowState):
state = await checkpoint.load_or_init(state["workflow_id"], state)
while state["current_node"] != "finalize":
node = state["current_node"]
try:
if node == "plan":
state["plan"] = await call_model_for_plan(state["task"])
state["current_node"] = "draft_action"
elif node == "draft_action":
state["draft_action"] = await create_action_payload(state)
state["current_node"] = "approval"
elif node == "approval":
approval = await approval_store.get(state["workflow_id"])
if not approval:
await approval_store.create(
state["workflow_id"], state["draft_action"]
)
state["current_node"] = "waiting_approval"
await checkpoint.save(state)
return {"status": "waiting_approval"}
validate_approval(approval, state["draft_action"])
state["approval"] = approval
state["current_node"] = "execute_tool"
elif node == "execute_tool":
key = build_idempotency_key(state)
result = await tool_executor.run_once(
key, state["draft_action"]
)
state["tool_results"].append(result)
state["current_node"] = "verify_result"
elif node == "verify_result":
await verify_result(state)
state["current_node"] = "finalize"
await checkpoint.save(state)
except RetryableError as e:
await retry_policy.wait_and_retry(node, e)
except Exception as e:
state["errors"].append(serialize_error(e))
await checkpoint.save(state)
raise
return {"status": "done", "state": state}
这段代码的重点不是框架 API,而是执行纪律:每个节点之后保存状态,副作用通过 run_once 管理幂等,审批暂停会把状态写入检查点,恢复时从同一个 workflow ID 继续。
适用场景
| 场景 | 典型特征 | 关键需求 |
|---|---|---|
| 长时间运行的代码 Agent | 跨多轮模型调用、命令执行、测试修复、人工确认和 PR 提交 | 中断后恢复、避免重复操作 |
| 企业流程 Agent | 工单处理、合同审阅、数据报表生成、运营动作执行 | 业务状态管理、人审节点、副作用控制 |
| 半自动化运维 Agent | 诊断告警、生成修复方案、等待 SRE 审批、执行安全脚本 | 审批边界、审计追溯、安全恢复 |
| 高价值内容生产 Agent | 自动写文章、生成图片、发送邮件、发布 CMS | 防重复发送、中断恢复、发布状态追踪 |
常见误区
误区一:只保存聊天历史就等于可恢复
聊天历史不能替代工作流状态。恢复需要知道执行节点、工具结果、审批状态和副作用是否已发生。聊天历史可以作为模型上下文,但不应该是唯一事实来源。
误区二:所有失败都自动重试
模型调用、只读查询和幂等写入的重试策略不同。对于不可逆副作用,自动重试必须建立在幂等键、确认查询和补偿机制之上。
误区三:人审只需要一个确认按钮
人审节点必须保存被审批内容的结构化快照和摘要哈希。否则审批通过后,实际执行内容可能已被后续模型调用或代码变更悄悄改变。
误区四:工作流版本可以随时改
长任务可能跨越数小时甚至数天。发布新版本后,旧任务如何继续?是继续使用旧版本,还是迁移到新版本?迁移是否需要重新审批?这些都需要版本策略,而不是让旧任务直接跑新代码。
误区五:状态越完整越好
检查点不是无限制日志仓库。过大的状态会增加读写成本、恢复延迟和隐私风险。实践上应保留结构化核心状态,把大文件、长日志和模型上下文摘要分离存储。
上线检查清单
| 维度 | 检查项 |
|---|---|
| 状态与恢复 | 每个关键节点是否有状态写入?服务重启后是否能从同一个 workflow ID 恢复?恢复后是否会重复执行已成功工具调用? |
| 副作用安全 | 所有写入型工具是否有幂等键?是否有 tool call log?外部系统成功但本地失败时,是否能查询确认? |
| 人审流程 | 审批内容是否结构化展示?审批 payload 是否有摘要校验?审批过期后是否阻止继续执行?拒绝后是否进入明确终态或重新计划? |
| 重试与超时 | 不同节点是否有独立 retry policy?是否区分 run timeout 和 idle timeout?重试耗尽后是否有 error handler 或人工处理入口? |
| 版本治理 | 工作流定义、提示词、工具 Schema、模型配置是否有版本?旧任务遇到新版本发布时是否有兼容策略? |
| 可观测性 | 是否能按 workflow_id 查询完整执行轨迹?是否记录每个节点耗时、失败原因、重试次数、人工等待时间和外部系统返回 ID? |
| 数据治理 | 检查点中是否存储敏感信息?是否有保留周期、加密、脱敏和删除策略? |
小结
LLM Agent 的生产化,不只是让模型更聪明,也要让执行过程更可控。持久化工作流的目标,是把一次不透明的 Agent run 拆成可恢复的状态机:每个节点有检查点,每个副作用有幂等键,每个人审有恢复指针,每次失败都有明确的重试、补偿或人工处理路径。
当 Agent 开始处理代码、工单、邮件、审批、运维和业务数据时,这套机制会比单纯优化 prompt 更重要。因为真正的生产事故往往不是模型少说了一句话,而是系统不知道自己执行到了哪里,也不知道下一步能不能安全继续。