ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

litellm统一大模型网关实战:模型路由、重试限流与踩坑复盘

litellm统一大模型网关实战:模型路由、重试限流与踩坑复盘

接手那个项目的第一周,我数了数代码里躺着的模型接口:官方SDK、某云厂商的兼容协议、一个自托管推理服务的HTTP封装……每家认证方式不一样,错误码含义不一样,连超时参数叫法都不同。最崩溃的是线上报错,我得先猜它是哪家的返回结构,才能开始排查。后来我把所有调用收敛到一个叫litellm的统一网关后面,这群问题才算真正被按住。这篇文章我用自己的部署和压测经历聊聊 litellm:它解决什么问题、核心机制怎么运作、怎么从零搭一个能用的服务,以及那些踩完才知道的坑。适合已经不止用一个模型接口、开始被兼容性和路由问题折磨的团队。

1. 为什么需要一个“统一的大模型网关”

1.1 多模型接口混用带来的真实痛苦

我见过太多项目是这样起步的:需求方说“先接A家的模型上线”,于是代码里出现A家的SDK;过两个月发现B家更便宜,又加一套;再后来要自研评测,接了个本地开源的推理服务。表面上看都是“调一个接口发一段文本”,实际上每家的请求结构、认证方式、限流语义、错误码体系完全是各过各的。

真正让人崩溃的有三件事。第一是错误处理没法统一:A家限流返回HTTP 429,B家却返回200然后消息体里带一个错误标记,C家直接连响应字段都不一样。第二是重试策略没法共用:有的模型偶尔抽风,需要退避重试;有的模型你重试反而触发更严的限制,尤其并发场景下,重试策略稍微激进一点,限流反过来把你打得更惨。第三是迁移成本,今天想从A模型切到B模型,改动点散落在业务代码、缓存策略、异步任务里,没人敢拍胸脯说不会漏一刀。

有段时间团队里准备自己写一个适配层,把几个SDK包一层。做了两个星期发现,我们真正要处理的不是“格式转换”,而是“生产级调用管理”——重试、限流、熔断、按调用方计费、模型切换、日志审计,这些堆起来工作量不比业务功能小。这也是后来我转向 litellm 的根本原因:它把“需要但不想自己造”的那层东西做成了现成的。

1.2 litellm 到底解决的是哪一层问题

简单说,它就是大模型接口的“总机”——把不同模型的通信协议翻译成统一格式,再帮你完成路由、重试、限流和密钥管理。实际使用中有两种形态:

SDK 形态:在 Python 里直接调用litellm.completion(),传入model="openai/gpt-4o"或者model="anthropic/claude-3-5-sonnet",库内部负责把请求转成对应厂商的格式。这种形态适合在单个服务里轻度改造,不需要额外部署进程。

代理服务形态:跑一个独立的服务,对外暴露一个 OpenAI 兼容的/v1/chat/completions接口。你的业务代码根本不用装各家SDK,只要把配置里的base_url指向这个代理,鉴权、模型映射、负载均衡都由代理完成。这种形态最适合多服务复用的团队,我后来线上一直用的是这个。

曾经有个同事问我:litellm 是不是像那种“一个模型聚合平台”?不是。它不卖模型、不托管模型、不生成内容,它做的事情是“管理调用”。把模型想象成不同的插座接口,litellm 是一个标准插线板——插座还是原来那些,但你不需要为每个电器准备不同的插头了。

1.3 什么时候不需要 litellm

讲真,不是所有场景都适合上网关。如果你的项目只调一个模型、调用量每天几百次、改动又不频繁,那直接调官方SDK最省事,没必要引入额外依赖。

还有一个我明确的劝退场景:端到端延迟极其敏感的任务。代理形态会多一跳网络开销,SDK形态虽然只有进程内转换开销,但对于以毫秒计数的指令级应用,这层开销也会被算进成本里。还有纯离线脚本、一次性数据清洗任务,也没必要为它搭一套配置体系。

litellm 的价值随模型种类和调用方数量增长而增长。模型超过两三个、调用方超过两三个、开始要按部门计费、需要对线上模型做灰度切换的时候,它的性价比才真正显现出来。

2. 拆解 litellm 的核心能力:模型注册、路由与密钥

2.1 model_list:模型注册表是网关的心脏

litellm 的所有行为几乎都围绕model_list展开。它是配置文件里的一个模型数组,每个条目有两个关键字段:对外暴露的model_name,以及真正发起请求的litellm_params。前者是你业务代码里写的模型名,后者是“实际去哪个厂商、用哪个模型”的描述。

