跳转到内容

可靠性、恢复与持久执行

长时间运行的智能体工作会跨越不可靠边界:模型提供方、网络、队列、存储、用户审批、外部 API、工作进程,以及不断变化的部署。它们中的任何一个,都可能在接收工作后、报告结果前失败。因此,进程重启并不是困难场景。真正困难的是:在不丢失已经确认并接受的进展、不重复产生后果严重的外部影响、不信任过时状态,也不在外部实际状态仍不一致时宣称成功的情况下完成恢复。

本章假定你已了解《架构》中定义的显式运行状态、带类型的结果、权威状态和执行约定。它也假定你已了解《可观测性》和《事件响应》中关于生产证据与响应流程的内容。本章的职责,是提供使一次运行能够安全等待、失败、重启并继续的机制。

可靠的智能体执行并不要求每个模型决策或依赖都具有确定性。它要求持久记录已经确认并接受的进展,明确外部影响的语义,让重试规则与故障类别相匹配,并在继续之前重新验证当前实际状态。

持久性不是“让智能体进程一直存活”。一个持久运行可能在其大部分生命周期里没有任何工作进程或模型处于活动状态。它的状态、待处理义务、计时器和继续执行条件会保存在其他地方。当执行恢复时,系统会重建已知信息、解决不确定之处,并且只执行仍被授权且仍有必要的工作。

“这个智能体可靠”太模糊,无法进行工程化。你必须明确说明哪些用户与系统属性需要在故障后保持不变:

  • 已接受的请求不会悄无声息地丢失;
  • 已提交的进展可以在给定的损失容忍度内被重建;
  • 一个逻辑意图不会产生重复的有后果外部影响;
  • 部分和未知的外部影响在核对并纠正前保持可见;
  • 等待、审批、截止时间、取消和所有权能够在进程重启后保持;
  • 过时工作进程与并发恢复不会覆盖更新后的真实状态;
  • 恢复能在合适时间内完成且不会压垮依赖;
  • 最终结果反映的是已核对过的外部状态,而不只是恢复后的对话。

要分别度量这些属性。实用目标包括恢复时间、最大可接受状态损失、重复外部影响率、未知外部影响时长、成功恢复率、补偿积压、过时工作进程拒绝率,以及恢复为已验证结果的任务比例。高服务可用率百分比可能与丢失审批或重复转账同时存在。

并非所有任务都需要持久编排。短暂、只读的请求通常可以明确失败,并由用户重试。随着工作持续时间变长、跨越更多依赖、等待外部事件、累积已验证进展,或可能产生昂贵且不可逆的外部影响时,持久性才变得更有价值。

按“另一次尝试是否能改变结果”来对待故障:

故障类别 示例 默认响应
无效请求或违反不变量 参数格式错误、非法状态转换 修复请求或结束运行;不要原样重试
授权或策略拒绝 缺少权限、审批已过期 获取有效授权或升级处理;不要把它重新解释为暂时性故障
业务拒绝 余额不足、预订冲突 选择另一条合法路径或报告拒绝
瞬时依赖失败 限流、暂时不可用 仅当操作安全且预算仍充足时重试
过载 队列或提供方容量饱和 在重试前施加背压、丢弃部分负载、等待或降级
部分外部影响 三条请求记录中有两条已更改 保留已完成的外部影响;有意地继续、核对并纠正,或补偿
未知结果 发送后写入超时 在另一次写入前,按操作身份查询或读取权威状态
并发冲突 暂停期间状态版本发生变化 重新加载、重新验证,并基于更新版本重新规划
取消或过期 用户在操作进行中取消 停止新工作,处理进行中的外部影响,并进入显式终止或补偿状态
损坏或不兼容的恢复状态 快照 schema 无法迁移 隔离并升级处理,而不是猜测或悄悄重新开始

