跳到主要内容

智能体灾难恢复:当工作记忆随区域一同失效时

· 阅读需 14 分钟
Tian Pan
Software Engineer

你团队每季度演练的灾备 (DR) 操作手册是为了一套你已经不再完全运行的技术栈编写的。手册上写着:提升从库、重新指向 DNS、清空队列。它假设状态存储在数据库、队列和对象存储中 —— 这些是 SRE 团队已经管理、命名并测试了十年的地方。接着在上个季度,你上线了一个智能体 (agent)。现在,工作内存存在于推理提供商的会话缓存中、工作节点本地磁盘上的草稿文件里、尚未回写的在途工具调用结果中,以及仅存在于单次模型调用提示词历史中的部分“计划-执行”轨迹 (trace) 里。这些都不在资产登记簿上,也不在操作手册里。

当区域宕机时,智能体并不会干净利落地失败,而是处于一种“半完成”的状态。用户看到工作流已经开始,但故障转移后的区域无法恢复进度;客户收到了两次账单,或者根本没收到,因为幂等键存在于已经失效的工作节点上;值班工程师读着 Slack 频道里的讨论,开头是“编排器已启动,但是...”,六小时后以处理信用卡拒付队列告终。

这就是没人点破的鸿沟:智能体特性拥有现有灾备计划未曾描述的状态模型。如果团队还没有记录下这些状态表面,那么只需一次区域性停机,他们就能深刻体会到操作手册的缺失所带来的代价。

你的操作手册未涵盖的状态表面

按类别梳理智能体的状态究竟存在于何处,并检查你的 DR 操作手册中哪些条目涵盖了它们。

第一个表面是推理提供商中的工作内存。现代提供商会缓存提示词前缀、工具模式 (schemas) 和会话状态,以降低输入 Token 的成本。长期运行的智能体依赖于这些缓存;一些提供商甚至提供会话亲和性 (session affinity) 标头,以便具有相同标识符的请求路由到同一个模型实例。当智能体绑定的区域失效时,缓存状态就会丢失。故障转移区域虽然可以提供服务,但它是“冷启动”的 —— 任何假设对话具有连续性的智能体逻辑,现在面对的都是一个“陌生人”。

第二个表面是工作节点的本地磁盘。智能体会写入草稿文件来记录“计划-执行”轨迹、放不下上下文的中间工具输出,以及刚下载准备上传到别处的产物。本地磁盘比持久化存储快,且比将每个产物都存入 S3 便宜。但它也会在工作节点失效的瞬间消失,故障转移区域无法读取它。

第三个表面是在途的工具结果。智能体在三分钟前调用了一个支付 API,工具返回了结果,结果传给了模型,模型决定了下一步操作 —— 但那个决定以及它所依赖的工具结果仅存在于一个开启的推理调用中。模型返回了,但接收返回结果的工作节点已经失效。结果从未被持久化,故障转移后的工作节点根本不知道支付已经执行过。

第四个表面是部分“计划-执行”轨迹。智能体已经执行了计划中的七个步骤中的三步。计划以文本形式存在于提示词历史中,随着轮次逐一累积。目前还没有“已完成、下一步、已决定”的结构化表示。如果另一个工作节点尝试恢复,它必须从头开始推演计划 —— 即使输入相同,不同的模型调用也可能做出与第一次不同的决定。

这些表面中的每一个对 SRE 团队来说都是不可见的,因为每一个都被视为智能体运行时的实现细节,而不是独立的状态服务。灾备计划无法对它无法命名的东西进行故障转移。

任务级幂等,而非请求级幂等

大多数服务框架提供请求级幂等:一个带有 Key 的 HTTP 请求进入,处理器运行,结果被缓存,重试时返回相同的响应。这种原语无法延伸到智能体,因为智能体的“任务”不是一个请求。它是一系列模型调用和工具调用,可能持续数分钟或数小时,跨越多次重试,并在过程中产生多个步骤的副作用。

必须建立的规范是:幂等键在任务创建时生成,而不是在智能体内部生成。当用户提交“订机票并发送行程单”时,编排器在智能体执行任何操作之前就生成一个任务 ID。智能体发出的每个工具调用都会继承该任务 ID 和一个步骤序号:task=abc, step=1, action=search_flights。支付 API 和邮件服务被配置为基于 (task_id, step_ordinal) 进行去重。如果智能体在第 3 步后失效,故障转移区域在第 3 步接手任务,去重键是相同的 —— 第二次尝试要么不执行操作,要么返回缓存的结果。