model_list: - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY rpm: 1000 tpm: 100000 - model_name: claude-3-5-sonnet litellm_params: model: anthropic/claude-3-5-sonnet api_key: os.environ/ANTHROPIC_API_KEY rpm: 500 tpm: 50000

这里一个很容易被忽视的设计:你可以让多个model_name指向同一个底层模型,也可以让同一个model_name下挂多个等价模型条目,用来做负载均衡。我见过一个比较优雅的用法是,对外统一叫chat-main,下面挂两个不同厂商的模型,哪个挂了自动切换,业务代码完全无感。

给我的建议是,model_name的命名要有约束力,别用当天的心情乱起名。已经上线的模型名一旦改动,客户端配置、预算、权限数据全部要跟着动。我们内部约定:厂商-用途-能力,比如openai-chat-large、openai-embed-small,虽然丑,但一眼能看懂。

2.2 负载均衡与路由策略的选择

同一个model_name下挂了多个实例后,litellm 会按照路由策略挑选目标。它提供的几个策略各有侧重,我实际测试下来的感受是这样:

策略工作逻辑推荐场景
simple-shuffle随机洗牌后轮询各实例规格一致、延迟相近的默认选择
round-robin严格轮流需要可预测的分配顺序时
least-busy按当前负载选最空闲实例实例延迟方差大、部分实例被打满时
usage-based-routing按历史 token/请求量分配多个实例单价不同、想优先用低价资源时

很多人上来就选 least-busy,觉得“聪明”。但最少连接策略依赖准确的实时负载数据,如果你的调用周期波动大、并发模型多,它反而可能因为采样偏差把请求打到一个刚空闲但状态还没更新的实例上。我自己的结论是,在绝大多数普通业务里,simple-shuffle 已经足够稳;只有当某些模型实例明显慢于其他实例时,least-busy 才有肉眼可见的优势。

路由之外,litellm 还做了冷却(cooldown)机制:某个目标连续失败达到阈值后,会被暂时移出候选池,等到冷却时间结束再放回来。这个机制比单纯重试更克制,避免把一个已经出问题的实例打到恢复为止。

2.3 密钥管理与可观测端点

代理服务跑起来后,你可以设置一个master_key作为管理员凭证,然后给每个调用方签发独立的虚拟密钥。虚拟密钥可以绑定能访问的模型范围、每秒请求数上限、每分钟 token 上限,以及预算金额。

这个设计我挺喜欢,它把密钥和成本控制绑在了一起。真实厂商密钥只存在网关的配置里,业务方拿到的都是受限的虚拟密钥,就算某一天虚拟密钥泄露,影响范围也被限制在配额内,不会拖垮整个账号。

运维侧,litellm 提供了一组端点:/health/liveliness做存活检查,/spend看消费明细,/key/info看某个密钥的使用情况,启用 Prometheus 后还能通过/metrics拉取指标进监控大盘。上线前把这些端点配进告警,比等用户反馈“怎么突然报错了”要舒服得多。

3. 从零搭建一个可以上线的 litellm 代理

3.1 环境准备:容器优先

要试用 litellm,最快的路线是pip install 'litellm[proxy]',然后命令行启动。如果是给团队搭正式环境,我建议直接走 Docker,依赖隔离、版本回滚、配置挂载都省心。

docker run -d \ --name litellm-proxy \ -p 4000:4000 \ -v $(pwd)/config.yaml:/app/config.yaml \ -e OPENAI_API_KEY=${OPENAI_API_KEY} \ -e ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \ ghcr.io/berriai/litellm:main-latest \ --config /app/config.yaml --port 4000

启动之后,本地的http://localhost:4000就是一个 OpenAI 格式的接口。这里有一个容易疏忽的点:容器内的环境变量来自-e参数或环境文件,配置文件里写的os.environ/OPENAI_API_KEY并不会自动读取宿主机变量,必须显式传给容器。我第一次部署就折在这上面,后面会细讲。

3.2 一份最小可用的 config.yaml

最精简的配置其实上面已经给过了,这里补充几个字段的说明,方便你对号入座:

字段作用备注
model_name对外暴露的模型名客户端请求里填这个名字
model真实调用的厂商模型格式是厂商/模型名,如openai/gpt-4o
api_key真实密钥或环境变量引用推荐os.environ/变量名形式
rpm / tpm每分钟请求数 / token 数上限限流参数,后面细聊
model_info模型元数据可标注 mode、支持函数调用等

启动后可以用 curl 验证链路:

curl http://localhost:4000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-your-master-key" \ -d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "你好"}]}'

Python 客户端也完全不用改动现有逻辑,只要把 base_url 指过来,官方 OpenAI SDK 就能直接工作:

from openai import OpenAI client = OpenAI( api_key="sk-your-master-key", base_url="http://localhost:4000/v1", ) resp = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "你好"}], ) print(resp.choices[0].message.content)

配置和客户端能跑通之后,先把master_key换成一个强随机值。默认的sk-1234只配拿来第一次本地冒烟。

3.3 接入数据库:密钥与预算才真正生效

如果你想用虚拟密钥拆分、预算控制和调用明细统计,就必须给 litellm 配一个数据库,官方默认推荐 PostgreSQL。没有数据库时,它能跑,但虚拟密钥、消费记录、budget 都存在内存里,服务一重启全没了。很多人“昨天还能用的 key 今天全部失效”,多半就是这个原因。

general_settings: master_key: sk-.... database_url: postgresql://litellm:password@postgres:5432/litellm

数据库就位后,生成虚拟密钥走管理接口:

curl -X POST http://localhost:4000/key/generate \ -H "Authorization: Bearer sk-master-key" \ -H "Content-Type: application/json" \ -d '{ "models": ["gpt-4o"], "max_budget": 20, "budget_duration": "30d" }'

返回的 key 直接交给调用方。设置预算后,一旦消费达到上限,该 key 就会收到限流/拒绝响应,防止某个调用方把月度成本跑穿。

3.4 上线前的检查清单

根据我的经验,新部署 litellm 代理上线前至少做一遍这五件事:第一,确认每个配置的模型都能真实请求一次,特别是环境变量没漏传;第二,用虚拟密钥而非 master key 做业务调用;第三,数据库连接正常,调用后能在 spend 里看到记录;第四,配好健康检查和告警,至少盯/health/liveliness和/metrics;第五,给客户端配好退避重试,做好面对网关返回 429 的心理准备。

这套检查只需要十分钟,但能省掉上线后“为什么这个 key 无效”“为什么预算没生效”之类一连串问题。

4. 重试、限流与 fallback:三个最关键的机制

4.1 一个请求从客户端到模型再回来的完整旅程

客户端的请求到达 litellm 代理后,经历的过程比想象中长:先做鉴权,确认虚拟密钥有效且有权访问该模型;然后检查限流,看看这个密钥和模型的 rpm/tpm 是否超限;接着进入路由选择,从候选实例里挑一个目标;再把 OpenAI 格式的请求转成目标厂商的格式,发起真实调用;拿到响应后,再把厂商格式转回 OpenAI 格式返回给客户端。

这趟旅程里最容易低估的是限流检查这一步。它不只是一个简单计数器,而是基于密钥和模型维度的滑动窗统计。如果你在配置里设了rpm: 100,请求量超过这个值后不会排队等待,而是直接返回 429。客户端不处理这个错误,就会看到一堆意外的调用失败。

4.2 重试与冷却:不是撞运气,是策略

重试配置有两个位置很重要:一个是litellm_params里的max_retries,控制对单次请求内置的重试次数;另一个是路由层面的num_retries和cooldown_time,控制对整个模型实例的重试和熔断。把这两层分开理解,才不会在问题出现时调错参数。

router_settings: routing_strategy: least-busy num_retries: 2 cooldown_time: 30 request_timeout: 60

我自己比较喜欢的重试策略是:对 429 做指数退避重试,对 5xx 做有限次数的快速重试,对超时几乎不重试——超时重试往往把已经拥堵的上游打得更狠。

Fallback 则是另一层保障:主模型挂了或者被限流,自动切到备用模型。可以用全局配置的方式声明:gpt-4o失败时去找claude-3-5-sonnet。这比在业务代码里写 if-else 干净得多,也方便无感灰度替换。但要注意,fallback 只解决“接口可用性”问题,两个模型的能力差异不可能被网关抹平,降级前最好在业务层想想:备用模型给出的结果是否还能接受。

4.3 限流:tpm/rpm 的软硬两种模式

litellm 的限流表现在两个维度:rpm(每分钟请求数)和tpm(每分钟 token 数)。tpm 的统计对于流式响应尤其重要,因为 token 是在流式输出过程中累积的,超出后可能中途就被掐断。

这就是我之前说的“硬模式”:超出配额直接返回 429,网关不代替你排队。想做成“软模式”,就只能靠客户端配合——收到 429 后等一段时间再重试。这里的退避间隔不能用固定值,多个客户端同时退避会造成同步踩踏,最好用指数退避加随机抖动。

我踩过一次典型的坑:某个大并发任务同时压上来,模型真实的 tpm 上限是 60000,我在配置里设了 50000,结果一大半请求直接 429。后来我把限额调高并让客户端严格做退避,问题就消失了。限流参数不是越小越安全,它要和模型真实承载能力匹配,否则只是把故障提前变成报错。