没有语义,仅靠错误码是不够的。timeout 描述的是调用方等待了多久,而不是服务器是否已提交。500 不能保证没有副作用。rate_limited 可能稍后安全重试,而 permission_denied 不会因为指数退避而改善。

要将基础设施故障与语义性任务失败区分开。另一次相同的模型调用可能产生不同答案,但随机性不是恢复策略。只有在产品可以承受另一个样本、且运行记录明确说明哪些输出被接受时,才重试模型决策。如果需要新证据或不同路径,应将其表示为策略变化,而不是不可见的传输重试。

可靠运行时将编排状态外部工作分离:

持久运行记录与事件历史
├─ 决定下一条合法转换
├─ 记录意图、身份、版本、限制和外部影响键
有边界的执行单元:模型调用、API 请求、人工任务或计算
记录带类型的结果和权威证据
└─ 更新状态、等待、重试、补偿、升级处理或结束

持久记录应使已接受的转换能够被重建。外部活动可能会尝试多次,因此其约定必须携带身份、超时、重试和外部影响语义。不要把未记录的网络调用放在会被重放以重建状态的逻辑中。

Temporal 是这种分离的一种具体实现:工作流状态从事件历史中重建,而 Activities 执行易失败的外部工作,并建议保持幂等性(Temporal workflow executionTemporal Activities)。OpenAI 当前的 Agents SDK 暴露了一个可序列化的 RunState 作为暂停与恢复边界,并记录了与持久编排器的集成,用于跨等待和重启的运行(OpenAI Agents SDK run staterunning agents)。这些实现展示了这种模式;但二者都不能免除应用对外部影响正确性的责任。

对于小型产品,一个数据库行和作业队列可能就足够了。当存在许多计时器、长等待、子任务、补偿路径、版本和运维查询时,工作流引擎会更有吸引力。设计要求是可重建性和受控外部影响,而不是采用某个特定平台。

只有在以下条件成立时,重试才有正当性:

  1. 故障类别允许再次尝试;
  2. 操作是只读、幂等,或以其他方式受到保护,避免重复外部影响;
  3. 调用方仍拥有有效的截止时间、权限和资源预算;
  4. 再次尝试有可信的不同结果可能;
  5. 尝试和最终处置保持可观测。

对每个依赖和操作定义:

  • 可重试与不可重试结果;
  • 最大尝试次数和总耗时;
  • 每次尝试的超时和端到端截止时间;
  • 指数退避或其他退避方式及随机抖动;
  • 是否遵循 Retry-After 或提供方特定退避要求;
  • 由哪一层独占重试,或跨层协调预算;
  • 恢复期间的并发和速率限制;
  • 幂等和核对并纠正行为;
  • 重试耗尽后进入的状态。

无限重试会把依赖问题转化为队列、成本和容量问题。多层中的重试会相乘:三层各四次尝试,最底层可能产生 64 次尝试。Google SRE 记录了重试如何把过载放大为级联故障,并建议使用随机退避、有限的每请求尝试次数、重试预算、清晰的错误类别、截止时间传播,以及避免多层同时重试(Google SRE, “Addressing Cascading Failures”)。

退避不是允许无限等待。要传递绝对任务截止时间,这样下游工作就不能在用户可见的操作已过期后继续执行。要为结果处理和清理预留时间。一个长时间运行的业务任务可能有数天的完成截止时间,而每次模型或 API 尝试则有更短的超时时间。

在重试风暴开始前,先使用背压和准入控制。限制队列、在途工作、子运行、模型调用和恢复并发。在过载期间,拒绝或延迟低优先级工作,往往比什么都尝试却什么都完成不了更能保住有用结果。

幂等键应标识一次逻辑外部影响,而不是一次网络尝试。对同一意图的重试复用同一个键;对真正的新意图创建新键。

一份可靠的约定包括:

主体与租户范围
操作名称与目标
逻辑意图或外部影响标识符
规范化请求摘要或受保护的版本引用
创建时间与过期时间
状态:accepted / running / succeeded / rejected / unknown
规范响应或外部影响证据

