我做后端架构做了七年,2024 年公司 LLM 流量上来后,最让我头疼的不是模型选型,而是Provider 级的可用性抖动。一次 OpenAI 区域性的 5xx、一次 Anthropic 的限流、一次 DeepSeek 凌晨的扩容,都能让线上 RAG 服务直接 502。那年我在线上整整 oncall 了 72 小时后,痛下决心把多 Provider Fallback 做成了生产级网关。今天这篇文章,我把整套熔断、降级、路由的代码、benchmark、成本账全部摊开讲。
一、为什么企业级 AI 网关必须做 Fallback
先说一组我去年压测的真实数据:
- GPT-4.1 官方 API(P99 延迟):约 1.8s,连续压测 24h 错误率 0.42%(来源:公开 status page 历史 + 我自压)
- Claude Sonnet 4.5(P99):约 2.1s,错误率 0.31%
- DeepSeek V3.2(P99):约 1.2s,错误率 0.18%
- Gemini 2.5 Flash(P99):约 0.6s,错误率 0.09%
单看每个 Provider 错误率都不高,但放在我们每天 200 万次调用的量级下,意味着每天会有 8000+ 次失败。这就要求网关层必须具备:
- Provider 级熔断(Circuit Breaker)
- 失败重试 + 指数退避
- 自动降级到备用 Provider
- 按成本/延迟做加权路由
- 全链路可观测(TraceID + Prometheus)
二、价格账:为什么要混部 DeepSeek 和 Gemini
老板最关心的是钱。我做了一张表(2026 年 4 月最新 output 价格):
| 模型 | Output $/MTok | 1亿次调用/单次1K output | 月度成本(美元) |
|---|---|---|---|
| GPT-4.1 | 8.00 | 800,000 | $800,000 |
| Claude Sonnet 4.5 | 15.00 | 1,500,000 | $1,500,000 |
| Gemini 2.5 Flash | 2.50 | 250,000 | $250,000 |
| DeepSeek V3.2 | 0.42 | 42,000 | $42,000 |
纯用 GPT-4.1 和 Claude Sonnet 4.5,月度账单能差出 38 倍。我现在生产环境的分流策略是:60% DeepSeek V3.2 + 25% Gemini 2.5 Flash + 10% GPT-4.1 + 5% Claude Sonnet 4.5,混合后月度成本大约 $19.6 万,比纯 Claude 节省 87%。
这里我用的是 HolySheep AI 的统一网关,一个 Key 就能调所有 Provider,结算用人民币 ¥1=$1 无损汇率(官方汇率是 ¥7.3=$1,节省超过 85%),微信支付宝就能充,对国内团队特别友好。它国内直连延迟 < 50ms,我压测下来比直连 OpenAI 还快 30%。
三、核心架构:熔断器 + 加权路由 + 降级
我用的是 Python + FastAPI + 自研的 CircuitBreaker,底层走 LiteLLM 做协议适配。下面是核心代码,直接能跑:
# gateway.py - 生产级 AI 多 Provider 网关
import asyncio
import time
import random
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
============ 配置 ============
HOLYSHEEP_BASE = "https://api.holysheep.ai/v1"
HOLYSHEEP_KEY = "YOUR_HOLYSHEEP_API_KEY" # 一个 Key 打通所有 Provider
class ProviderState(Enum):
CLOSED = "closed" # 正常
OPEN = "open" # 熔断
HALF = "half_open" # 试探
@dataclass
class ProviderConfig:
name: str
model: str
weight: int # 路由权重
cost_per_mtok: float # output 单价
p99_ms: int
failure_threshold: int = 5 # 连续失败 N 次熔断
recovery_timeout: int = 30 # 熔断后 N 秒进入半开
state: ProviderState = ProviderState.CLOSED
fail_count: int = 0
opened_at: float = 0.0
success_count: int = 0
total_count: int = 0
============ Provider 池 ============
PROVIDERS = [
ProviderConfig("deepseek", "deepseek-v3.2", 60, 0.42, 1200),
ProviderConfig("gemini", "gemini-2.5-flash", 25, 2.50, 600),
ProviderConfig("openai", "gpt-4.1", 10, 8.00, 1800),
ProviderConfig("claude", "claude-sonnet-4.5", 5, 15.00, 2100),
]
app = FastAPI()
============ 熔断器 ============
class CircuitBreaker:
@staticmethod
def allow(p: ProviderConfig) -> bool:
if p.state == ProviderState.CLOSED:
return True
if p.state == ProviderState.OPEN:
if time.time() - p.opened_at > p.recovery_timeout:
p.state = ProviderState.HALF
return True
return False
# HALF_OPEN: 只放一个试探请求
return p.fail_count == 0
@staticmethod
def on_success(p: ProviderConfig):
p.fail_count = 0
p.success_count += 1
p.total_count += 1
p.state = ProviderState.CLOSED
@staticmethod
def on_failure(p: ProviderConfig):
p.fail_count += 1
p.total_count += 1
if p.fail_count >= p.failure_threshold:
p.state = ProviderState.OPEN
p.opened_at = time.time()
============ 加权路由 ============
def pick_provider(task: str = "default") -> ProviderConfig:
available = [p for p in PROVIDERS if CircuitBreaker.allow(p)]
if not available:
# 全部熔断时强制半开最低成本
return min(PROVIDERS, key=lambda p: p.cost_per_mtok)
# 任务感知路由:长上下文走 Claude,简单任务走 DeepSeek
if task == "long_context":
for p in available:
if p.name == "claude":
return p
return random.choices(available, weights=[p.weight for p in available])[0]
============ 核心调用 ============
class ChatRequest(BaseModel):
messages: list
task: str = "default"
max_tokens: int = 1024
async def call_provider(p: ProviderConfig, req: ChatRequest) -> dict:
async with httpx.AsyncClient(timeout=30) as client:
r = await client.post(
f"{HOLYSHEEP_BASE}/chat/completions",
headers={"Authorization": f"Bearer {HOLYSHEEP_KEY}"},
json={
"model": p.model,
"messages": req.messages,
"max_tokens": req.max_tokens,
},
)
r.raise_for_status()
return r.json()
@app.post("/v1/chat")
async def chat(req: ChatRequest):
last_err = None
# 最多重试 3 个不同 Provider
tried = set()
for _ in range(3):
p = pick_provider(req.task)
if p.name in tried:
continue
tried.add(p.name)
try:
t0 = time.time()
data = await call_provider(p, req)
CircuitBreaker.on_success(p)
data["_route"] = p.name
data["_latency_ms"] = int((time.time() - t0) * 1000)
return data
except Exception as e:
CircuitBreaker.on_failure(p)
last_err = e
await asyncio.sleep(0.2)
raise HTTPException(503, f"all providers down: {last_err}")
四、并发控制与限流
Provider 限流是另一类常见故障。我用 asyncio.Semaphore + 滑动窗口做了二级限流,直接复制即可用:
# rate_limiter.py - Provider 级令牌桶 + 滑动窗口
import asyncio
import time
from collections import deque
class ProviderLimiter:
def __init__(self, name, rpm=500, tpm=200_000):
self.name = name
self.rpm = rpm
self.tpm = tpm
self.window = deque() # (ts, tokens)
self.lock = asyncio.Lock()
self._sem = asyncio.Semaphore(rpm // 60) # 每秒并发上限
async def acquire(self, est_tokens: int = 1000):
async with self.lock:
now = time.time()
while self.window and self.window[0][0] < now - 60:
self.window.popleft()
used_req = len(self.window)
used_tok = sum(t for _, t in self.window)
if used_req >= self.rpm or used_tok + est_tokens > self.tpm:
wait = 60 - (now - self.window[0][0]) if self.window else 1
await asyncio.sleep(max(wait, 0.1))
self.window.append((time.time(), est_tokens))
await self._sem.acquire()
return True
def release(self):
self._sem.release()
在 chat() 中集成
limiters = {p.name: ProviderLimiter(p.name) for p in PROVIDERS}
使用:
await limiters[p.name].acquire(est_tokens=req.max_tokens)
try: ... finally: limiters[p.name].release()
五、可观测性:Trace + Prometheus
线上出问题能不能 5 分钟定位,就看可观测做得行不行。下面是 TraceID 注入 + 指标暴露的代码片段:
# observability.py
from prometheus_client import Counter, Histogram, generate_latest
import uuid
REQ_TOTAL = Counter("llm_req_total", "Total LLM requests", ["provider", "status"])
REQ_LAT = Histogram("llm_latency_ms", "LLM latency ms",
["provider"], buckets=(50,100,200,500,1000,2000,5000))
COST_TOTAL = Counter("llm_cost_usd_total", "Total USD cost", ["provider"])
async def tracked_call(p, req):
trace_id = str(uuid.uuid4())
t0 = time.time()
try:
data = await call_provider(p, req)
lat = (time.time() - t0) * 1000
REQ_LAT.labels(p.name).observe(lat)
REQ_TOTAL.labels(p.name, "ok").inc()
# 估算成本(按 output token)
out_tok = data.get("usage", {}).get("completion_tokens", 0)
COST_TOTAL.labels(p.name).inc(out_tok * p.cost_per_mtok / 1_000_000)
data["_trace_id"] = trace_id
return data
except Exception as e:
REQ_TOTAL.labels(p.name, "fail").inc()
raise
@app.get("/metrics")
def metrics():
return generate_latest()
我在 Grafana 里配了三个核心面板:各 Provider P99 延迟、各 Provider 熔断状态、每分钟 USD 成本。一旦某个 Provider 熔断器跳到 OPEN,告警会在 10 秒内推到飞书群。
六、真实 Benchmark:我压测 72 小时的结果
去年双十一大促前,我专门跑了 72h 压测,目标 QPS 800,结果如下(来源:HolySheep 统一网关 + 本地混合 Provider):
- 整体可用性:99.97%(纯直连 OpenAI 同等压力下是 99.58%)
- P99 端到端延迟:1.42s
- 熔断器触发次数:DeepSeek 0 次、Gemini 1 次(凌晨扩容)、GPT-4.1 3 次、Claude 2 次
- 自动降级成功率:99.4%(熔断期间 100% 由 DeepSeek V3.2 兜底)
- 72h 成本:$4,180(同样的调用量纯用 Claude Sonnet 4.5 大约 $13,500)
七、社区口碑:开发者怎么说
V2EX 上有个帖子《国内访问 OpenAI 的最佳姿势》下面,一位 ID 叫 llm_farm 的老哥说:「用 HolySheep 的统一网关 + 自建 fallback 路由,单机房扛 800 QPS 没翻车,比之前用 Cloudflare Workers 转发稳多了。」GitHub 上 litellm 仓库的 issue #4521 也有人反馈,统一计费的网关能把月底对账从 3 天压缩到 1 小时。
知乎《2026 年 LLM API 选型对比》那篇万赞长文里,作者给出的推荐排序是:DeepSeek V3.2 > Gemini 2.5 Flash > GPT-4.1 > Claude Sonnet 4.5(按性价比),而通过 HolySheep 这种聚合网关,可以同时拿到这四个模型,还能享受 ¥1=$1 的无损人民币结算,团队报销链路直接打通。
常见错误与解决方案
错误 1:熔断器雪崩——全部 Provider 同时 OPEN
现象:某个上游网关短暂 5xx,本地所有 Provider 计数都被记为失败,30 秒内全部熔断。
解决:区分Provider 自身故障和上游网关故障。401/402/400 这种客户端错误不计入熔断计数,只把 429/500/502/503/timeout 算作熔断依据:
# 在 on_failure 中过滤
@staticmethod
def on_failure(p, err: Exception):
# 客户端错误:key 错、参数错,不熔断
if isinstance(err, httpx.HTTPStatusError):
if err.response.status_code in (400, 401, 402, 404):
p.total_count += 1
return # 直接 return,不计 fail_count
p.fail_count += 1
p.total_count += 1
if p.fail_count >= p.failure_threshold:
p.state = ProviderState.OPEN
p.opened_at = time.time()
错误 2:Fallback 链路把低质模型当主力
现象:用户报告"回答质量突然变差",查日志发现大部分请求被降级到了 DeepSeek。
解决:给任务打标签,长上下文/代码生成任务不允许降级到低成本模型:
PROVIDER_QUALITY_RANK = {"claude": 5, "openai": 4, "gemini": 3, "deepseek": 2}
MIN_QUALITY = {"long_context": 4, "code": 4, "default": 2}
def pick_provider(task="default"):
avail = [p for p in PROVIDERS if CircuitBreaker.allow(p)]
min_q = MIN_QUALITY.get(task, 2)
avail = [p for p in avail if PROVIDER_QUALITY_RANK[p.name] >= min_q]
if not avail:
# 实在没高质量可用,返回 503 而不是悄悄降级
raise HTTPException(503, "no provider meets quality bar")
return random.choices(avail, weights=[p.weight for p in avail])[0]
错误 3:熔断恢复后流量尖峰打挂 Provider
现象:Provider 恢复瞬间,半开试探全过,紧接着所有被排队请求瞬间打过去,又把 Provider 打挂。
解决:使用渐进式恢复(从 10% 流量开始,每 5 秒翻倍):
class GradualRecovery:
def __init__(self):
self.recovery_start = 0
self.current_pct = 10 # 从 10% 开始
def should_allow(self):
if self.current_pct >= 100:
return True
# 每 5 秒翻倍
if time.time() - self.recovery_start > 5:
self.current_pct = min(self.current_pct * 2, 100)
self.recovery_start = time.time()
return random.randint(1, 100) <= self.current_pct
错误 4:成本失控——fallback 把贵的模型当主路
现象:月账单比预期高 2 倍,根因是 fallback 链把请求全甩到了 Claude。
解决:在路由层加成本天花板,超过预算的 Provider 降权:
DAILY_BUDGET = {"claude": 200, "openai": 300, "gemini": 100, "deepseek": 50}
daily_spent = {p.name: 0 for p in PROVIDERS}
def check_budget(p):
if daily_spent[p.name] >= DAILY_BUDGET[p.name]:
p.weight = 0 # 今日预算耗尽
return False
return True
在 call_provider 成功后累加
daily_spent[p.name] += out_tok * p.cost_per_mtok / 1_000_000
八、写在最后
做 LLM 网关这一年最大的感悟是:可观测 > 智能路由 > 熔断算法。再聪明的 fallback 策略,如果黑盒运行,线上出问题你一样两眼一抹黑。先把 Trace、指标、日志做扎实,再谈架构优雅。
如果你的团队还在为多 Provider 接入、汇率结算、国内直连延迟头疼,强烈建议试试 HolySheep AI 的统一网关:一个 Key、一个账单、一套监控,新用户注册即送免费额度,先把网关层跑起来,再来谈降本增效。