LLM Provider SLA 工程实践:把第三方模型服务的不稳定性挡在你的系统边界之外

0 阅读11分钟

关键词: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;
  }
}

切换策略的几个关键判断:

  1. 切换前探测 backup:backup 可能也有问题,切过去更糟。必须先确认 backup 健康。
  2. Canary 回切而不是直接全切:主 Provider 恢复后不要立刻全量回切,分阶段验证。
  3. 设置切换冷却期:防止主 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-MaxQwen-Plus¥8同厂商降级,低风险
DeepSeek-V3Qwen-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全链路 TraceSLA 事件必须有 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、各云厂商统一网关)做适配。