文章

Agent Durable Execution 生产实战:用 Checkpoint、幂等副作用与 Replay Contract 防止长任务重复执行

长任务 Agent 恢复时最怕重复调用工具、重复写库和重复发消息。本文结合 LangGraph、Temporal 与 Microsoft Agent Framework,讲清检查点边界、幂等副作用、确定性回放与安全恢复的生产设计,给出可落地的 operation_id、side-effect ledger 与状态机方案。

Agent Durable Execution 生产实战:用 Checkpoint、幂等副作用与 Replay Contract 防止长任务重复执行

背景:Agent 最危险的故障不是「失败」,而是「恢复后重复成功」

单次聊天失败通常容易处理:返回错误,用户重试即可。但生产级 Agent 往往是一个持续数分钟甚至数小时的状态机:

  1. 调用 LLM 规划任务;
  2. 查询数据库;
  3. 创建工单;
  4. 等待人工审批;
  5. 调用支付或订单接口;
  6. 发送邮件;
  7. 汇总结果。

如果 Worker 在第 5 步之后崩溃,系统必须知道:前四步哪些已经完成,第五步是否已经真正落到外部系统,第六步是否应该继续。

这时只保存 conversation history 不够。Durable Execution 的目标不是「记住更多上下文」,而是让执行进度、非确定性结果和外部副作用之间形成可恢复的一致性边界。

LangGraph、Temporal 和 Microsoft Agent Framework 的具体 API 不同,但它们都暴露出三个必须单独治理的问题:

  • Checkpoint Boundary:从哪里恢复;
  • Side-effect Boundary:哪些外部动作可能重复;
  • Replay Contract:恢复时哪些代码可以重新执行,哪些结果必须复用历史记录。

理解这三个边界,比简单地「给 Agent 加一个 Redis 状态表」更重要。

核心原理一:Checkpoint 保存的是执行状态,不是 exactly-once 保证

Checkpoint 很容易给人一种错觉:既然状态保存了,恢复后就不会重复执行。实际上,Checkpoint 只能回答「恢复时系统知道什么」,不能自动回答「外部世界已经发生了什么」。

LangGraph 的检查点在执行边界保存图状态,官方文档明确提醒:节点在中断或恢复后可能从头重新执行,因此中断前发生的副作用必须具备幂等性,或者被拆到独立 Task/Node 中。Microsoft Agent Framework 同样在 superstep 结束后创建 Checkpoint,并保存 executor state、pending messages、pending requests/responses 和 shared state,可以恢复工作流,但并不意味着任意外部写操作天然拥有 exactly-once 语义。

因此生产设计中,应把状态分为两类:

1. 可重放状态(适合进入 Checkpoint)

  • 当前步骤;
  • Planner 输出;
  • 已获得的工具结果;
  • 待审批请求;
  • 中间推理结果;
  • 下一步路由。

2. 外部副作用(必须设计自己的幂等协议)

  • 扣款;
  • 创建订单;
  • 发券;
  • 发短信;
  • 创建工单;
  • 写第三方 CRM;
  • 调用不可查询状态的遗留系统。

这些动作不能仅依赖 Checkpoint 保证正确性。

核心原理二:Side Effect 必须拥有稳定的逻辑身份

最实用的方法不是问「这个函数执行过没有」,而是给每一次业务动作一个稳定的 logical operation id

import hashlib


def operation_id(run_id: str, step: str, business_key: str) -> str:
    raw = f"{run_id}:{step}:{business_key}"
    return hashlib.sha256(raw.encode()).hexdigest()[:32]

假设 Agent 要为订单 ORD-20260902-001 创建退款申请,可以生成:

operation_id = operation_id(run_id, "create_refund", order_id)

然后将这个 ID 同时用于:

  • 下游 API 的 Idempotency-Key
  • 本地 side-effect ledger 的唯一键;
  • Outbox 表唯一约束;
  • 日志与 Trace 关联字段。

这样即使 Worker 崩溃后重新进入同一步骤,系统也不会把「同一个逻辑动作」误认为一项全新的业务请求。

推荐的 Side-effect Ledger:

CREATE TABLE agent_side_effect (
    operation_id   VARCHAR(64)  PRIMARY KEY,
    run_id         VARCHAR(64)  NOT NULL,
    action_name    VARCHAR(128) NOT NULL,
    business_key   VARCHAR(128) NOT NULL,
    request_hash   VARCHAR(64)  NOT NULL,
    status         VARCHAR(32)  NOT NULL,
    external_ref   VARCHAR(128),
    result_json    TEXT,
    created_at     TIMESTAMP    NOT NULL,
    updated_at     TIMESTAMP    NOT NULL
);

执行逻辑不要写成:

result = external_api.create_order(payload)
checkpoint["order_id"] = result["id"]

更稳妥的是:

op_id = operation_id(run_id, "create_order", business_key)

cached = ledger.get(op_id)
if cached and cached.status == "SUCCEEDED":
    return cached.result

ledger.mark_started(op_id, request_hash(payload))
result = external_api.create_order(
    payload,
    idempotency_key=op_id,
)
ledger.mark_succeeded(op_id, result)
return result

这里的关键不是数据库表本身,而是恢复之后仍然能计算出同一个 operation_id

核心原理三:LLM 与外部 I/O 不应该直接进入可回放控制逻辑

Temporal 对这一点的约束最明确:Workflow 代码需要保持确定性,而 LLM 调用、工具调用、数据库访问和外部 API 都属于非确定性 I/O,应放进 Activity。

原因并不复杂。假设恢复时重新执行:

decision = llm.invoke(prompt)

第一次模型输出 call_tool("refund");第二次由于模型采样、模型版本或服务端行为变化,可能输出 call_tool("cancel_order")。此时系统已经不是「恢复」,而是在历史中间重新生成了一条新的未来。

Durable Runtime 更合理的语义是:

Workflow / Orchestrator
   |
   +-- schedule LLM Activity
   |        |
   |        +-- historical result recorded
   |
   +-- replay uses recorded result

恢复时复用已经完成的非确定性结果,而不是重新询问模型。LangGraph 的 Functional API 也采用相似思路:Task 和 subgraph 的结果可以持久化,恢复时已完成的 Task 不必重新计算;同时文档明确要求副作用函数保持幂等。

Replay Contract:代码升级也可能破坏恢复

Durable Execution 还有一个经常被忽略的问题:一个运行中的 Agent 可能跨越应用版本升级。

例如某个任务周一启动:

planner -> lookup -> approval -> payment -> notify

周二发布新代码,变成:

planner -> lookup -> risk_check -> approval -> payment -> notify

周三一个旧任务从 approval Checkpoint 恢复。此时新的代码拓扑、节点身份、Task 顺序和历史 Checkpoint 是否兼容?这就是 Replay Contract

Microsoft Agent Framework 的 Checkpoint 文档明确指出,rehydration 时需要保留工作流 topology 和 executor identity。LangGraph 则提醒,当节点内部 Task 或 interrupt 的顺序发生改变时,恢复可能无法正确匹配已经保存的结果。

因此,长期运行 Agent 不应该只给 Prompt 和 Tool Schema 做版本管理,还需要给 workflow definition 做版本治理。建议至少记录:

run_id: agt_20260902_001
workflow_name: claims-agent
workflow_version: 7
checkpoint_schema_version: 3
tool_catalog_version: 18
prompt_release: prod-2026-09-02
model_policy_version: 11

恢复时先做兼容性判断,而不是直接拿最新代码强行继续。

工程落地:把一个 Agent Step 拆成四个阶段

生产中可以把每个具有副作用的步骤拆成四段:

Prepare -> Execute -> Commit -> Checkpoint
  • Prepare:计算 operation_id、请求参数、request hash、当前业务前置条件。此阶段不产生不可逆副作用。
  • Execute:真正调用外部系统。如果下游支持 idempotency key,必须传递 operation_id;如果下游不支持,就需要通过本地 ledger + 查询接口 + 唯一业务键尽量构造幂等语义。
  • Commit:记录 external reference、执行结果、committed timestamp、response hash。此时本地系统已经知道外部动作完成。
  • Checkpoint:最后再推进 Workflow 状态。

这种顺序无法消灭所有分布式系统中的不确定窗口,但能大幅缩小「外部已经成功、本地完全不知道」的范围。

最棘手的窗口:外部成功,本地 Commit 前崩溃

