做 AI 应用开发这两年,我花在“对接模型”上的时间比写业务逻辑还多。不同厂商的大模型各有各的长处,有的擅长长文本,有的便宜,有的大并发下延迟低。可一旦要同时接入几家,事情就变了味:接口格式不统一,参数名对不上,流式返回的协议完全不同,报错信息也各说各话。团队里每个后端都要维护一份“翻译代码”,换一个模型就得把调用链路的每个环节重新过一遍。后来我注意到一个开源项目叫 LiteLLM,它把各种模型通道收拢成一套统一的调用接口——思路很朴素,却实实在在帮我把模型接入工作量从按周计算压缩到了按小时计算。这篇文章就记录一下我是怎么用 LiteLLM 重构模型接入层的,以及部署到生产环境之后踩过的坑。
先说明一下,LiteLLM 是一个开源的模型网关库,支持对接市面上绝大多数在线大模型服务,也支持本地部署的模型。它最大的特点是把“不同厂商各自的调用协议”翻译成一种统一的、业界最常见的对话补全接口格式,并提供模型路由、故障转移、成本追踪、预算限制、代理服务等一整套配套能力。如果你也正在被多模型切换和重复对接折磨,这篇内容应该能帮你少走不少弯路。
1. 为什么模型接入这么乱:三个供应商三种脾气
1.1 每家接口都有自己的“方言”
不同厂商的模型服务,虽然都是发一个 HTTP 请求、接收一段 JSON,但细节差异非常折磨人。
有的服务用max_tokens控制生成长度,有的用max_output_tokens;有的要求把历史对话放在messages数组里,有的仍然用prompt字符串拼接;鉴权方式也不一样,有Authorization: Bearer的,有自定义头部的,还有用签名参数的。流式输出更是重灾区——有的用纯文本增量,有的用data:前缀再加 JSON,有的把事件类型放在event:字段里。当你同时维护三到四个模型通道时,这些看似不大的差异会变成一坨到处打补丁的代码。
更头疼的是错误码。同一个“超限”错误,在 A 家是 429,在 B 家是 400 外加一个奇怪的业务码,在 C 家干脆返回 200 但响应里写着错误信息。业务方根本没法统一处理重试、限流和异常上报。我最早做的多模型接入层,本质上就是一张巨大的if vendor == ...判断表,每次加新模型都要碰一遍所有分支,改完还不敢确定有没有漏网之鱼。
1.2 统一层解决的不是“哪个模型更强”,而是“怎么都接得动”
在选型的时候,团队也有过争论:为什么不直接让各业务方自己去适配模型?答案很简单——一个厂商接口变更,所有业务方都要跟着改;一个模型性能不好要降级,所有业务方都要发一次版本。这种成本会随着接入方数量线性放大,团队规模一大基本每周都在处理这类重复劳动。
LiteLLM 的思路是加一层“翻译官”。它对上是统一的调用入口,对下则是每个模型各自独立的适配器。你只需要按照同一套格式写一次调用代码,剩下的差异由库去消化。
我画过一张简单的对比:假设有 3 个模型通道、5 个业务方,不做统一层时,模型侧变更影响面是 3×5=15 处;做了统一层之后,影响面收敛到 1(网关)加上 5 个业务方各 1 次适配,而且业务方那次适配通常只是改模型名配置,代码本身不动。
这里还要夸一句它的设计取向。LiteLLM 没有发明一套新的协议,而是直接兼容了业界事实标准的那套“对话补全”接口风格。这意味着团队里任何用过主流模型 SDK 的人,拿起 LiteLLM 都能直接上手,几乎没有学习成本。老项目里已经写好的调用代码,通过一个小适配层就能迁过来。
2. 十分钟跑通第一行代码:安装、密钥与最小调用
2.1 安装并发出第一个补全请求
LiteLLM 是纯 Python 库,装起来非常友好。如果你的项目只需要编程调用,装基础版就够了;如果要用代理网关,就装带[proxy]扩展的版本。
pip install 'litellm[proxy]'装完之后的第一段代码,其实比你想象中还短:
import litellm response = litellm.completion( model="provider-a/chat-model", messages=[ {"role": "system", "content": "你是一个智能助手。"}, {"role": "user", "content": "请用一句话介绍你自己。"}, ], ) print(response.choices[0].message.content)注意看这里的model参数,我用的是provider-a/chat-model这种“供应商前缀/模型名”的格式。provider-a是模型供应商的标识,chat-model是具体模型名。你实际使用时把它换成对应的标识和模型名即可。LiteLLM 会自动识别前缀,然后走对应的适配器去请求真实模型服务。
response的结构也是标准化的,包含choices、usage等字段,跟主流 SDK 的返回格式保持一致。这意味着你后续代码里访问结果的方式非常自然:response.choices[0].message.content。
一个小提醒:如果你是第一次在脚本里跑,建议先打印一下完整的response对象,看看里面有哪些字段。因为不同模型的返回里,额外信息差异还挺大,有的会带回finish_reason,有的会带后处理需要的结构化字段,提前熟悉结构能少走很多弯路。
2.2 密钥管理:别把令牌写进代码里
LiteLLM 读取密钥的方式,优先从环境变量里找。它会自动读取形如PROVIDER_A_API_KEY之类的环境变量名称。比如你实际使用的供应商是某在线服务商,就设置成对应的键名。
最省事的做法是放在.env文件里,然后用工具加载:
PROVIDER_A_API_KEY=sk-xxxxxxxx PROVIDER_B_API_KEY=sk-yyyyyyyy在代码里引入:
import os from dotenv import load_dotenv load_dotenv() import litellm这样做的好处是:密钥不进代码仓库,不会因为一个 commit 就泄露到远端;本地开发、测试、生产环境可以各自配不同的密钥;后面如果切换到别的模型服务,只需要改环境变量,代码一行都不用动。
2.3 从半小时到三分钟:一次真实的迁移对比
我之前有个内部小工具,原本只对接一个模型时,代码还很清爽。后来要加入第二个模型做对比测试,我就顺手把 LiteLLM 引入进来做统一调用。结果发现原本最长的那段“供应商判断”代码全部可以删掉,只剩一个model参数在变。
老代码改完之后,我们的业务流程从“每个新模型都要开发半天到一天”变成了“填一个模型名,跑一轮测试,看效果”。团队里一个刚入职的同学,按照我写的示例,从零开始跑通一个新的模型接入加完成基础对话测试,全程不到三分钟。这个速度让我确定了把 LiteLLM 作为团队标准接入层的方向。
3. 从“能用”到“好用”:路由、故障转移与流式输出
3.1 模型路由:一个逻辑模型名对应多个后端
仅凭“能调用多个模型”这句话,LiteLLM 还不至于让我写这么多字。真正有价值的是它提供的高级路由能力。LiteLLM 里有一个核心概念叫路由器,它允许你把多个后端服务注册到一个“逻辑模型名”下面。
先看一段典型配置:
from litellm import Router model_list = [ { "model_name": "chat-main", "litellm_params": { "model": "provider-a/chat-model", "api_key": "sk-xxxx", "rpm": 1000, }, }, { "model_name": "chat-main", "litellm_params": { "model": "provider-b/chat-model", "api_key": "sk-yyyy", "rpm": 500, }, }, ] router = Router(model_list=model_list) response = router.completion( model="chat-main", messages=[{"role": "user", "content": "今天天气怎么样?"}], )逻辑模型名chat-main底下挂了两套真实后端。业务调用方永远只写chat-main,至于背后的流量怎么分给两个后端,由路由器决定。
这带来一个很重要的好处:切换模型的时候,业务代码是零改动。比如某天provider-a/chat-model被评估为效果不理想,我只需要在路由器配置里把它的权重调低,让流量更多流向provider-b/chat-model,不用通知业务线改任何代码。
rpm字段是每分钟最大请求数。这个参数建议务必配。很多在线模型服务都有严格的速率限制,不显式声明的话,LiteLLM 只能按照全局默认值去估算,容易在高峰期触发限流。
3.2 故障转移:主模型挂了,备用模型无缝接管
在线模型服务再稳定,也逃不过偶发的超时、限流、返回异常。以前我处理这种问题的方式是写一堆重试逻辑,还得小心别把重试请求打到同一个故障节点上。
LiteLLM 的故障转移机制把这件事简化成了配置。当路由器发现一个后端不可用时,会按顺序尝试下一个。更进一步,你还可以显式配置fallbacks:
router = Router( model_list=model_list, fallbacks=[ { "model_name": "chat-main", "fallbacks": ["chat-backup"], } ], )注意,chat-backup需要在model_list里也存在。这样chat-main全部失败时,请求会用chat-backup兜底。
我在一次压测里故意停掉了主用模型的密钥,让请求持续打到路由器上。观察到的行为是:前几个请求能感受到明显变慢(因为超时等待),随后的请求就平滑切换到备用通道,业务方基本没有感知。不过要单独说一句,故障转移的触发依赖超时判断,所以超时参数要仔细调,设得太短容易误判,设得太长又会让用户等待太久。这个问题我在第六部分详细讲。
3.3 流式输出:请求体验不能降级
很多应用场景要求“边生成边显示”,对话助手尤其如此。LiteLLM 对流式调用的支持也比较完整,你只需要传stream=True,然后用迭代方式消费返回结果。
response = router.completion( model="chat-main", messages=[{"role": "user", "content": "给我讲一个冷笑话"}], stream=True, ) for chunk in response: if chunk.choices and chunk.choices[0].delta: print(chunk.choices[0].delta.content or "", end="")这里有个小事值得注意:流式返回的chunk对象不是完整的响应,里面只有增量内容。判断delta.content是否存在是必要的,因为有些 chunk 只携带了空的角色字段。如果你直接访问choices[0].message.content,大概率会拿到一个空值,然后开始怀疑人生。
流式的故障转移和普通模式稍有不同。因为流一旦建立,服务端已经开始吐数据了,中途切换不太现实。所以我的经验是:需要强一致的场景不要走流式,或者在做切换判断时要接受“前几段可能来自主模型、后面来自备用模型”的拼接情况。目前来看,绝大多数在线服务在首个响应 chunk 之前就能暴露大部分连接错误,所以故障转移对流式的影响没有想象中那么大。
4. 成本追踪与预算控制:老板再也不用追着我对账
4.1 每次请求的成本是怎么算出来的
在线大模型是按 token 计费的,每个模型单价不一样。以前我们按月度账单人工估算成本,遇到突发流量完全说不清是哪个业务方消耗的。LiteLLM 在响应里直接带上了usage信息,包含prompt_tokens、completion_tokens、total_tokens。
要得到金额,需要你自己维护一份模型单价表。比如某模型输入 1 万 token 是 0.1 元,输出 1 万 token 是 0.3 元,那么一次请求的成本就是:
cost = prompt_tokens * input_price_per_token + completion_tokens * output_price_per_tokenLiteLLM 也内置了不少常见模型的单价数据,在你显式开启成本计算后,响应对象里会带上_response_ms、cost之类的字段。但这个数据覆盖面不一定跟得上你实际使用的模型,尤其是自建微调模型或刚上线的新版本,建议还是自己维护一份价格配置,比较放心。我把这份价格表放在一个单独的 JSON 文件里,每次模型价格调整只改这个文件,然后所有成本统计结果自动更新。
有了每次请求的成本数据,就可以做汇总了。我们当时的做法是把每次调用的 model、请求方标识、token 数和估算成本写入一张日志表,每天跑一个定时任务做聚合。终于能回答“上周哪个业务方花了多少钱”这类问题了。
4.2 预算上限与限流配置
成本统计只是事后记账,更关键的是在烧钱之前踩刹车。LiteLLM 支持在路由器上配置预算和速率限制。
一个比较常用的配置是设置每日或每月的总预算上限:
router = Router( model_list=model_list, max_budget=1000, # 月度总预算(单位是人民币) )当累计成本超过这个值,请求会被拒绝或者降级到备用通道。这非常像手机流量包里的“超出后限速”,比直接断掉更友好一些。类似地,你还可以为每个模型设置每分钟请求数和每分钟 token 数的限制,防止某个异常脚本一口气把额度全部打满。
这里要提醒一句:预算控制依赖你开了成本计算,而成本计算依赖单价表准确。如果你维护的单价表缺失了某个新模型的条目,LiteLLM 很可能会把该模型的成本按 0 计算,导致预算永远不触发。所以我现在的习惯是:每次上线新模型,第一件事就是手工调用一次并打印完整响应,确认usage和cost字段都出来了,再把它接入流量。
5. 把 LiteLLM 变成团队网关:一条命令统一所有后端
5.1 起一个代理服务,前端只认一个地址
如果你已经想清楚了“接入层收敛到一处”,那直接用 Python 包的方式还不够彻底。更省心的方式是把它跑成独立代理服务,让整个团队所有业务服务都走这一个 HTTP 入口。
LiteLLM 提供了代理服务模式,一条命令就能启动:
litellm --model provider-a/chat-model --port 4000这样你就有了一台监听在本机 4000 端口的模型网关。它的对外接口完全兼容那种最常见的对话补全 HTTP 路径——/chat/completions,任何会用 HTTP 发请求的语言都能直接对接。不光是 Python 后端,Node.js、Go、Java 服务,甚至一个 curl 命令,全部可以统一走这个地址。
curl http://localhost:4000/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "chat-main", "messages": [{"role": "user", "content": "你好"}] }'这个代理模式真正的价值是把语言差异也给屏蔽了。后端不管用什么技术栈,都不需要再折腾 Python 环境的模型调用库,只需维护一个公共的请求格式。
5.2 密钥隔离与虚拟令牌
代理模式跑起来之后,最让我省心的是密钥隔离。以前每个业务服务都要配置上游模型的真实密钥,密钥一旦分散在很多个服务里,泄露面就很大。有了网关之后,真实密钥只存在网关这层,业务方根本接触不到。
你还可以在网关上创建虚拟令牌。不同的业务线用不同的虚拟令牌调用,网关在转发时替换成真实密钥,同时在日志里记录是哪个令牌发起的请求。这样出了成本异常或者调用量暴涨,一眼就能查到来源。我给不同业务方签发了不同的虚拟令牌,配合预算限制,每个业务方的月度成本就变成了一个可以预期的数字。
代理模式还顺带解决了团队协作的问题。后端同学不用关心“这个模型是哪个服务商的”“参数该用哪个格式”,他们只知道统一网关地址和一个逻辑模型名。模型侧的配置、密钥、路由、成本策略,全部由一个人维护在网关这层。这种“入口收敛”的做法,后期你管理多模型时一定深有体会。
6. 落地两个月踩过的坑:超时、别名与版本锁定
6.1 超时参数不显式设置,长文本请求会卡到怀疑人生
这是我在生产环境遇到的第一个大坑。LiteLLM 封装了不同模型的内部请求,但默认超时行为并不总是符合你的业务预期。有一次我们的对话服务在高峰时段频繁卡住,查日志发现请求先被上游模型服务拖了 40 秒,LiteLLM 还在等待响应,前端页面一直转圈,用户已经开始反馈问题了。
问题的根源是模型服务本身的响应变慢,而根本没有显式设置超时时间。后来我在初始化路由器时统一加上超时和重试配置:
router = Router( model_list=model_list, timeout=30, num_retries=2, request_timeout=300, )这里的几个参数各自含义不同:timeout是连接超时和单次读取间隔空闲超时,request_timeout是整个请求的执行总超时。建议你根据业务场景分别设置,不要只调一个。比如对话类服务用户耐心有限,总超时控制在 30 秒以内比较好;而离线批处理任务可以放宽到几分钟。
超时参数需要结合故障转移一起调。如果总超时太短,模型偶尔慢一点就被误判为故障,触发备用通道切换,反而把成本和延迟都拉高。我当时是先观测线上 P95 响应时间,再往上留一分钟左右的余量,然后反复压测确定最终值。
6.2 模型别名与真实模型分不清,排查问题全靠猜
路由器里model_name是逻辑名,自己随便起。方便是方便,但也带来了沟通上的模糊地带。有一回我们排查线上输出质量下降的问题,几个人围着监控面板看,都认为是 A 模型的输出有问题,结果最后发现流量早已经被路由到了 B 模型。
这件事之后,我们定了两条规矩:第一,逻辑模型名必须能体现切换状态,比如chat-main、chat-backup、chat-experiment,不能三个都是chat;第二,每次调整路由配置,必须在团队端口登记变更记录,包括谁在什么时间把什么逻辑名改成了什么后端。没有这两条规矩,统一入口带来的便利会被“找不到真相”的混乱迅速抵消。
另外,监控和日志里一定要把真实模型标识打出来。LiteLLM 的响应和日志里通常都会带真实模型字段,我们当时是让业务方统一把这个字段透传到链路追踪系统里。这样每一条线上请求实际走了哪个后端,可查可追溯,问题分析起来清晰很多。
6.3 版本迭代快是优点,也是风险
LiteLLM 的功能更新很勤,社区也活跃,但版本之间出现过配置项改动和行为变化。最典型的是一次升级后,路由器fallbacks的配置格式有了调整,我们的生产配置在重启后直接报错,整个网关不可用。
吃一堑长一智,我现在对依赖版本做了三件事:
- 在项目的依赖文件里锁定精确版本号,不写“大于等于某个版本”这种宽松约束;
- 每次升级前先在测试环境跑通全链路测试用例,重点覆盖路由、故障转移、流式、预算这几个关键能力;
- 把 LiteLLM 的版本升级当作独立需求来做,不和业务需求混在一起发。
依赖锁版本这一点,很多开发者在前期都会忽略,因为本地跑的时候没问题,上线后也没人主动升级,风险被掩盖了。一旦某个深夜值班的同事顺手pip install --upgrade了一把,第二天早上你就等着被告警吵醒吧。
还有一个容易被忽略的是 Python 版本兼容性。较新的 LiteLLM 版本对 Python 版本有要求,如果你的运行环境还需要支持老版本 Python,注意别把基础环境的版本升级带上去了。建议用独立的虚拟环境或者容器镜像来部署网关,把运行时和业务环境隔离开。
最后再分享一点个人心得
用 LiteLLM 做了半年多的模型接入层之后,我最大的体会是:它解决的是“接入”问题,而不是“选型”问题。它能让你轻松地把多个模型接到同一条流水线上,但“哪个模型适合哪类请求”“什么时候切换”“预算切分怎么定”仍然是需要你自己持续去观察和决策的事。
我现在的工作习惯是:网关层独立部署,版本固定;新模型先在实验逻辑名下跑一段时间,记录真实的延迟、成本和效果之后,再决定要不要把流量切过去。这个节奏让我们的多模型策略有了灰度空间,也让每一次模型切换都可回滚、可追踪。如果你正被一堆模型 SDK 的差异折磨,不妨先拿一个小功能试试 LiteLLM 的接入方式,再慢慢把整个调用层迁移过来。