AI Agent 的预写日志:借鉴数据库恢复模式实现崩溃安全执行
你的 Agent 正在执行一个 12 步工作流的第 7 步——它已经查询了三个 API、写入了两个文件、发送了一条 Slack 通知——这时进程崩溃了。接下来会发生什么?如果你的答案是"从第 1 步重新开始",那你将重新发送那条 Slack 消息、重新写入那些文件,并再次消耗你的 LLM token 预算。这正是数据库几十年前通过预写日志解决的问题。这个模式可以高度精确地映射到 Agent 架构中。
核心思路很简单:在 Agent 执行任何步骤之前,先记录它打算做什么。在继续下一步之前,记录发生了什么。这个仅追加的日志成为恢复的唯一真实来源——不是 Agent 的内存状态,不是世界的快照,而是一个可以确定性重放的意图和结果的顺序记录。
Agent 架构中的持久性缺口
大多数 Agent 框架将执行视为临时性的 。一个函数运行、调用 LLM、调用一些工具、返回结果。如果执行过程中出现任何故障,框架会从头重试整个函数。这对于无状态的请求-响应模式来说完全没问题,但 Agent 并不是无状态的。
一个典型的生产级 Agent 工作流包含:
- 多次相互依赖输出的 LLM 调用
- 具有真实世界副作用的工具调用(发送邮件、创建数据库记录、调用外部 API)
- 依赖中间结果的分支逻辑
- 可能暂停数小时或数天的人工审批环节
复合可靠性问题是残酷的。如果 10 步工作流中每一步的可靠性为 99%,整体成功率将降至 90.4%。如果每步可靠性为 95%,整个工作流的成功率只有 59.9%。而且这还假设故障是干净的——实际上,部分故障更糟糕。它们会让系统处于不一致状态,其中一些副作用已经触发而另一些还没有。
从头重试不仅浪费 token。它还会产生重复的副作用。你的 Agent 发送了两次邮件、创建了重复记录、再次向客户收费。数据库领域在 1990 年代就解决了这个问题。是时候让 Agent 领域迎头赶上了。
预写日志的工作原理(以及为何能映射到 Agent)
在数据库中,WAL 遵循一个简单的规则:在修改磁盘上的任何数据页之前,先将预期的变更写入顺序日志。该日志是仅追加的、持久的、有序的。如果数据库在事务中途崩溃,恢复过程会向前读取日志,并根据日志内容完成或回滚每个 事务。
映射到 Agent 执行是直接的:
- 数据页 变为 Agent 状态(Agent 积累的上下文、中间结果和记忆)
- 事务 变为 工作流步骤(每个工具调用、LLM 调用或决策点)
- 日志 变为 执行日志,记录每个步骤的意图和结果
关键属性是相同的:日志在动作执行之前写入,结果在 Agent 进入下一步之前记录。这为你提供了从头重试无法实现的三种恢复能力。
第一,对已完成步骤的跳过重放。如果第 5 步的结果已经记录在日志中,你不需要重新执行它——你重放缓存的结果并从第 6 步继续。这就是 Temporal 的事件溯源模型的工作方式:它重放事件历史来重建应用状态,使用存储的返回值而不是重新执行活动。
第二,副作用的精确一次语义。因为日志记录了哪些工具调用已成功完成,恢复过程知道不需要重新执行它们。有副作用的工作——发送 Slack 消息、信用卡扣款——即使周围的工作流多次重启也只执行一次。
第三,非确定性操作的确定性恢复。LLM 调用本质上是非确定性的——相同的提示可能产生不同的输出。没有日志的话,重试工作流第二次可能会走完全不同的执行路径。有了日志,存储的 LLM 输出在恢复时被重放,保留了原始执行路径。
检查点粒度:工程权衡
并非所有检查点策略都是相同的。持久化状态的粒度决定了你的恢复精度、存储成本和写入开销。Agent 工作流有三个自然边界。
每次工具调用检查点 在每个工具调用后记录结果。这提供了最细粒度的恢复——你永远不会重新执行单个工具调用——但带来了最高的写入开销。对于每次工具调用都有显著副作用或高延迟(外部 API 调用、数据库写入)的工作流,这通常是正确的选择。
每个计划步骤检查点 在 Agent 计划的逻辑边界处记录状态。如果你的 Agent 将任务分解为"研究→草稿→审查→发布",你在每个阶段之间设置检查点。这减少了写入开销,但意味着在"草稿"阶段崩溃需要重新执行该阶段内的所有工具调用。当单个工具调用便宜且幂等,但整个工作流重启代价高昂时,这种方式效果很好。
每个决策点检查点 仅在 Agent 基于 LLM 输出做出分支决策时记录状态。这是最粗粒度的有用级别——它确保你不会重新执行决定执行路径的 LLM 调用,但接受在决策之间重新执行确定性工作。当大多数步骤快速且无副作用,而昂贵的 LLM 推理发生在关键节点时,这是合适的。
正确的选择取决于你的成本函数。如果重新执行一步的 API 费用为 0.50 美元,而检查点存储每次写入成本为 0.001 美元,那就检查点一切。如果你的步骤很便宜但数量成千上万,在逻辑边界处设置检查点以避免日志成为瓶颈。
持久化执行:WAL 原则作为运行时
WAL 模式已经演变为一种完整的编程模型,称为持久化执行,现在由 Temporal、Restate 和 Inngest 等平台提供。核心思想是:你将 Agent 逻辑写成普通的顺序代码,运行时透明地记录每个非确定性操作。如果进程崩溃,运行时重放日志以重建 Agent 状态,并从最后完成的步骤继续执行。
这清晰地分离了 Agent 的关注点:
- 编排逻辑(工作流)是确定性的和无状态的。它做决策、分支、循环——但从不直接调用外部服务。
- 有副作用的操作(在 Temporal 术语中称为活动)被包装在持久化上下文中,运行时可以记录、重试和重放。
实际影响是显著的。开发者编写 Agent 代码时就好像它会永远运行而不会崩溃——不需要手动检查点逻辑、恢复处理程序或幂等性密钥管理。运行时处理一切。当 LLM 调用返回结果时,该结果被记录。当工具调用完成时,其输出被记录。恢复时重放这些存储的结果而不是重新执行操作。
这个模型还优雅地解决了人工介入问题。当 Agent 在继续之前需要审批时,工作流只需等待一个信号。运行时持久化工作流状态,进程可以完全关闭。当审批到达时——数小时或数天后——运行时重放日志以重建 Agent 状态并继续执行。不需要长连接,等待期间不消耗资源。
副作用是最难的部分
WAL 模式保证你的 Agent 正确恢复,但它不会自动让你的副作用安全。如果你的 Agent 调用一个不支持幂等性密钥的外部 API,重放日志不会阻止重复效果——日志会正确地跳过重新执行,但前提是原始执行在崩溃前被正确记录。
有 一个狭窄的脆弱窗口:Agent 执行工具调用,副作用触发,然后进程在结果被记录到日志之前崩溃。这与数据库面临的撕裂写入问题相同,解决方案也类似。
幂等性密钥 是主要防御手段。每个工具调用都应包含一个从工作流 ID 和步骤编号派生的唯一密钥。如果工具调用由于日志记录窗口中的崩溃而被重放,外部服务会识别重复的密钥并返回缓存的结果而不是再次执行。这将精确一次保证推到了外部服务中。
补偿操作 处理幂等性不可能的情况。如果你的 Agent 发送了一封邮件然后崩溃了,你无法撤回邮件——但你可以在日志中记录发送并在恢复时跳过它。对于真正不幂等的操作,saga 模式提供了一种结构化的方式来定义补偿操作(发送更正邮件、撤销扣款),以恢复一致性。
读写分离 通过对工具调用进行分类来简化恢复。读操作(获取数据、查询 API)本质上可以安全重试。写操作(发送消息、创建记录)需要幂等性保护。用这种区分来构造 Agent 的工具可以让恢复逻辑更清晰,减少需要仔细处理的表面积。
实践中的样子
生产生态系统已经收敛于基于日志的恢复作为标准方法。LangGraph 在每次节点执行后将图状态持久化为检查点,推荐 PostgreSQL 作为生产后端。Temporal 在其事件历史中记录每个活动调用和返回值。Restate 将非确定性操作包装在自动记录的持久化上下文中。
这些平台的实现模式惊人地一致:
- Agent 状态在每步之后被序列化和存储
- 恢复重放存储的结果而不是重新执行操作
- 有副作用的操作被隔离在带有重试策略的包装上下文中
- 工作流标识(线程 ID、工作流 ID)提供检查点检索的关联键
采用这些模式的团队报告在故障场景中减少了 3-5 倍的重复副作用,并通过不重新执行昂贵的 LLM 调用实现了显著的成本节约。日志记录的写入开销通常可以忽略不计——每步几千字节的序列化状态,相比于流经 LLM 调用本身的数兆字节 token。
何时 WAL 是过度设计
并非每个 Agent 都需要持久化执行。如果你的 Agent 是单次 LLM 调用且没有副作用——一个回答问题的聊天机器人——从头重试完全没问题。WAL 模式在以下情况下才值得其复杂性:
- 工作流跨越超过 3-5 个步骤
- 步骤具有昂贵或不幂等的副作用
- 执行时间超过几分钟
- 人工审批环节暂停执行
- 故障成本高(金融交易、面向客户的操作)
对于简单的 Agent,带有指数退避的基本 try-catch 就足够了。运行持久化执行平台的运维复杂性——额外的基础设施、学习曲线、调试日志重放——在你的故障成本超过基础设施成本之前是不合理的。
决策框架很直接:估算在故障时从头重新执行整个工作流的成本(token 成本 + 重复副作用 + 处理不一致性的工程时间)。如果这个数字足够高,可以证明基础设施投资的合理性,就采用持久化执行。如果你的工作流短且幂等,保持简单。