接收服务应以原子方式把该键与外部影响或持久操作记录关联起来。带有相同键且意图等价的重复请求,应返回既有结果或状态。相同键但关键参数不同的请求应被拒绝,而不应悄悄当作重复请求或新请求处理。去重记录应至少保留到可信的重试与延迟送达窗口结束。

AWS Builders’ Library 的文章建议使用调用方提供的请求标识符,因为相同参数并不可靠地意味着相同意图;它还强调要将令牌与变更操作原子地记录在一起(AWS Builders’ Library, “Making retries safe with idempotent APIs”)。这个键的强度只取决于执行它的服务。仅在智能体运行框架中生成一个 UUID,并不会让未受保护的下游外部影响变成幂等。

幂等并不意味着函数只执行了一次。工作进程可能完成一次外部写入,在记录完成前崩溃,然后导致另一次尝试。工程上的主张是:重复尝试只会产生一次已接受的逻辑外部影响,或一个等价且可观察的结果。要将尝试标识符与逻辑外部影响标识符分开保存,这样运维人员就能同时看到两者。

在一次已发送的写入操作超时后,可能同时满足三个事实:

  • 调用方未收到响应;
  • 服务器可能已经提交了该外部影响;并且
  • 另一次尝试可能会产生重复。

安全的下一步通常是核对并纠正,而不是猜测:

  1. 保留逻辑外部影响和尝试标识符;
  2. 查询操作状态端点或权威状态;
  3. 验证观察到的资源是否由此次意图产生;
  4. 如果操作仍处于等待中,就继续等待;
  5. 仅当约定允许时,才用相同幂等键重试;
  6. 当证据无法解决有后果的外部影响时,升级处理或进入人工复核。

要设计支持这条路径的接口。一个既不返回操作标识符、也不提供读回方法的 POST,会把调用方置于不安全的选择中。对于外部影响可能超出请求生命周期的场景,优先使用异步操作记录、稳定资源标识符、状态查询、webhook 或事件关联。

不要在任意超时后把 unknown 变成 failed。要持续记录结果处于未知状态的时间,在超出约定时限后告警,并明确由谁负责解决。在所需外部影响被确认之前,或者最终结果显式报告未解决状态之前,完成状态都不应解除阻塞。

检查点应保存真实状态,而不只是对话

Section titled “检查点应保存真实状态,而不只是对话”

一个有用的检查点应保存另一个进程安全继续所需的信息:

  • 运行、任务、租户、主体和所有权标识符;
  • 目标与已接受的成功标准;
  • 权威状态版本与已完成的已验证步骤;
  • 待处理意图、进行中的尝试、已提交的外部影响和未知结果;
  • 审批、其范围与过期时间,以及待处理的人工决策;
  • 模型、提示词、策略、上下文、接口、工作流和 schema 版本;
  • 已消耗和剩余的预算、截止时间、计时器、重试状态和子运行状态;
  • 通过持久引用保存的产物和证据;
  • 取消与补偿状态;
  • 精确的继续执行条件和下一条合法转换。

对话摘要或模型生成的进展说明是一种派生视图。它可以帮助下一个模型理解任务,正如 Anthropic 的长时间运行编码实验通过结构化进展工件和清晰的增量交接所发现的那样(Anthropic, 2025)。但它不能替代权威的外部影响、审批和状态记录,而且 Anthropic 也明确将那项工作描述为一个面向编码的运行框架实验,而不是普遍可靠性的证明。

应在语义边界写入检查点:在一次已接受的状态转换之后,在一次有后果的外部影响之前和之后,在进入等待时,收到审批后,以及把工作移交给另一个所有者之前。每个 token 都做检查点既昂贵又通常没有意义;只在最终输出时做检查点则会丢失太多。