4.4 流式与异步的取舍

如果业务用到了流式输出,注意 litellm 代理的流式路径是“边转发边转码”的,也就是说它要持续接收上游的流式块,转换成 OpenAI 格式后再发给客户端。这中间增加的延迟很小,但肯定存在。对流式体验要求极高的场景,可以先压测一下看是否能接受。

异步接口acompletion是另一个值得早点用上的能力。业务代码里如果同时发起多个模型的调用,用同步方式会浪费大量等待时间,而异步方式可以让这些请求并发发出,整体耗时接近最慢的那个请求。我们的应用在改用异步后,多模型评测场景的延迟从串行的十几秒降到了并发的四五秒。

5. 我在生产环境踩过的坑(按排查链路复盘)

5.1 模型前缀写错,认证和 404 一起出问题

litellm 的model字段是厂商/模型名格式,openai/gpt-4o、anthropic/claude-3-5-sonnet、azure/gpt-4o各有各的规则。我犯过的错是把本地自托管模型写成hosted/llama-3却没有配api_base,结果网关按默认的 OpenAI 地址去请求,返回 404 和认证错误,日志里还没写明是路由问题。

这类问题排查起来很快,但容易绕弯:先确认model前缀是否和厂商协议匹配,再确认是否需要额外的api_base、api_version参数。很多“连不上模型”的报错,根因都在这一行配置上。

5.2 Docker 里环境变量“消失”

部署时我明明在主机上 export 了密钥,容器起来却提示认证失败。排查半天发现,配置文件里的os.environ/OPENAI_API_KEY读取的是容器内环境变量,而 docker run 没加-e OPENAI_API_KEY=...,变量自然为空。

这类问题需要在启动命令里显式传环境变量,或者用--env-file加载环境文件。上线前我习惯手动进容器执行env | grep 模型密钥名验证一遍,确认配置里的环境变量确实存在且非空。

5.3 内存模式下虚拟密钥全部失效

团队第一次测试虚拟密钥时没有配数据库,key 生成后也能正常调用。第二天再调,全部 401。当时第一反应是 key 被删了,查了半天发现服务重启过,而内存模式下所有虚拟密钥都跟着没了。

这几乎是新手期必踩的坑。没有持久化,密钥、预算、消费记录都不可靠。生产环境一定要先把数据库这一层配起来,再开始签发票据给调用方。

5.4 压测时遇到的超时与连接池

压测脚本跑起来后,很快出现大量超时错误。一开始以为是模型服务扛不住,仔细看才发现是 litellm 代理侧的 HTTP 连接数不足,请求在等连接复用而不是真正发到模型那里。

处理方式是调整代理进程的并发和 HTTP 客户端配置,例如加大连接池数量、放宽超时时间。同时客户端侧的 timeout 也要和网关的 timeout 匹配,否则会出现“客户端已经等不及断开,网关还在等上游返回”的浪费。这类问题靠看监控里“上游耗时”和“总耗时”的差值就能定位,差值持续偏大,多半是代理自身处理不过来了。

5.5 路由策略别只看“聪明”

用 least-busy 跑了一段时间后发现,某些时候它反而把请求集中到同一个实例上。原因是它的负载采样有滞后,在突发流量下,刚空闲的实例还没来得及反映真实状态,数据上显得“空闲”,于是又被打满。

后来回到 simple-shuffle 为主,只有在明显感知到“一个实例慢导致整体变慢”时才切 least-busy。路由策略这种细节,没有银弹,生产环境要结合自己的流量特征去试。一个小技巧:给不同实例配置不同的权重或 rpm/tpm,利用限流参数做软路由,比依赖算法猜测更直观。

我把这阶段遇到的问题汇总成一张表,方便你对照排查:

症状根因解决
模型返回 404 / 认证错误model 前缀或 api_base 配错核对厂商协议与必要参数
容器内密钥为空未传环境变量使用 -e 或 --env-file
重启后 key 全部失效内存模式未持久化配置 PostgreSQL
压测大量超时连接池不足或超时参数不匹配调整 HTTP 客户端与客户端超时
路由把请求集中到单点负载采样滞后换 simple-shuffle 或加权重

最后说一个我长期保留的配置:线上所有模型都设一笔保守的 tpm 上限,同时要求客户端做指数退避。刚配的时候看起来浪费了一点配额,但它救过我两次,一次是上游模型故障,一次是业务突然发起大并发任务。如果你刚开始用 litellm,我建议从config.yaml加 PostgreSQL 的最小组合跑起,先观察一周消费曲线和日志,再去折腾复杂的路由策略。毕竟网关的作用是把调用管理得简单,而不是为了让架构图更复杂。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进