Agent 工具调用熔断降级与 Saga 补偿工程 2026
把熔断、降级、Saga 补偿三类机制放进 agent 工具调用链路:circuit breaker 主动断路避免雪崩,fallback 链按 staleness budget 收窄能力,Saga 反向操作账本处理已部分成功的副作用。结合 OpenTelemetry trace 与反事实日志,给出六条可执行工程推论与 SRE 30 秒响应手册。
约 31 分钟阅读9,120 字16 次阅读博主

把熔断、降级、Saga 补偿三类机制放进 agent 工具调用链路:circuit breaker 主动断路避免雪崩,fallback 链按 staleness budget 收窄能力,Saga 反向操作账本处理已部分成功的副作用。结合 OpenTelemetry trace 与反事实日志,给出六条可执行工程推论与 SRE 30 秒响应手册。

Agent 在生产环境里的失败,绝大多数不是单步 LLM 推理的失败,而是长链路工具调用的崩塌。一条典型的 agent 链路可能是:模型推理 → 工具 1(HTTP API)→ 工具 2(DB 写入)→ 工具 3(消息推送)→ 总结生成。任何一步失败都可能让前面已经付出的副作用变成"已完成但用户没看到"的脏状态。更糟的是,重试会放大这个脏状态——它以为是在"恢复",实际上是在"加深"。
我们见到过三类最致命的故障模式,它们都不是用 retry 能解决的:
第一类,超时雪崩:工具 1 的 P99 延迟从 800ms 漂移到 12s,所有等待中的 agent 都进入超时重试链路,下游工具的连接池被吃满,紧接着工具 2 也开始超时。retry 在这里扮演的角色是"压力放大器",而不是"故障恢复器"。
第二类,部分成功状态:工具 1 成功、工具 2 成功、工具 3 失败。此时已经发生了"两个真实副作用 + 一个未完成事务"。如果 agent 拿到的错误是 transient 性质的并触发 retry,工具 3 的副作用可能被重复执行;如果 retry 时跳过已成功的步骤(这是常见优化),那步骤 1 和 2 的副作用就永久生效了——而用户期望的是"要么都成功,要么都没发生"。
第三类,跨系统事务断裂:工具 2 是数据库写入、工具 3 是支付扣款。两边都成功是 happy path,工具 3 失败需要回滚工具 2 的写入。但传统 DB 事务(XA / 两阶段提交)在 agent 这种异步长链路里既不可行也不经济——锁的持有时间不可控,参与方可能根本不支持两阶段。
这三类故障有一个共同的名字:失败不可补偿。它不只是一个错误码的问题,而是一个工程范式的问题。生产环境的 agent 团队通常用四种武器应对:熔断器(circuit breaker)、降级(fallback / degradation)、补偿事务(compensating saga)、重试预算(retry budget)。但这四种武器之间不是"并列"的,它们有严格的层次——重试在最内圈做"快恢复",熔断在最外圈做"主动断路",降级在中间做"能力收窄",补偿在事务边界做"反向操作账本"。本文要论证的就是这套四层体系如何被工程化,以及它们在 agent 工具调用这个具体场景下的边界。
我们用一个四元组 (C, S, T, R) 来形式化整个体系:
{CLOSED, OPEN, HALF_OPEN, FORCED_OPEN},状态迁移由观测窗口的失败率 / 错误计数 / 慢调用比例驱动。熔断状态机的迁移概率不是确定性的,它是带有观察窗口的随机过程。我们用 N 表示窗口大小(窗口内请求数),用 e 表示窗口内的失败计数。CLOSED → OPEN 的迁移条件通常是 e / N > p_threshold 且 N > min_requests。OPEN 状态下持续 sleep_window 时间,期间所有请求被快速失败(fail fast)。OPEN → HALF_OPEN 由定时器触发,进入半开探测:放行 half_open_max_concurrent 个请求。HALF_OPEN → CLOSED 当探测成功率达 success_threshold;HALF_OPEN → OPEN 当任一探测失败。
Saga 反向操作是用一张有向补偿图来描述的。节点是 S,边是 R。如果正向链路是 S1 → S2 → S3 → S4,补偿链路就是 R(S4) → R(S3) → R(S2) → R(S1)。补偿图有两个不变量:(1) 补偿可逆性——R(R(S)) 应当回到 S 的初始状态或语义等价状态;(2) 补偿单调性——补偿过程中不允许引入新的对外可见副作用,只能消耗或回滚。两个不变量是 Saga 模式与"自动撤销"模式的根本区别:Saga 是"事后尽力回滚",自动撤销是"事前不可提交"。
四元组 (C, S, T, R) 不是孤立的,它们通过两条规则耦合:(a) 熔断优先于补偿——当 C = OPEN 时,补偿链路也应该快速失败而不是疯狂重试(否则就是雪崩的延续);(b) 降级优于熔断——当一个 S 有 fallback 路径时,C 的状态应该从 fallback 的成功率反推,而不是从原始工具的成功率。这条耦合是 agent 场景下和传统微服务场景下熔断设计的最大区别。
熔断器的实现不是"加一个 if 失败就跳过"的函数。它是一组窗口策略 + 状态机 + 探测协议的组合。
窗口策略有三种实现:
生产推荐是自适应滑窗:它既避免了固定窗的边界抖动,又比完整滑窗省内存。但要小心一个坑:子桶合并的权重是均匀的,所以"近期失败"的权重并不天然高于"远期失败"。如果业务对突发敏感,可以让近期子桶权重更高(比如按时间衰减加权)。
错误率阈值与最小请求量阈值是一对必须同时配置的参数。光配置错误率阈值(比如 50%)是不够的——如果窗口内只有 3 个请求,1 个失败就达到 33% 接近阈值,但样本量太小不该触发熔断。所以必须配 min_requests_in_window,常见的工业经验值是窗口大小 N 的 20%。
半开探测的并发控制经常被忽视。HALF_OPEN 状态下放行多少个探测请求、放行的速率(ramp-up)是多少、探测结果以什么节奏反馈回状态机——这些参数决定了熔断器从"断路"到"恢复"的曲线。AWS 的 Builders Library 推荐把 half_open_max_concurrent 设为正常峰值的 1%-5%,然后逐步 ramp-up 到 100%。这个保守起点的目的是防止熔断器恢复瞬间把还没恢复的下游再次打挂——这就是著名的"雪崩二次冲击"问题。
与重试的耦合体现在 retry budget 上。如果一个 S 在 CLOSED 状态下失败,agent 会重试,但每个 agent 实例的总重试次数应该有一个全局预算(比如"每分钟不超过原始 QPS 的 20%")。当 budget 耗尽,即使熔断器仍处于 CLOSED 状态,也应该直接放弃重试——把请求从"重试队列"转到"熔断候选"。这个机制是 Polly(.NET 弹性库)和 AWS SDK 默认 retry policy 的标配,但在 LLM agent 框架里还远未普及。
降级不是"返回一个错误"。降级是承认当前能力不够用,返回一个仍然对用户有用的响应。agent 场景下的降级分三层:
第一层,模型降级:当主模型(GPT-5 / Claude Opus 4)超时或 rate-limited,降级到次级模型(GPT-4.1 mini / Claude Haiku)。模型降级的关键不是"用便宜模型回答",而是"用便宜模型能回答"——有些问题次级模型真的答不了,硬降级会产生比 timeout 更糟糕的体验。所以模型降级必须配能力白名单:哪些类型的请求允许降级、哪些不允许。
第二层,工具降级:当工具 X 不可用时,降级到工具 X'(功能近似但不同的工具)。例如查实时股票的工具挂了,降级到查收盘价;航班搜索挂了,降级到查时刻表。工具降级比模型降级难,因为工具的 schema 可能不同、降级路径可能需要不同参数。这要求 agent 的工具注册中心维护一份 fallback graph:每个工具节点的 fallback 列表、fallback 之间的 schema 兼容矩阵、以及 fallback 之间的输出映射规则。
第三层,能力降级(capability degradation):最激进也最容易被忽视——承认某些功能就是不能用了,返回一个"受限但仍然有用"的响应。例:用户问"帮我订明天去上海的机票并查天气",天气 API 挂了,那就返回"机票已订,天气信息暂时无法获取,建议您出发前再确认"。能力降级要求 agent 的 prompt 模板里内置降级话术骨架,并且能让 LLM 在调用失败时选择正确的骨架。
cached reply 是降级体系里的"最后一道防线":当主路径、fallback、降级全失败,返回缓存的上一个成功响应。cached reply 的关键不是"有没有缓存",而是staleness budget——每个缓存条目都有一个可容忍的陈旧时长(基于业务场景,比如新闻 1 小时可接受,订单状态 30 秒就过期)。超过 staleness budget 的缓存不应当被当作降级响应——否则会给出"看似正常实则失真"的答案,这比超时还危险。
降级体系必须有一个统一的降级接口:对 agent 主流程来说,所有降级路径看起来都像一个"可调用的备选响应源"。OpenAI Agents SDK 里的 Runner.run 接受一个 fallback_model 参数是一个好的开始,但真正可用的降级体系需要降级触发器 + 降级目标 + 降级成本评估三个组件同时存在。
Saga 模式的核心不是"反向操作存在",而是反向操作链与正向操作链在拓扑上对偶。这意味着:每一条正向调用链,都必须存在一条对应的补偿调用链,并且补偿链的执行顺序与正向链严格相反。
反向操作的设计原则有三条,按重要性排序:
saga.compensated=true。否则事后复盘时会找不到"为什么这条记录最终是这个状态"。补偿图与正向图的拓扑关系是 Saga 设计里最容易被忽略的细节。正向图通常是有向无环图(DAG),补偿图是反向的有向图(也应当是 DAG,因为不能有循环补偿)。两个图叠加形成的总图必须满足:任意节点 S 的入度(被补偿次数)≤ 1。否则意味着这个节点的副作用被补偿了两次,违反单调性。
不可补偿操作是 Saga 模式的硬边界。三种场景下反向操作不存在或不可靠:
对这些不可补偿操作,正确的做法是隔离模式:
传统分布式事务(TCC / XA)和 Saga 的取舍点在于锁的持有时间。TCC 在 Try 阶段就要持有业务锁直到 Confirm / Cancel,对长链路 agent 不现实。Saga 不持锁但要求每个 S 的反向操作预先设计,对工程纪律要求更高。agent 场景几乎只能用 Saga,因为 agent 的工具调用经常跨越不同延迟、不同可用性的服务,TCC 锁会成为瓶颈。
熔断 + 降级 + Saga 补偿三件套如果不能被完整观测,就是黑魔法。生产事故复盘时,没有 trace 的补偿系统是最难调试的东西。
三个 trace 维度必须同时存在:
OTel span 标签体系里,agent 工具调用应当至少有以下属性:
agent.tool.name:工具名agent.tool.duration_ms:耗时agent.tool.result_code:success / transient_failure / permanent_failure / circuit_open / degradedagent.saga.step_index:在正向链路中的位置agent.saga.compensated:bool,是否经过了补偿agent.circuit.state:CLOSED / OPEN / HALF_OPEN / FORCED_OPENagent.fallback.target:如果走了降级,记录降级到哪个目标补偿溯源的反事实日志是一份特殊的 trace:它记录"如果当初没有走补偿,现在的预期状态 vs 实际状态"。这种日志格式通常是一对快照:补偿前的状态快照 + 补偿后的状态快照 + 差异说明。事故复盘时,反事实日志能直接回答"补偿真的把状态恢复了对吗?"——这是任何传统 trace 都回答不了的问题。
可观测性的另一个隐性要求是降级指标单独上报。如果所有失败(包括降级触发)都被记成 error,那么"看似失败率上升"实际上可能是降级体系在正常工作——你把降级算成了故障。正确的做法是把 agent.tool.result_code=degraded 从错误率里摘出来,作为单独的"降级率"指标。生产环境的 dashboard 应当至少有四类指标:原始成功率、降级率、补偿率、熔断状态持续时长。
下面六条是基于前面形式化和工程经验的清单,每一条都来自生产事故复盘或框架源代码审阅。
每个对外工具调用都应当配 idempotency_key,且 key 必须在整个链路中传递:包括重试、补偿、降级三种路径都使用同一个 key。LangChain 的 tool_call_id、Anthropic 的 message_id、OpenAI 的 previous_response_id 都可以充当这个 key,但必须保证 ID 拼接规则的一致性——比如 agent_run_id:tool_name:step_index。
熔断阈值配置必须同时配 min_requests_in_window:仅配失败率阈值会导致冷启动场景下的误熔断。推荐 min_requests = max(10, 窗口内峰值 QPS × 0.1)。同时配 sleep_window_in_seconds(建议 5-30s)和 half_open_max_concurrent(建议峰值 QPS × 0.01-0.05)。
降级接口必须有 staleness budget:返回缓存前必须检查 staleness 是否在预算内。预算应基于业务场景分桶:实时数据(订单状态、库存)≤ 30s;准实时数据(天气、新闻)≤ 1h;离线数据(历史订单、归档)≤ 24h。预算超出的必须走 fallback 链或返回错误,不允许假装正常。
补偿链路必须经过完整集成测试:单元测试只覆盖单步反向操作是不够的,必须有"模拟下游故障 → 触发补偿 → 验证最终状态"的端到端测试。推荐用 toxiproxy 或类似的故障注入工具,在测试环境里制造超时、5xx、连接重置等故障。LangGraph 的 checkpointer + 故障注入是当下最成熟的开源实现。
降级率与故障率必须分别上报:把 result_code=degraded 单独计入降级率,而不是故障率。否则降级体系正常工作时反而触发误报警。告警阈值建议:降级率 > 5% 持续 5 分钟告警,原始失败率 > 2% 持续 5 分钟告警(这两个阈值需要根据实际业务调整,但必须分开)。
补偿日志应当包含完整的反事实快照:每个 R 触发时记录"补偿前状态快照 + 补偿后状态快照 + 差异说明"。生产事故复盘时这是定位"补偿是否真正回滚了"的最快途径。建议用 PostgreSQL 的 JSONB 列或 OpenTelemetry 的 span event 来存储这两个快照。
四种策略的适用边界可以用一张决策表概括:
| 故障类型 | 首选策略 | 次选策略 | 不适用 |
|---|---|---|---|
| 瞬时网络抖动 | 重试(带 jitter) | 重试预算 + 熔断 | 补偿 |
| 下游容量耗尽 | 熔断 | 降级 + 限流 | 重试(会加剧雪崩) |
| 工具永久不可用 | 降级(fallback 链) | 能力降级 | 重试(无意义) |
| 部分成功链路 | Saga 补偿 | 人工介入 | 重试(破坏副作用) |
| 未知故障 | 重试预算 + 熔断 | 全链路 trace | 降级(无可降目标) |
这张表不是教条,它只是给了一个默认起点。实际工程里你会发现熔断和降级经常被混用——比如"熔断后自动降级到 fallback"。这种组合在生产里是合理的,但需要明确两个语义:(a) 熔断是状态、降级是动作;(b) 熔断器不需要知道降级的存在,降级触发器独立运行,熔断只是它的一个观察信号。
Saga 的局限主要有三个:
熔断器的局限:误熔断会严重影响用户体验(明明可用却被告知不可用),熔断恢复曲线很难调(恢复太慢影响可用性,恢复太快可能二次雪崩)。这两个局限没有银弹,只有通过灰度发布 + 真实流量验证才能找到平衡点。
降级的局限:fallback 链设计不当会产生"看似正常实则错误"的响应——比如把"实时股票价格"降级到"上一交易日收盘价"而用户以为是实时价格。这种"幻觉降级"是 agent 系统的最大可信度风险。
补偿的局限(与重试不同):补偿需要业务侧配合(每个 S 都要有 R),而业务侧不一定能配合。当工具是第三方 API(航班搜索、支付扣款)时,第三方不提供补偿接口,你就只能用隔离模式(sandbox / 两阶段 / escrow),但这些模式本身也有成本。
生产 agent 系统的故障响应,关键不是"找到根因",而是前 30 秒决定走哪条恢复路径。下面是给 SRE 的运行时清单。
30 秒决策树:
收到告警 (失败率 > 2% 或 降级率 > 5% 或 P99 延迟 > 阈值)
│
├─ 检查熔断器状态 (circuit.state)
│ ├─ 全部 CLOSED → 检查具体失败的工具 → 进入工具级响应
│ ├─ 部分 OPEN → 等待 sleep_window + 检查下游健康度
│ └─ 全部 OPEN → 下游全挂 → 启动全链路降级
│
├─ 检查补偿率 (saga.compensated)
│ ├─ < 5% → 补偿正常 → 等待自动恢复
│ ├─ 5%-20% → 部分链路在补偿 → 抽样检查补偿是否成功
│ └─ > 20% → 补偿链出问题 → 暂停新请求,启用 sandbox 模式
│
└─ 检查降级率 (result_code=degraded)
├─ < 10% → 降级正常 → 观察
├─ 10%-30% → 降级链繁忙 → 检查 fallback 目标健康度
└─ > 30% → 主路径系统性失败 → 切到能力降级话术模板
必备仪表盘(建议至少四个 panel):
告警项建议(按优先级):
Runbook 模板(每个工具一份):
最后一条建议往往被忽视:为 agent 工具调用链路建立"故障日历"。每个月的故障按"熔断 / 降级 / 补偿 / 重试"四类归类,季度回顾时看哪一类占主导——如果熔断类故障最多,说明下游服务不稳定;如果补偿类最多,说明业务链路设计有系统性问题;如果降级类最多,说明 fallback 链设计不充分。这种横向对比比单个故障复盘更有价值,因为它揭示的是系统结构性问题而不是单点问题。
一句话摘要:把熔断当作"主动断路"、把降级当作"能力收窄"、把 Saga 补偿当作"反向操作账本"——Agent 工具调用链路的可靠性不是一个 retry 函数,而是一个分布式系统的运行时工程问题。
Conversation
0 条