耐久化 Agent 工作流:LangGraph、Temporal 与长任务恢复
工作流一旦跨越分钟、小时甚至人工审批周期,问题就从“模型回答得怎样”变成“进程重启、网络超时、重复消息、审批延迟后还能不能正确继续”。这类系统的核心不是记忆,而是耐久执行(durable execution)。
一、什么叫耐久执行
耐久执行不是把完整对话存到数据库。它至少意味着:
- 每个运行有稳定身份,例如
thread_id/workflow_id/run_id。 - 关键状态与执行进度在进程外持久化,服务重启后可定位继续点。
- 运行历史能够说明已完成哪些节点、用了什么输入、为什么路由。
- 重放时能识别已发生的副作用,避免重复执行。
- 等待人工、定时器、回调或长工具任务时,计算资源不必一直占着。
- 版本升级后旧运行仍能以兼容策略完成或被安全迁移。
耐久并不等于完全不重复。很多运行时会从某个节点边界重新执行;因此节点设计必须考虑可重入与幂等。最危险的误解是:“有 checkpoint,就不会重复发邮件。”Checkpoint 记录的是工作流世界的状态,外部邮件、扣款、写库属于业务世界,仍需由业务键和回执保护。
二、LangGraph:显式图状态与 Interrupt
LangGraph 将长运行 Agent 表达为 State、Node、Edge 与 checkpoint。官方文档强调 durable execution、流式输出、人机协作和持久化;interrupt() 可在节点中暂停,保存状态并等待用相同 thread_id 恢复。这很适合“模型参与决策、流程仍需工程约束”的场景。
State 应该存什么
type InvestigationState = {
runId: string
actor: { id: string; tenant: string; roles: string[] }
workflowVersion: string
currentCase: { id: string; version: number }
evidence: EvidenceRef[]
toolCalls: ToolCallSummary[]
proposal?: { action: string; params: unknown; idempotencyKey: string }
approval?: { status: 'pending' | 'approved' | 'rejected'; decisionId?: string }
budget: { stepsLeft: number; tokensLeft: number; deadlineAt: string }
errors: NodeError[]
}存“事实、引用和控制状态”,少存格式化后的长文本;存工具结果的引用或摘要,避免状态无限膨胀;同时记录模型、Prompt、工具 Schema 和策略版本,才能做回放与问题定位。
Node 的三条铁律
- 单一职责:一个节点要么检索,要么调用一个外部服务,要么生成提案,要么审批;不要把三种外部副作用塞进一个节点。
- 可重入:节点可能在恢复时从头运行。调用外部系统前使用幂等键,或先读取结果;写操作放在审批/确认之后。
- 结构化结果:节点返回状态增量和错误类型;不要以一段自然语言决定边。
Interrupt 的工程含义
LangGraph 的 interrupt 在暂停时保存状态,恢复后节点会从头开始执行。官方文档特别提醒:interrupt 之前的副作用应幂等,interrupt 不能被宽泛的 try/catch 吞掉,多个 interrupt 的顺序不能随条件随机改变。这些规则不是框架细节,而是分布式恢复的一般规律。
一个安全的审批节点结构是:先构造纯数据的提案 -> interrupt 等待批准 -> 恢复后校验批准令牌与对象版本 -> 在独立写节点执行幂等动作。不要在 interrupt 前创建正式订单,然后期待恢复时“不再创建”。
三、Temporal:耐久业务编排的另一类答案
Temporal 更像面向分布式业务流程的耐久工作流平台。它的核心价值是让 Workflow Execution 具备持久执行历史,使用 Worker 执行 Activity,并围绕重试、定时、信号、查询和版本演进组织长任务。它并不要求工作流中必须有 LLM,恰好适合承载“AI 之外的可靠协调”。
LangGraph 与 Temporal 不应简单互相替代
| 问题 | LangGraph 更自然 | Temporal 更自然 |
|---|---|---|
| Agent 的状态、工具循环、子图、人工 steer | 是 | 需自行建模 Agent loop |
| LLM 节点的上下文、路由和流式体验 | 是 | 通常由 Activity/服务实现 |
| 长事务、定时器、跨服务补偿、严格业务工作流 | 可做但需要更多基础设施 | 是 |
| 业务 SLA、稳定 Worker、版本演进 | 可结合周边系统实现 | 是 |
| 低代码业务自动化 | 不以此为主 | 不以此为主 |
常见组合:Temporal 管理订单/案件/审批等外层业务生命周期;某个 Activity 调用 LangGraph 运行受预算限制的 Agent;Agent 返回结构化提案而不是直接提交业务动作。也可以反过来让 LangGraph 主导交互图,把跨日等待、重试与补偿委托给外部耐久服务。关键是只定义一个最终业务动作的权威协调者,避免两个运行时各自重试。
四、恢复、重试、补偿的分层
1. 模型级恢复
结构化输出不合法、上下文过长、模型限流、内容审核拦截。策略可包括修复 Prompt、换更强/更小模型、缩短上下文、重试有限次数、输出部分答案或转人工。模型错误不应被包装成“业务失败”。
2. 工具级恢复
网络抖动、429、5xx 可以指数退避并加抖动;4xx 参数/权限错误通常不重试;超时后必须查询下游是否已处理。工具结果应带 correlation_id 或业务 id,以便对账。
3. 工作流级恢复
节点失败后按错误类型路由:回到上一步修复、切换替代工具、暂停人工、跳过非关键分支、进入补偿或终止。要记录这次选择的原因,防止自动重试造成难以解释的循环。
4. 业务级补偿
跨系统操作没有天然分布式事务时用 Saga。例:先冻结额度 -> 创建订单草稿 -> 风控审批 -> 扣减额度 -> 通知。中间失败时释放额度、取消草稿或标记人工处理。补偿动作本身也需要幂等,并要考虑“补偿失败怎么办”的人工处置路径。
五、长任务的状态设计与数据生命周期
状态分层
| 层 | 内容 | 存储原则 |
|---|---|---|
| 控制状态 | 节点、边、重试、预算、审批 | 强一致、可恢复 |
| 业务事实 | 订单/案件版本、外部回执 | 领域系统是权威源 |
| 运行证据 | Prompt、模型、工具摘要、引用 | 可检索、脱敏、可审计 |
| 大对象 | 文档、图片、完整 trace | 对象存储/日志平台,状态只存引用 |
| 会话记忆 | 偏好、上下文摘要 | 受权限/保留期约束,不能替代事实 |
这样做能避免把几十 MB 文档或无限聊天历史塞进 checkpoint,也能让删除请求、数据保留和审计导出有清晰边界。
超时与取消
长任务必须有截止时间、心跳、取消令牌和生命周期状态:queued -> running -> waiting_human -> retrying -> succeeded | failed | cancelled | expired。取消不是简单杀进程:若外部动作正在进行,要查询回执、标记不再发起新动作、必要时进入补偿或人工确认。前端显示“已取消”之前要区分“停止计划”与“外部副作用已撤销”。
六、版本演进:最容易被忽略的生产题
假设一个审批任务已等待三天,此时团队发布了新图、新 Prompt 和新 Tool Schema。如何保证它恢复后不会走到错误分支?
- 每个 run 固定
workflow_version、prompt_version、tool_schema_version和策略版本。 - 新版本默认只接新 run;旧 run 仍由旧兼容处理器完成,或显式迁移状态。
- 迁移是带审计的函数:输入旧状态、输出新状态、校验不变量、支持回滚。
- Tool API 应做向后兼容:新增字段有默认值,删除字段走双读/双写或适配层。
- 人工审批页面展示“批准的是哪个版本的提案”;若对象/政策变了,旧批准失效并要求重审。
面试里若能主动讲到“等待中的旧 run”,通常说明你理解的不是 demo,而是长期运行系统。
七、系统设计题:跨日投研/尽调 Agent
需求
用户发起一项企业尽调,系统需要检索公开资料、读取内部已授权文档、调用结构化数据、生成风险报告;任务可能持续数小时,部分来源失败需要重试,报告发布前由分析师审核,不能把未授权内部资料带到外部模型。
推荐编排
Case Workflow (Temporal/耐久协调)
-> 创建案件与授权快照
-> LangGraph Research 子图
-> 规划 -> 受控检索 -> 证据归档 -> 事实冲突检测 -> 草稿
-> 规则与数据出境 Gate
-> 人工审核 Interrupt/Signal
-> 发布报告 / 归档 / 反馈入评测必答点
- 外层案件工作流管理超时、重试、审批、通知、版本与补偿;研究子图管理开放式工具循环。
- 每份证据带来源、抓取时间、授权域、哈希与引用位置;模型只能访问经过数据出境策略批准的内容。
- 每个外部抓取、数据库查询和报告发布都用可追踪的 request ID;发布动作依赖审批令牌和不可变报告版本。
- 任务恢复先读取案件/报告外部状态,若已发布就停止,不重复发送。
- 指标不仅是报告“好不好”,还包括证据覆盖、引用准确率、无授权数据外发、恢复成功率、人工修改率和单案件预算。
八、项目讲法模板
我们把长任务拆成两个运行层:外层耐久工作流负责案件生命周期、超时、通知、审批和版本;内层 LangGraph 负责研究任务的动态检索与工具选择。State 只保存案件事实、证据引用、预算和审批状态,完整文档放对象存储。每个写操作带幂等键并以外部回执为准;人工审批通过 interrupt/signal 恢复时会重新校验案件版本和授权。这样服务重启、模型限流或审批跨天都不会让任务丢失,也不会因为重放而重复发布报告。
九、面试高频问答
Q:Checkpoint 能解决哪些问题,不能解决哪些问题? 它能保存图状态、定位恢复点、支持人工暂停和回放;不能自动保证外部写操作恰好一次,也不能自动完成分布式事务。外部动作仍需要幂等、回执、对账和补偿。
Q:为什么要把副作用拆成独立节点? 恢复可能重新执行一个节点。把纯计算、审批、写入拆开,能让副作用拥有单独的幂等键、授权和监控,也能减少重复执行范围。
Q:LangGraph 的 interrupt 前能否做写操作? 最好不要;如果必须做,写操作必须幂等且恢复时能识别已完成。因为恢复会从节点开头重新运行。更安全的设计是把写入放在审批恢复后的独立节点。
Q:什么时候选 Temporal? 当主要困难是跨服务长事务、可靠定时/重试、审批等待、版本演进和业务 SLA 时。若主要困难是 Agent 的上下文、工具循环与状态图,LangGraph 更自然;两者可以组合但要避免双重重试和双重权威状态。