数据截至 (上游 commit 3f15dc32871c)
Router:模型组、负载均衡、冷却与回退
30 秒导读: 前面四章讲的是「怎么把一次调用打到一个模型上」。这一章讲的是上面那一层:你有 4 把 Azure GPT-4o 的 key、2 个 OpenAI 账号,想让它们对外只是一个名字
gpt-4o,还想要负载均衡、限流退避、坏节点自动摘除、模型挂了自动换一家。Router就是干这个的——它是 litellm 从「SDK」变成「能上生产」的那道分水岭。
1. 这是什么(零基础也能懂)
一句话定义: Router 是一个调度层——你给它一张「同一个对外模型名下挂了哪几份真实部署」的表,它替你决定每次请求打给哪一份,并在失败时自动重试或换一家。
它解决什么问题。 假设你在做一个内部 AI 网关:
- 单个 Azure 部署有 TPM/RPM 限额,一份不够用,你开了 4 份 → 需要负载均衡。
- 其中一份突然 429/500 了 → 需要暂时别再往那儿打(冷却)。
- 整个 OpenAI 区域挂了 → 需要整体切到 Anthropic(跨模型组回退)。
- 用户塞了一个 300K token 的 prompt → 需要换一个上下文更长的模型,而不是直接报错。
这四件事,你自己写要写几百行状态管理;Router 把它们做成了固定流水线。
核心概念:模型组(model group)。 这是全章的地基,只有一句话:
model_list里model_name相同的若干条,就是一个「模型组」;每一条叫一份「部署(deployment)」。
# 示意,非源码
from litellm import Router
router = Router(
model_list=[
# ↓ 这三条 model_name 都叫 "gpt-4o" —— 它们构成一个模型组
{"model_name": "gpt-4o",
"litellm_params": {"model": "azure/my-gpt4o-east", "api_key": "...", "rpm": 600}},
{"model_name": "gpt-4o",
"litellm_params": {"model": "azure/my-gpt4o-west", "api_key": "...", "rpm": 600}},
{"model_name": "gpt-4o",
"litellm_params": {"model": "openai/gpt-4o", "api_key": "...", "rpm": 200}},
# ↓ 另一个模型组,用来当回退目标
{"model_name": "claude",
"litellm_params": {"model": "anthropic/claude-sonnet-4-5", "api_key": "..."}},
],
routing_strategy="usage-based-routing-v2", # 怎么在组内挑一份
num_retries=2, # 组内重试几次
fallbacks=[{"gpt-4o": ["claude"]}], # 组都不行了,换哪个组
redis_host="...", # 多副本网关共享状态
)
resp = await router.acompletion(model="gpt-4o", messages=[{"role": "user", "content": "hi"}])
调用方只写 model="gpt-4o"——组名;三份部署里选哪一份、失败了怎么办,全在 Router 内部。
一句话直觉: 把模型组当成 Nginx 的 upstream,把部署当成 upstream 里的 server。weight 是权重、冷却是健康检查摘除、fallbacks 是「整个 upstream 挂了就转到备用 upstream」。差别只在于:LLM 的「健康」不是能不能 TCP 连上,而是这一分钟的配额还剩多少、错误率多高、上下文放不放得下。
2. 顶层全景(它大概怎么转)
一次 router.acompletion() 从外到内套了三层壳,最里面才是 01 章讲的那条 completion() 主线。怎么读这张图:从上往下是「由外到内」,任何一层成功就直接一路 返回,不再往下走。
router.acompletion(model="gpt-4o", ...) ← 调用方只知道组名
│
▼
┌───────────────────────────────────────┐
│ ① 跨模型组回退壳 │ gpt-4o 整组不行 → 换 claude 组
│ async_function_with_fallbacks │ router.py:6540
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ ② 同组重试壳 │ 同一组内再试 num_retries 次
│ async_function_with_retries │ router.py:6635
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ ③ 选一份部署 │ 筛候选 → 按策略挑一份
│ async_get_available_deployment │ router.py:11129
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ ④ 真正发请求 │ → 01/02/03 章的主线
│ litellm.acompletion(**deployment) │ router.py:2923 _acompletion
└────────────────── ─────────────────────┘
各部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
Router.__init__ | 吃下 model_list,建索引、建缓存、装策略、校验回退表 | litellm/router.py:382 |
set_model_list | 把每条配置补上 model_info.id,建 model_name → 下标 索引 | litellm/router.py:8174 |
async_get_available_deployment | 选部署总入口:预路由钩子 → 筛候选 → 策略选一 | litellm/router.py:11129 |
async_get_healthy_deployments | 候选过滤流水线(健康/冷却/预算/上下文/tag/order) | litellm/router.py:10993 |
router_strategy/ | 各种「怎么挑一份」的策略实现 | 见 §3.3 表 |
async_function_with_retries | 同组重试 + 退避 + 「这个错该不该重试」 | litellm/router.py:6635 |
async_function_with_fallbacks_common_utils | 跨组回退:按异常类型挑回退表 | litellm/router.py:6287 |
router_utils/cooldown_handlers.py | 失败后决定要不要把这份部署关小黑屋 | _set_cooldown_deployments:413 |
Router.cache(DualCache) | 内存 + Redis 双层,存 TPM/RPM/延迟/冷却,多副本共享 | litellm/router.py:584 |
主线走一遍(高层): 请求进来 → 组名解析成一批候选部署 → 一串过滤器砍掉不能用的 → 策略从剩下的里挑一份 → 发请求 → 成功回调记账、 失败回调可能触发冷却 → 失败则重试 → 还失败则换组。
3. 核心原理
3.1 模型组是怎么长出来的
要解决的小问题: 用户写的是一个扁平列表,Router 需要「按名字快速拿到一组部署」。
set_model_list(litellm/router.py:8174)遍历原始列表,给每条没有 id 的部署生成一个稳定 id(_generate_model_id),然后 _create_deployment 把它塞进 self.model_list 并顺手建三张索引:model_id_to_deployment_index_map、model_name_to_deployment_indices、team_model_to_deployment_indices。最后一行 self.model_names = {m["model_name"] for m in model_list} 就是「有哪些模型组」。
要点有两个:
- id 是冷却和计量的主键。 后面所有「这份部署本分钟用了多少 tpm」「这份部署在不在冷却里」都以
model_info.id为 key,不是以模型名。 - 一份配置可能展开成多份部署。 例如
organization传了一个列表时,会按 organization 循环调用_create_deployment(router.py:8212起),一条配置变多条部署。
__init__ 里还有一步容易被忽略的启动期校验:validate_fallbacks(litellm/router.py:1707,在 router.py:696 / :710 被调用)要求回退表的每个元素都是「恰好一个 key 的 dict」,写错了在启动就炸,而不是等到线上真回退时才炸。
3.2 候选过滤:选之前先砍
要解决的小问题: 「组里有 6 份」不等于「6 份都能接这个请求」。有的在冷却、有的这分钟配额用完了、有的上下文窗口装不下、有的不在允许的区域。
async_get_healthy_deployments(litellm/router.py:10993)是一条顺序过滤流水线,每一步都只做减法:
组名 "gpt-4o"
│
▼ _common_checks_available_deployment:9930 ← 别名/通配符/团队路由 → 展开成候选池
[6 份候选]
│
├─▶ 团队 / web-search 过滤 filter_team_based_models
├─▶ 健康检查过滤 _async_filter_health_check_unhealthy_deployments
├─▶ 冷却过滤(§3.5) _filter_cooldown_deployments
├─▶ 被 block 的过滤 _filter_blocked_deployments
├─▶ 回调过滤(预算 / 亲和) async_callback_filter_deployments:7161
├─▶ pre_call_checks(§3.2.1) _pre_call_checks:9698
├─▶ tag 路由过滤 get_deployments_for_tag
└─▶ order / 加权失败排除 _get_order_filtered_deployments
▼
[剩 2 份] ── 空了就 raise ──▶ async_raise_no_deployment_exception
注意顺序有意义:冷却过滤发生在 _pre_call_checks 之前,所以「已经进小黑屋的部署」根本不参与后面的 token 计数,省掉了昂贵的 tiktoken 调用。