1. 从一个真实困境说起:多模型调用的碎片化之痛
如果你正在做AI应用开发,大概率遇到过这样的场景:项目初期用某一家的大模型API跑通了原型,效果不错,上线后想加一个备用模型做降级容错,结果发现两家的接口参数格式完全不一样——一个用messages数组,一个用prompt字符串;一个返回choices[0].message.content,另一个返回completion.choices[0].text。于是你不得不写一堆if-else来适配,代码里到处是if provider == "openai"这样的分支判断。
更麻烦的是,当你想对比不同模型在同一个任务上的表现时,每换一个模型就要改一遍调用代码、改一遍参数映射、改一遍返回解析。这种碎片化的调用方式,让模型切换的成本高得离谱,也让代码的可维护性急剧下降。
litellm这个项目,解决的正是这个问题。它做的事情用一句话概括:用一套统一的OpenAI格式接口,去调用几乎所有主流大模型服务。你只需要把base_url和api_key换一下,模型名换一下,其余代码完全不用动。对于需要频繁切换模型、做多模型对比、或者搭建模型路由层的开发者来说,这是一个能显著降低工程复杂度的工具。
这篇文章我会从实际使用角度出发,把litellm的核心机制、接入方式、代理模式、成本追踪、踩坑经验这几个维度拆开讲清楚。不管你是刚听说这个工具,还是已经用过但遇到了一些困惑,应该都能从中找到有用的东西。
2. litellm到底做了什么:统一接口的底层逻辑
2.1 核心设计思路:格式转换层
litellm的本质是一个格式转换中间层。它做的事情可以拆成三步来理解:
第一步,接收标准OpenAI格式的请求。你按照OpenAI的API规范构造请求体,包括model、messages、temperature、max_tokens这些参数。
第二步,根据model字段识别目标服务商,把请求转换成该服务商要求的格式。比如Anthropic要求system字段单独提取,Google的Gemini用contents而不是messages,Cohere的参数命名又不一样。litellm内部维护了一套映射规则,自动完成这些转换。
第三步,把服务商返回的结果再转回OpenAI格式。不管你调的是哪家,拿到的都是response.choices[0].message.content这样的结构。
这个设计的好处在于:你的业务代码只需要面向一种格式编程。模型切换变成了改一个字符串的事,而不是重写整个调用层。
2.2 支持的模型范围与调用方式
litellm支持的模型列表相当长,覆盖了市面上绝大多数主流服务。调用方式统一成两种:
# 方式一:completion函数,适合简单场景 from litellm import completion response = completion( model="gpt-4o", messages=[{"role": "user", "content": "你好"}] ) # 方式二:带provider前缀,明确指定服务商 response = completion( model="anthropic/claude-3-5-sonnet-20241022", messages=[{"role": "user", "content": "你好"}] )模型名的命名规则是provider/model_name的格式。如果不写provider前缀,litellm会根据模型名去猜测,但我强烈建议始终写全前缀,因为猜测逻辑在模型名有歧义时会出错。比如claude-3到底走Anthropic官方还是走某个兼容层,不写清楚就容易出问题。
对于自部署的模型服务,只要它兼容OpenAI接口,就可以通过api_base参数接入:
response = completion( model="openai/your-model-name", api_base="http://your-local-endpoint:8000/v1", api_key="dummy-key", messages=[{"role": "user", "content": "测试"}] )这里有个细节:api_key即使本地服务不需要,也建议填一个占位值,因为某些版本的litellm会检查这个字段是否存在。
2.3 环境变量配置的约定
litellm读取API密钥的方式遵循一套约定:每个服务商对应一个特定的环境变量名。比如OpenAI对应OPENAI_API_KEY,Anthropic对应ANTHROPIC_API_KEY,Azure对应AZURE_API_KEY加AZURE_API_BASE。
这套约定看起来简单,但实际用的时候有几个容易踩的点:
- 环境变量名必须完全匹配,大小写敏感。
openai_api_key和OPENAI_API_KEY是两个不同的变量。 - 如果你在代码里同时设置了环境变量和传参,传参的优先级更高。
- 某些服务商需要额外的配置项,比如Azure需要
AZURE_API_VERSION,这些在官方文档里都有说明,但容易被忽略。
提示:建议在项目根目录放一个
.env文件统一管理这些密钥,用python-dotenv加载。不要把密钥硬编码在代码里,也不要把.env提交到版本控制。
3. 从零接入:环境准备与第一个可运行示例
3.1 安装与版本选择
安装本身很简单:
pip install litellm但版本选择有个坑要注意。litellm的迭代速度非常快,不同版本之间的API行为可能有细微差异。我在实际项目中遇到过升级版本后某些参数默认值变化导致行为不一致的情况。建议在requirements.txt或pyproject.toml里锁定具体版本号,而不是用>=或不做限制。
如果你只需要基础的completion功能,安装核心包就够了。但如果要用到代理服务器(proxy)功能,需要额外安装:
pip install 'litellm[proxy]'代理模式会引入FastAPI等依赖,包体积会大不少。所以按需安装,不要一股脑全装上。
3.2 最小可运行示例的完整拆解
下面这段代码是我在实际项目中最常用的基础模板:
import os from litellm import completion # 设置密钥(实际项目中用.env管理) os.environ["OPENAI_API_KEY"] = "your-key-here" # 构造请求 messages = [ {"role": "system", "content": "你是一个简洁的技术助手。"}, {"role": "user", "content": "用一句话解释什么是向量数据库。"} ] # 发起调用 try: response = completion( model="gpt-4o-mini", messages=messages, temperature=0.3, max_tokens=200 ) print(response.choices[0].message.content) except Exception as e: print(f"调用失败: {e}")这段代码有几个值得说明的地方:
temperature=0.3是我做技术问答类任务时的常用值。温度越低输出越确定,适合需要稳定回答的场景。如果是创意写作类任务,可以调到0.7到0.9。
max_tokens=200是硬性截断。设置这个值有两个作用:一是控制成本,二是防止模型输出过长导致后续处理变慢。但要注意,如果设得太小,回答会被截断,所以要根据任务类型合理设置。
异常捕获这块,litellm会抛出不同类型的异常,比如AuthenticationError、RateLimitError、ContextWindowExceededError等。生产环境中建议针对不同异常类型做不同处理,而不是笼统地except Exception。
3.3 多模型切换的实操演示
假设你有一个问答功能,想在不同模型之间做对比测试。用litellm可以这样写:
from litellm import completion models = [ "gpt-4o-mini", "anthropic/claude-3-haiku-20240307", "deepseek/deepseek-chat" ] question = "解释一下什么是RAG架构。" for model in models: try: resp = completion( model=model, messages=[{"role": "user", "content": question}], temperature=0.3, max_tokens=300 ) print(f"=== {model} ===") print(resp.choices[0].message.content) print() except Exception as e: print(f"{model} 调用失败: {e}")这段代码的价值在于:你不需要为每个模型写不同的调用逻辑。同一个completion函数,同一个messages格式,同一个返回解析方式。切换模型只是改一个字符串。
我在做模型选型评估时经常用这个模式。把同一批测试问题跑过多个模型,收集输出结果做对比,整个过程代码量极少,大部分时间花在分析输出质量上,而不是折腾接口适配。
4. 代理模式:把litellm变成一个统一网关
4.1 为什么需要代理模式
直接用Python库调用适合脚本和单机应用,但如果你有多个服务需要调用大模型,每个服务都各自管理密钥、各自处理重试和限流,就会很混乱。代理模式解决的是这个问题:把litellm跑成一个HTTP服务,所有模型调用都走这个统一入口。
这样做的好处有几个:密钥集中管理,不用在每个服务里都配一遍;可以在网关层做统一的限流、重试、日志记录;不同语言的服务都能通过HTTP接口调用,不限于Python。
4.2 配置文件的结构与关键字段
代理模式通过一个YAML配置文件来定义模型列表:
model_list: - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY - model_name: claude-sonnet litellm_params: model: anthropic/claude-3-5-sonnet-20241022 api_key: os.environ/ANTHROPIC_API_KEY general_settings: master_key: sk-your-master-key litellm_settings: drop_params: true set_verbose: false这里有几个关键点:
model_name是你对外暴露的名称,客户端调用时用这个名字。litellm_params.model是实际的服务商模型标识。两者可以不一样,这样你可以在不改客户端代码的情况下,后端切换实际模型。
api_key: os.environ/OPENAI_API_KEY这个写法表示从环境变量读取,注意前缀是os.environ/,不是直接写环境变量名。
master_key是代理服务的访问密钥,客户端需要带上这个key才能调用。生产环境必须设置,否则任何人都能白嫖你的API额度。
drop_params: true这个设置很实用。不同模型支持的参数不一样,比如某些模型不支持temperature,如果不开启这个选项,请求会直接报错。开启后,不支持的参数会被自动丢弃,请求仍能正常执行。
4.3 启动代理与客户端调用
启动命令:
litellm --config config.yaml --port 4000启动后,代理服务监听在http://0.0.0.0:4000。客户端调用方式和OpenAI官方接口完全一致:
from openai import OpenAI client = OpenAI( api_key="sk-your-master-key", base_url="http://localhost:4000" ) response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "你好"}] )注意这里用的是openai库而不是litellm库。因为代理服务暴露的就是OpenAI兼容接口,任何支持自定义base_url的OpenAI客户端都能直接用。这意味着你现有的基于OpenAI SDK的代码,只需要改base_url和api_key两个地方,就能接入litellm代理。
4.4 负载均衡与降级策略
代理模式还支持在配置层面做负载均衡和降级。比如同一个model_name可以对应多个实际模型:
model_list: - model_name: chat-model litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY - model_name: chat-model litellm_params: model: anthropic/claude-3-5-sonnet-20241022 api_key: os.environ/ANTHROPIC_API_KEY这样配置后,请求会在两个模型之间轮询分发。如果其中一个调用失败,litellm会自动重试另一个。这个机制对于提高服务可用性很有价值,但要注意:不同模型的输出风格和能力有差异,轮询分发可能导致用户体验不一致。所以这种配置更适合做容错降级,而不是日常的负载均衡。
5. 成本追踪与预算控制:容易被忽视但极其重要
5.1 成本数据的获取方式
litellm内置了成本计算功能。每次调用后,可以从返回对象中获取本次调用的费用:
response = completion( model="gpt-4o", messages=[{"role": "user", "content": "测试"}] ) cost = response._hidden_params.get("response_cost", 0) print(f"本次调用费用: ${cost:.6f}")这个成本是根据模型的token单价和实际用量算出来的。litellm内部维护了一份各模型的定价表,但这份定价表可能不是最新的。如果服务商调价了,litellm的版本还没更新,算出来的成本就会有偏差。所以对成本精度要求高的场景,建议自己维护定价表,或者定期核对。
5.2 在代理层设置预算上限
代理模式下可以给每个key设置预算:
general_settings: master_key: sk-master database_url: "sqlite:///litellm.db" max_budget: 100 budget_duration: "30d"max_budget是总预算上限(美元),budget_duration是预算周期。超过预算后,请求会被拒绝。这个功能对于团队共用API key的场景特别有用,可以防止某个成员意外大量调用导致费用失控。
但要注意,预算追踪依赖数据库。database_url配置了存储位置,SQLite适合单机测试,生产环境建议用PostgreSQL。数据库不可用时会怎样,不同版本行为不一样,建议在测试环境验证一下。
5.3 按模型、按用户的成本拆分
代理模式还支持更细粒度的成本追踪。通过给不同的key打标签,可以按用户、按项目、按团队来拆分成本:
general_settings: database_url: "postgresql://user:pass@localhost/litellm" custom_key_generate: - key_alias: "team-a" max_budget: 50 models: ["gpt-4o", "claude-sonnet"] - key_alias: "team-b" max_budget: 30 models: ["gpt-4o-mini"]这样team-a和team-b各有独立的预算和可用模型列表。team-b只能用便宜的小模型,team-a可以用贵的大模型但预算更高。这种配置在多团队共用一套模型资源的场景下非常实用。
6. 实际使用中踩过的坑与应对方案
6.1 流式输出的参数兼容问题
流式输出是很多应用的基本需求。litellm支持stream=True,但不同服务商的流式实现有差异。我遇到过的情况是:某些模型在流式模式下不支持temperature参数,传了会报错;某些模型的流式返回格式和OpenAI不完全一致,导致解析出错。
应对方案是在代理配置里开启drop_params: true,同时在代码里做好异常处理。对于流式解析,建议用litellm提供的统一迭代方式:
response = completion( model="gpt-4o", messages=[{"role": "user", "content": "写一段话"}], stream=True ) for chunk in response: content = chunk.choices[0].delta.content if content: print(content, end="", flush=True)注意delta.content可能为None,需要判空。这个细节在官方示例里不一定有,但实际跑的时候经常遇到。
6.2 超时与重试的配置
默认超时时间在某些慢模型上可能不够用。可以通过参数调整:
response = completion( model="claude-3-5-sonnet-20241022", messages=[{"role": "user", "content": "分析这段代码"}], timeout=60, num_retries=3 )timeout是单次请求超时秒数,num_retries是失败重试次数。重试策略要注意:对于幂等性不保证的操作,重试可能导致重复执行。比如让模型生成一段代码然后自动执行,重试就可能执行两次。所以重试次数不是越多越好,要根据业务场景权衡。
6.3 模型名称映射的坑
litellm的模型名映射表更新频率跟不上新模型发布的速度。新模型出来时,litellm可能还不认识这个名字,会报BadRequestError。这时候有两个办法:一是升级litellm到最新版,二是通过custom_llm_provider参数手动指定服务商:
response = completion( model="new-model-name", custom_llm_provider="openai", api_base="https://api.example.com/v1", messages=[{"role": "user", "content": "测试"}] )这样litellm会跳过模型名猜测,直接用指定的provider和base_url去调用。
6.4 日志与调试信息的控制
litellm默认会输出一些日志信息,在生产环境可能过于嘈杂。可以通过环境变量控制:
export LITELLM_LOG="ERROR"可选级别有DEBUG、INFO、WARNING、ERROR。调试阶段用DEBUG可以看到详细的请求和响应内容,生产环境建议设为ERROR减少日志量。
另外,set_verbose: false在代理配置里也可以关闭详细输出。但要注意,关闭日志后排查问题会变困难,所以建议在代理层单独配置一个日志文件,而不是完全关闭。
7. 一些实战中的经验体会
用litellm有一段时间了,有几个感受比较深。
第一,它最大的价值不是省代码,而是省决策成本。以前选模型要考虑“换了模型我的代码要改多少”,现在这个问题不存在了,选型时可以更纯粹地关注模型能力和成本。这个心理负担的减轻,比代码量的减少更有意义。
第二,代理模式适合团队,库模式适合个人。如果只是自己写脚本做实验,直接pip install litellm用库模式最轻量。但如果是团队协作或者要对外提供服务,代理模式的集中管理优势就体现出来了。不要一上来就搞代理,增加不必要的复杂度。
第三,成本追踪功能要尽早开启。很多人是等到账单出来才发现费用超预期。litellm的成本追踪虽然精度不是100%,但作为预警机制完全够用。设置一个合理的预算上限,能避免很多意外。
第四,版本锁定很重要。litellm迭代快,新版本可能引入行为变化。生产环境一定要锁定版本,升级前在测试环境充分验证。我一般会在requirements.txt里写死版本号,比如litellm==1.52.0,而不是litellm>=1.52.0。
最后分享一个小技巧:如果你需要频繁在多个模型之间切换做对比,可以写一个简单的封装函数,把模型名、温度、最大token数作为参数传入,返回统一的文本结果。这样你的测试脚本会非常简洁,大部分精力可以花在分析输出质量上,而不是折腾接口调用。