要像保护生产状态一样保护检查点。加密敏感字段,应用租户隔离和保留规则,校验完整性,并测试恢复,而不只是测试备份创建。

恢复不是加载字节并继续下一行。时间已经过去,所以假设可能不再成立。在执行下一项实质操作之前:

  1. 验证检查点 schema,并通过已测试路径迁移;
  2. 使用租约或等效的隔离机制重新取得执行权;
  3. 重新加载权威状态并拒绝过时写入;
  4. 核对进行中的和未知的外部影响;
  5. 重新验证身份、权限、策略、审批、密钥和数据访问;
  6. 检查模型、接口、提示词和工作流兼容性;
  7. 应用用户更正、取消、截止时间和更新的事件;
  8. 基于当前记录重建有界模型上下文;
  9. 决定继续、重新规划、补偿、升级处理或终止。

对于一个金额、目标、文档版本或策略状态的审批,在恢复后不应在悄无声息中授权一个已变更的操作。模型升级可能提高能力,同时改变工具选择或输出语义;在安全的情况下固定旧配置,带着评估证据迁移,或者要求新的决策。

使用乐观并发、租约和 fencing token(防止旧持有者继续提交的单调递增令牌),这样旧工作进程就不能在新工作进程接管同一运行之后提交更改。仅靠租约超时并不足够:暂停的进程可能在失去租约后再次醒来。外部影响边界必须拒绝它携带的过期 fencing token 或状态版本。

一个正在等待人工、webhook、计划任务、依赖或限流窗口的运行,应持久保存订阅或计时器并释放计算资源。它不应反复调用模型询问时间是否已经过去。持久计时器和事件会在命名条件发生时恢复该运行。

对于长活动,心跳可以记录存活状态和粗粒度进度,但它们不能证明某个外部影响可以安全重试。心跳检查点必须是持久的,并且在语义上足以恢复其所声称覆盖的工作。

取消是向所拥有工作传播的请求;它并不证明每个操作都已停止。定义清楚:

  • 谁可以在什么范围内取消;
  • 取消是优雅停止还是立即停止;
  • 子运行、队列、计时器和活动如何接收取消;
  • 已经提交或正在进行中的操作会怎样;
  • 哪些清理或补偿必须在取消后仍然完成;
  • 如何拒绝或核对迟到的结果;
  • 哪些证据标记取消已完成。

绝不要因为一个延迟回调到达,就把已取消的运行重新改回 running。要记录该回调、核对其影响,并且如果工作应该继续,则要求新的已授权转换。

分布式外部影响通常无法以原子方式回滚。补偿是一种新的领域操作,它让系统朝可接受状态移动:取消预订、发起退款、撤销访问权限、发布更正,或者创建一个人工工作项。

对于每一个可补偿的外部影响,记录:

  • 原始意图、已接受结果和权威证据;
  • 允许或要求补偿的条件;
  • 补偿操作、授权、截止时间和幂等键;
  • 依赖和顺序约束;
  • 预期的残余影响、费用、通知或法律记录;
  • 验证和人工升级路径。

补偿也可能失败,并且可能需要它自己的重试、检查点和告警。它未必能恢复到完全相同的原始状态,因为并发参与者已经改变了世界。Microsoft 的 Compensating Transaction 模式同样警告说,补偿是特定于应用的、最终一致的、可能失败的,而且不一定是按相反顺序恢复(Microsoft Azure Architecture Center)。

把不可逆步骤放在所有可行验证和审批之后。如果不存在负责任的补偿,就缩小授权范围、把结果暂存待审,或者在提交前向已授权人员明确说明其不可逆性。

恢复依赖,但不要把故障转移出去

Section titled “恢复依赖,但不要把故障转移出去”

降级方案和故障转移改变的是系统,而不只是可用性。新的模型提供方可能具有不同的数据处理、工具支持、延迟或输出行为。副本可能是过时的。恢复的队列可能释放出一波过时工作。

