模型网关与多模型路由

当业务从调用单一模型演进到同时使用十余个模型时,散落在各服务里的 API Key、限流与重试逻辑会迅速失控。本文以 LiteLLM Proxy 为核心给出模型网关落地方案:统一 OpenAI 兼容接口、密钥托管、限流配额与审计,详解成本优先、延迟优先、质量优先、加权路由与 fallback 熔断五类策略,并给出语义路由与 token 预算的可运行代码,最后量化路由带来的成本与延迟收益并总结常见坑。

在 2023 年,一个应用通常只调用一个模型;到 2026 年,一个稍具规模的产品会同时使用 6 到 15 个模型:日常对话用便宜的小模型,复杂推理切到旗舰模型,图片理解走多模态模型,代码补全走专门的代码模型,再加上嵌入模型与重排模型。模型数量一多,密钥管理、限流、配额、审计、可观测与故障转移这些横切关注点就会在每一个业务服务里重复实现一遍,最终演变成一场灾难。

模型网关(Model Gateway)要解决的就是这个问题:把「如何选模型、如何调用模型、如何兜底」从业务代码里抽出来,收敛到一个统一入口。本文以 LiteLLM Proxy 为主线,给出从部署到路由策略再到收益量化的完整工程方案。

一、为什么需要模型网关

1.1 从散落调用到统一入口

没有网关时,典型代码是这样的:业务服务直接 requests.post() 到 OpenAI,另一处又直接调用 Anthropic,密钥硬编码在环境变量里,重试逻辑各写各的,某个供应商挂了只能等用户投诉。这种模式的成本随模型数量线性增长,而风险则是指数增长。

网关把这一切收敛到一层,业务侧只面向一个 OpenAI 兼容端点:

from openai import OpenAI

client = OpenAI(
    base_url="http://llm-gateway.internal:4000/v1",
    api_key="sk-team-a-virtual-key",   # 网关签发的虚拟 key,不是真实供应商 key
)

resp = client.chat.completions.create(
    model="chat-default",              # 逻辑别名,由网关解析为真实模型
    messages=[{"role": "user", "content": "用一句话解释什么是 RAG"}],
)
print(resp.choices[0].message.content)

业务代码里没有出现任何真实供应商的密钥、base_url 或重试逻辑,全部下沉到网关。这一层抽象是后面所有能力(路由、限流、配额、审计)的地基。

1.2 网关的核心能力矩阵

一个生产级模型网关至少要提供下表的能力。缺失任何一项,都会在规模上来之后变成痛点。

能力解决的问题没有它的后果LiteLLM 支持
统一接口各供应商 SDK 与参数不一致业务代码充满 if-else 分支OpenAI 兼容 /v1/chat/completions
密钥托管真实 key 散落各处泄露风险、轮换困难model_list 集中配置 + 虚拟 key
限流供应商侧 RPM/TPM 被打爆429 雪崩、级联失败rpm / tpm 逐模型限流
配额团队/租户超预算账单失控、无法归因虚拟 key 绑定 max_budget
审计谁在什么时候调了什么无法追责、无法合规请求日志 + 回调
可观测延迟/错误/成本盲区故障排查靠猜Prometheus / Langfuse / OTel
路由与兜底单点供应商故障全站不可用fallbacks + Router 策略
缓存重复请求浪费钱成本虚高Redis 语义缓存

1.3 网关与推理引擎的分工

需要澄清一个常见混淆:网关不等于推理引擎。网关负责「选哪个模型、怎么调用、怎么兜底」,而推理引擎(vLLM、SGLang、TensorRT-LLM)负责「单个模型怎么跑得快」。自托管模型时,网关把请求转发到推理引擎的 OpenAI 兼容端点;调用商业 API 时,网关直接转发到供应商。两者是上下游关系,详见 推理引擎对比 。

网关在高并发下必须做到几乎无状态、低开销:一次路由决策的 CPU 开销应控制在 1 毫秒以内,否则它自己就会成为 P99 延迟的瓶颈。

二、LiteLLM Proxy 落地

2.1 安装与启动

LiteLLM 是当前最成熟的开源网关实现,Proxy 模式提供完整的 OpenAI 兼容服务端。生产部署通常用 Docker Compose 或 Kubernetes:

export LITELLM_MASTER_KEY="sk-master-please-rotate"
export DATABASE_URL="postgresql://litellm:pass@localhost:5432/litellm"

pip install "litellm[proxy]==1.74.0"

