第 18 章 韧性工程:扛得住部分失效
系统总会部分失效,关键是扛得住
前几章让 Agent 输出可靠、评测跟上、还能看得见。但还有个现实:依赖的东西总会失效。外部 API 会挂、模型提供商会抖、一次提示词改动可能让整个 Agent 行为崩盘、定时任务可能悄无声息地死掉。韧性工程回答的是:这些失效发生时,系统怎么扛住、怎么恢复、怎么不把用户坑了。
这一章讲四个模式,从"挡住重复失败"到"盯住静默死亡":
- Agent Circuit Breaker(Agent 熔断器):坏掉的工具别反复调,暂时熔断。
- Failover-Aware Model Fallback(模型回退):按失败语义智能切换模型。
- Canary Rollout & Auto-Rollback(金丝雀发布与自动回滚):策略改动先放小流量,越界自动回滚。
- Dead-Man's Switch(Dead-Man's Switch):定时任务靠"成功哨兵"报警,而不是报错。
模式一:Agent 熔断器(Agent Circuit Breaker)
问题
用外部工具的 Agent(API、数据库、网页爬虫、代码执行器)面临一个常见的失败模式:某个工具端点降级或不可用,Agent 却一直调它,在永远不会成功的重试上白烧 token。
这带来三个级联问题:
- Token 浪费:每次失败的工具调用都花输入/输出 token,Agent 还经常生成大段重试推理。
- 延迟放大:在一个死端点上顺序重试,几秒变几分钟,毫无进展。
- 级联失败:一个工具挂了(比如搜索 API),Agent 可能整个卡死,而不是改用其它办法。
模型层的故障转移(一个提供商挂了,在 GPT-4 和 Claude 之间切换)解决不了这个问题。工具层的失败需要不同策略:Agent 要在会话中途学会"这个工具有毛病,别用"。
方案
把分布式系统的经典熔断器模式用到 Agent 工具调用上。 熔断器包住每次工具调用,跟踪失败率,在三个状态间切换:
- Closed(关闭):调用正常放行,失败被计数。
- Open(打开):调用立即被阻断,返回缓存的错误或回退,实际请求根本不发。
- Half-Open(半开):放行一个探测调用。成功就重置回 Closed,失败就回到 Open。
状态机:
Closed → Open: 失败数 >= 阈值
Open → HalfOpen: 冷却时间到了
HalfOpen → Closed: 探测成功
HalfOpen → Open: 探测失败class AgentCircuitBreaker:
def __init__(self, failure_threshold=3, cooldown_seconds=60):
self.state = "closed"
self.failure_count = 0
self.threshold = failure_threshold
self.cooldown = cooldown_seconds
self.opened_at = None
def call(self, tool_fn, *args, **kwargs):
if self.state == "open":
if time.time() - self.opened_at >= self.cooldown:
self.state = "half_open" # 放一个探测
else:
raise CircuitOpenError(f"熔断中,工具禁用 {self.cooldown}s")
try:
result = tool_fn(*args, **kwargs)
if self.state == "half_open":
self.state = "closed"
self.failure_count = 0
return result
except Exception:
self.failure_count += 1
if self.failure_count >= self.threshold:
self.state = "open"
self.opened_at = time.time()
raiseAgent 特有的适配(和传统熔断器不同的地方):
- Token 感知阈值:烧了 N 个 token 就熔断,而不只是 N 次失败。
- 回退路由:熔断打开时,通知 Agent 的系统提示词,让它选替代工具。
- 按工具粒度:每个工具(搜索 API、代码执行器、数据库)有自己的熔断器。
- 会话级状态:电路状态在 Agent 会话之间重置(不像微服务的持久熔断器)。
证据
- 证据等级:中(
medium)。 - 有价值发现:熔断器在微服务架构里是生产验证过的成熟模式(Netflix Hystrix、Resilience4j、Polly),能直接迁移到 Agent 工具调用;生产 Agent 系统报告,熔断器阻止了对降级工具的无效重试循环,省了 40%-60% 的 token;Anthropic 的 Agent 可靠性研究强调"快速失败"优于重试重策略。
- 未验证:最优阈值和冷却值因工具类型和延迟特征差异很大。
怎么用
什么时候用:
- Agent 用 3+ 个可能独立失败的外部工具。
- 工具可靠性参差(有限流的 API、网页爬虫、第三方服务)。
- Agent 会话长到工具可能在会话中途恢复。
实施步骤:
- 每个工具包一个熔断器实例。
- 按工具特征设阈值:快 API(搜索、天气)threshold=3、cooldown=30s;慢工具(爬虫、编译)threshold=2、cooldown=120s。
- 定义熔断后的回退行为:有缓存就返回缓存/过期结果;路由到替代工具(换搜索提供商);告诉 Agent 这个工具不可用,让它调整计划。
- 记录熔断状态变化,供可观测性用。
取舍
- 好处:防止在坏工具上无谓重试烧 token;正常降级,Agent 用可用工具继续干活;自愈,半开探测在工具恢复后自动复原;实现简单(核心逻辑约 50 行);会话级状态省掉持久熔断器的状态管理复杂度。
- 代价:给工具调用加了一层间接;阈值要理解每个工具的失败特征来调;可能掩盖本来单次重试就能自然解决的间歇性错误;要提示 Agent 优雅处理
CircuitOpenError(回退意识);只有 1-2 个高度可靠工具时没用。
模式二:模型回退(Failover-Aware Model Fallback)
问题
AI 模型请求会以各种各样、往往不透明的方式失败。简单重试逻辑分不清:
- 瞬时失败(超时、限流):加退避重试有效。
- 语义失败(认证错误、计费问题):重试没用。
- 用户中止:重试浪费资源还惹恼用户。
用多个模型或提供商的 Agent,需要尊重失败语义的智能回退策略,避免重试循环,并给出清晰的诊断。
方案
语义错误分类 + 智能回退链。 每个失败被归类成具体的原因类型,回退行为按原因定制。多模型回退链按请求配置,带提供商白名单和冷却跟踪。
核心概念:
- 错误分类:把失败映射成语义原因类型(
timeout、rate_limit、auth、billing、format、context_overflow),把提供商特有的错误码映射成统一的语义类型,一致处理。 - 按原因回退:不同原因触发不同回退行为:
timeout、rate_limit:用链里下一个模型重试。auth、billing:立即失败,重试没用。format、context_overflow:可以用调整后的请求重试。
- 用户中止检测:区分用户发起的中止和超时引起的中止。用户中止立即重抛;超时才触发回退。
- 多模型链:有序的
{provider, model}候选列表。每次尝试用下一个候选,直到成功或用尽。 - 提供商白名单:按提供商限制模型,防止回退到不兼容的模型。
- 诊断跟踪:每次失败尝试都记录错误详情、原因、状态码、提供商/模型,供调试。
async function runWithModelFallback({ candidates, run }) {
const attempts = [];
for (const candidate of candidates) {
try {
const result = await run(candidate.provider, candidate.model);
return { result, ...candidate, attempts };
} catch (err) {
const reason = classifyFailoverReason(err);
if (reason === "auth" || reason === "billing") throw err; // 重试没用
if (isUserAbort(err)) throw err; // 用户取消,不回退
attempts.push({ ...candidate, error: err, reason });
// 继续下一个候选
}
}
throw new Error(`所有模型都失败了: ...`);
}
// 错误分类示例
function classifyFailoverReason(err) {
if (status === 402) return "billing";
if (status === 429) return "rate_limit";
if (status === 401 || status === 403) return "auth";
if (status === 408) return "timeout";
if (message.includes("context window")) return "context_overflow";
return null;
}配置示例:
agents:
defaults:
model:
primary: "anthropic/claude-sonnet-4-20250514"
fallbacks:
- "openai/gpt-4o"
- "google/gemini-2.0-flash"证据
- 证据等级:生产验证(
validated-in-production),来自 Clawdbot。
怎么用
- 定义回退链:按用例(编码 vs 通用聊天)指定替代模型的有序列表。
- 配白名单:把回退限制在支持你请求格式的模型(比如只要图像模型)。
- 分类错误:把提供商特有错误码映射成语义原因,一致处理。
- 跟踪尝试:每次回退尝试记录提供商、模型、错误、原因,供可观测性。
- 处理耗尽:所有候选都失败时,聚合错误消息,给出可行动的反馈。
要避开的坑:
- 过度回退:回退链太多会在提供商之间级联失败。用指数退避 + 抖动,防止惊群问题。
- 语义不匹配:回退模型可能能力不同(视觉、工具)。按必需特性过滤。
- 静默失败:有些错误(
format)说明请求不兼容,回退可能同样失败。
取舍
- 好处:瞬时失败(超时、限流)不阻塞 Agent;成本优化,高级模型不可用时回退到便宜模型;诊断清晰,尝试历史显示哪些模型为何失败;尊重用户中止,区分取消和超时,避免无谓回退。
- 代价:每次失败尝试都加一轮往返延迟;不同模型响应不同,可能影响下游解析;回退链可能让单个逻辑请求产生多次 API 计费;配置(白名单、链、提供商特有行为)增加运维开销。
模式三:金丝雀发布与自动回滚(Canary Rollout & Auto-Rollback)
问题
Agent 行为频繁变化:提示词更新、工具策略、路由规则、评估器阈值。即使很小的策略改动,也可能在成本、延迟、安全或任务质量上造成大面积回归。 不经过分段曝光就全量发布,回滚慢,用户影响大。
方案
把 Agent 策略改动当生产发布来对待:先放小流量,监控先行指标,越界就自动回滚。
核心组件:
- 流量切分器:把固定百分比的路由到新策略。
- 策略版本注册表:不可变的标识符。
- 实时监控:质量、延迟、失败率、安全标志、花费、目标达成率、无限循环检测。
- 回滚自动化:无需人工干预恢复到上一个稳定策略。
- 可选的影子模式:在用户曝光前先验证技术稳定性。
推荐阶段:
1%流量金丝雀:快速发现异常。5-10%验证阶段:更严格的阈值。25-50%浸泡期:在混合负载下验证稳定性。100%全量:只在所有 SLO 和安全条件都满足时。
policy = registry.current_candidate()
traffic = splitter.assign(request, canary_percent=5)
response = run_agent(policy if traffic.canary else registry.stable())
metrics.ingest(response, policy.version)
if monitors.breach(policy.version):
registry.rollback_to_stable()
alert("自动回滚已执行", policy.version)证据
- 证据等级:成熟(
established),来自 Codex(OpenAI)。 - 金丝雀发布是 SRE 的成熟实践,直接套用到 Agent 策略;MI9 Runtime Governance(2025)、AGENTSAFE Safety Evaluation(2025)提供了运行时治理和安全评估的参考。
怎么用
- 任何可能改变外部行为的改动都用:提示词、工具、评估器逻辑、记忆策略、路由。
- 发布开始前定义回滚触发器。设观察窗口(比如 2+ 分钟),避免误报。
- 回滚要确定性:总是恢复到最后一个已知良好的版本。
- 策略产物带版本化元数据存储,让事故可复现。
取舍
- 好处:限制爆炸半径,缩短恢复时间。
- 代价:需要发布编排、更丰富的遥测、清晰的版本卫生。
模式四:Dead-Man's Switch(Dead-Man's Switch for Scheduled Agent Jobs)
问题
一个 Agent 工作区会累积定时任务:晨间摘要、后台任务管理器、每周自审、记忆合并。这些任务静默失败。调度器配置错误、OAuth token 过期、权限回归,都不会在人类看的地方抛出任何错误,任务日志只是不再出现。没有东西盯"缺失",所以一个坏掉的每日任务可能坏了几个星期,才有人注意到输出没了。
方案
把监控方向反过来:对"缺失的成功"报警,而不是对"错误"报警。
- 每个定时任务在每次运行的日志末尾,写一个显式的成功哨兵(比如
MORNING_BRIEF_OK)作为最后一步。 - 一个新鲜度检查器知道每个被跟踪任务、它的节奏、它的过期窗口。它扫每个任务的最新日志:哨兵在且新鲜 = FRESH;缺失、过期、没哨兵 = 记一条发现。
- 发现落进 Agent 自己的任务队列,也就是运维本来就在看的地方,而不是一个没人打开的单独 dashboard。
- 一个按任务的
manual标志,豁免按需任务不被判过期,避免检查器对本来就该不定期跑的任务乱报警。 - 检查器用和它盯的任务不同的节奏跑(每周审计 + 按需),在不引入新基础设施的情况下回答"谁来盯检查器"。
for task in tracked_tasks:
log = latest_log(task)
if task.manual and log is None: status = MANUAL_OK
elif log is None or age(log) > task.window: status = STALE
elif task.sentinel not in log.text: status = NO_SENTINEL
else: status = FRESH
if status not in (FRESH, MANUAL_OK):
file_finding(task, status) # 落进 Agent 的任务队列证据
- 证据等级:中(
medium),来自 James Ross 的生产工作区。 - 有价值的发现:在生产里抓住了一个多日静默失败(权限回归让每日任务以只读模式运行;配置看起来正确,缺失的哨兵证明了问题)。底层反转对人类 cron 任务早就是成熟做法(Healthchecks.io)。
- 未验证:Agent 特定变体只在一个生产工作区验证过;它抓不住"跑了但产出是错的"的任务(那类要配合合成金丝雀)。
怎么用
- 工作区有两个以上定时 Agent 任务时就值得用。要求:持久的按次运行日志、写进每个任务指令的成功哨兵约定、用你本来信任的节奏调用的检查器(循环自审是天然宿主)。
- 按任务调过期窗口:两小时跑一次的任务和每周跑一次的任务,"迟到"的定义完全不同。
取舍
- 好处:抓住配置检查抓不住的失败类别("配置了"不等于"在跑");不需要外部服务;哨兵约定每个任务就一行。
- 代价:任务来来去去,按任务配置要持续维护;跑了但产出是垃圾的任务照样过哨兵;检查器自己需要一个宿主节奏。
四个模式怎么选
| 场景 | 推荐模式 |
|---|---|
| 外部工具反复失败,白烧 token | 熔断器 |
| 模型提供商抖动,要智能切换 | 模型回退 |
| 改提示词/策略,怕大面积回归 | 金丝雀发布与自动回滚 |
| 定时任务静默失败,没人发现 | Dead-Man's Switch |
四个模式是韧性工程的四道防线:熔断器挡住"工具层"的重复失败,模型回退扛住"模型层"的提供商抖动,金丝雀守住"发布层"的改动风险,Dead-Man's Switch盯住"任务层"的静默死亡。从最细的工具调用,到最大的策略发布,四道防线把 Agent 系统撑起来。
实践清单
- 每个外部工具配熔断器:坏工具立即熔断,半开探测自愈
- 熔断打开时通知 Agent 改用替代工具,记录状态变化
- 模型请求按语义分类错误(timeout / rate_limit / auth / billing / format)
- 回退链按原因触发:瞬时失败换模型,auth/billing 直接失败,用户中止不回退
- 回退加指数退避 + 抖动,防惊群;按必需能力过滤回退模型
- 策略改动走金丝雀:1% → 5-10% → 25-50% → 100%,越界自动回滚到已知良好版本
- 回滚触发器发布前定义好,观察窗口 2+ 分钟防误报,策略版本化存储
- 定时任务写成功哨兵,新鲜度检查器对"缺失的成功"报警
- 检查器落进 Agent 自己的任务队列,用不同节奏跑(谁盯检查器)
- 按任务调过期窗口,manual 任务豁免,防乱报警
本章小结
- 熔断器:坏工具立即熔断,不再白烧 token,省 40%-60%。
- 模型回退:按失败语义智能切模型,瞬时失败重试、语义失败直抛、用户中止不回退。
- 金丝雀发布:策略改动先放小流量,越界自动回滚,限制爆炸半径。
- Dead-Man's Switch:成功哨兵 + 新鲜度检查器,盯"缺失的成功",抓静默死亡。
- 韧性工程是最后一道关:评测告诉你对不对,可观测告诉你为什么坏,韧性让系统坏的时候不崩。
到这里,第四部分"让它可靠"就讲完了。你现在的链路是:结构化输出(14)→ 验证循环(15)→ 评测基建(16)→ 可观测性(17)→ 韧性工程(18),从"输出稳"到"系统扛得住"。下一部分,进入"让它安全":信任边界。先从第 19 章威胁模型开始。