这听起来很简单,原则上也确实如此。陷阱在于,开发者上线的智能体其幂等键是在智能体的“计划-执行”循环内部生成的 —— 可能是从模型输出中采样、从当前提示词哈希得来,或者是根据调用的墙钟时间衍生。这三种方式在故障转移时都会失效,因为恢复的任务会产生不同的 Key。团队只有在客户被收了两次费时才会发现这个问题。

修复方法是将任务 ID 和步骤序号视为编排器拥有的原语,智能体必须使用但不能自行生成。编排器在调用之前为每个工具位 (slot) 分配一个 Key。如果智能体在调用中途崩溃,恢复者会重复使用相同的 Key。下游 API 强制执行去重。客户只会被收一次费。

在每次工具调用前进行 Checkpoint,而非会话结束时

朴素的 Checkpoint 节奏是“在会话结束时保存状态”。这是聊天类产品使用的节奏,它对聊天有效,因为状态只是没有外部副作用的对话记录。但它对 Agent(智能体)无效,因为 Agent 在会话中期会产生不可逆的副作用——支付、发送邮件、数据库写入、开启工单——而两个副作用之间的状态才是你真正需要恢复的状态。

正确的节奏是在每次工具调用之前编写持久化 Checkpoint。在 Agent 调用工具之前,编排器(orchestrator)会持久化:当前计划、提示词前缀、工具名称、参数、幂等键(idempotency key)和步骤序号(step ordinal)。如果 Worker 在任何时间点宕机——调用前、调用中、调用后但在处理结果前——故障转移(failover)后的 Worker 就有足够的状态来恢复运行;或者,如果副作用的状态未知,可以通过使用幂等键查询下游 API 来进行对账。

这种模式正是 Temporal、Restate 和 Azure 的 Durable Task 运行时等持久化执行框架(durable-execution frameworks)的设计初衷,而 Agent 平台供应商也已开始向其靠拢。LangGraph 的持久化执行模式会对每一次状态转换进行 Checkpoint;Temporal 的 Agent SDK 将每一次工具调用都视为一个自动记录的“步骤”。微软的 Foundry Agent Service 发布了明确的故障转移指南:跨区域复制 Agent 的持久化状态,并记录手动切换的步骤。无论你在哪里看到这种模式,其规律都是一样的:持久化的单位是工具调用,而不是会话。

没有人明说的成本框架是:持久化的 Agent 状态是团队总是预算不足的 Checkpoint 开销。每次调用前的 Checkpoint 都会增加延迟(取决于存储选择,通常在几十到一百毫秒之间)和存储成本(写入的每一步都要保留一定的存储周期)。在设计阶段没有考虑这一预算的团队,其发布的 Agent 在开发环境下能达到延迟 SLO,但在生产环境下会超标,接着他们会将 Checkpoint 频率降低到“每 N 步一次”,直到遇到第一次区域性事故,才发现丢失的步骤正是代价最高昂的。

故障转移策略:故障安全中止优于故障前向重放

当一个部分完成的 Agent 任务在故障转移区域出现时,编排器有两个选择:从上一个 Checkpoint 开始前向重放(replay)任务,或者中止任务并通知用户。

重放是框架默认的选择,在 PPT 上看起来很优雅。Agent 的上一个良好状态是步骤 4。故障转移 Worker 读取步骤 4,使用恢复的提示词调用模型,执行步骤 5。非常完美。但问题在于,步骤 5 的输入取决于步骤 4 的结果——如果步骤 4 是一个状态不确定的外部副作用,重放会做出团队在凌晨 3 点不敢担保的假设。

对于副作用是外部且不可逆的 Agent,更安全的策略是带有用户通知的故障安全中止(fail-safe abort)。故障转移区域观察到一个只完成了一半的任务,其最后的操作是“扣除客户 470 美元”。与其自动恢复并冒着二次扣费或遗漏后续操作的风险,编排器不如将任务标记为已中止,向用户展示清晰的信息(“我们在完成你的预订时遇到了问题;这是我们已知的扣费情况;请在继续之前进行确认”),并将部分状态写入对账队列供操作员处理。

重放适用于那些副作用在端到端上真正幂等的任务,在这些任务中,步骤序号加上去重键可以确保不会发生重复执行,并且团队已经通过混沌测试验证了这一属性。对于任何涉及资金、消息传递或团队无法控制的第三方 API 的任务,重放适合作为默认选项,因为默认重放的团队会通过客户支持而不是通过自己的监控来发现坏情况。

