05 - 选型与落地
前置:不需要。本篇是全专题结论,可独立阅读,细节按需回查前四篇。
本篇回答:四条路线该选哪条、重放会怎么污染 trace、以及什么都不引入时最小可用方案怎么搭。
本篇会用到的词:
| 词 | 意思 |
|---|---|
| 幂等键 | 让重跑变得无害的那个确定性字符串。判断树里唯一一个「无论选哪条路线都必须自己做」的东西 |
| 无人值守 | 任务跑起来之后没有人盯着,也没有用户会回头来看结果。这是 LangGraph 那条路线的分界线 —— 它的检查点需要有人主动来捡 |
| 跨服务编排 | 一个任务的步骤分散在多个服务里,需要统一的状态和重试。这是选 Temporal 而不是更轻方案的主要理由 |
| 重放污染 | 重放式方案恢复时会让已完成的步骤再次经过埋点代码,于是 trace 里出现重复 span、按 span 累加的成本虚高 |
| Activity | Temporal 里包裹「真正干活」那部分代码的单元。重放时它不执行,所以埋点应该放在这里而不是工作流函数里 |
一、四条路线定位
| 路线 | 代表 | 状态形式 | 自动恢复 | 确定性约束 | 额外基础设施 |
|---|---|---|---|---|---|
| 框架内置 | LangGraph Checkpointer | 快照 | ❌ | 无 | 无 |
| 数据库即状态 | DBOS | 步骤输出 | ✅ | 强 | 无 |
| 单独运行时 | Restate | 执行日志 | ✅ | 强 | 一个进程 |
| 工作流引擎 | Temporal | 事件历史 | ✅ | 强 | 一个集群 |
二、选型判据
红色节点是这棵树上唯一的强制项。01 篇 3.1 节已经论证过:崩溃窗口无法消除 → 某些步骤必然重跑 → 幂等键是唯一出路。
顺序反了的后果:得到一个能可靠地把邮件发两遍的系统。
三、重放对可观测性的污染
这是持久化执行与 Agent 可观测性的交叉点,四篇里都提到过,此处集中说明。
3.1 现象
重放式方案(Temporal / DBOS / Restate)在恢复时会把工作流代码从头重跑。若在工作流函数里直接埋点,一个被重放三次的工作流会产生三份重复 span。
3.2 三条处理原则
| 原则 | 做法 |
|---|---|
| 埋点放在被记录的步骤里 | Activity / step / run() 内部只在真正执行时运行,重放时不执行,天然不重复 |
| 成本以实际调用为准 | 最可靠的口径是网关侧记录的真实请求 —— 重放不产生网关请求。见 可观测性 03 篇 |
| 必须在工作流层埋点时,带重放标记 | 各家 SDK 都提供"当前是否处于重放中"的判断,用它标记或跳过 |
# 若一定要在工作流函数里记录,先判断是否处于重放
if not workflow.unsafe.is_replaying():
logger.info("进入第二阶段") # 只在首次真实执行时记录
这也是评估可观测性方案的一个探针:问供应商如何处理持久化执行的重放,能答上来说明其在生产环境跑过。
四、可自行实现的最小方案
尚未引入任何框架时,下面四步能覆盖大部分需求:
# ① 为每个任务分配 ID,每个步骤生成确定性的键
# 确定性是关键 —— 重跑时必须算出完全相同的值,不能用 uuid4() 或时间戳
step_key = f"{task_id}:{step_index}"
# ② 每步执行前先查、执行后再写
result = store.get(step_key)
if result is None:
result = run_step(...)
store.put(step_key, result)
# ③ 所有外部副作用带上同一个键做去重
send_email(..., idempotency_key=step_key)
# ④ 定时任务扫描超时未完成的 task_id 并重新 拉起。
# 拉起前必须取分布式锁,否则多实例会重复执行同一任务
# —— 即 04 篇 1.3 节 DBOS 用 Conductor 解决的问题,
# 自行实现时这是最常见的遗漏点。
if lock.acquire(f"recover:{task_id}", ttl=300):
resume(task_id)
这套方案缺少的是:跨服务编排、挂起不占资源、自动重试策略、可视化。当这几项开始成为痛点时再选框架 —— 那时你已经理解了这些框架在解决什么,选型会准确得多。
五、检查点数据的安全边界
这一点在其他资料里很少被提及,但影响不小。
一个运行三个月的 Agent 平台,检查点表里累积着:所有用户的完整对话、上传文档的内容、工具返回的内部数据。
这张表的敏感级别不低于用户表,但它通常没有对应的:
| 缺失项 | 后果 |
|---|---|
| 访问控制 | 任何能读该库的服务都能读到全部对话内容 |
| 字段级加密 | 数据库备份泄露等同于对话内容泄露 |
| 保留期策略 | 无限期 留存,合规问询时无法回答 |
| 脱敏 | 用户粘贴的身份证号、密钥被原样存下 |
02 篇 3.2 节提到的 prune / delete_thread 不只是省存储 —— 它们是数据保留合规的执行点。
六、与本板块其他专题的关系
| 专题 | 交叉点 |
|---|---|
| 网关 | 网关重试处理单次调用,持久化处理整个任务。叠加使用:网关把单次失败率压到 0.01%,其余由持久化兜住 |
| 安全 | 检查点是高价值数据资产,也是高价值攻击目标 —— 见第五节 |
| 可观测性 | 重放污染 —— 见第三节 |
七、结论
持久化执行的本质是:承认失败必然发生,把"失败之后怎么办"从临场处理变成设计期决策。
四条路线的差别只是这个决策落在哪一层 —— 框架内、数据库里、运行时里,还是一个独立集群里。
无论选哪条,有两件事始终是使用方自己的责任:幂等键,以及检查点里那份数据的安全。
← 回到 专题索引 · Agent Infra 板块总览