这是所有 Durable Agent 都绕不开的经典问题:

Agent -> Payment API: charge()
Payment API -> success
Agent process crashes
Ledger.mark_succeeded() 尚未执行

恢复后,本地看到的仍是 STARTED。此时正确策略不是盲目重试,而是进入 reconciliation

  1. 如果下游支持 idempotency key,使用同一 key 重试;
  2. 如果下游支持查询,按 business key / external request id 查询;
  3. 如果无法确认,标记为 UNKNOWN
  4. 高风险业务进入人工补偿,而不是让 Agent 猜。

推荐状态机:

PENDING
   |
   v
STARTED
   ├──→ SUCCEEDED
   ├──→ FAILED_RETRYABLE
   ├──→ FAILED_FINAL
   └──→ UNKNOWN

其中 UNKNOWN 是非常重要的状态。支付、理赔、订单、发券等业务宁可进入人工核对,也不要把「不知道是否成功」错误地压缩成「失败」。

Retry Policy:不要让每一层都自己重试

Agent 系统很容易出现重试风暴:

HTTP Client retry 3 次 × Tool SDK retry 3 次 × Agent node retry 3 次 × Workflow retry 3 次 = 最坏 81 次调用

生产环境应该确定唯一的 retry owner

层次职责
HTTP SDK关闭自动重试
Tool Adapter只做错误分类
Durable Activity负责网络类重试
Workflow负责业务级补偿和状态推进

还需要区分错误类型:

Retryable

  • 429;
  • 短暂 5xx;
  • 网络超时;
  • 临时依赖不可用。

Non-retryable

  • 参数非法;
  • 权限不足;
  • 内容策略拒绝;
  • 明确业务拒绝;
  • Schema 不兼容。

这不仅降低故障放大,也使历史记录更容易解释。

Human-in-the-loop:暂停越久,版本漂移越值得警惕

人工审批是 Durable Execution 最典型的使用场景之一。一个 Agent 可以今天发出审批请求,下周才收到回复。Microsoft Agent Framework 的 Checkpoint 能保存 pending request,并在恢复时重新发出相应事件。

但生产系统还应额外保存:

  • 审批请求对应的 workflow version;
  • 审批时看到的业务快照;
  • 审批对象 hash;
  • 过期时间;
  • 恢复前是否需要重新校验业务条件。

例如,一周前批准的是 500 元退款,但恢复时订单已经被人工处理。Agent 不应该仅凭旧 Checkpoint 继续执行。因此,Checkpoint 保证执行连续性,业务前置条件校验保证语义仍然有效。

上线监控:不要只监控 Agent 成功率

Durable Agent 至少需要下面几类指标:

恢复指标

  • workflow_resume_total
  • checkpoint_restore_failure_total
  • resume_latency
  • checkpoint_age

重放指标

  • replayed_step_total
  • task_result_reused_total
  • workflow_version_mismatch_total

副作用指标

  • side_effect_duplicate_prevented_total
  • idempotency_key_hit_total
  • side_effect_unknown_total
  • reconciliation_total

重试指标

  • activity_retry_total
  • retry_exhausted_total
  • non_retryable_failure_total

其中最值得告警的不是普通 retry,而是:

side_effect_unknown_total > 0

因为它意味着系统已经进入「无法确认外部副作用是否完成」的灰区。

适用场景

Durable Execution 特别适合:

  • 跨多个外部系统的业务 Agent;
  • 需要人工审批的 Agent;
  • 数分钟到数天的长任务;
  • 多 Agent 协作工作流;
  • 支付、订单、工单、通知等带副作用场景;
  • Worker 需要弹性伸缩、允许随时重启的运行环境。

如果 Agent 只是一次请求内完成的问答,没有外部写操作,也不需要暂停恢复,引入完整 Durable Runtime 可能反而增加复杂度。

常见误区

误区一:有 Checkpoint 就是 exactly-once。 不是。Checkpoint 解决恢复状态,幂等解决重复副作用,它们是两件事。

误区二:恢复就是从最后一行代码继续。 多数框架的恢复语义更接近「从一个稳定边界重新执行,并复用已持久化结果」,而不是进程级指令指针恢复。

