先说一个最近真实的感受:团队里模型越接越多,代码里开始出现各种各样的switch case,A厂商的SDK一套写法,B厂商的SDK另一套写法,参数名字对不上,错误码对不上,想做个模型容灾切换还得给每个厂商单独写一遍重试逻辑。后来我把接入层统一切到LiteLLM,代码里就只剩一种调用方式,模型随便换,预算还能盯着,省下来的时间比预期多得多。
这篇就围绕LiteLLM展开,讲它到底解决什么问题、SDK和Proxy两种形态怎么用、真正部署上线会遇到哪些坑,以及什么情况下其实不应该用它。适合正在做多模型接入、想搭内部统一模型网关的后端和算法同学参考。
1. 统一接入层到底在解决什么问题
1.1 一个被我反复提起的真实痛点
做LLM应用的同学应该都有过这种经历:一开始只用一家的模型,代码写得很顺,SDK怎么调、参数怎么传、流式怎么处理,全都烂熟于心。但当业务要接第二家、第三家模型时,问题就来了。
首先是协议不统一。不同模型服务商的接口设计各有各的逻辑,有的遵循一个风格,有的完全另起炉灶。请求体结构不同,鉴权方式不同,流式返回的数据格式不同,甚至连max_tokens这种基础参数,在不同服务商那里可能叫法都不一样。代码里要针对每一家写一个适配封装,再写一层逻辑判断当前请求应该走哪家的客户端。
其次是错误处理没法统一。某一家限流了返回429,另一家可能返回别的状态码;某一家超时后可以重试,另一家重试反而会重复扣费;某一家报错信息是JSON,另一家可能是一大段HTML。这些细碎差异在联调阶段不算什么,一旦上了生产,各种偶发故障会让你焦头烂额。
还有一个不那么显眼但很实际的痛点:模型能力在不断变化。今天这个模型是主力,明天可能就要切到另一个性能更好、价格更低的模型。如果每个调用方都直接依赖特定服务商的SDK,切换就等于改代码、发版本、回归测试,整个流程跑下来非常重。
统一接入层的价值就在这里:它在所有模型服务商之上抽象出一套标准接口,业务代码只依赖这套标准,具体请求落到哪家模型、怎么适配各家差异,都由接入层来完成。
1.2 LiteLLM的核心形态:SDK与Proxy
LiteLLM给我最深刻的印象是它把“统一接入”这件事拆成了两种形态,分别应对不同的使用场景。
第一种是Python SDK,直接嵌入到你的应用进程里。安装后import,调用一个统一的入口函数,传入模型名、消息内容、参数,剩下的适配工作LiteLLM在内部完成。这种形态适合你的服务本身就是一个Python进程,你不想额外部署一个独立服务,只想把多模型适配逻辑收拢到一个库里面。
第二种是Proxy模式,也就是把一个轻量的网关服务跑起来,它对外暴露一个标准的接口地址,所有调用方通过网络请求访问这个地址,由网关统一路由到真实模型服务商。这种形态适合团队内部多个服务共享一套模型接入能力,你不想每个服务都装一遍SDK、各自维护一套配置,而是希望有一个集中的入口,顺便做Key管理、预算控制、负载均衡。
打个不那么精确但好懂的比方:SDK形态像在项目里内置了一个翻译,它帮你和不同模型服务商对话;Proxy形态像在公司里设置了一个前台,所有部门要联系外部模型厂商,都得通过这个前台。前者轻量、零额外部署成本,后者重一点,但能承载更多治理能力。
我在实际项目中两种都用过:早期几个内部工具直接用SDK,后来需求多了,要给别人发Key、要看预算,就上了Proxy。这两种形态不是互斥的,Proxy的实现底层也依赖SDK那套适配逻辑,完全可以先跑通SDK,再平滑升级到Proxy。
2. 十分钟跑通SDK:一行代码切换不同模型
2.1 安装与最基础的调用
在开始之前先说明一下,下面的示例代码都是基于LiteLLM的常规用法,具体版本如果升级了,细节可能略有差异,但核心思路不变。
安装非常简单,直接装主包就行:
pip install 'litellm[proxy]'如果你只是用SDK,不打算跑Proxy,装litellm就行。带[proxy]的扩展会额外装上FastAPI、uvicorn等网关组件,建议一开始就装完整版,省得后面想用Proxy还要再补。
最简单的调用长这样:
import litellm response = litellm.completion( model="gpt-4o-mini", messages=[ {"role": "user", "content": "用一句话介绍你自己"} ], ) print(response.choices[0].message.content)这段代码的形态和你直接用某主流厂商SDK很像,但区别在于model参数很灵活。你换一个模型名,LiteLLM会自动把消息格式、鉴权方式、返回结构都适配好。
那API Key怎么处理?最推荐的方式是通过环境变量:
export OPENAI_API_KEY="你的key" export ANTHROPIC_API_KEY="你的key" export GEMINI_API_KEY="你的key"LiteLLM在内部会根据模型名识别对应服务商,然后读取对应的环境变量。如果你不想用环境变量,也可以在调用时显式传入api_key参数,不过生产环境还是建议用环境变量或者配置文件统一管理,别把Key写在代码里。
2.2 参数映射:temperature、max_tokens这些词为什么能统一
很多同学第一次用LiteLLM时会疑惑:明明不同模型的参数不一样,为什么我统一用temperature、max_tokens这些词就行?这背后其实是LiteLLM做了一层参数翻译。
以max_tokens为例,某些模型服务商原生字段叫max_tokens,另一些服务商在接口里用的是max_completion_tokens或max_output_tokens。你在LiteLLM层统一传max_tokens,它会根据目标模型服务商自动翻译成对方认识的字段。
我整理了一个常见参数的对应关系,方便理解:
| LiteLLM统一参数 | 典型用途 | 不同服务商可能存在的差异 |
|---|---|---|
model | 模型标识 | 各家命名规则差异很大 |
messages | 对话消息列表 | 有的服务商支持system角色,有的靠内部处理 |
temperature | 随机性控制 | 默认值范围略有差异 |
max_tokens/max_completion_tokens | 最大输出长度 | 部分新模型用max_completion_tokens |
top_p | 采样概率 | 部分服务商不支持或默认值不同 |
stream | 是否流式输出 | 流式切分格式差异很大 |
api_base | 自定义端点 | 自建代理或网关时必须用 |
这套映射机制给业务代码带来的好处是:你只需要维护一套调用参数语义,模型切换时不用满世界去找“这个服务商是不是叫另一个名字”。
2.3 异步、流式与超时控制
真实线上应用很少直接用同步阻塞调用,异步和流式几乎是标配。LiteLLM对这两块支持得都不错。
异步调用示例:
import asyncio import litellm async def main(): response = await litellm.acompletion( model="gpt-4o-mini", messages=[{"role": "user", "content": "你好"}], ) print(response.choices[0].message.content) asyncio.run(main())流式输出示例:
response = litellm.completion( model="gpt-4o-mini", messages=[{"role": "user", "content": "写一段产品文案"}], stream=True, ) for chunk in response: delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True)这里有个容易被忽略的点:流式返回的chunk结构在LiteLLM层已经被统一过了。不同模型厂商的流式数据格式差异非常大,有的按token切,有的按字符切,有的会把content字段放在不同层级。LiteLLM帮你归一化之后,业务代码只需要统一解析choices[0].delta.content。
超时和重试我建议在调用前就配置好,别等线上超时故障了才想起来:
import litellm litellm.request_timeout = 60 # 全局超时这个超时设置对不同模型的适配效果会有一点差别,因为底层走的协议不同,但大部分场景下设一个合理值就够了。重试策略我习惯在Proxy层配置,后面会细说。
2.4 成本追踪:一个容易被忽略的实用功能
LiteLLM一个很实用的能力是成本估算。很多模型服务商的返回结果里有usage信息,LiteLLM会把它们解析出来,并根据当前模型的价格估算出这一次请求的花费。
用法很简单:
response = litellm.completion( model="gpt-4o-mini", messages=[{"role": "user", "content": "写一段话"}], ) print(response.usage) print(response._cost) # 本次请求估算成本,单位美元我一般会把_cost记到日志里,按天汇总,就能知道每个业务线大概烧了多少钱。传到内部监控平台后,还能按模型、按调用方维度分析成本结构。对预算敏感或需要给客户分摊成本的团队,这个功能很值钱。
3. Proxy网关:把模型路由能力变成团队公共设施
3.1 一个最小的config.yaml长什么样
当需要把模型接入能力共享给团队时,SDK就不够用了,这时候需要上Proxy。Proxy的核心配置在一个YAML文件里,所有模型路由规则、Key管理、预算策略都通过这个文件声明。
一个最小的配置大概是这样的:
model_list: - model_name: gpt-4o-mini litellm_params: model: gpt-4o-mini api_key: os.environ/OPENAI_API_KEY - model_name: claude-sonnet litellm_params: model: anthropic/claude-sonnet api_key: os.environ/ANTHROPIC_API_KEY litellm_settings: request_timeout: 60 drop_params: true general_settings: master_key: sk-my-master-key这里注意model_name和model字段的区别。model_name是你给这个接入定义的别名,对外暴露给调用方;model是LiteLLM内部真正要调用的模型标识。别名的好处是可以隐藏底层厂商信息,将来切换实现时也方便。
启动Proxy只需要一行命令:
litellm --config ./config.yaml --port 4000服务起来后,它会在本地监听4000端口,等待模型调用请求。
3.2 用OpenAI兼容格式对接Proxy
Proxy对外提供的是兼容主流模型服务商格式的API,这意味着你甚至不需要使用LiteLLM的SDK,直接用原来的SDK换一个base_url就能访问Proxy。
比如用Python SDK访问:
from openai import OpenAI client = OpenAI( base_url="http://localhost:4000", api_key="sk-my-master-key", ) response = client.chat.completions.create( model="claude-sonnet", messages=[{"role": "user", "content": "你好"}], ) print(response.choices[0].message.content)这里有几个点值得多说一句。
第一,Proxy把各家模型都伪装成了统一格式,调用方拿到的响应结构是一样的,所以原来针对某一家模型写的代码,只要把base_url和api_key换掉,就能走Proxy访问别的模型。
第二,model参数直接传你在config里定义的别名,也就是model_name,而不是底层真实模型名。比如上面配置里claude-sonnet对应的是某家的模型,但调用方不需要知道这层映射。
第三,鉴权用的是master_key,这是Proxy自己管理的主Key。为什么要这样设计?因为在多团队共享场景下,你肯定不希望让每个开发都直接持有真实模型服务商的Key,一旦泄露,风险和损失都不可控。
3.3 负载均衡与自动降级:fallback配置
模型接入统一之后,下一步自然就是容灾。LiteLLM Proxy支持同一个别名挂多个底层模型,请求进来后可以根据配置做负载均衡或故障切换。
举个例子:
model_list: - model_name: chat-model litellm_params: model: gpt-4o-mini api_key: os.environ/OPENAI_API_KEY model_info: mode: chat - model_name: chat-model litellm_params: model: anthropic/claude-sonnet api_key: os.environ/ANTHROPIC_API_KEY model_info: mode: chat litellm_settings: fallbacks: - chat-model: - claude-sonnet这里两个模型都叫chat-model,Proxy会按顺序尝试,如果一个挂了或返回异常,就自动切换下一个可用模型。fallbacks配置则是在某个别名失败时,直接降级到另一个别名。
实际用下来的感受是:这种容灾机制比自己在业务代码里写try...except...要干净得多,而且你可以随时通过改配置文件调整主备模型,不用动业务代码。
不过也要注意,容灾不是万能的。不同模型的能力边界不同,如果主用模型支持某些高级功能而备用模型不支持,降级后可能出现参数不兼容。我的建议是,做模型容灾前先梳理好功能矩阵,明确哪些场景可以降级,哪些场景宁可报错也不能降级。
3.4 Key管理与预算控制:给团队发Key而不是发厂商Key
Proxy的另一个核心价值是Key治理。你可以为每个成员或每个项目生成独立的虚拟Key,每个Key有自己的预算上限、速率限制和使用记录。
生成虚拟Key可以调用管理接口,也可以在启动时配置预生成。实际用的时候大概是这样的流程:
- 团队管理员持有
master_key。 - 某个项目要接入模型,管理员调用管理接口创建一个虚拟Key。
- 给这个Key设定预算,比如每月100美元,超过之后请求会被拒绝。
- 开发代码里只用这个虚拟Key,不接触真实厂商Key。
这种做法切切实实解决了我以前很头疼的几个问题:
一是Key泄露风险。就算某个项目的Key被人拿走了,也只是一个受限的虚拟Key,真实模型服务商的主Key还掌握在管理员手里,损失可控。
二是预算失控。以前用共享Key的时候,月初没人管,月底账单出来吓一跳。现在每个Key有预算限额,超了自动熔断,账单突然飙升的情况基本不会再出现。
三是责任可追溯。谁哪个Key在什么时间调用了什么模型,Proxy里都有记录,排查问题和分摊成本都方便。
4. 部署和踩坑记录:真正上线才会遇到的细节
4.1 环境变量和配置文件的加载顺序
用LiteLLM部署Proxy时最常踩的坑之一,就是环境变量没进去。比如你明明在config.yaml里写了api_key: os.environ/OPENAI_API_KEY,启动后却报找不到Key。
我排查过好几次,问题基本都在加载顺序上。LiteLLM的配置来源大致有三个层次:环境变量、config.yaml、调用时显式传入的参数。三者的优先级是调用参数最高,其次是config.yaml里的显式配置,最后才是环境变量。
但os.environ/这个写法比较特殊,它表示从环境变量读取,这就要求环境变量在启动进程之前就已经存在。如果你用Docker部署,但又忘了在docker run时加-e参数,或者忘了在编排文件里声明环境变量,进程起来后自然找不到Key。
后来我的习惯是:所有密钥类配置一律通过环境变量注入,config.yaml里只写os.environ/XXX这种引用,不写真实值。这样既能避免密钥被提交到代码仓库,又能保证多环境部署时不改配置文件。
4.2 兼容性陷阱:非OpenAI原生模型的接入
LiteLLM对主流模型服务商做了很完善的支持,但遇到一些相对小众的自建模型或需要通过自定义域名访问的场景时,还是需要手动指定接入方式。
比如某个模型服务商提供的是兼容主流格式但走自定义端点的接口,用SDK方式接入大概是这样:
import litellm response = litellm.completion( model="mymodel", api_base="https://your-endpoint.example.com", api_key="your-key", custom_llm_provider="openai", # 告诉LiteLLM走哪种协议适配 messages=[{"role": "user", "content": "你好"}], )custom_llm_provider这个参数很关键,它告诉LiteLLM该用哪套协议规则去适配这个端点。不少同学在这里栽过跟头,原因是忘传了custom_llm_provider,导致LiteLLM按它自己的猜测去解析,结果请求发过去对不上,报错信息也莫名其妙。
接入这类模型的时候,我建议先随便发一个最简单的请求,确认基本链路能通,再逐步加参数。这样如果有问题,至少能快速缩小范围,知道是协议适配的问题,还是模型本身的问题。
4.3 配置变更后为什么没生效
Proxy跑起来之后,如果你改了config.yaml,却发现自己添加的新模型无法调用,或者改了某个参数后行为没变化,多半是没触发配置重载。
我在早期就犯过这个错:改完配置后觉得Proxy会实时读取文件,直接调接口测,结果还是旧配置的行为。后来查文档和源码才明白,Proxy在启动时会把配置加载进内存,后续的请求都基于这份内存配置运行。你只改磁盘上的YAML文件,内存里还是旧的那份。
解决办法有两种。一种是改完配置后重启Proxy进程,简单粗暴,但如果你有多个实例,逐个重启期间会有短暂不可用。另一种是调用LiteLLM提供的配置热更新接口,让它把新配置加载进来,不需要重启。
实际项目中我倾向于用热更新接口配合脚本做一键更新,先把新配置推上去,再触发重载,最后用一个小请求验证新模型是否可用。整个流程稳定很多。
4.4 高可用部署:Redis、多实例与限流
当Proxy从一个单点服务变成团队公共设施后,高可用和状态共享就成了必须面对的问题。
Proxy不是完全无状态的。虚拟Key的预算、速率限制、使用记录这些数据,默认情况下是存储在本地内存或本地数据库里的。如果你只部署一个实例,没问题;但当流量上来后,你想横向扩容成多个实例,就会发现每个实例各自记录各自的数据,同一个Key在A实例扣的预算,B实例不知道,配额逻辑就乱了。
解决方向是引入Redis作为共享存储,让多个Proxy实例读写同一份状态数据。启用之后,Key的速率限制和预算控制就能在集群层面保持一致。审计类数据建议接入外部数据库,比如把每次请求的日志写到PostgreSQL,方便后续做统计和分析。
这个架构落地后,前面挂一个负载均衡器分发流量,后面多个Proxy实例共享Redis,整体有基本的水平扩展能力。如果对可用性要求很高,还可以考虑把Proxy部署成多副本,并做好健康检查。
不过我还是要提醒一句:LiteLLM Proxy本身是一个应用层网关,它的性能瓶颈通常不在框架本身,而在上游模型服务的响应延迟。对大部分内部场景来说,单实例或双实例已经绰绰有余,不必一开始就追求大规模集群。
5. 哪些场景不适合用LiteLLM:选型中的冷静判断
5.1 单模型固定使用的场景
如果你整个应用只用一个模型服务商的一个模型,那我的建议很简单:直接用官方SDK就好。
理由也直接:多一层抽象,就多一层理解成本和排障成本。官方SDK通常能第一时间支持新功能,LuaLiteLLM这类统一层在某个模型发布新能力时,可能要等适配更新。而且只接一个模型时,LiteLLM带来的统一、切换、治理这些好处你一个都用不上,反而要承担依赖升级、参数映射不透明等风险。
我见过一些团队为了“以后可能接入更多模型”这个假设,在项目一开始就强行引入统一网关,结果一年过去了,还是只用那一个模型,中间还因为适配层升级折腾过几次。这种为未来过度设计的情况,其实挺常见的。
5.2 延迟极度敏感的场景
统一接入层一定会带来额外开销,关键是这个开销你能不能接受。
SDK形态的开销比较小,因为它只在进程内做了适配和转发,没有网络跳转。但Proxy形态不同,请求要先到你的Proxy,再由Proxy转发到上游模型服务商。多一跳网络,延迟自然多一截。如果你的业务对首token延迟或者总延迟极其敏感,比如实时语音交互、在线代码补全,那Proxy引入的额外时延需要认真评估。
我自己做过一个简单的压测对比:直连上游模型服务商的P99延迟,和经过Proxy后的P99延迟,差距随着上游延迟增大而缩小。原因是模型生成耗时占了整体延迟的大头,Proxy本身的转发耗时占比并不高。但如果你的上游模型响应本身很快,比如一些轻量模型返回就一两百毫秒,那Proxy增加的这部分就不容忽视了。
5.3 自己写适配层 vs 用LiteLLM vs 商业网关
偶尔会有同学问:我能不能自己写一个适配层,就几十行代码,把两个模型统一成一样的调用方式?
我完全理解这种想法,但不太建议这么干。自己写的适配层初期好像很清爽,一旦模型服务商升级接口、新增字段、调整流式格式,你的测试和适配工作会跟着没完没了。LiteLLM作为社区持续维护的开源项目,这些适配工作是持续跟进的,你一个人维护的版本很难赶上这个节奏。
我列一个简单的对比表,帮助选型:
| 维度 | 自己写适配层 | LiteLLM | 商业网关产品 |
|---|---|---|---|
| 功能覆盖 | 只覆盖你需要的 | 覆盖面广,社区更新快 | 覆盖面广,有商业支持 |
| 维护成本 | 高,靠人力兜底 | 中,需跟踪版本变化 | 低,厂商负责 |
| 可控性 | 最高,代码完全在自己手里 | 较高,开源可改但需维护 | 较低,依赖外部服务 |
| 学习成本 | 看自己写多少 | 中,需要熟悉配置体系 | 低,通常有控制台 |
| 数据隐私 | 取决于自己部署 | 自部署,数据在自己环境 | 需评估SaaS模式合规性 |
| 成本 | 人力成本高 | 开源免费,但有部署运维成本 | 按用量或订阅付费 |
判断标准我觉得不复杂:当你需要接入的模型数量超过三个,或者有多个团队都要用模型能力,又或者需要集中做预算管理时,LiteLLM很值得引入。如果只是一个简单场景,连官方SDK都用得磕磕绊绊,那先别折腾网关,把基本功练好更重要。
结尾:一点个人的使用体会
最后分享一个我实际踩过之后养成的习惯。我一般会在Proxy后面接一个简单的请求日志收集,每次调用都记录模型名、调用来源、延时、usage和估算成本。这些数据平时不看似乎没用,可真到月底对账单、或者遇到某个模型效果变差想回溯时,你会发现提前埋点太值了。别嫌多写这几行日志的功夫,后面省的是大功夫。
如果你也在考虑引入LiteLLM,建议先在一个非核心项目里试用,把SDK调用、Proxy配置、Key管理这套流程跑顺,再逐步推广到正式业务。不要一上来就追求高可用和复杂容灾,模型接入层的复杂度应该跟着你的实际需求走,而不是一开始就把它设计得跟大型网关一样重。