策略决策属于 Agent 的任务类型元数据:每种任务类型声明故障转移时恢复是否安全。新的任务类型最初应设为故障安全中止,只有在拥有端到端幂等性证明和演示过该属性的混沌演练后,才能晋升为重放。

实战演练:工具调用过程中的混沌工程

未经演练的灾备(DR)计划只是愿望,而不是计划。对于 Agent 来说,正确的演练是能映射出运行手册(runbook)沉默背后所隐藏的故障模式:在工具调用中途杀死 Worker。

具体来说,演练会模拟一个具有代表性的 Agent 任务——该任务至少涉及一个外部副作用——并在执行过程中的随机时间点终止 Worker 进程。编排器必须恢复该任务,断言结果是二进制的:恢复路径要么产生单一的完成结果,要么产生干净的中止。绝不能出现二次副作用,也绝不能出现无声的部分完成。

这与更广泛的区域故障转移测试相结合。AWS 的 Fault Injection Service 提供了可以切断与主区域连接的跨区域场景;ARC 负责编排区域切换;Agent 平台的工作则是作为在该切换中幸存下来并提供任务级保证的层。最能暴露问题的演练是 Worker 在结果不确定的工具调用期间宕机的场景——因为这正是生产代码路径未曾设计的案例,也是区域停机将会产生的情况。

从业者演练不足的一个相关演练是提示词缓存逐出(prompt-cache eviction)。Agent 的延迟预算假设推理服务提供商的区域内有热提示词缓存。故障转移将 Agent 置于冷区域。延迟翻倍。在热区域有效的工具调用超时现在在冷区域触发,在已经降级的路径上产生虚假的重试。演练它:预热缓存,强制故障转移,测量冷启动延迟概况,并确认超时和重试预算是针对冷情况而非热情况调整的。

组织失效模式:SRE 与 AI 团队未共享资产登记表

架构问题往往伴随着组织结构的阴影。SRE 组织负责服务的灾备(DR)。AI 团队负责智能体(Agent)运行时。两者之间的鸿沟在于 Worker 的本地临时目录(scratch directory)、推理提供商的会话缓存(session cache)以及运行中的工具执行结果 —— 当 SRE 编写灾备计划时,这些都没有出现在资产登记表中;而 AI 团队也没把它们视为“状态”,因为他们认为这些只是实现细节。

解决办法是程序化的:进行联合评审,将 Agent 运行时的状态表面(state surfaces)添加到与数据库、队列和对象存储相同的资产登记表中。每一个状态表面对应一行记录:它存在于何处、其复制策略是什么、RPO 和 RTO 分别是多少、故障转移流程如何、谁负责维护运维手册(runbook)条目。未通过评审的状态表面要么被提升到持久化状态(例如将临时草稿区移动到对象存储,或复制会话状态),要么被明确降级为“瞬态,故障转移时丢失”,并配合 Agent 层的策略来处理受此丢失影响的任务。

这里的成本认知陷阱在于,AI 团队觉得这项工作是 SRE 的流程开销,而 SRE 团队觉得这是 AI 的实现细节,因此两个组织都不会主动去处理。直到第一次区域性故障产生高达六位数的客户赔付账单后,这件事才会被提上日程 —— 这是每个组织在第一次未将状态表面列入登记表时,都必须缴纳的“税费”。

总结:在故障替你命名状态表面之前,先为它命名

你现有的灾备计划只适用于你过去运行的技术栈。你的团队在上个季度发布的 Agent 引入了计划中未描述的状态表面,而这些表面现在已成为任何区域性宕机场景中的主要失效模式。操作顺序是固定的:命名状态、为每个表面选择持久化策略、在任务作用域内生成幂等键(idempotency keys)、在每个外部副作用发生前设置检查点(checkpoint)、默认采用安全失败(fail-safe)中止策略,并针对“Worker 在工具调用中途宕机”的情况进行演练,直到恢复路径变得清晰明确。

这些都不需要新的框架。持久化执行(durable-execution)模式已有数十年的历史;新颖之处在于将它们应用于一个团队尚未完全意识到其状态模型的运行时。命名状态表面的团队将拥有相应的运维手册。而没能做到这一点的团队,只能通过客户支持工单来了解它。

References:Let's stay in touch and Follow me for more thoughts and updates