litellm --config ./config.yaml --port 4000 --num_workers 4

--num_workers 4 让网关以多进程模式运行,单进程大约能扛 800 QPS 的纯转发(不含上游延迟),4 进程配合 4 核可稳定支撑 2500 QPS 左右。若需要横向扩展,多副本部署加一个共享的 Postgres 与 Redis 即可,网关本身无状态。这套部署形态在 Kubernetes 上做服务化 时尤为自然。

2.2 config.yaml 逐段解析

下面是一份可直接运行的生产级配置,覆盖了模型清单、路由策略、限流与回退:

model_list:
  # ---- 旗舰模型:质量优先场景 ----
  - model_name: chat-premium            # 对外暴露的逻辑名
    litellm_params:
      model: openai/gpt-4.1             # 真实模型
      api_key: os.environ/OPENAI_API_KEY
      rpm: 5000
      timeout: 60
    model_info:
      input_cost_per_token: 0.000002    # $2 / 1M tokens
      output_cost_per_token: 0.000008

  # ---- 主力模型:性价比之选 ----
  - model_name: chat-default
    litellm_params:
      model: openai/gpt-4.1-mini
      api_key: os.environ/OPENAI_API_KEY
      rpm: 10000
      timeout: 30
    model_info:
      input_cost_per_token: 0.0000004   # $0.40 / 1M tokens
      output_cost_per_token: 0.0000016

  # ---- 自托管模型:走本地推理引擎 ----
  - model_name: chat-selfhosted
    litellm_params:
      model: openai/qwen3-32b
      api_base: http://vllm-inference.svc:8000/v1
      api_key: os.environ/VLLM_KEY
      rpm: 2000

  # ---- 备用供应商:故障转移目标 ----
  - model_name: chat-backup
    litellm_params:
      model: anthropic/claude-sonnet-4-6
      api_key: os.environ/ANTHROPIC_API_KEY
      rpm: 3000

router_settings:
  routing_strategy: cost-based-routing   # 成本优先,见第三节
  num_retries: 2
  retry_after: 1
  allowed_fails: 3                       # 连续失败 3 次进入冷却
  cooldown_time: 30                      # 冷却 30 秒
  timeout: 30
  fallbacks:
    - chat-premium: ["chat-backup", "chat-default"]   # 旗舰挂了降级
    - chat-default: ["chat-backup"]
  context_window_fallbacks:
    - chat-default: ["chat-premium"]     # 上下文超限时切到大窗口模型
  redis_host: os.environ/REDIS_HOST
  redis_port: 6379

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  database_url: os.environ/DATABASE_URL
  # 预算与配额
  max_budget: 5000                       # 全局月度预算(美元)
  budget_duration: 30d

litellm_settings:
  drop_params: true                      # 自动丢弃上游不支持的参数
  cache: true
  cache_params:
    type: redis
    ttl: 3600
  request_timeout: 30
  set_verbose: false

2.3 模型名映射与别名

映射关系必须做到「业务侧看到的逻辑名」与「真实模型名」严格解耦。下表给出一个典型的三层命名约定:

逻辑别名真实模型定位输入价 $/1M输出价 $/1M
chat-premiumgpt-4.1复杂推理、长文2.008.00
chat-defaultgpt-4.1-mini通用对话0.401.60
chat-selfhostedqwen3-32b (vLLM)高并发、可控成本自托管算力自托管算力
chat-backupclaude-sonnet-4-6故障转移3.0015.00
embed-defaulttext-embedding-3-large向量化0.130
rerank-defaultbge-reranker-v2-m3重排自托管自托管

命名约定的价值在于:某天 gpt-4.1-mini 涨价或下线,只需要改 config.yaml 里的一行,业务代码零改动。这是网关相对于「硬编码模型名」最直接的收益。

三、路由策略

LiteLLM 的 Router 内置了多种策略。选择哪一种,取决于当前业务的首要目标:省钱、省时间还是保质量。

3.1 成本优先

cost-based-routing 会在候选模型里优先选择单位 token 成本最低者。适合大规模、质量容忍度高的场景,如客服摘要、分类打标。代价是可能把复杂请求也丢给弱模型,导致质量回退,因此通常配合语义路由或质量门禁使用。

3.2 延迟优先

latency-based-routing 依据历史响应延迟(默认滑动窗口)选择当前最快的部署。适合交互式场景,如输入联想、实时助手。注意它只看延迟不看质量,因此更适合在同一档位的多个等价部署之间做负载均衡,而不是跨档位选择。

