1. 从一个真实困境说起:多模型调用的碎片化之痛
如果你正在做AI应用开发,大概率遇到过这样的场景:项目初期用OpenAI的接口跑通了原型,效果不错,上线后想接入Claude做备用模型,或者想用国产模型降低推理成本,结果发现每家的SDK、鉴权方式、请求格式、返回结构都不一样。你不得不为每个模型写一套适配层,代码里到处是if-else分支,维护成本直线上升。
更麻烦的是,当你想要对比不同模型在同一个任务上的表现时,需要反复切换API Key、改base_url、调整参数格式,测试脚本改到怀疑人生。团队里如果有人想从OpenAI切到其他模型做A/B测试,光是环境配置就能耗掉半天。
这就是litellm要解决的核心问题。它本质上是一个统一的LLM调用层,用一套OpenAI风格的接口去调用市面上几乎所有主流大模型。你只需要把base_url和API Key换一下,代码几乎不用动,就能在OpenAI、Anthropic、Cohere、HuggingFace、Ollama以及各类兼容OpenAI协议的服务之间自由切换。
这篇文章适合三类人看:一是正在做多模型集成的后端开发者,二是需要快速对比模型效果做技术选型的算法工程师,三是想在自己项目里加一个"模型热插拔"能力的独立开发者。我会从实际使用角度出发,把litellm的核心机制、配置细节、踩坑经验和进阶玩法讲透,让你看完就能直接上手。
2. litellm到底做了什么:统一接口背后的映射逻辑
2.1 核心设计思路:用一套协议吃掉所有差异
litellm最核心的价值在于它定义了一套标准化的请求与响应格式,然后为每个支持的模型提供商写了一个适配器。当你调用completion(model="claude-3-sonnet", messages=[...])时,litellm内部会做几件事:
第一,解析model参数,识别出这是Anthropic的模型,找到对应的适配器。第二,把你传入的OpenAI格式的messages转换成Anthropic要求的格式,比如system消息要单独提取出来,不能放在messages数组里。第三,把参数名做映射,比如OpenAI的max_tokens在Anthropic里叫max_tokens_to_sample,litellm会自动转换。第四,发送请求后,把返回结果再转回OpenAI的响应结构,让你拿到的response.choices[0].message.content和调OpenAI时一模一样。
这个映射逻辑听起来简单,但实际覆盖的细节非常多。比如流式响应的处理,不同厂商的SSE格式差异很大,litellm在中间做了一层归一化,保证你拿到的chunk结构一致。再比如function calling,OpenAI叫tools,Anthropic叫tools但参数结构不同,litellm也做了统一。
2.2 支持的模型范围与路由策略
litellm目前支持的模型列表相当长,我整理了几类常用的:
| 提供商类型 | 代表模型 | 接入方式 |
|---|---|---|
| 闭源商业API | GPT-4o、Claude-3.5、Gemini Pro | 官方API Key |
| 开源模型托管 | Llama-3、Mistral、Qwen | Together、Fireworks等平台 |
| 本地部署 | Ollama、vLLM、LM Studio | 本地endpoint |
| 兼容OpenAI协议 | 各类自建推理服务 | 自定义base_url |
路由策略上,litellm支持几种模式。最简单的是直接指定模型名,它会自动路由到对应的提供商。你也可以用model="openai/gpt-4o"这种带前缀的写法,显式指定提供商,避免歧义。更高级的用法是配置fallback列表,当主模型调用失败时自动切换到备用模型,这对生产环境的稳定性很有帮助。
2.3 为什么不用LangChain的LLM抽象
很多人会问:LangChain不是也有多模型支持吗?为什么还要用litellm?
我的实际体验是,LangChain的抽象层太重了。它把LLM调用和Chain、Agent、Memory等概念耦合在一起,你只想调个模型,却要引入一大堆依赖。而且LangChain的版本迭代快,API经常变,升级一次可能就要改代码。
litellm的定位更纯粹:它就是一层薄薄的适配器,不涉及编排逻辑,不强制你用什么框架。你可以把它当成一个独立的工具库,在FastAPI、Flask、Django或者纯脚本里都能用。这种"只做一件事"的设计哲学,在实际项目中反而更灵活。
3. 环境搭建与最小可运行示例
3.1 安装与依赖管理
安装litellm很简单:
pip install litellm但这里有个坑要注意:litellm的依赖包比较多,如果你在一个已有项目里安装,可能会和现有的openai、anthropic等SDK版本冲突。我的建议是在虚拟环境里先装litellm,再装其他包,让它自己决定依赖版本。
如果你只需要用某几个提供商,可以按需安装:
pip install litellm[openai,anthropic]这样能减少不必要的依赖。litellm的extras机制做得不错,支持[proxy]、[all]等多种组合。
3.2 环境变量配置的规范做法
litellm读取API Key的方式是环境变量。每个提供商对应的变量名不同:
export OPENAI_API_KEY="sk-xxx" export ANTHROPIC_API_KEY="sk-ant-xxx" export COHERE_API_KEY="xxx"我习惯用.env文件管理这些密钥,配合python-dotenv加载:
from dotenv import load_dotenv load_dotenv() from litellm import completion response = completion( model="gpt-4o", messages=[{"role": "user", "content": "用一句话解释什么是向量数据库"}] ) print(response.choices[0].message.content)注意:不要把API Key硬编码在代码里,也不要把
.env提交到Git仓库。我见过太多因为密钥泄露导致账单暴涨的案例。
3.3 第一个跨模型调用示例
下面这段代码演示了如何用同一个函数调用三个不同厂商的模型:
from litellm import completion def ask(model, question): response = completion( model=model, messages=[{"role": "user", "content": question}], temperature=0.7, max_tokens=500 ) return response.choices[0].message.content models = ["gpt-4o", "claude-3-5-sonnet-20241022", "command-r-plus"] for m in models: print(f"--- {m} ---") print(ask(m, "解释一下Transformer的自注意力机制"))实测下来,这段代码可以直接跑通,不需要为每个模型写额外的适配代码。唯一需要注意的是,不同模型的max_tokens上限不同,如果设得太大,某些模型会报错。
4. 参数映射与响应结构的那些坑
4.1 参数名不一致的自动转换
litellm帮你做了参数名的映射,但不是所有参数都能完美对应。我整理了几个常见的差异:
| OpenAI参数 | Anthropic对应 | 注意事项 |
|---|---|---|
| max_tokens | max_tokens | 必填,且上限不同 |
| temperature | temperature | 范围都是0-1 |
| top_p | top_p | 一致 |
| stop | stop_sequences | 参数名不同 |
| presence_penalty | 不支持 | 会被忽略 |
当你传入一个目标模型不支持的参数时,litellm默认会忽略它,而不是报错。这个行为有利有弊:好处是代码兼容性好,坏处是你可能以为参数生效了,实际上被静默丢弃了。
我的做法是在关键参数上做显式检查,比如:
response = completion( model="claude-3-5-sonnet-20241022", messages=messages, max_tokens=1000, temperature=0.5 )对于不支持的参数,我会在代码注释里标明,避免团队成员误用。
4.2 流式响应的统一处理
流式输出是很多应用的刚需。litellm的流式接口用起来和OpenAI一致:
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)但这里有个细节:不同厂商的流式chunk结构在litellm归一化之前差异很大。比如Anthropic的流式响应里,第一个chunk可能是message_start类型,最后一个可能是message_stop,中间才是content_block_delta。litellm把这些都转成了统一的delta结构,你不需要关心底层差异。
不过实测中发现,某些国产模型的流式实现不太规范,可能会出现chunk丢失或顺序错乱的情况。如果遇到这种问题,建议先用非流式模式验证功能,再排查流式实现。
4.3 错误处理与重试机制
生产环境里,API调用失败是常态。litellm内置了重试逻辑,可以通过参数控制:
response = completion( model="gpt-4o", messages=messages, num_retries=3, timeout=30 )num_retries指定重试次数,timeout指定超时时间。litellm会对某些错误类型自动重试,比如速率限制(429)和服务器错误(5xx)。但对于认证失败(401)这种错误,重试没有意义,litellm会直接抛出异常。
我建议在调用层再包一层自己的异常处理:
from litellm.exceptions import RateLimitError, APIError try: response = completion(...) except RateLimitError: # 触发降级逻辑或排队等待 pass except APIError as e: # 记录日志,切换到备用模型 pass5. 进阶玩法:代理服务与模型路由
5.1 用litellm proxy搭建统一网关
如果你在团队里推广litellm,不可能让每个人都去配环境变量。这时候可以用litellm的proxy模式,起一个本地服务,所有请求都走这个网关:
litellm --model gpt-4o --port 8000然后你的代码里只需要把base_url指向这个服务:
from openai import OpenAI client = OpenAI(base_url="http://localhost:8000", api_key="anything") response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "hello"}] )这样做的好处是:密钥集中在网关管理,团队成员不需要各自配置;可以在网关上做统一的日志、限流、计费统计;切换模型时只需要改网关配置,客户端代码不动。
proxy模式还支持配置文件,可以定义多个模型、路由规则、fallback策略。配置文件用YAML格式:
model_list: - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY - model_name: claude litellm_params: model: anthropic/claude-3-5-sonnet-20241022 api_key: os.environ/ANTHROPIC_API_KEY router_settings: fallbacks: [{"gpt-4o": ["claude"]}]这个配置定义了两个模型,并且设置了fallback:当gpt-4o调用失败时,自动切换到claude。
5.2 基于成本的智能路由
litellm的router支持更复杂的路由策略,比如根据成本选择最便宜的可用模型:
from litellm import Router router = Router( model_list=[ {"model_name": "cheap", "litellm_params": {"model": "gpt-4o-mini"}}, {"model_name": "expensive", "litellm_params": {"model": "gpt-4o"}}, ], routing_strategy="cost-based-routing" )不过说实话,成本路由在实际项目里用得不多,因为不同模型的能力差异太大,单纯看价格选模型往往效果不好。更实用的是基于延迟的路由,把请求发给响应最快的模型。
5.3 缓存机制减少重复调用
litellm支持多种缓存后端,包括内存、Redis、磁盘。开启缓存后,相同的请求会直接返回缓存结果,省时省钱:
import litellm litellm.cache = litellm.Cache(type="redis", host="localhost", port=6379) response = completion( model="gpt-4o", messages=[{"role": "user", "content": "什么是RAG"}], caching=True )缓存的key是根据请求参数生成的哈希值。如果你希望某些请求不走缓存,可以设置caching=False。需要注意的是,缓存对温度参数敏感,temperature=0的请求缓存命中率最高。
6. 实战中的经验与避坑清单
6.1 模型名称的坑
litellm的模型命名规则有时候不太直观。比如Claude的模型名要带日期后缀,claude-3-5-sonnet-20241022和claude-3-5-sonnet可能指向不同的版本。我建议始终使用完整的模型名,避免因为版本更新导致行为变化。
另外,某些模型需要通过特定前缀指定提供商。比如ollama/llama3表示用Ollama本地部署的Llama3,together_ai/meta-llama/Llama-3-70b表示用Together平台托管的Llama3。如果不加前缀,litellm可能会猜错提供商。
6.2 Token计数与成本估算
litellm提供了token计数功能:
from litellm import token_counter count = token_counter(model="gpt-4o", messages=[{"role": "user", "content": "hello"}]) print(count)但要注意,不同模型的tokenizer不同,计数结果会有差异。litellm内部用的是tiktoken做估算,对于OpenAI系列模型比较准,对于其他模型可能偏差较大。如果需要精确计费,建议以提供商返回的usage字段为准。
成本估算方面,litellm内置了价格表,可以通过completion_cost函数计算:
from litellm import completion_cost cost = completion_cost(completion_response=response)这个价格表更新不一定及时,如果对成本敏感,建议自己维护一份价格配置。
6.3 并发调用与速率限制
批量调用多个模型时,很容易触发速率限制。我的做法是用asyncio配合信号量控制并发:
import asyncio from litellm import acompletion async def call_model(model, question, semaphore): async with semaphore: response = await acompletion( model=model, messages=[{"role": "user", "content": question}] ) return response.choices[0].message.content async def main(): semaphore = asyncio.Semaphore(5) tasks = [call_model(m, "test", semaphore) for m in models] results = await asyncio.gather(*tasks) return results信号量设为5表示最多同时5个请求。具体数值要根据提供商的速率限制调整。OpenAI的免费额度通常限制比较严,付费账户会宽松很多。
6.4 日志与可观测性
litellm可以输出详细的调用日志,方便排查问题:
import litellm litellm.set_verbose = True开启后,每次调用都会打印请求参数、响应内容、耗时等信息。生产环境建议把日志接入统一的日志系统,而不是直接打印到控制台。
另外,litellm支持回调机制,可以在调用前后执行自定义逻辑:
def log_callback(kwargs, completion_response, start_time, end_time): # 记录到数据库或监控系统 pass litellm.success_callback = [log_callback]这个机制可以用来做用量统计、异常告警、性能监控等。
7. 我对litellm的实际使用体会
用了一年多litellm,最大的感受是它确实解决了多模型调用的碎片化问题,但也不是银弹。它的优势在于轻量、灵活、上手快,适合快速原型和中小型项目。但如果你的项目对性能要求极高,或者需要深度定制某个提供商的特性,可能还是直接调原生SDK更合适。
另外,litellm的版本迭代比较快,升级时要注意看changelog,有些breaking change不会在文档里特别标注。我的习惯是锁定版本号,在测试环境验证后再升级生产环境。
最后分享一个小技巧:如果你不确定某个模型名怎么写,可以用litellm.model_list查看所有支持的模型,或者直接看源码里的model_prices_and_context_window.json文件,里面列出了所有模型及其参数。这个文件也是更新价格表的地方,如果发现价格不对,可以手动改。