1. 先从我们被多模型接入逼疯的那几周说起
今年上半年,我们把一个内部问答助手从单模型切换成多模型对比评估,本来以为只是换几次调用参数的事,结果越接越乱。最开始只用了一家头部厂商的对话接口,后来为了比较效果和成本,又陆续接了两家云端厂商,还加了一个跑在内部 GPU 机器上的开源模型。真正的麻烦不在模型本身,而在接入这层:每个厂商给的接口风格不一样、参数名不一样、错误码不一样,连鉴权方式都有差异。我们团队一度在业务代码里堆了三四套调用分支,每次加新功能都要在所有分支上同步改,维护成本高得离谱。
1.1 多厂商 SDK 并存时的三类阵痛
第一类是接口形态不统一。有的厂商走"角色 + 消息"的对话风格,有的厂商更强调"指令 + 多轮消息"的结构,字段命名五花八门:有的叫max_tokens,有的叫max_new_tokens,有的叫temperature但取值范围都不一样。第二类是依赖冲突。每接一个厂商就要引入一套 SDK,这些 SDK 的底层 HTTP 库、序列化库版本经常互相打架,为了兼容它们,我们的打包体积和依赖锁定工作都变重了。第三类是故障处理各自为政。厂商 A 限流了要按它的错误码退避,厂商 B 超时了要按它的超时语义重试,完全没有统一的处理入口。
这几个问题单独看都不致命,但叠加在一起,每次想"换一个模型试试"就变成一次小型重构。我们很快就意识到,缺的不是更好的某个 SDK,而是一个位于业务代码和各家模型服务之间的接入网关,把"调用什么模型"这件事从代码里抽出来。
1.2 网关应该解决的问题清单
在选型之前,我们把需求写成了一个清单,核心就五条:
- 统一接口:业务代码只认一种请求格式、一种响应格式,切换模型只改一个名字。
- 集中密钥:厂商密钥只保存在网关侧,不散落在各业务服务的环境变量里。
- 路由与切换:同一个对外模型名背后可以挂多个真实模型或密钥,支持轮询、重试、降级。
- 治理能力:按团队、按项目分配独立的密钥,能设预算、限速率、实时查用量。
- 可观测性:有统一的请求日志、token 消耗和成本统计。
我们也认真考虑过自己写一个适配层,把各厂商的差异封装一遍。但算了一笔账之后放弃了:适配层要维护厂商接口变更的同步、要处理鉴权和重试、要自己做用量统计,前期的开发和后期的维护成本都不低。后来选型时把市面上的方案筛了一遍,最终落地的是开源项目 LiteLLM。它把上面这些能力基本都覆盖了,而且部署非常轻——既可以作为 Python 库直接在代码里调用,也可以作为独立网关进程跑起来。这篇文章按我们实际落地的顺序来写:先讲它的两种形态怎么选,再讲网关怎么配置和验证,接着讲生产环境的密钥与预算治理,最后把我们在过程中踩过的坑完整记下来。
2. LiteLLM 的两种形态:先分清 SDK 直调与网关模式
LiteLLM 最迷惑人的地方,就是它同时提供了两种用法,而这两者在面对的场景、部署方式、管理边界上差别很大。我们团队一开始只把它当 Python 库用,后来才升级成独立网关,中间绕了不少弯子。搞清楚这两种形态,算是落地第一步。
2.1 SDK 直调模式:适合原型期和脚本类任务
SDK 直调的使用方式很简单,核心就一个completion函数。安装时只需要基础包:
pip install litellm然后在代码里:
from litellm import completion resp = completion( model="starhub/chat-pro-v3", messages=[ {"role": "user", "content": "用一句话解释什么是模型网关"} ], ) print(resp.choices[0].message.content)这里最关键的是model参数的写法:厂商前缀/模型ID。比如starhub/代表星枢的云服务,lanxiang/代表澜象的云服务,local/代表本地推理服务。LiteLLM 会根据前缀自动匹配对应的鉴权字段、请求格式、响应解析逻辑。对开发者来说,切换模型只需要改这个字符串,底层的 SDK 差异完全感知不到。
这种模式适合什么场景呢?原型验证、离线脚本、小流量的内部工具。它把"多厂商调用差异"压缩成了一个字符串,收益已经很直观。但它的局限也很明显:密钥还是在业务代码里管理,模型切换还是要重新发版,预算和用量统计基本靠自己记。说白了,它解决的是"接口调用层"的问题,没触及"模型治理层"。
2.2 网关模式:团队级接入的事实标准
当我们把服务从三五个并发升到几十个并发、接入的团队从一个变到三个之后,SDK 直调就不再够用了。这时候要用 LiteLLM 的第二种形态:把 LiteLLM 作为一个常驻网关进程跑起来,对外暴露统一的对话接口,业务服务只认这一个端点。
网关模式的价值在于把问题分层:
- 业务侧:只维护一份通用的对话客户端配置,把
base_url指向网关地址,把 key 换成网关发的子密钥,模型名用网关定义好的对外名。 - 平台侧:所有的厂商密钥、模型映射、重试策略、预算限额、用量日志都在网关这一层集中配置和维护。
这样模型上线、切换、回滚都不需要动业务代码。比如某个模型效果不好要降级,在配置里把对外名指向另一家模型,重启网关,业务方什么都不用改。为了更直观地区分两种形态,我把它们的差异整理成了下表:
| 对比维度 | SDK 直调 | 网关模式 |
|---|---|---|
| 部署成本 | 无额外进程 | 需部署一个常驻服务 |
| 密钥管理 | 分散在各服务 | 集中在一处 |
| 切换模型 | 改代码重新发版 | 改配置,业务无感 |
| 预算与用量 | 需要自己实现 | 内置支持 |
| 适用阶段 | 原型、脚本、小流量 | 生产、多团队 |
需要说明的是,这两种形态不是二选一的对立关系。网关模式启动后,业务代码依然可以继续用 LiteLLM 的 SDK 去调网关,只是把model参数换成网关对外名。我们目前的架构就是"SDK 做客户端,网关做服务端",两边各干各的活。
3. 把网关跑起来:配置、启动与接口验证
选型定了之后就是落地。这里我把从零到通的完整过程写一遍,包括安装、最小配置、真实请求验证,以及中间最容易出错的几个细节。
3.1 安装与启动:注意基础包和网关依赖的区别
如果只在业务代码里用 SDK 直调,装基础包就够了。但网关模式需要额外的服务依赖,安装命令是:
pip install "litellm[proxy]"一个很常见的坑是只装了基础包就去找网关命令,结果提示找不到命令。装完带网关依赖的版本后,启动方式很直接:
litellm --config config.yaml --port 4000默认端口是 4000。启动后可以先探一下健康检查端点:
curl http://localhost:4000/health返回正常状态说明网关进程起来了。此时还没有任何模型可用,因为配置文件里什么都还没配。这一步先确认进程能起,再往下配模型,排查时会清晰很多。
3.2 model_list 是配置的核心:一个对外名对应一个真实调用串
网关有一个 YAML 配置文件,核心结构是model_list。下面是我们早期的一个最小示例:
model_list: - model_name: chat-main litellm_params: model: starhub/chat-pro-v3 api_key: os.environ/STARHUB_API_KEY - model_name: chat-lite litellm_params: model: lanxiang/turbo-2 api_key: os.environ/LANXIANG_API_KEY general_settings: master_key: sk-gw-local-001这里最需要理解的是"两个名字"的区分:model_name是网关对外暴露的名字,业务代码里用的就是它;litellm_params.model是真正发给厂商的调用串。api_key支持从环境变量读取,写成os.environ/XXX这种形式,避免把密钥直接落在配置文件里。master_key是网关自己的管理密钥,后面生成子密钥、查用量、管理配置都要用它。
还有几个常用字段值得提前了解:
| 字段 | 作用 | 备注 |
|---|---|---|
model_name | 对外模型名 | 业务方请求时使用的名字 |
litellm_params.model | 真实调用串 | 厂商前缀 + 真实模型 ID |
litellm_params.api_key | 该模型的密钥 | 可从环境变量读取 |
litellm_params.timeout | 请求超时秒数 | 可按模型差异化设置 |
model_info | 模型元信息 | 如自定义单价、备注 |
一个容易踩的认知误区是:一个model_name只能对应一个真实模型。实际上配置允许多个条目使用同一个model_name,网关会自动把这些条目组成一个"模型组",路由时按策略轮询或负载均衡。这个特性我们后面在密钥池和故障转移里用得非常多。
3.3 用命令行完成一次真实请求验证
配置写好、网关启动后,验证请求很简单。因为网关对外暴露的是业界通用的对话补全接口风格,直接用一个 curl 就行:
curl http://localhost:4000/v1/chat/completions \ -H "Authorization: Bearer sk-gw-local-001" \ -H "Content-Type: application/json" \ -d '{ "model": "chat-main", "messages": [{"role": "user", "content": "你好,介绍一下你自己"}] }'如果一切正常,会得到一个标准的对话补全响应,choices里带着模型返回的内容。这一步验证成功后,再试一下流式:
curl http://localhost:4000/v1/chat/completions \ -H "Authorization: Bearer sk-gw-local-001" \ -H "Content-Type: application/json" \ -d '{ "model": "chat-main", "messages": [{"role": "user", "content": "从 1 数到 5"}], "stream": true }'流式响应的验证非常关键,因为后面我们在生产环境遇到的最隐蔽的问题之一就出在流式上,这个放到第五节细讲。验证通过后,业务代码只需要把原来直连厂商的客户端配置改成"网关地址 + 网关密钥 + 网关模型名",接入就算完成了。整个过程我们当时只花了一个下午,比预想中顺利得多。
4. 生产环境真正值钱的治理能力:密钥、预算、轮询与降级
如果说前三节是"能用",那这一节是"敢上生产"。LiteLLM 网关在密钥管理、预算控制、故障转移上的能力,才是我们团队决定长期使用它的根本原因。
4.1 虚拟密钥与预算隔离:每个业务线一个钥匙
网关的master_key是管理员的钥匙,不能直接发给每个业务服务。正确的做法是让管理员调用网关自己的接口来生成"虚拟密钥":
curl http://localhost:4000/key/generate \ -H "Authorization: Bearer sk-gw-local-001" \ -H "Content-Type: application/json" \ -d '{"models": ["chat-main", "chat-lite"], "max_budget": 50}'返回结果里会带一个新的密钥,这个密钥只能调用chat-main和chat-lite这两个模型,并且累计花费上限是 50 美元(金额单位按网关配置走)。这样的好处非常实际:团队 A 和团队 B 各拿各的密钥,互不干扰;某个密钥泄露了,单独吊销它就行,不用把主密钥和所有密钥一起换掉;成本超了,请求会被自动拒绝,不会出现月底对账时才发现超支的情况。
要启用预算统计,生产环境需要给网关配置一个独立的数据库连接,用于持久化用量和花费数据,配置在general_settings里加database_url就行。这里我们踩过一个选择上的坑:早期为了省事用过轻量级文件数据库做实验,结果数据一多查询就明显变慢,日志轮转也不可控。后来换成了独立的开源关系型数据库,稳定性和查询体验都上了一个台阶。预算功能在验证阶段可以不开,但生产环境强烈建议从一开始就配上。
4.2 多密钥轮询与故障转移:让网关替业务扛住厂商抖动
在生产环境,我们最担心的事有两件:单厂商限流、单厂商故障。LiteLLM 网关对这两件事都有比较成熟的机制。
第一层是密钥池。同一个真实模型可以配置多个api_key,网关在发起真实调用时会按策略轮询这些密钥。比如厂商对单个密钥有每分钟调用上限,配置两个密钥后,网关会交替使用,等于把限流阈值翻倍。我们在高峰期最大的感受是:业务代码完全没变,但厂商返回限流错误的次数明显下降。这里要提醒一句,不同厂商对同时使用多个密钥的合规要求不同,加池子之前最好确认一下服务协议。
第二层是模型组的故障转移与冷却。网关内置了路由策略,常见的有按最少并发数、按最少失败次数等。相关配置项大致是:
router_settings: routing_strategy: least-busy num_retries: 2 allowed_fails: 1 cooldown: 10语义很直接:同一模型组内有多个真实模型或多个密钥可用时,优先挑当前并发少的;请求失败后自动重试最多 2 次;某个端点连续失败达到阈值后,会在 10 秒内对它做冷却,暂时不把新流量打过去。这层机制配合请求级别的降级配置,可以实现"主模型挂了自动切到备用模型"的效果,而不需要业务代码里写任何重试逻辑。我们上线后的几次厂商突发故障,基本都是靠这一层自动扛过去的,等我们发现的时候,故障窗口已经过去了。
4.3 用量统计与成本观测:从"估计"变成"可对账"
没有网关之前,我们每个月的人工成本核算是靠各厂商后台的账单手动汇总,不仅麻烦,还经常对不上。网关把这个问题解决了:每次请求的 token 消耗、花费、模型、密钥都会被记录下来。
网关自带标准指标端点,运维上可以直接接入监控大盘,看请求量、延迟分布、错误率这些基础指标。更底层的是按密钥维度的用量明细,可以查某个虚拟密钥在某段时间内总共消耗了多少 token、花了多少钱,也能按模型维度汇总。这就相当于给每个业务线、每个模型都装了一个计量表。我们现在的节奏是:每周拉一次用量明细,和厂商账单交叉核对,误差基本能控制在忽略不计的范围;每周成本构成一目了然,谁家太贵、哪个模型该降级,数据说话。
另外,很多厂商的价格和模型权重是动态调整的,LiteLLM 内置了一套常见的模型价格表,但新模型或特殊渠道的价格不一定覆盖。这类情况可以通过模型的元信息字段手动指定单价和 token 换算比例。投产前最好把关键模型的价格核对一遍,否则成本统计会出现系统性偏差。
5. 我们踩过又修掉的几个坑(附完整排查链路)
再顺的工具,踩过的坑才是真经验。下面这几个问题都是我们在网关上线后真实遇到过的,我把现象、排查链路和最终解法都记录下来,方便大家对照。
5.1 流式响应变慢:首字延迟从 400ms 涨到 2.5s
现象很明确:直接调厂商接口时,流式响应首字大约 400ms;切到网关后,同样的请求首字延迟飙升到 2.5 秒左右。一开始我们怀疑是网络路径变长导致,但内网延迟很低,不至于差这么多。后来抓了网关出网的请求,发现厂商返回的第一个数据块早就到了,但业务端一直没收到。
问题的根子是网关在下行链路上没有把流式行为完整透传:业务端确实发了stream: true,但网关内部有一段处理逻辑默认把完整响应缓冲了,直到整个响应结束才一次性下发。这在非流式请求下没有任何问题,但在流式场景下等于把流式退化成了一次性返回。我们最终的解法分为两层:一是确保业务请求在网关侧能命中流式处理分支,不额外包一层缓冲;二是把网关升级到修复相关问题的更新版本,并在压测脚本里专门加了"首字时间"指标做回归验证。这次排查给我们的最大启发是:流式功能必须用首字延迟做验收,只看总耗时会被平均数据骗过去。
5.2 fallback 失灵:对外模型名和真实模型 ID 混用了
某次我们配置了主模型和备用模型,想着主模型故障时自动降级。结果模拟故障后发现备用模型完全没被调用。排查日志时发现,请求到达网关时带的model字段是真实厂商模型 ID,而不是我们配置的对外模型名chat-main。网关按对外名去找模型组,自然匹配不上 fallback 规则,只能当作一个不认识的模型处理。
根子出在业务侧的老代码里:之前直连厂商时,代码里写的就是真实模型 ID,切到网关后只改了base_url,没改model字段。这个坑很有代表性——网关语义和厂商语义在"模型名"上的约定不同,迁移时最容易漏。我们的处理方式分两步:一方面在业务侧统一把model字段改成对外名,另一方面在网关配置里加了一层校验逻辑,遇到直接请求真实模型 ID 的情况,在日志里打明显告警,防止下次再有团队带病上线。后来我们还养成了一个习惯:每次配置模型组,先通过网关自身的模型信息查询端点确认映射关系,再放流量进来。
5.3 本地模型与云端厂商混跑:超时策略不能一刀切
我们的网关里同时挂了云端厂商模型和内部 GPU 机器上的开源模型。云端模型通常几秒内就返回,而本地模型在冷启动或并发高的时候,一个请求等十几秒都很正常。起初我们给所有模型配了同一个超时时间,结果本地模型频繁触发超时重试,又把负载加重,形成恶性循环。
这块最终是按模型组差异化设置超时:云端模型走短超时,快速失败快速重试;本地模型走长超时,并且关闭或减少自动重试,避免雪上加霜。另外我们专门为本地模型加了排队观察,发现本地推理服务的瓶颈往往在算力调度而不是网络,所以网关层面不该简单用重试来解决,而应该从容量规划入手。如果有团队在网关后面接本地模型,建议先做一轮基准压测,拿到正常的延迟分布,再倒推超时和重试参数,不要拍脑袋。
5.4 值得长期坚持的习惯:把网关配置当"模型配置中心"
用了一段时间后,我们发现网关配置文件的价值远远超出"接入"本身。它实际上成了一个模型配置中心:哪些模型对外可用、每个模型对应哪家厂商、成本怎么算、谁有权限用哪些模型,全部集中在这一个文件和一个管理接口里。
我们现在的流程是:业务方提出"想试某个新模型",平台同学在配置里加一条映射,先在灰度环境验证,效果稳定后再把同样配置发布到生产网关;如果模型效果退化,回滚也就是把配置改回去的事,整个过程业务代码零改动。为了保险,我们还维护了一个小的冒烟脚本,里面固定放十几个测试问题,每次变更配置后自动跑一遍,对比新旧配置下每个模型的返回质量和首字延迟,给决策做参考。
最后再分享一个运维层面的小习惯:网关自身的版本升级不要在生产上直接顺手就做,先在灰度环境把流式、轮询、预算这些关键链路回归一遍再上。我们有一次升级后遇到兼容性问题,就是因为跳过了这一步。总的来说,把模型接入这件事从代码问题变成配置问题之后,团队的迭代速度和出错的成本都改善了很多——这也是我觉得 LiteLLM 这类网关最值得投入的地方。