3.3 质量优先与加权路由

当质量是硬约束时,用 simple-shuffle 加权或 least-busy 在多个高质量部署间分配。加权路由适合灰度:新模型给 10% 流量,观察一周再逐步放量。灰度期间必须同时观察质量与成本两类指标,确认无回退后再逐步加大权重。

下表总结了各策略的适用面:

策略决策依据适用场景主要风险
cost-based-routing单位 token 价格批量、低成本容忍质量回退
latency-based-routing历史延迟实时交互忽略质量差异
usage-based-routing-v2TPM/RPM 余量多部署负载均衡与成本无关
least-busy当前并发抖动大的自托管集群需要准确并发计数
simple-shuffle (权重)固定权重灰度、A/B需人工调参
语义路由请求语义分类混合负载分类器本身有开销

3.4 故障转移、重试与熔断

兜底是网关最重要的价值。fallbacks 定义降级链,num_retries 定义重试,allowed_fails + cooldown_time 实现熔断:某部署连续失败 3 次后被摘除 30 秒,期间流量走备用。

下面是一个在代码侧显式控制路由与回退的完整示例:

import os
from litellm import Router

router = Router(
    model_list=[
        {
            "model_name": "chat-default",
            "litellm_params": {
                "model": "openai/gpt-4.1-mini",
                "api_key": os.environ["OPENAI_API_KEY"],
                "rpm": 10000,
            },
        },
        {
            "model_name": "chat-backup",
            "litellm_params": {
                "model": "anthropic/claude-sonnet-4-6",
                "api_key": os.environ["ANTHROPIC_API_KEY"],
                "rpm": 3000,
            },
        },
    ],
    routing_strategy="cost-based-routing",
    num_retries=2,
    allowed_fails=3,
    cooldown_time=30,
    fallbacks=[{"chat-default": ["chat-backup"]}],
    redis_host=os.environ.get("REDIS_HOST", "localhost"),
)

def ask(prompt: str) -> str:
    resp = router.completion(
        model="chat-default",
        messages=[{"role": "user", "content": prompt}],
        timeout=30,
    )
    # 通过响应头/元数据判断实际命中了哪个部署
    served = resp._hidden_params.get("model_id", "unknown")
    print(f"[route] served by {served}")
    return resp.choices[0].message.content

if __name__ == "__main__":
    print(ask("把下面这段话压缩成一句话:……"))

_hidden_params 里记录了实际命中的部署与重试次数,是排查路由行为的关键字段,务必打点到日志或追踪系统里。

四、语义路由

规则路由(按成本、按延迟)解决不了「同一个接口既要处理简单问答又要处理复杂推理」的问题。语义路由用一个小而快的分类器或嵌入模型判断请求的难度与类型,再决定交给哪个档位的模型。

4.1 原理

做法通常有两种:一是用一个轻量分类器(如 100M 级别的模型)把请求分为 simple / complex 两类;二是把请求向量化后与预先定义的意图原型做最近邻匹配。分类器本身的延迟必须远小于它节省的模型延迟,否则得不偿失。

4.2 可运行实现

下面用嵌入相似度做一个极简的语义路由,把「翻译、改写」这类简单任务导向便宜模型,把「推理、证明」导向旗舰模型:

import numpy as np
from openai import OpenAI

client = OpenAI(base_url="http://llm-gateway.internal:4000/v1",
                api_key="sk-team-a-virtual-key")

PROTOTYPES = {
    "chat-default": ["把这句话翻译成英文", "帮我改写这段文字", "提取关键词"],
    "chat-premium": ["证明这个定理", "分析这段代码的并发缺陷", "设计一个分布式方案"],
}

def embed(texts: list[str]) -> np.ndarray:
    r = client.embeddings.create(model="embed-default", input=texts)
    return np.array([d.embedding for d in r.data], dtype=np.float32)

def route(query: str) -> str:
    proto_vecs = {tier: embed(samples) for tier, samples in PROTOTYPES.items()}
    q = embed([query])[0]
    q = q / (np.linalg.norm(q) + 1e-9)
    best, best_score = "chat-default", -1.0
    for tier, vecs in proto_vecs.items():
        sims = vecs @ q / (np.linalg.norm(vecs, axis=1) + 1e-9)
        score = float(sims.max())
        if score > best_score:
            best, best_score = tier, score
    return best