对每个关键依赖,定义:

  • 超时和端到端截止时间;
  • 可重试与不可重试分类;
  • 熔断或临时隔离;
  • 并发、队列和重试预算;
  • 降级模式及其保留的产品承诺;
  • 故障转移资格、一致性、安全性和评估要求;
  • 恢复顺序、预热和积压清空速率;
  • 如何处理部分、迟到和重复的结果。

优雅降级应保留真实信息。返回一个范围更窄但带有明确限制的答案可以是可靠的;但悄悄用过时或编造的数据替换已验证的当前数据则不是。若缺失的依赖保护的是授权、隐私或不可补偿的外部影响,就要安全失败。

恢复流量也需要准入控制。故障之后,排队的运行、计划重试、缓存和客户端可能会同时重启。增加抖动、优先处理时间敏感工作、丢弃过期工作、在重放写入前先核对并纠正,并将积压量降到依赖的安全容量以下。

仅靠重试辅助函数的单元测试是不够的。要在每个关键边界上测试系统:

  • 在派发前、派发后、外部影响提交后以及记录结果前崩溃;
  • 丢弃、重复、延迟和重排消息与回调;
  • 返回部分、格式错误、过时和未知结果;
  • 在等待期间让审批、凭据、策略、截止时间和租约过期;
  • 在两台工作进程上并发恢复同一个检查点;
  • 部署兼容和不兼容的工作流、schema、模型、提示词和接口版本;
  • 耗尽重试、队列、预算、存储和提供方容量;
  • 在模型生成、外部写入、子工作和补偿期间取消;
  • 使补偿操作失败并要求人工介入;
  • 从备份恢复,并在不引发另一轮级联故障的前提下清空一个真实的积压。

验证最终权威状态、外部影响、证据和用户可见结果——而不仅仅是工作进程是否返回。把已确认的失败输入评估系统和可观测性告警。记录恢复时间、丢失的进展、重复和未知外部影响、重试放大以及人工成本。

选择 收益 成本或风险
简单作业队列和数据库 运维和概念开销低 应用方自行承担计时器、去重、版本管理、补偿和恢复查询
持久工作流引擎 持久化历史、计时器、重试和运维控制 平台依赖、重放约束、事件历史成本和迁移工作
细粒度执行单元 更小的重试和补偿单元 更多事件、协调、延迟和 schema 维护范围
粗粒度执行单元 更少的编排开销 重复外部影响和重启的范围更大;取消和诊断能力更弱
频繁检查点 更少的进展损失 更多写入、存储、争用和版本化状态
长去重窗口 处理延迟重试和回调 存储、隐私和键生命周期成本
激进重试 掩盖短暂瞬时故障 成本放大、过载、重复外部影响和更长尾延迟
严格快速失败行为 保留容量和显式真实状态 更多可见失败以及人工/用户重试
自动补偿 对明确案例收敛更快 错误补偿可能加重损害
人工恢复 处理含糊的有后果案例 缓慢、昂贵,并依赖良好的证据和人力配置

在崩溃后,系统让模型从聊天历史中推断进展,并重复那些其外部影响已经提交过的操作。

验证、权限、业务拒绝、过载和未知写入结果全都进入同一个重试循环。永久性错误浪费资源;含糊的写入会重复外部影响。

每次网络重试都生成一个新键,因此接收服务会正确地把每次尝试都视为新的外部影响。

系统在更改目标、金额或操作参数后复用同一个键。服务要么返回无关的旧结果,要么在含糊身份下应用一个危险的新请求。

调用方在写入超时后,在没有查询状态或读回的情况下重试。两次尝试都提交成功了。

SDK、模型网关、服务、队列消费者和工作流各自独立重试。一个小故障变成流量和成本级联。

检查点里没有进行中的外部影响

Section titled “检查点里没有进行中的外部影响”

