LLM 应用的灾难恢复与韧性
AI 应用的故障源比传统软件多——模型 API 挂、向量库挂、工具挂、自家服务挂。本文讲清韧性设计:降级、熔断、重试、兜底,多模型多供应商容灾,数据备份(向量库/记忆/会话),以及监控+混沌演练,让你的 AI 应用在故障下不裸奔。
本文要点
- 主题聚焦:AI、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
传统 Web 应用挂了,你至少能返回一个 500,用户刷新还能重试。LLM 应用挂了更尴尬:模型 API 超时你给不出回答,向量库挂了你检索为空,工具调用失败你 Agent 卡在中间一步——用户看到的是一个转了三十秒的 loading,然后一句"出错了请重试"。
AI 应用的"在线"依赖一条很长的链路:模型 API、向量库、检索、工具、自家业务服务、网络,任何一环都是单点。把这些当默认可靠,就是让生产环境裸奔。本文讲清 LLM 应用的灾难恢复与韧性设计:从故障源全景到四件套,到多供应商容灾、数据韧性、监控和混沌演练。
一、AI 应用的故障源全景
先盘清楚会挂的地方,比闷头写代码重要。LLM 应用的故障源大致分六类:
用户请求
→ 网关 / Edge 网络抖动、限流、区域级故障
→ 模型 API 超时、429 限流、503、上下文超限、内容被拒
→ 向量库 / 检索 查询超时、索引损坏、召回为空、embedding 服务挂
→ 工具调用 (MCP/函数) 上游 API 挂、返回非法 JSON、权限失败、慢
→ 自家业务服务 DB/Redis/Queue,传统故障但会让 Agent 失语
→ 记忆 / 会话存储 丢失=用户"失忆",体验比报错还差
每类故障的"症状"和"正确反应"都不一样。模型 429 该退避重试;向量库挂了该走 keyword 兜底检索;工具失败该让 Agent 换路径或降级;会话存储挂了该走只读缓存。韧性设计的前提,是把这些链路上的依赖一个个列出来,标注"挂了怎么办"。
经验:画一张依赖图,每个外部依赖旁边标三件事——它挂了什么症状?能不能降级?有没有兜底。这张图就是你的容灾设计文档。
二、韧性设计四件套:降级、熔断、重试、兜底
这四件套在传统微服务里都成熟了,LLM 应用要照搬,但参数要按模型特性调。
1. 重试(指数退避 + 抖动)
模型 API 的 429、503、网络抖动是瞬时的,重试最有效。关键是指数退避 + 随机抖动,否则所有客户端同时重试会产生惊群。只对幂等和瞬时错误重试,5xx/429/超时重试,4xx(除 429)不要重试——400 是参数问题,重试一万次还是 400。
async function withRetry<T>(
fn: () => Promise<T>,
opts: { retries?: number; baseMs?: number } = {},
): Promise<T> {
const { retries = 3, baseMs = 500 } = opts;
let lastErr: unknown;
for (let i = 0; i <= retries; i++) {
try {
return await fn();
} catch (e: any) {
lastErr = e;
const retryable = e.status === 429 || e.status >= 500 || e.code === 'ETIMEDOUT';
if (!retryable || i === retries) throw e;
const jitter = Math.random() * baseMs;
const delay = baseMs * 2 ** i + jitter;
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastErr;
}
2. 熔断(连续失败短路)
重试解决瞬时抖动,熔断解决"持续性故障"。如果模型 API 已经挂了 30 秒,你还每个请求重试 3 次,只是在徒增延迟和成本。熔断器在连续失败到阈值时"打开",后续请求直接失败 fast,不再打下游,给下游喘息,也保护自己的延迟。
3. 降级(切小模型 / 简陋模式)
主模型挂了或超时,降级到更快更便宜的小模型;小模型也挂了,降级到"简陋模式"——关掉 RAG 走纯对话,关掉多轮工具调用走单轮回答。降级不是"返回错误",而是"用一个更糙但能用的体验顶上"。
async function answer(query: string): Promise<string> {
// 主链路:大模型 + RAG + 工具
try {
return await withRetry(() => callLLM('claude-sonnet', query, { rag: true, tools: true }), { retries: 2 });
} catch (e) {
metrics.degradation('sonnet-rag-failed');
}
// 降级一:小模型 + RAG,不带工具
try {
return await withRetry(() => callLLM('claude-haiku', query, { rag: true, tools: false }));
} catch (e) {
metrics.degradation('haiku-rag-failed');
}
// 降级二:纯对话,无检索
return await withRetry(() => callLLM('claude-haiku', query, { rag: false, tools: false }));
}
4. 兜底(缓存 / 静态回复)
所有链路都挂了,也别给用户一个 stack trace。语义缓存(把高频问题的回答存进 KV 或向量库,下次相似 query 直接命中)能在模型挂掉时救命;实在没有,返回一句诚实的兜底:"服务暂时不可用,已记录,稍后重试",并触发告警。
经验:四件套要组合成一个"失败阶梯"——重试 → 熔断触发后降级 → 降级也失败走兜底。别只做重试不做降级,那只是把故障延迟放大;别只做降级不做兜底,最后一公里还是 500。
三、多模型多供应商容灾
把宝押在一家模型供应商上,等于把生产 SLO 绑在别家的 status page 上。主备链路是 AI 应用容灾的核心:
主:OpenAI (gpt-4o)
↓ 连续失败 / 熔断
备 1:Anthropic (claude-sonnet)
↓ 连续失败
备 2:自托管 (vLLM + Qwen,内网,SLA 自控)
主备切换的关键不是"调三家 SDK",而是抽象出统一的调用层,让上层不感知具体供应商。失败时按优先级自动 failover,并记录"某供应商当前不健康"的状态(配合熔断器,几分钟后半开探测恢复)。
const providers = [
{ name: 'openai', call: (q: string) => openai.chat(q), circuit: new CircuitBreaker() },
{ name: 'anthropic', call: (q: string) => anthropic.messages(q), circuit: new CircuitBreaker() },
{ name: 'self-hosted', call: (q: string) => vllm.generate(q), circuit: new CircuitBreaker() },
];
async function resilientComplete(query: string): Promise<string> {
for (const p of providers) {
if (p.circuit.isOpen()) continue; // 熔断跳过
try {
const out = await withRetry(() => p.call(query), { retries: 2 });
p.circuit.recordSuccess();
return out;
} catch (e) {
p.circuit.recordFailure();
metrics.providerFailover(p.name);
}
}
throw new Error('all providers unavailable');
}
几个实战要点:
- prompt 要跨供应商兼容。不同厂商的 system / tool / JSON 模式语法略有差异,统一调用层负责适配,避免 failover 后格式错乱。
- 降级链也是成本链。主用贵的旗舰模型,备用便宜模型,failover 时成本反而下降,体验略损但不断服——这是符合直觉的。
- 自托管兜底是终极保险。哪怕只能跑一个 7B 小模型,在全网故障时它就是你唯一的在线凭证。
四、数据韧性:向量库、记忆、会话
模型挂了能切,数据丢了切不回来。LLM 应用有三类核心数据,各自的恢复策略不同。
向量库 / 知识库:这是 RAG 的命根子。重建一次 embedding 索引可能要几小时几千块,丢了不能现场重算。核心做法是快照 + 副本:定期快照到对象存储(S3/OSS),生产集群开副本(Pinecone/Qdrant/Milvus 都支持),重建时从快照恢复而不是从原始文档重 embed。重要变更(文档增删)走版本化,出问题能回滚到上一个一致状态。
记忆 / 会话历史:用户聊了一半,会话丢了等于"AI 失忆",体验比报错还差。会话存储要当业务核心库对待——持久化到 DB 而不是只在内存/Redis,Redis 只做热缓存。关键操作(写入消息、更新记忆)用幂等键保证重试不会重复写入:
// 每条消息用稳定的幂等键,网络重试不会产生重复消息
await db.message.upsert({
where: { idempotencyKey: `${sessionId}:${messageId}` },
create: { idempotencyKey: `${sessionId}:${messageId}`, role, content, createdAt },
update: {}, // 已存在则跳过
});
可重放的请求:一个 Agent 任务可能跑 10 步,中间第 5 步挂了。如果整个任务是不可重入的(比如已经发了一封邮件),重试会重复发。给 Agent 任务设计成可重入的——每一步落库 checkpoint,失败从上一步续跑,而不是从头重来。这比传统微服务的幂等要求更高,因为 Agent 的副作用链更长。
经验:问自己一句"如果这一步现在重放一次,会出问题吗?" 答不了"不会",就还没设计好。Agent 场景下,重放安全比跑得快重要得多。
五、监控:健康度、错误率、降级触发
韧性设计做完了,还要能"看见"它在工作。LLM 应用的监控要覆盖三个层次:
依赖健康度:每个外部依赖(模型供应商、向量库、工具上游)一个独立的健康探针,周期性探测并暴露 /health 状态。熔断器状态、当前走的是主还是备链路、降级到了哪一级,都应该是可观测的指标。
错误率与错误类型:不要只看"总错误率"。按错误类型拆——模型超时、429、内容安全拒、向量库超时、工具失败——每种错误的趋势独立看,定位问题才快。
降级触发率:这是 LLM 应用独有的关键指标。降级触发率高,说明主链路在持续不健康,虽然用户还能拿到回答(被兜底了),但体验在悄悄下降。把"降级率""failover 次数""熔断打开时长"做成告警,别等用户投诉才知道主模型其实挂了半小时。
告警规则示例(Prometheus 风格):
# 模型供应商错误率 5 分钟 > 10%
rate(llm_provider_errors_total[5m]) / rate(llm_calls_total[5m]) > 0.1
# 降级触发率突增(主链路异常)
rate(degradation_triggered_total[5m]) > 1
# 熔断器打开超过 2 分钟
circuit_breaker_open_seconds > 120
六、混沌演练:主动注入故障
韧性设计写完了,不演练等于没写。混沌工程的核心是:在故障真的发生前,先主动制造一次。 LLM 应用要重点注入这几类故障:
- 模型超时/挂掉:用代理层把对模型供应商的请求延迟 30 秒或直接 503,验证熔断和 failover 是否如期触发、用户是否拿到了降级回答。
- 向量库为空/超时:在 staging 把向量库连接指向一个空实例,验证 RAG 是否优雅退化为纯对话,而不是报错。
- 工具调用失败:Mock 工具上游返回 500 或非法 JSON,验证 Agent 是否能在工具失败后换路径或给用户合理反馈。
- 会话存储不可用:断开 DB,验证服务是否还能用只读缓存提供有限服务,而不是整体崩。
每次演练前先写好预期行为(熔断应在 N 秒内打开、降级率应升到 X、用户应看到 Y),再注入故障看实际曲线。预期和实际不符的地方,就是韧性设计的 bug。 没演练过的容灾,只是 PPT 里的容灾。
七、故障手册与复盘
线上一定会挂。挂了之后能不能快速止血,取决于你有没有一份故障手册(runbook)。每个关键故障场景(模型供应商全线故障、向量库索引损坏、会话库主库挂了)配一页手册:症状、影响范围、止血操作(切备/回滚/降级开关)、验证步骤、负责人。
挂完之后必须复盘,且复盘要 blameless(对事不对人),聚焦三件事:
- 检测时长(MTTD):从故障发生到告警触发用了多久?太久就补监控。
- 恢复时长(MTTR):从告警到服务恢复用了多久?太久就补自动化(自动 failover、一键降级)。
- 韧性短板:这次故障暴露了哪个没考虑到的依赖或边界?补进设计、补进混沌演练的剧本。
把每次故障沉淀成"新增的降级路径""新增的告警规则""新增的演练场景",系统的韧性才会越用越强。
小结
- 故障源先盘清:模型 API、向量库、检索、工具、自家服务、网络——画依赖图,每个标注挂了怎么办。
- 四件套是地基:重试(指数退避 + 抖动)→ 熔断(连续失败 fast fail)→ 降级(切小模型/简陋模式)→ 兜底(缓存/静态回复),组合成失败阶梯。
- 多供应商主备:抽象统一调用层,主备自动 failover,自托管小模型做终极兜底。
- 数据韧性是底线:向量库快照 + 副本,会话/记忆走幂等与可重入,Agent 任务设计成可重放。
- 监控 + 混沌 + 复盘:盯降级率和熔断时长(不只是错误率),定期注入故障演练,挂完 blameless 复盘补短板。
韧性不是一次性的架构图,而是一种持续的习惯:每个新依赖都问一句"它挂了怎么办",每次故障都把答案沉淀回系统。能让 AI 应用在故障下还稳稳在线的,不是更聪明的模型,而是更谨慎的工程师。
Related