if __name__ == "__main__":
    for q in ["帮我把这段话翻译成日语", "证明 P 是否等于 NP 的思路"]:
        print(q, "->", route(q))

生产环境不要把原型向量每次重算,应离线构建并缓存在内存或 Redis 中。语义路由的分类准确率若低于 90%,带来的成本收益会被误判成本(简单请求被送到旗舰模型)抵消掉,上线前务必用真实流量做一次离线评测。

五、Token 计数与预算

5.1 精确计数

成本控制的前提是精确计数。不同模型的 tokenizer 不同,用错 tokenizer 会导致预算偏差 10% 到 30%。LiteLLM 提供 token_counter 按模型精确计数:

import litellm

def count_cost(model: str, messages: list[dict], max_output: int = 512) -> float:
    in_tokens = litellm.token_counter(model=model, messages=messages)
    # 按最坏情况预留输出 token,避免预算被击穿
    in_cost = in_tokens * litellm.model_cost[model]["input_cost_per_token"]
    out_cost = max_output * litellm.model_cost[model]["output_cost_per_token"]
    return in_cost + out_cost

msgs = [{"role": "user", "content": "用 200 字总结这篇文章"}]
print(f"预估单次成本: ${count_cost('gpt-4.1-mini', msgs):.6f}")

5.2 预算控制

预算是网关的硬约束。为每个团队签发虚拟 key 并绑定 max_budget,超预算直接拒绝而不是等到月底看账单:

curl -s -X POST http://llm-gateway.internal:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "team_id": "team-a",
    "max_budget": 200,
    "budget_duration": "30d",
    "rpm_limit": 600,
    "models": ["chat-default", "embed-default"]
  }'

关键设计是「按最坏情况预留」:在请求发出前就用 max_tokens 估算上限并扣减配额,请求结束后再按实际用量回补。这样即使出现异常长的输出,也不会击穿预算。预算与成本归因的完整体系见 LLMOps 成本治理 。

六、可观测与审计

网关是唯一能看到全部模型流量的地方,因此也是可观测的最佳埋点位置。若不做埋点,路由策略的好坏、成本的去向、故障的根因都无从判断。一个成熟网关至少应暴露三类信号:路由决策、成本归因、健康状态。

6.1 必须打点的路由元数据

每次请求结束后,应把下列字段写入日志或追踪系统。它们是后续所有分析与调优的原始数据:

字段含义典型用途
logical_model业务侧请求的逻辑名统计别名命中分布
served_deployment实际命中的部署排查路由是否符合预期
retry_count重试次数识别不稳定供应商
fallback_triggered是否发生降级监控主模型健康度
prompt_tokens输入 token 数成本归因、预算预警
completion_tokens输出 token 数成本归因
cost_usd本次请求成本按团队/模型聚合
cache_hit是否命中缓存评估缓存收益
ttft_ms首 token 延迟交互体验核心指标
total_ms端到端延迟路由策略效果评估

6.2 通过回调接入追踪

LiteLLM 的 callback 机制可以在不侵入业务代码的前提下完成打点。下面把每次调用写进结构化日志,并推送到 OpenTelemetry 兼容的后端:

import json
import logging
import time
from litellm.integrations.custom_logger import CustomLogger

logger = logging.getLogger("llm_gateway_audit")
logger.setLevel(logging.INFO)

class AuditLogger(CustomLogger):
    def log_success_event(self, kwargs, response_obj, start_time, end_time):
        meta = kwargs.get("litellm_params", {}).get("metadata", {})
        hidden = getattr(response_obj, "_hidden_params", {}) or {}
        record = {
            "ts": time.time(),
            "team": meta.get("user_api_key_team_id", "unknown"),
            "logical_model": kwargs.get("model"),
            "served_deployment": hidden.get("model_id"),
            "retry_count": hidden.get("retries", 0),
            "cost_usd": kwargs.get("response_cost", 0.0),
            "total_ms": (end_time - start_time).total_seconds() * 1000,
            "status": "success",
        }
        logger.info(json.dumps(record, ensure_ascii=False))

    def log_failure_event(self, kwargs, response_obj, start_time, end_time):
        record = {
            "ts": time.time(),
            "logical_model": kwargs.get("model"),
            "error": str(kwargs.get("exception")),
            "total_ms": (end_time - start_time).total_seconds() * 1000,
            "status": "failure",
        }
        logger.warning(json.dumps(record, ensure_ascii=False))

audit = AuditLogger()

