文章

LLM Agent 持久化工作流生产实战:用检查点、状态机与人审恢复跑稳长任务

本文讲解如何把长任务型大模型 Agent 从一次性脚本改造成可恢复工作流,覆盖检查点、状态机、人审中断、重试超时、幂等副作用和上线检查清单,适合需要稳定运行的生产场景。

背景问题: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、影响行数估计、回滚方案

审批通过后,恢复时要校验三件事:

  1. 审批的 payload digest 与当前待执行 payload 是否一致
  2. 工作流版本和工具版本是否仍兼容
  3. 审批是否仍在有效期内

如果任一条件不满足,工作流应回到重新计划或重新审批,而不是继续执行旧计划。

一个简化的实现结构

下面是一个不绑定具体框架的伪代码,用来表达核心结构:

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 更重要。因为真正的生产事故往往不是模型少说了一句话,而是系统不知道自己执行到了哪里,也不知道下一步能不能安全继续。


参考资料

常见问题

LLM Agent 持久化工作流和普通会话记忆有什么区别?
会话记忆主要解决多轮上下文延续;持久化工作流解决长任务在失败、重启、人审中断后如何从安全状态继续执行。两者可以同时存在,但不要混成一个存储模型。
Agent 工作流一定要用 Temporal 或 LangGraph 吗?
不一定。关键是具备状态机、检查点、幂等副作用、重试超时和人工恢复能力。框架可以降低实现成本,但生产边界仍需要团队自己定义。
什么时候应该引入人审中断?
当 Agent 将执行不可逆副作用、修改关键数据、发送外部消息、提交代码或触发财务类操作时,应在工具调用前后设置可恢复的人审节点。