从很早开始,我的工作就离不开各种大语言模型的接入。最初的需求很单纯——把几个不同厂商的模型接进公司的内部工单系统,我很快就发现了一个麻烦:每家服务商都有自己的SDK和请求格式,OpenAI 风格的用起来顺手,另一家的又要单独处理,再换一家又变了。改着改着,一半代码都在做"格式适配",而不是在写业务逻辑。后来一个同事往群里丢了个开源项目:litellm,一句话介绍是"用OpenAI格式调用所有模型"。我试了一下,然后彻底回不去了。这篇就结合我实际接入、部署、调优的过程,聊聊这套工具到底解决了什么问题、怎么跑起来、有哪些值得注意的坑。
这个项目最适合谁用?如果你只是自己在脚本里调一两个模型,可能用不上;但如果你要维护一个被多个模型、多个Key、多个业务方共享的服务,或者想让团队不用纠缠各家SDK,那litellm基本能省掉一大半事。下面我尽量讲透,不光是安装和调用,还有它背后为什么要这样设计。
1. 多模型接入的痛点和我最初的"笨办法"
1.1 每家厂商一套SDK,项目代码被格式绑架
先说我最早的状态。当时项目里要接入三个模型:一个通用对话模型、一个专注代码生成的模型、还有一个要跑在合规环境里的私有化部署模型。每一个都是官方SDK接进来的,按供应商的说法"开箱即用",但真正用起来之后,代码长得完全不像。
比如A家SDK的客户端要用A.complete(prompt=...),B家要B.messages.create(model=..., content=...),C家更狠,直接走原生HTTP的SSE流,返回格式还要自己解析。业务层每接一个新模型,就要新写一层适配。更痛苦的是,不同模型的参数名也不一样:有的叫temperature,有的叫top_p但语义还略有差异,有些甚至不叫这个名字;返回内容在content字段,有些藏在text字段里。这种问题本身不难,难的是烦——每加一个模型就做一遍做过的活,改错一处还得调试半天。
后来我为了省事,直接在代码里写了一个巨大的条件分支:
if provider == "A": result = a_sdk.chat(prompt=prompt, temp=0.7) elif provider == "B": result = b_sdk.messages.create(model=model_name, content=prompt, temperature=0.7) elif provider == "C": result = c_raw_request(payload={"input": prompt, "params": {"temperature": 0.7}})这一段代码简直是定时炸弹。每来一个新模型,我就要继续往里面堆分支。更气人的是,有些供应商自己也会更新API版本,升级后返回结构变了,调用方全得跟着调。业务还没跑多远,光适配层就越堆越厚。那段时间我最深的体会是:大模型能力本身是商品,但接入过程一点都不"商品化"。
1.2 从直接改代码到找一个统一切入点
被折腾了大概两周之后,我开始意识到一个问题:我要的不是"每个模型的最好用法",而是"一种能够让不同模型共同工作的方式"。项目里真正需要的是抽象层,把差异挡在外面,让上游业务只面对一个稳定接口。
我考虑过自己写一个内部SDK,把我们都用到的能力封装一遍。这个思路没问题,但它是个长期的维护活——供应商更新接口、新增模型、调整参数,都要有人跟着改。对一个组里只有两三个人的场景来说,成本偏高。这时候有人提了litellm。我看了下它的定位:对外提供一个统一的接口,内部通过插件式的适配器去映射到各家模型。
它在设计上做了一个很聪明的选择:对外暴露的接口直接采用OpenAI的格式。这是行业的"最大公约数"。也就是说,你写代码时只需要记住一种请求结构,litellm去负责翻译。我第一反应是"靠谱",因为OpenAI风格已经是被模仿最多的接口风格,很多新出的模型甚至原生就兼容这套格式,适配成本天然低。
1.3 这类工具要解决的三个基本问题
用了一段时间之后,我总结litellm实际上瞄准了三个基础问题。第一个是接口碎片化,这个上面已经说了。第二个是模型路由和容灾:一个服务不可能只靠一家供应商,万一上游限流、断连、或者某模型突然不可用,其他模型要能自动顶上。litellm支持配置多个模型之间的路由策略,比如主用A、备用B,权重、优先级、冷备热备都能配。第三个是成本与权限管理:多个Key、多个项目、多个人的情况下,你得知道谁在用什么模型、花了多少钱、有没有超预算。它在这块提供了一个轻量的管理面,后面我会详细讲。
这三个问题单独拎出来,都有对应的组件能做,但litellm的价值是把它们整合在了同一个接入层里。你不用为了接口统一装一个框架,为了路由再装一个负载均衡器,为了统计又搞一套日志平台——当然,生产环境你也可以把日志导出到你自己的平台,但至少入口是收敛的。
2. 统一接口协议:为什么litellm把宝押在OpenAI格式上
2.1 用一套请求结构描述所有模型
litellm最核心的一件事,就是把任意模型的调用转换成OpenAI Chat Completions格式。如果你用过OpenAI的API,下面这段就是它的基本请求:
from litellm import completion response = completion( model="openai/gpt-4o-mini", messages=[{"role": "user", "content": "你好,介绍一下你自己"}], temperature=0.7, )如果我想换成别的模型,不需要改调用方式,只需要换model这个参数的前缀路径。比如用Anthropic的模型,就写model="anthropic/claude-3-haiku";用Azure部署的模型,就写model="azure/gpt-4o";用本地的Ollama模型,就写model="ollama/qwen2.5"。前缀anthropic/、azure/、ollama/是litellm约定好的provider标识,后面跟的是该provider下的具体模型名。
completion()这个函数的参数也很接近OpenAI风格:messages传会话消息,temperature控制随机性,max_tokens限制输出长度,stream开启流式。如果你之前写过OpenAI的调用,几乎可以零成本切换到litellm。这背后的原理不复杂,litellm内部像查表一样把OpenAI风格参数映射到各家的真实参数,再统一把各家返回结构解析成OpenAI响应结构。
这个设计给我带来一个很直接的好处:业务代码里再也不用写条件分支了。不管底层模型是谁,我的调用代码都长一个样。后面就算要换一个还没用过的模型,大概率也只是换个model名。
2.2 有些参数不能"一刀切",透传机制起了关键作用
不同模型之间并非所有能力都一样。有的支持stop_sequences,有的只支持stop;有的支持thinking模式,有的压根没这个概念。如果litellm硬要把所有模型的差异都统一成一套简洁参数,就会出现两种尴尬:要么某些能力被砍掉,要么接口变得臃肿不堪。
litellm的解决方案是"主参数统一,个性化参数透传"。对通用参数,它做转换;对某个模型独有的参数,你可以通过extra_body或additional_args直接往下游传。我在实际中遇到过一个需要给某个模型单独开"思考预算"的场景,标准参数集合里没有这个字段,用透传就可以完成:
response = completion( model="某个支持思考模式的模型", messages=[{"role": "user", "content": "做个逐步推理"}], extra_body={"thinking_budget": 2048} )这类设计很重要。因为真正的抽象不应该把底层能力抹平,而应该保留每个模型的特长。如果一个统一接口只能做到"所有模型都能用最基础的能力",那它的价值就要大打折扣。我在用litellm的过程中会刻意区分哪些是通用代码、哪些是模型专属参数,前者放主调用里,后者走透传,这样既统一又不损失灵活性。
2.3 请求、响应、异常三层的兼容
除了请求格式,响应格式同样需要统一。我最早自己写适配时,最烦的就是从不同返回结构里取"模型回答内容":有的在choices[0].message.content,有的在output,还有的嵌套更深。litellm把这一切收敛成OpenAI风格的响应对象,取内容永远是这一行:
text = response.choices[0].message.contentusage字段也一样,不管底层的令牌统计叫什么名字,litellm都会映射成prompt_tokens、completion_tokens、total_tokens。在很多场景里,token统计比内容本身还重要——你要是对接过一个计费API就应该懂,模型输出的字数不仅影响体验,还直接影响成本,而一套能对得上的统计字段是做成本分析的前提。
异常层面,litellm把不同provider的错误也做了分类映射。APIKey失效、余额不足、内容被安全策略拦截、context超长、服务过载,这些错误在litellm里都有相对统一的异常基类和错误信息。虽然不同服务商的错误码千差万别,但你的捕获逻辑只要写一次。这一点在稳定运行阶段非常重要,因为线上出问题的时候,你要是面对的是几十种错误码,排查起来心态容易崩。
3. 把litellm部署成代理网关:团队协作和成本管理的关键
3.1 从SDK到代理模式:一次真正的架构升级
如果你只在Python脚本里调用litellm,那它对你来说只是一个普通SDK。但是litellm真正强大的地方,在于它附带了一个代理服务(Proxy Server)模式。你把它跑起来之后,它会在本地或服务器上开一个HTTP服务,对外暴露一个/chat/completions端点,这个端点完全兼容OpenAI格式。
这意味着什么呢?你的应用不需要再装litellm的SDK,只需要把请求发到代理地址。比如你原本用OpenAI的SDK,现在只需要把base_url改成litellm代理的地址。你可以用任何语言、任何框架、在任何环境里调用,只要你会发HTTP请求,就能用它。
我实际部署的时候用的是Docker,一条命令就起来了:
docker run -d \ -p 4000:4000 \ -e OPENAI_API_KEY="你的OpenAI密钥" \ -e ANTHROPIC_API_KEY="你的Anthropic密钥" \ ghcr.io/litellm/litellm:main-latest \ --config /app/config.yaml注意要有一个配置文件config.yaml,里面写着模型路由的规则。没有配置文件也能跑,但那就只能做单模型的透传,价值不大。我的建议是:代理模式一定要配config.yaml,后面你在上面做的所有模型管理、预算管理、密钥管理全靠它。
3.2 Key管理和预算控制的实际用法
代理模式带来的第一个直接好处是:下游应用不再需要知道上游的真实密钥。所有请求都打到litellm,由litellm来保管主Key、分发虚拟Key。你甚至可以给每个业务方、每个开发者、每个环境各发一个虚拟Key,然后在litellm里控制每个Key的预算、限速、可用模型范围。
我举一个实际例子。假设你有三个业务方在共用这个网关:客服机器人用的、内容生成用的、测试开发用的。你不想让测试开发的请求把生产预算烧光,也不想让某个业务方随意调用贵模型。在litellm的配置里可以这样控制:
litellm_settings: num_retries: 2 request_timeout: 30 model_list: - model_name: gpt-4o-mini litellm_params: model: openai/gpt-4o-mini api_key: os.environ/OPENAI_API_KEY model_info: max_tokens: 8192 - model_name: claude-haiku litellm_params: model: anthropic/claude-3-haiku api_key: os.environ/ANTHROPIC_API_KEY model_info: max_tokens: 8192 router_settings: routing_strategy: "usage-based-routing-v2"虚拟Key、预算、限速这些,我建议通过管理API来配置,但最关键的一点是:你可以在一个地方看到所有模型调用情况。不用再一个个去各厂商的后台数次数,litellm的日志页里有每次请求的模型、Token、耗时、状态,还能按Key筛选。这个能力对于成本敏感的项目来说,时间一长价值就非常明显。
3.3 观测、日志和Prometheus指标
跑在生产环境里的网关,观测性是不能缺的。litellm代理内置了/metrics端点,暴露Prometheus格式的指标。我这边直接把/metrics接到监控平台上,拉通看这几个指标:请求总量、延迟分位数、Token消耗速率、错误率、按模型拆分的调用量。
第一次在监控面板上看到模型级别的延迟曲线时,我意识到之前用各家SDK直接调用的方式,等于让系统对模型状态完全"失明"。你会等到用户投诉"回复变慢了",才反应过来某个模型可能过载了;而有了统一网关和指标,你可以在上游服务SLA亮红灯之前,就发现异常趋势,提前把流量切到备用模型上。
日志方面,litellm会记录每次请求的详细元数据,包括调用方Key、模型名、输入输出Token数、延迟、错误信息。这些结构化日志可以导到统一的日志平台。有一次线上出现了某个Key短时间大量调用高成本模型的异常,就是靠这些日志查出来的,最后发现是测试脚本把循环忘关了。这种问题在传统多Key直连模式下,你想查都无从下手。
4. 从同步调用到抵抗故障:一份带错误处理的实战笔记
4.1 基础调用:同步、流式和超时重试
代理模式可以跑起来了,但对开发者来说,最常用的还是在代码里直接调用litellm的SDK。我先从同步调用说起。大多数时候,一个简单请求几秒内就该返回。为了不让某个慢模型拖垮上游,我会把超时时间设置得明确一点:
import litellm response = litellm.completion( model="openai/gpt-4o-mini", messages=[{"role": "user", "content": "写一段200字的文案"}], timeout=10.0, )如果你要做流式输出,比如让用户看到打字机效果,litellm也支持stream=True,返回的是可迭代的chunk:
response = litellm.completion( model="openai/gpt-4o-mini", messages=[{"role": "user", "content": "分段念一首诗"}], stream=True, ) for chunk in response: delta = chunk["choices"][0]["delta"].get("content", "") if delta: print(delta, end="", flush=True)流式响应在应用侧是一个加分项,用户等待的体感会好很多。不过要注意,代理模式下流式响应会占用连接较长的时间,你需要在网关前面保证连接超时配置合理,否则代理长时间占用连接没返回,调用端可能先断开了。我踩过一次,当时是网关设在标准HTTP服务器后面,结果前置Nginx默认超时就给断了,调了半天才想起来去查网关前置连接配置。
4.2 错误处理:模型不存在、余额不足、上下文超长的应对
模型接入多了以后,线上最常遇到的不是"模型回答错了",而是各种可预期的"调用失败"。litellm把异常归纳成比较清晰的类型,我总结一下常用的处理姿势。
- 模型不存在或名称拼错:
litellm.exceptions.BadRequestError,这类通常是配置问题,抛出来后让配置方改,不需要重试。 - APIKey无效或权限不足:
litellm.exceptions.AuthenticationError,重试没用,要检查凭据。 - 余额不足或达到限额:不同provider表现不一样,有些会抛
RateLimitError,有些会直接报额度错误。如果项目里有多个可用模型,这种时候可以考虑切换到备用模型。 - 上下文超长:这类错误最值得处理。模型自身的上下文窗口是固定的,比如8K、32K、128K,超出后请求会失败。如果你在业务里做了用户输入,却忘了看累计历史消息是否超长,就要在调用前检查内容长度,或者用滑窗截断历史。
- 服务过载或网络抖动:这类通常不是参数的错,重试一两次大概率能恢复。
于是我会写一个很务实的使用模式,把不同类型的错误分开处理,而不是一把梭:
import litellm def safe_completion(model, messages, max_retries=2): for attempt in range(max_retries + 1): try: return litellm.completion(model=model, messages=messages) except litellm.exceptions.RateLimitError: if attempt >= max_retries: raise time.sleep(1.5 * (attempt + 1)) except litellm.exceptions.ContextWindowExceededError: messages = messages[-4:] # 粗暴的截断策略,仅作示例 if len(messages) <= 1: raise except litellm.exceptions.AuthenticationError: raise # 不好处理,直接暴露给调用方这个示例并不完美,但它表达了一个关键思路:别把所有错误都当成同一个错误来处理。尤其对于上下文超长这类问题,重试只会浪费额度,你需要对输入做裁剪或摘要。
4.3 成本统计和模型路由的进阶玩法
litellm还内置了一个简单的路由逻辑。你可以给同一个逻辑模型配多个供应商,比如model_name: chat同时映射到OpenAI和Anthropic。这样调用model="chat"的时候,litellm会根据你的策略选一个后端。当某家服务商挂了,路由会自动把请求转到另一家。实测中,这种切换对调用方是透明的,但延迟会略有增加,因为代理作决策需要网络探测。
我比较常用的是预算与配额控制。比如某业务方每个月只能花一定额度,超过就拒绝后续请求。这个能力在直连模式下很难实施,因为你没有一个统一出入口去统一记账。litellm把 "谁在调、调的什么模型、花了多少Token" 这件事集中收敛,配合tag和metadata,还可以在事后回看每个业务单元的模型消耗。其实做AI应用到了一定阶段,成本控制不是可选项,而是必选项。
5. 常见坑和我对选型的一些建议
5.1 SDK模式还是代理模式:很多人第一步就纠结错了
我发现很多刚接触的人在选型上一上来就问"我应该用SDK还是用Proxy"。我的答案是:大部分需要有人写业务代码的场景,直接用SDK模式集成足够;而要是给一个团队做共享网关,直接上Proxy。
单独开发一个脚本、写个小工具,用SDK模式简单直接,装一个Python包就能跑,日志直接打在本机;但你要是维护一个多业务方共用的平台,代理模式带来的集中管理价值,超过它增加的部署复杂度。如果你不确定,可以从SDK模式起步,把调用核心逻辑封装一个模块,后续切代理模式时,你的业务代码只需要改base_url或者注入客户端,代价不会太大。
5.2 我踩过的三个坑
第一个坑是版本更新太快。litellm的迭代频率非常夸张,我第一次部署时用的某个配置写法,过了没几周,参数名就变了。我的对策是:固定版本,不要追最新;升级前先看变更记录;生产环境用容器镜像锁版本。如果谁告诉你"直接用最新版就行",别听他的。
第二个坑是超时设置不统一。在SDK模式下你可以在每次调用里设置timeout,但代理模式下,HTTP层的超时、请求体大小限制、以及模型本身的调用超时分别由不同环节控制。线上如果出现"偶发超时",先确认是哪一层的超时触发,不要盲目把重试次数调到很大,否则模型侧可能已经处理完了,只是响应没回来,重试会造成重复消费。
第三个坑是把"兼容OpenAI格式"理解成"OpenAI功能全兼容"。用OpenAI SDK去连代理时,有些能力字段在代理层是透传给具体模型的,你代码里用到的功能,如果底层模型不支持,它不会"自动变出来"。比如某个模型没有工具调用能力,你却依样画葫芦传了tools参数,报错时不要怪litellm,去查底层模型能力清单更现实。
5.3 什么情况下不要上它
聊到这儿,我反而想说点"别用它"的场景,因为任何一个工具都有边界。
如果你只是个人项目,一行脚本偶尔调一两次模型,那直接装官方SDK或者发HTTP请求就够了。引入litellm虽然不重,但也没必要为了一个请求搞个抽象层。
如果你的合规要求非常严格,所有请求只能走固定通道且不允许第三方代理逻辑介入,那你就得先评估litellm的代理模式是否符合规范,不要为了统一接口去绕过安全审查。此外,如果项目里所有模型调用都稳定指向同一个供应商、同一个模型,未来的扩展性需求也不大,那你确实不一定需要它——统一抽象的收益来自"变化"和"差异"足够大的时候,没有这个前提,一切都是额外成本。
不过从我自己的实践来看,团队走到"第二个模型接入"的时间点,往往就是引入统一接入层的最佳时机。第一次改造总给人感觉"加了不少代码",但第二次、第三次、每一次新模型接入时,省下来的都是实打实的开发时间。litellm我用了挺长时间,整体感受是:方向是对的,社区很活跃,但你也别拿它当银弹,把它当作一个质量不错、能帮你把模型接入这块收拢起来的实用组件就好。最后就提一个最简单也最有效的建议:把你的模型调用从业务代码里抽出来,哪怕是先抽成一层独立的函数,以后再进化成统一网关都会顺手很多。