关键词:LLM、SLA、SLO、Provider 可靠性、AI Engineering、大模型工程、故障降级
你以为 99.9% 就够了
上线一个 LLM 应用,最容易忽视的问题不是 Prompt 质量,也不是 Token 成本,而是:你的系统对 Provider 故障的容忍度是多少?
主流大模型 API 的企业 SLA 通常在 99.9%,折算下来每月允许 43 分钟不可用。看上去很高,但这里有几个陷阱:
陷阱一:Provider 的"可用"和你的用户感知的"可用"不是一回事。
Provider 的 SLA 通常定义为 API 端点可达(HTTP 200 或有效的错误码),但应用层会遇到:
- 请求成功返回,但 P95 延迟从 1.2s 涨到 12s
- 没有 429,但 throughput 从 50 tokens/s 掉到 8 tokens/s
- 特定模型版本持续超时,但其他模型正常,状态页写"investigating"
这些情况在 Provider 的 SLA 统计里可能是"正常",对你的用户来说就是"坏了"。
陷阱二:你对故障的发现依赖 Provider 的状态页,而不是你自己的观测。
大模型服务状态页的更新通常比实际故障滞后 15-30 分钟。如果你的监控只读状态页,用户已经在投诉了,你才刚收到通知。
陷阱三:99.9% 是全局平均,不是你实际用的那个模型、那个区域的数字。
云上托管大模型在区域切换时有 2-5 分钟高错误率窗口。不同模型版本的可用性历史上差异显著。Provider 不会给你按模型、按区域细粒度的 SLA 承诺。
所以问题就清楚了:第三方 LLM Provider 的不稳定性是你系统边界之外的风险,但必须由你的系统来吸收。 本文讲怎么做到这件事。
生产中真实出现过的 5 类故障模式
在讲解法之前,先把故障模式理清楚。以下 5 类故障的共同特点是:不一定触发 Provider 的 SLA 事件,但对用户有实质影响。
1. 无错误延迟尖刺(Latency Spike without Error)
最常见,最难发现。表现是成功率 100%,但 P95/P99 延迟突然拉高。
原因通常是 Provider 内部模型实例扩容滞后。请求没有被拒绝,只是在排队。应用层如果只监控 error rate,这种情况完全感知不到,直到用户反馈"网页卡住了"。
实测数据:在一次大模型容量事件期间,P95 latency 从基线 1.1s 升到 14.3s,持续约 23 分钟,期间 error_rate 始终低于 0.5%,状态页面没有事件记录。
2. 模型级局部故障(Partial Outage by Model)
Provider 内部的模型不是统一的服务,而是按版本部署的独立实例。某个模型版本挂了,其他模型不受影响。
典型场景:高性能版本正常,轻量版本持续超时 → 只用轻量版做批量任务的服务全线阻塞,但状态页显示"系统正常"。
工程影响:如果你的降级策略是"主模型失败切换到轻量模型",而轻量模型本身才是故障方,降级逻辑会失效。
3. 软限速降级(Soft Rate Limit Degradation)
Provider 通过限制后端资源(而非返回 429)来保护自己。表现是没有错误码,但 throughput 持续低于正常水平。
这种情况用标准的 retry-on-error 逻辑完全感知不到,因为请求没有失败,只是很慢。典型触发条件是账号 tier 被临时降级、同一 org 内其他项目消耗突增,或 Provider 在做容量调整。
4. 区域切换延迟(Region Failover Latency)
支持多区域的大模型服务在做区域级故障切换时,会有一个 2-5 分钟的高错误率窗口。这个窗口通常不计入 SLA,但对实时应用来说是致命的。
如果应用没有主动的跨区路由能力,在这个窗口期内只能等待,用户请求全部失败。
5. 模型版本漂移(Model Version Drift)
Provider 在不显著通知的情况下切换底层模型版本。这不是"故障",不触发 SLA 条款,但输出行为可能发生变化,破坏依赖特定输出格式的应用。
这类问题的麻烦在于:没有错误,没有告警,只有安静的行为漂移,直到某个依赖输出格式的功能悄悄坏掉。
5 层 Provider SLA 工程体系
针对上面 5 类故障,建立一个应用层的 Provider SLA 管理体系,分 5 层。每层独立可部署,可以从 Layer 1 开始逐步建设。
┌──────────────────────────────────────────────────────────────┐
│ 应用层 SLA 工程体系 │
│ │
│ Layer 5: Cost-Availability Tradeoff Budget │
│ ────────────────────────────────────────── │
│ Layer 4: Provider Switch Trigger Policy │
│ ────────────────────────────────────────── │
│ Layer 3: Degradation Contract │
│ ────────────────────────────────────────── │
│ Layer 2: Anomaly Detection │
│ ────────────────────────────────────────── │
│ Layer 1: SLO Baseline Measurement │
└──────────────────────────────────────────────────────────────┘
Layer 1:SLO 基线测量
不要依赖 Provider 的状态页,应用层必须主动探测,建立自己的 SLO 基线。
探测方案设计:
interface ProviderProbeConfig {
provider: string;
model: string;
interval_seconds: number; // 建议 30s
probe_prompt: string; // 固定 prompt,小 token
timeout_ms: number; // 建议 5000ms(远小于正常业务超时)
metrics: ProbeMetrics[];
}
interface ProbeMetrics {
ttfb_ms: number; // Time To First Byte(流式首字延迟)
total_latency_ms: number; // 完整响应耗时
tokens_per_second: number; // 吞吐量
success: boolean; // HTTP 200 + 有效响应
error_code?: string; // 如果失败,记录错误码
}
探测 prompt 要满足:固定输入(每次一样,排除 LLM 随机性影响)、小 token(不浪费钱)、有可验证输出(能检测模型版本漂移)。
推荐 probe prompt 格式:
const PROBE_PROMPT = {
system: "Reply with exactly: PROBE_OK",
user: "health check"
};
// 验证方式:response.trim() === "PROBE_OK"
// 如果返回不一致,说明模型行为漂移
基线计算——用动态基线,不用固定阈值:
class ProviderSLOBaseline {
private window: ProbeMetrics[] = [];
private windowSize = 100; // 最近 100 次探测(约 50 分钟)
add(metric: ProbeMetrics) {
this.window.push(metric);
if (this.window.length > this.windowSize) {
this.window.shift();
}
}
getBaseline(): { mean: number; stddev: number; p95: number } {
const latencies = this.window.map(m => m.total_latency_ms);
const mean = latencies.reduce((a, b) => a + b, 0) / latencies.length;
const variance = latencies.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / latencies.length;
const stddev = Math.sqrt(variance);
const sorted = [...latencies].sort((a, b) => a - b);
const p95 = sorted[Math.floor(sorted.length * 0.95)];
return { mean, stddev, p95 };
}
isAnomaly(current: number): boolean {
const { mean, stddev } = this.getBaseline();
return current > mean + 2 * stddev; // 2σ 超出基线
}
}
探测成本估算:
以轻量级大模型(如 DeepSeek-V3 Turbo / Qwen-Turbo)为例,probe prompt 约 20 input tokens + 5 output tokens:
- 每 30s 一次 = 每小时 120 次
- 每次成本极低(< ¥0.001)
- 每月探测总成本 < ¥1 / provider / model
代价极低,但信息价值极高。
Layer 2:异常检测
基线建立后,接入状态机驱动的异常检测。
状态定义:
type ProviderStatus =
| "NORMAL" // 一切正常
| "WARNING" // 单次异常,可能是抖动
| "DEGRADED" // 持续异常,开始影响用户
| "CRITICAL" // 高错误率或严重延迟,需要切换
| "SWITCHED" // 已切换到 backup provider
状态转移逻辑:
class ProviderHealthMonitor {
private status: ProviderStatus = "NORMAL";
private warningCount = 0;
private degradedAt?: Date;
evaluate(probe: ProbeMetrics, baseline: ReturnType<ProviderSLOBaseline['getBaseline']>) {
const latencyAnomaly = probe.total_latency_ms > baseline.mean + 2 * baseline.stddev;
const errorRateHigh = this.recentErrorRate() > 0.05; // 5%
const tpsLow = probe.tokens_per_second < baseline.p95 * 0.3; // 低于 p95 的 30%
const isAbnormal = !probe.success || latencyAnomaly || errorRateHigh || tpsLow;
switch (this.status) {
case "NORMAL":
if (isAbnormal) {
this.warningCount++;
if (this.warningCount >= 3) {
this.status = "DEGRADED";
this.degradedAt = new Date();
this.alert("DEGRADED", { probe, baseline });
} else {
this.status = "WARNING";
}
}
break;
case "WARNING":
if (!isAbnormal) {
this.status = "NORMAL";
this.warningCount = 0;
} else {
this.warningCount++;
if (this.warningCount >= 3) {
this.status = "DEGRADED";
this.degradedAt = new Date();
}
}
break;
case "DEGRADED":
if (errorRateHigh && this.secondsSince(this.degradedAt!) > 60) {
this.status = "CRITICAL";
this.alert("CRITICAL", { probe, baseline });
}
if (!isAbnormal && this.secondsSince(this.degradedAt!) > 300) {
this.status = "NORMAL"; // 自愈
this.warningCount = 0;
}
break;
}
}
private secondsSince(date: Date): number {
return (Date.now() - date.getTime()) / 1000;
}
private alert(level: string, context: any) {
// 接入告警系统:钉钉、企业微信、飞书 等
console.error(`[PROVIDER_ALERT] ${level}`, context);
}
}
关键参数说明:
| 参数 | 建议值 | 说明 |
|---|---|---|
| WARNING 触发 | 单次异常 | 不立即报警,避免误报 |
| DEGRADED 触发 | 连续 3 次异常 | 约 90s 确认 |
| CRITICAL 触发 | error_rate > 5% 持续 60s | 或 DEGRADED 超过 2 分钟 |
| 自愈确认 | 正常持续 5 分钟 | 防止抖动触发回切 |
Layer 3:降级契约
检测到 DEGRADED 或 CRITICAL 后,不同功能的降级行为需要预先定义,而不是临时决策。
降级契约设计:
interface DegradationContract {
feature: string;
degraded_behavior: DegradedBehavior;
critical_behavior: DegradedBehavior;
user_message?: string;
}
type DegradedBehavior =
| { type: "fallback_model"; model: string } // 切换到更便宜/更快的模型
| { type: "async_queue"; ttl_seconds: number } // 放入异步队列延迟处理
| { type: "cached_response"; max_age_seconds: number } // 返回缓存结果
| { type: "reject"; message: string } // 直接拒绝,告知用户
// 配置示例
const DEGRADATION_CONTRACTS: DegradationContract[] = [
{
feature: "streaming_chat",
degraded_behavior: {
type: "fallback_model",
model: "qwen-turbo" // 降质量保可用
},
critical_behavior: {
type: "fallback_model",
model: "deepseek-v3" // 切换到其他 Provider
},
user_message: "当前响应速度较慢,已自动切换到备用模式"
},
{
feature: "batch_summarization",
degraded_behavior: {
type: "async_queue",
ttl_seconds: 3600 // 最多等 1 小时
},
critical_behavior: {
type: "async_queue",
ttl_seconds: 7200
},
user_message: "摘要生成已加入队列,预计稍后完成"
},
{
feature: "real_time_tool_calling",
degraded_behavior: {
type: "cached_response",
max_age_seconds: 300 // 返回 5 分钟内的缓存
},
critical_behavior: {
type: "reject",
message: "当前 AI 功能暂时不可用,请稍后重试"
}
}
];
关键原则:不同功能的容忍度不同。
- 实时聊天:用户感知直接,优先切换 Provider 保可用
- 批量处理:可以队列化,保质量牺牲速度
- 实时工具调用:不可降质,要么缓存要么拒绝
把这个逻辑写进配置文件,而不是硬编码在业务代码里,这样可以在不重启服务的情况下调整降级策略(配合 Feature Flag 系统)。
Layer 4:Provider 切换触发策略
CRITICAL 状态触发后,执行 Provider 切换。核心问题是:切换本身也会引入风险,必须有安全机制。
class ProviderSwitchController {
private primaryProvider: string;
private backupProviders: string[];
private activeProvider: string;
async triggerSwitch(reason: string) {
const backup = this.selectBackup();
if (!backup) {
console.error("No healthy backup provider available");
return;
}
// 切换前探测 backup provider
const backupHealth = await this.probe(backup);
if (!backupHealth.healthy) {
console.error(`Backup provider ${backup} also unhealthy, abort switch`);
return;
}
this.activeProvider = backup;
this.scheduleAutoReturn();
}
private scheduleAutoReturn() {
// 30 分钟后尝试回切
setTimeout(async () => {
await this.attemptReturn();
}, 30 * 60 * 1000);
}
private async attemptReturn() {
// Primary 必须连续 5 分钟健康才回切
const primaryHealth = await this.probeContinuous(
this.primaryProvider,
5 * 60 * 1000, // 5 分钟
30 * 1000 // 每 30s 探测
);
if (!primaryHealth.allHealthy) return;
// Canary 回切:5% → 25% → 100%
await this.canaryReturn(this.primaryProvider, [0.05, 0.25, 1.0], 2 * 60 * 1000);
}
private async canaryReturn(
target: string,
stages: number[],
stageInterval: number
) {
for (const ratio of stages) {
this.setTrafficRatio(target, ratio);
await sleep(stageInterval);
const health = await this.checkRecentHealth(target);
if (!health.acceptable) {
// 回切失败,继续用 backup
this.setTrafficRatio(this.activeProvider, 1.0);
return;
}
}
// 全量回切成功
this.activeProvider = this.primaryProvider;
}
}
切换策略的几个关键判断:
- 切换前探测 backup:backup 可能也有问题,切过去更糟。必须先确认 backup 健康。
- Canary 回切而不是直接全切:主 Provider 恢复后不要立刻全量回切,分阶段验证。
- 设置切换冷却期:防止主 Provider 一会好一会坏时引发的震荡。
Layer 5:成本-可用性权衡预算
Backup Provider 通常成本不同。需要预设一个 Cost Budget,当切换成本超过预算时,执行更激进的降级而不是继续用 backup。
class CostAwareSwitchPolicy {
private budget = {
max_extra_cost_per_hour_cny: 300, // 每小时最多多花 ¥300
current_extra_cost_cny: 0,
fallback_to_degraded_at: 0.8 // 80% 预算消耗后改为降级
};
shouldSwitch(): boolean {
const budgetUsed = this.budget.current_extra_cost_cny /
this.budget.max_extra_cost_per_hour_cny;
if (budgetUsed > this.budget.fallback_to_degraded_at) {
// 成本超阈值,改为降级模式(队列/缓存/拒绝)而不是 Provider 切换
console.warn(`Cost budget ${(budgetUsed * 100).toFixed(0)}% consumed, switching to degraded mode`);
return false;
}
return true;
}
}
成本对比示例(国内大模型):
| Primary 模型 | Backup 模型 | 每 1M token 价差 | 策略 |
|---|---|---|---|
| Qwen-Max | Qwen-Plus | ¥8 | 同厂商降级,低风险 |
| DeepSeek-V3 | Qwen-Max | ¥4 | 跨厂商切换,需验证兼容性 |
| 专有模型 | DeepSeek-V3 | 视情况 | 最后备选 |
原则:优先选同 API 格式的 backup(如都兼容 OpenAI SDK),切换时不需要修改代码,只改 base_url 和 model name。
完整系统的状态流转
把 5 层合在一起,一次典型的 Provider 故障处理流程:
t=0s 探测发现 P95 latency = 4.2s(基线 mean=1.1s, σ=0.3s,超过 mean+2σ)
→ status: NORMAL → WARNING (warningCount=1)
t=30s 再次异常(latency=5.1s)
→ status: WARNING (warningCount=2)
t=60s 再次异常(latency=6.8s)
→ status: WARNING → DEGRADED (warningCount=3)
→ 触发 DEGRADED 告警,streaming_chat 切换到 fallback_model: Qwen-Turbo
t=90s 探测仍然异常,error_rate 升至 8%(持续超 60s)
→ status: DEGRADED → CRITICAL
→ 触发 CRITICAL 告警
t=95s 探测 backup provider (DeepSeek-V3),确认健康
→ 成本预算检查: OK(0% 消耗)
→ 执行 Provider 切换,activeProvider = DeepSeek-V3
t=125s 用户请求全部路由到 DeepSeek-V3,错误率恢复 0%
t=30min 尝试回切 primary
→ 连续 10 次探测 primary 全部正常
→ 开始 canary 回切: 5% 流量 → 正常 → 25% → 正常 → 100%
t=36min 回切完成,activeProvider = Qwen-Max
→ 记录事件:duration=36min,extra_cost=¥X,切换次数=1
整个过程用户无感知(或感知到短暂的"备用模式"提示),服务没有中断。
实际部署的几个坑
坑 1:探测 prompt 选错了
如果探测 prompt 太复杂(比如用了 tool calling),探测失败可能是因为工具定义问题,而不是 Provider 故障。探测 prompt 必须是最简单的文本输入输出。
坑 2:backup 和 primary 没有 feature parity
如果 Primary 用了 JSON mode 或特定的 function calling 格式,backup provider 必须也支持同样的接口,否则切换过去之后请求还是会失败。选 backup 之前,必须验证 API 兼容性。
坑 3:切换后忘记同步 context
对话型应用切换 Provider 后,历史对话 context 必须能在新 Provider 上重放。如果 context 里有特定格式的字段(比如 tool_calls 格式),需要做转换。
坑 4:cost budget 没有按时间窗口重置
IncidentCostBudget 应该按小时重置,而不是按事件重置。否则多次短时故障叠加后,budget 耗尽,后续切换全部走降级,反而在真正需要切换时失去能力。
坑 5:探测时间没有考虑业务高峰
在业务高峰期,正常 latency 本来就高,探测结果会触发误报。基线计算需要按时段分桶(高峰期单独的基线),而不是 24 小时统一基线。
与其他工程组件的关系
这套 SLA 工程体系和其他 LLM 工程组件有依赖关系:
| 依赖组件 | 作用 | 怎么配合 |
|---|---|---|
| Circuit Breaker | 请求级别的快速失败 | SLA 监控提供 Circuit Breaker 的开关信号 |
| Feature Flag | 降级行为的动态控制 | DegradationContract 通过 Feature Flag 热更新 |
| Token Budget | 成本控制 | Cost Budget 集成到 Token Budget 的成本核算中 |
| Canary Deploy | 流量灰度 | Provider 回切复用 Canary 流量比例逻辑 |
| Observability | 全链路 Trace | SLA 事件必须有 Trace ID 关联到用户请求 |
总结:分层建设,从 Layer 1 开始
先建 Layer 1,其他的可以慢慢来。如果你现在还在靠 Provider 状态页发现故障,每次故障比实际情况滞后 15-30 分钟才感知,建立主动探测是投入产出比最高的一步。
| 层级 | 投入 | 核心收益 |
|---|---|---|
| Layer 1: 探测 | 1 天 | 把故障发现时间从 15-30 分钟缩短到 90 秒 |
| Layer 2: 异常检测 | 1 天 | 把人工判断变成自动状态机 |
| Layer 3: 降级契约 | 2-3 天 | 每个功能有明确的降级行为,不用临时决策 |
| Layer 4: Provider 切换 | 3-5 天 | Provider 故障对用户无感知 |
| Layer 5: 成本预算 | 1 天 | 切换成本可控,不会破坏月度预算 |
5 层全部到位,你的系统就有能力把第三方 Provider 的不稳定性完全吸收在自己的边界之内,用户看到的永远是你的 SLA,而不是上游服务的状态页面。
本文代码示例为 TypeScript,展示核心逻辑。生产实现中可参考 LiteLLM router 的 provider health check 实现,结合国内大模型 API 网关(如 Sub2API、各云厂商统一网关)做适配。