误区三:所有 API 都可以简单重试。 对于支付、发券、创建订单等副作用接口,如果没有幂等语义,自动重试本身就是风险源。

误区四:只有 Agent State 需要版本化。 长期任务还需要考虑 Workflow Definition、Tool Contract、Prompt Release 和 Checkpoint Schema 的版本兼容。

误区五:Agent Memory 等于 Durable Execution。 Memory 解决「Agent 记得什么」;Durable Execution 解决「Agent 做到哪一步、哪些动作已经发生、失败后如何继续」。两者职责不同。

上线检查

在生产发布前,建议至少验证:

  • 每个不可逆 Side Effect 是否都有稳定 operation_id;
  • 下游是否支持 idempotency key;
  • 不支持幂等的系统是否有 reconciliation 路径;
  • Checkpoint 恢复是否可能重新进入某个 Node;
  • LLM/Tool/API 是否被隔离到可持久化结果的 Task/Activity;
  • Retry owner 是否唯一;
  • 是否区分 retryable 与 non-retryable;
  • UNKNOWN 状态是否有人工处理流程;
  • Workflow 升级后旧 Checkpoint 是否仍可恢复;
  • Human-in-the-loop 恢复前是否重新校验业务前置条件;
  • 故障演练是否覆盖「外部成功、本地 Commit 前崩溃」。

FAQ

Q:Checkpoint 是否就意味着 Agent 获得了 exactly-once 执行语义?

不是。Checkpoint 能保存执行进度,但外部系统的副作用可能发生在两个 Checkpoint 之间。要防止重复动作,仍需要 idempotency key、唯一约束、side-effect ledger 或 reconciliation。

Q:为什么要强调 Replay Contract?

因为一个运行中的 Agent 可能跨越代码发布。恢复时,如果节点身份、Task 顺序、Workflow topology 或 Checkpoint Schema 已改变,历史状态未必能被新版本安全解释。Replay Contract 就是在明确什么变化兼容、什么变化必须启动新执行。

Q:Durable Execution 会不会让所有 Agent 都变复杂?

会增加基础设施和状态治理成本,所以不应该滥用。短请求、只读工具、无副作用场景可以保持简单;长任务、人工暂停和高价值副作用场景才最值得引入。

参考资料

  1. LangGraph Functional API — Durable execution & idempotency:https://docs.langchain.com/oss/python/langgraph/functional-api
  2. LangGraph Graph API — Re-execution and idempotency:https://docs.langchain.com/oss/python/langgraph/graph-api
  3. LangGraph Interrupts — Side effects before interrupt must be idempotent:https://docs.langchain.com/oss/python/langgraph/interrupts
  4. Temporal AI Agent Reference Architecture — Workflows orchestrate, Activities execute:https://go.temporal.io/platform-hub/ai-engineering/ai-reference-architecture
  5. Temporal AI Engineering Patterns:https://go.temporal.io/platform-hub/ai-engineering/ai-patterns
  6. Microsoft Agent Framework Workflows — Checkpoints:https://learn.microsoft.com/en-us/agent-framework/workflows/checkpoints
  7. Microsoft Agent Framework — Durable Extension:https://learn.microsoft.com/en-us/agent-framework/integrations/durable-extension

常见问题

Checkpoint 是否就意味着 Agent 获得了 exactly-once 执行语义?
不是。Checkpoint 解决的是状态恢复;外部 API、数据库写入、支付或消息发送等副作用仍可能在重试或恢复时再次发生,必须额外设计幂等键、唯一约束或去重记录。
为什么不能在可回放的 Workflow 代码里直接调用 LLM 或外部 API?
因为恢复过程可能重新执行 Workflow 代码,而 LLM 和外部 I/O 都是非确定性的。更稳妥的做法是把它们隔离到可记录结果、可独立重试的 Activity 或 Task 中。
什么场景值得引入 Durable Execution?
当 Agent 会运行数分钟以上、跨多个工具、等待人工审批、需要断点续跑,或任何一步具有不可重复副作用时,Durable Execution 的价值最明显。
为什么需要强调 Replay Contract?
因为运行中的 Agent 可能跨越代码发布。恢复时若节点身份、Task 顺序、Workflow topology 或 Checkpoint Schema 已改变,历史状态未必能被新版本安全解释。