Durable Agent:长任务执行架构
比较内存循环、Checkpointed Graph、Queue Workers 与 Durable Workflow 四种长任务执行拓扑:进度存在哪个节点、故障后从哪个单位恢复、副作用怎样避免做两次、等人批准时进程要不要活着。
一个运行数分钟或数小时的 Agent 不能只靠 process memory。部署、timeout、人工等待和 provider failure 都会中断 loop;系统必须知道已经完成什么、哪些 side effect 可以重试,以及从哪里恢复。四个问题决定了架构:进度存在哪个节点、故障后恢复的单位是什么、副作用怎样避免做两次、等人批准时进程要不要一直活着。
想亲手走一遍四种拓扑、注入故障再恢复,打开互动 Lab:/system-design-lab/durable-agent-execution-architectures。
有约束的设计问题
一个运维助手 Agent 要处理四类任务:几秒钟查完资料就总结的问答;走一张推理图、中间某一步必须等人批准、人可能几小时后才回复的变更方案;几百个互相独立、能并行、量忽大忽小的批量检查;以及跨几天、要等定时器和审批、有多个不能重复的副作用、事后必须能审计的故障处置。每一类该用哪种执行架构?
四种 topology signature
| 架构 | State owner | 恢复单位 | 副作用怎样避免做两次 | 怎样等人 | 适合 | 主要代价 |
|---|---|---|---|---|---|---|
| In-memory Loop | process memory | 整个 request 重跑 | 做不到,除非工具本身幂等 | 做不到,进程必须活着 | 秒级、无副作用 | 崩溃、部署、超时丢全部进度 |
| Checkpointed Graph | checkpoint store,按 thread id | graph node | interrupt 前的代码恢复时会重跑,副作用放在 interrupt 之后或幂等 | interrupt() 存档后无限等待,同一 thread id 恢复 | 可暂停 / 恢复的推理图 | 恢复从节点开头重跑;checkpoint 与外部副作用可能对不上 |
| Queue Workers | job store + queue | 一条 job 消息 | 执行前先读执行记录;每步幂等 | 一步写「等待中」,之后的消息再唤醒 | 大量独立异步步骤 | 至少一次投递会重复;不保证顺序;毒消息 |
| Durable Workflow | 引擎里的 event history | 一个 activity | activity 只跑一次,结果记入历史,回放时复用;仍要幂等以防重试 | 定时器和 signal 由引擎保管,等待不占进程 | 跨小时 / 天、timer、approval、审计 | 工作流代码必须确定性;历史膨胀;多一个引擎要运维 |
1. In-memory Loop:进度只在进程里
循环在一个进程里跑,每一轮调模型、调工具,中间结果放在变量里。进程一挂、一次部署、一次请求超时,进度全没,只能整个请求从头再来;已经做过的工具调用没有任何记录,重跑就再做一次。只适合几秒跑完、没有副作用、重跑无所谓的任务。一旦任务变长或要改外界,就该换架构。
2. Checkpointed Graph:每步存档,interrupt 等人
Agent 是一张图,运行器每执行完一个节点就把图状态按 thread id 存进 checkpoint store(LangGraph 的 checkpointer 就是这个角色)。遇到需要人批准的地方调用 interrupt():图状态被保存,运行无限期停下,不占任何进程;人带着同一个 thread id 发回 Command(resume=值),值会被送回 interrupt 调用点。
关键细节:恢复不是从 interrupt 那一行继续,而是从那个节点的开头重跑,节点里 interrupt 之前的代码会再执行一次。所以有副作用的调用要挪到 interrupt 之后,挪不了就给它幂等键;恢复前比对提案哈希,参数变了不能直接续跑。checkpoint store 写不进去时这一步不能算完成,不能带着没存档的状态进入下一个节点。
3. Queue Workers:每一步是一条消息
API 把任务的每一步变成一条消息放进队列,一群无状态的 Worker 竞争消费(Microsoft 的 Competing Consumers 模式)。队列保证每条消息至少送达一次,不保证只送一次,也不保证顺序,所以每一步必须幂等:Worker 执行前先按 job id 读作业仓库的执行记录,已经完成就只确认消息不再执行;每个副作用带稳定的幂等键。
取消息时加租约(peek-lock):锁着的时候别的 Worker 看不见它,成功就 complete,失败就 abandon 让它重新可见;长步骤要在到期前续锁,否则做到一半消息被别的 Worker 拿走。反复失败的消息在投递次数超过阈值后进死信队列,连同失败信息一起保存,供人分析后重放。Worker 和 API 不直接通信,结果写进作业仓库或回复队列,按原消息关联。
4. Durable Workflow:事件历史驱动回放
引擎(Temporal 是典型实现)为每次执行保存一份完整、有序的 event history,它是唯一真相。Worker 崩溃后引擎不恢复内存快照,而是让工作流代码从头重跑并回放历史,代码走到历史末尾就是崩溃前的位置。这要求工作流代码确定性:给同样的历史必须做同样的决定,读时钟、随机数、直接网络调用都会让回放走岔。
一切接触外界的事放进 activity:API 调用、查库、调模型。activity 只执行一次,结果作为事件记入历史,回放时直接复用不再执行;activity 可能被重试,所以仍要幂等,长 activity 用心跳上报进度,大任务拆成多个小 activity。定时器和 signal(例如人工批准)由引擎保管,等几天也不占 Worker。
托管引擎给出同样的取舍:AWS Step Functions 的 Standard 工作流最长跑一年、exactly-once、状态在转换之间持久化、执行历史保留 90 天,适合不幂等的动作;Express 工作流最长五分钟、at-least-once,适合幂等动作。
Exactly-once 是系统结论,不是 broker 配置
四种架构里没有一种能靠配置得到「恰好一次」。每个有副作用的步骤使用稳定的幂等键,把意图、对方响应和提交状态写进持久状态后再确认;Worker 收到可能重复的消息先读执行记录;等待人工批准时保存提案哈希,恢复后只有相同参数才能继续。
故障与恢复
| 架构 | 故障 | 用户看到什么 | 恢复 |
|---|---|---|---|
| In-memory | 进程在任务中途重启 | 进度全丢,重跑时副作用重复 | 短任务整个重跑并要求工具幂等;任务变长就换架构 |
| In-memory | 模型调用卡住,请求超时把进程杀了 | 超时错误,没有中间结果 | 每次调用单独超时重试;长任务改异步并持久化进度 |
| Checkpointed | interrupt 前的副作用恢复时又跑了一次 | 两个工单 | 副作用挪到 interrupt 之后或加幂等键;恢复前比对提案哈希 |
| Checkpointed | checkpoint store 不可用 | 所有线程无法推进或恢复 | 写失败就让这一步失败并重试;把仓库当核心依赖运维 |
| Queue | 同一条消息投递两次 | 副作用重复,例如退款两次 | 执行前读执行记录;副作用带幂等键 |
| Queue | 毒消息反复失败 | 其他任务被拖慢 | 投递次数上限后进死信队列;人工分析后重放 |
| Queue | 长步骤跑过了租约 | 两个 Worker 同时做同一步 | 续锁;把长步骤拆小 |
| Workflow | 工作流代码读时钟,回放走岔 | 工作流卡住报错 | 时钟、随机数、网络调用放进 activity;代码改动版本化 |
| Workflow | activity 重试时副作用做了两次 | 外部多一条重复记录 | activity 带幂等键;拆小并加心跳 |
怎么选
- 几秒跑完、没有副作用、重跑无所谓:In-memory Loop。
- 推理图要暂停等人、跨对话继续、副作用不多:Checkpointed Graph,副作用放在 interrupt 之后或幂等。
- 大量互相独立、能并行、顺序无所谓、能重复执行的步骤:Queue Workers,执行记录 + 幂等键 + 死信队列。
- 跨小时到几天、要定时器、重试、审批和审计,副作用不能重复:Durable Workflow,接受确定性约束和引擎运维。
- 一个 deterministic workflow 已能完成的任务,不要自动加 Agent。
回到开头的运维助手:秒级问答走内存循环;等人批准的变更方案走 Checkpointed Graph;批量检查走 Queue Workers;跨几天的故障处置走 Durable Workflow。
面试时这样回答
- 先复述约束:任务时长、有没有副作用、要不要等人、要不要审计。
- 说执行路径:点名图上的边,例如「每个节点后存 checkpoint,interrupt 后用同一 thread id 恢复」「队列租约投递,Worker 先读执行记录」「引擎把任务和历史交给 Worker 回放,activity 结果记入历史」。
- 说进度存在哪:进程内存、checkpoint store、job store,还是事件历史。
- 说代价与一个故障:例如恢复从节点开头重跑会重复 interrupt 前的副作用,修法是把副作用挪到 interrupt 之后或加幂等键。