快照记录了计划和消息,但没有记录一个已派发的操作。恢复后又把它重复执行了一遍。

运行在数天后醒来,带着已过期的审批、已变更的策略、已撤销的访问权或已被替代的用户更正,并仿佛什么都没变一样继续执行。

只有租约,没有防止旧持有者提交的机制

Section titled “只有租约,没有防止旧持有者提交的机制”

旧工作进程在租约过期后恢复,并覆盖了新所有者已经提交的工作。

恢复覆盖了并发产生的合法变化,或者假定某个外部影响可以完美反转。

所有延迟工作和重试同时恢复,过载了依赖,导致正常流量还没恢复就再次崩溃。

工作流引擎完美重放,但成功标准、配置兼容性、审批和外部影响从未被精确记录到足以安全继续。

对每个有后果的操作,维护一份恢复约定

逻辑意图和外部影响标识符:
主体、租户、目标和前置条件:
权威状态版本:
可能的成功、拒绝、部分和未知结果:
尝试超时和端到端截止时间:
可重试条件、所有者、尝试次数、退避、抖动和总预算:
幂等键范围、等价规则、存储和保留:
状态查询和核对并纠正方法:
外部影响前后检查点:
取消和迟到结果行为:
补偿操作、授权、顺序和验证:
不可逆点和所需审批:
人工升级和证据:

对每种持久运行类型,维护一份恢复约定

持久状态、事件历史、产物和 schema 版本:
检查点边界与最大可接受进展损失:
计时器、订阅、子工作和所有权:
租约、并发和 fencing token 规则:
配置固定与迁移策略:
身份、审批、策略、密钥和状态重新验证:
继续前的未知外部影响核对并纠正:
恢复时间和积压清空目标:
隔离与人工恢复路径:
恢复与故障注入测试证据:

如果一个团队无法说明重试意味着什么、什么算同一项逻辑外部影响,以及新工作进程如何区分已经完成与结果未知的工作,那么它还没有实现持久执行,只是把希望写进了持久化存储。

  • 可靠性目标是否以已接受进展、外部影响、恢复时间和已验证结果来表述,而不只是以正常运行时间来表述?
  • 每种关键故障类别是否都映射到重试、等待、修复、核对并纠正、补偿、升级处理或终止行为?
  • 编排状态是否与对重放不安全的外部工作持久地分离?
  • 可重试错误是否明确、受尝试次数和端到端截止时间限制,并由单层或共享预算负责?
  • 重试是否使用退避、抖动、准入控制和依赖容量限制?
  • 一个幂等键是否在多次尝试中代表一个逻辑意图,并且不匹配的参数会被拒绝?
  • 该键是否以原子方式与外部影响或持久状态关联,并在整个重试窗口内保留?
  • 每一次有后果的异步写入,是否都能通过稳定的操作身份进行查询或核对并纠正?
  • 部分和未知结果是否被阻止转化为静默成功或普通失败?
  • 检查点是否包含权威状态版本、待处理意图、进行中的尝试、外部影响、审批、预算、计时器、取消和兼容性元数据?
  • 恢复是否会重新验证状态、所有权、身份、策略、审批、配置、截止时间和更新事件?
  • 执行权变更后,是否可以通过 fencing token 或版本检查拒绝过时工作进程?
  • 等待是否释放计算资源,并通过持久事件或计时器恢复,而不是通过模型轮询?
  • 取消是否会在声明完成前解决进行中的和迟到的外部影响?
  • 补偿步骤是否具备领域特定、幂等、可观测、可恢复,并且能够升级处理?
  • 不可逆步骤是否被延迟到可行验证和审批完成之后?
  • 故障转移和降级是否保留安全性、一致性和用户承诺?
  • 积压恢复是否优先级明确、限速、带抖动,并经过再次级联故障测试?
  • 故障注入和恢复测试是否在每个崩溃边界验证权威状态和外部影响?