把 audit 注册到 litellm_settings.callbacks 后,成功与失败两条路径都会被记录。注意失败路径同样重要:只记录成功请求,会系统性低估错误率。

6.3 健康度告警阈值

打点之后需要阈值才能形成闭环。下表给出一组经过验证的告警线,可作为起步参考:

指标正常区间告警阈值处置建议
fallback 触发率< 1%> 5%检查主模型可用性
平均重试次数< 0.1> 0.5排查上游限流
缓存命中率20% - 40%< 10%检查缓存键设计
语义路由误判率< 8%> 15%重新训练分类器
单请求 P99 延迟< 3 s> 8 s检查超时与重试配置
日成本环比±10%+30%排查异常调用方

审计不只是运维需求,也是合规需求:谁在什么时候调用了哪个模型、输入了什么、花费多少,这些记录在受监管行业是硬性要求。把审计日志与业务请求 ID 关联,才能在出现问题时做到端到端追溯。

七、收益量化

下表来自一个真实迁移案例:某 SaaS 产品有 12 个业务服务各自直连 3 家供应商,迁移到统一网关后的前后对比。

指标迁移前迁移后变化
月均模型支出$18,400$6,900-62.5%
P99 端到端延迟3.2 s1.4 s-56.3%
供应商故障导致的全站不可用3 次/月0 次/月消除
密钥轮换工作量12 处代码1 处配置-92%
请求级成本归因无按团队/模型从无到有
缓存命中率0%31%新增

成本下降主要来自三块:语义路由把 68% 的请求从旗舰模型切到主力模型(贡献约 40% 节省)、语义缓存命中省掉 31% 的重复调用(贡献约 15%)、以及故障转移避免了重试风暴带来的重复计费。延迟下降主要来自延迟路由与缓存。

需要强调:这些收益不是「上了网关」自动获得的,而是路由策略与缓存配置得当的结果。只做统一接口而不配路由,成本几乎不会变。

八、常见坑清单

  • streaming 中断后 fallback 静默接管:流式响应一旦开始吐出第一个 token,就无法再回退到别的模型,否则用户会看到两段拼接的文本。正确做法是只在「首个 token 之前」允许 fallback,首 token 之后失败就报错。
  • fallback 掩盖真实错误:备用模型成功返回后,主模型的失败会被吞掉。必须把 _hidden_params 里的重试次数与命中部署打点到监控,否则主模型持续劣化也无人察觉。
  • 模型名映射不一致:config.yaml 里的逻辑名与业务代码里的 model= 参数拼写不一致时,网关会直接返回 400。建议用常量集中管理逻辑名,并在 CI 里校验所有引用都存在。
  • 限流参数照抄:rpm / tpm 必须与供应商账号的实际配额对齐,设得过高等于没有限流,设得过低会误伤正常流量。
  • token 计数用错 tokenizer:跨模型估算成本时用了默认 tokenizer,偏差可达 30%,导致预算严重失真。
  • 语义路由分类器过重:分类器本身若耗时 200 毫秒,而它节省的模型延迟只有 100 毫秒,就是负优化。
  • 冷却时间与重试次数相乘放大延迟:num_retries=3 配 cooldown_time=30,最坏情况下用户要等 90 秒以上,必须给整个调用链设置总超时。
  • 缓存键未包含模型与参数:不同模型、不同 temperature 的请求若共用一个缓存键,会返回错误结果。缓存键必须包含模型名、messages、temperature 等全部影响输出的参数。

小结

模型网关是 LLMOps 从「能跑」走向「可运营」的第一块基石。它把密钥、限流、配额、审计、可观测与故障转移这些横切能力从业务代码中抽出,收敛到一层 OpenAI 兼容入口。LiteLLM Proxy 用一份 config.yaml 就能覆盖模型清单、路由策略与回退链,配合虚拟 key 与预算实现细粒度成本控制。路由策略的选择取决于业务目标:成本优先省钱、延迟优先保体验、质量优先保正确性、语义路由在混合负载下兼顾三者。一个配置得当的网关可以带来六成以上的成本下降与过半的 P99 延迟改善,但前提是路由与缓存真正调优过,而不是仅仅做了接口统一。落地时务必盯紧 streaming 中断、fallback 掩盖错误与模型名映射不一致这三类高频坑。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「LLMOps」更多文章

  1. 语义缓存与 Prompt 缓存
  2. 结构化输出与函数调用
  3. 多智能体编排与工作流引擎