1. 从一个真实困境说起:多模型调用的碎片化之痛
如果你正在做AI应用开发,大概率遇到过这样的场景:项目初期用某一家的大模型API跑通了原型,效果不错,上线后想换个更便宜的模型降本,结果发现代码里到处都是openai.ChatCompletion.create的调用,换一家就得改一遍请求格式、认证方式、返回解析逻辑。更别提还要同时接入三四家模型做A/B测试,或者根据任务类型动态路由到不同模型——每接一家就是一套SDK、一套异常处理、一套重试逻辑,维护成本直线上升。
这就是litellm要解决的核心问题。它本质上是一个统一的LLM调用层,用一套OpenAI兼容的接口格式,去调用市面上几乎所有主流大模型服务。你写一次代码,改一个字符串参数就能切换模型提供商,不用改任何请求结构。对于需要快速迭代、多模型对比、成本优化的团队来说,这东西能省掉大量胶水代码。
我第一次接触它是在一个多模型评测项目里,当时需要同时对接好几家模型做同一批prompt的效果对比。按传统做法,我得为每家写一个适配器类,处理不同的认证头、不同的参数命名、不同的流式返回格式。光是统一返回结构就花了两天。后来换成litellm,核心代码从几百行缩到了几十行,切换模型只需要改model="xxx"这个参数。这篇文章就把我在实际使用中积累的经验、踩过的坑、以及一些官方文档没细说的细节,完整地梳理一遍。
适合谁看?如果你正在做AI应用开发,需要调用多个模型服务,或者想给自己的项目加一个"模型无关"的抽象层,这篇内容应该能帮你少走不少弯路。即使你只用一家模型,了解这套统一接口的设计思路,对理解LLM API的通用模式也有帮助。
2. litellm到底做了什么:核心机制拆解
2.1 统一接口的本质:请求翻译与响应归一
litellm的核心工作可以用一句话概括:把OpenAI格式的请求翻译成目标提供商的格式,再把目标提供商的响应翻译回OpenAI格式。它不代理模型推理,不存储你的数据,只是一个轻量的转换层。
具体来说,当你调用litellm.completion(model="某模型", messages=[...])时,内部发生了这几件事:
- 模型路由:根据
model参数的前缀或映射表,确定目标提供商和实际模型名。比如model="gpt-4"走OpenAI,model="claude-3-opus"走Anthropic,model="gemini-pro"走Google。 - 请求转换:把统一的
messages、temperature、max_tokens等参数,转换成目标API要求的字段名和格式。比如Anthropic要求system单独传,Google要求contents结构不同。 - 认证注入:从环境变量或显式参数中读取对应提供商的API Key,注入到请求头或请求体中。
- 响应归一:把不同提供商的返回结构统一成OpenAI的
ModelResponse对象,包含choices、usage、finish_reason等标准字段。 - 异常映射:把各家的错误码和错误信息,映射成统一的异常类型,方便上层统一处理。
这个设计的好处是,你的业务代码只依赖一套接口,模型切换、多模型并行、降级策略都变得非常轻量。
2.2 支持的提供商范围与接入方式
litellm支持的提供商数量相当可观,覆盖了主流商业API和自部署方案。按接入方式大致分几类:
| 类别 | 代表提供商 | 接入特点 |
|---|---|---|
| 商业API | OpenAI、Anthropic、Google、Cohere、Mistral | 需要API Key,按量计费 |
| 云平台托管 | Azure OpenAI、AWS Bedrock、Vertex AI | 需要云平台认证配置 |
| 自部署/本地 | Ollama、vLLM、HuggingFace TGI | 指向本地端点,无需外部Key |
| 聚合平台 | 各类模型聚合服务 | 统一Key,多模型路由 |
接入方式上,最常用的是环境变量配置。比如设置OPENAI_API_KEY、ANTHROPIC_API_KEY等,litellm会自动读取。也可以在代码里显式传api_key参数,适合多租户场景下动态指定。
对于自部署模型,通过api_base参数指向本地服务地址即可。比如Ollama默认在http://localhost:11434,vLLM通常暴露OpenAI兼容端点,直接配api_base就能用。
2.3 与直接调用SDK的对比:什么时候值得用
不是所有场景都适合上litellm。我总结了一个简单的判断标准:
适合用的场景:
- 需要同时接入两家以上模型服务
- 要做模型A/B测试或动态路由
- 希望业务代码与具体提供商解耦
- 需要统一的成本追踪和日志
- 想用一套重试/降级逻辑覆盖所有模型
可能不需要的场景:
- 只用一家模型,且短期内不会换
- 对延迟极度敏感,不能接受任何额外转换开销
- 需要用到某家提供商的独有高级功能(如特定的函数调用格式、特殊的流式控制)
实测下来,litellm的转换开销在毫秒级,对于大多数应用来说可以忽略。但如果你要做高频低延迟的推理,比如每秒几百次调用,这个开销就需要评估了。
注意:litellm的版本迭代很快,不同版本支持的提供商和参数可能有差异。生产环境建议锁定版本,升级前先在测试环境验证。
3. 环境搭建与最小可运行示例
3.1 安装与依赖管理
安装本身很简单:
pip install litellm但有几个细节值得注意。litellm的核心包比较轻量,但如果你要用到某些提供商的特定功能,可能需要额外安装依赖。比如用AWS Bedrock需要boto3,用Google Vertex AI需要google-cloud-aiplatform。这些在官方文档里有说明,但容易漏看。
我的建议是,在requirements.txt或pyproject.toml里明确锁定版本,并且把提供商相关的依赖单独列出来。因为litellm的依赖策略是"按需引入",核心包不会强制装所有提供商的SDK,这本身是好事,但如果你在Docker镜像里构建,可能会遇到运行时才发现缺依赖的情况。
# 核心包 litellm==1.x.x # 按需添加 boto3 # AWS Bedrock google-cloud-aiplatform # Vertex AI另外,如果你用Poetry或PDM管理依赖,注意litellm的某些版本对Python版本有要求,建议Python 3.9以上。
3.2 环境变量配置的坑
litellm读取环境变量的逻辑是:优先用显式传入的参数,其次读环境变量。环境变量的命名规则基本是{PROVIDER}_API_KEY,但有几个例外容易踩坑。
比如Azure OpenAI,它需要同时设置AZURE_API_KEY、AZURE_API_BASE、AZURE_API_VERSION三个变量,少一个都会报错。而且AZURE_API_BASE的格式必须是完整的URL,不能只写资源名。
再比如,如果你同时配了OPENAI_API_KEY和AZURE_API_KEY,调用时model参数写azure/xxx才会走Azure,写gpt-4默认走OpenAI。这个路由规则要记清楚,否则会出现"明明配了Key却报认证失败"的情况。
我习惯在项目里用一个.env文件管理这些变量,配合python-dotenv加载。但要注意,.env文件不要提交到版本控制,尤其是包含Key的文件。
import os from dotenv import load_dotenv load_dotenv() # 确认关键变量已加载 assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY not set"3.3 第一个可运行调用
最小示例:
import litellm response = litellm.completion( model="gpt-3.5-turbo", messages=[ {"role": "user", "content": "用一句话解释什么是统一接口"} ] ) print(response.choices[0].message.content) print(response.usage.total_tokens)这段代码跑通后,你可以试着把model换成其他提供商的名字,比如claude-3-haiku-20240307或gemini-pro,只要对应的API Key配好了,代码不用改其他任何地方。
这里有个小技巧:如果你不确定某个模型的确切名称,可以用litellm.model_list查看当前版本支持的模型列表。不过这个列表可能很长,建议配合过滤使用。
# 查看包含"claude"的模型 models = [m for m in litellm.model_list if "claude" in m.lower()] print(models[:10])3.4 流式输出的处理差异
流式输出是实际项目里绕不开的。litellm支持stream=True,返回一个迭代器,每个chunk是ModelResponse对象。但不同提供商的流式行为有差异,需要留意。
OpenAI的流式返回中,最后一个chunk通常包含finish_reason,但usage字段可能为空。Anthropic的流式返回结构不同,litellm会做归一化,但某些字段的填充时机可能不一致。实测中我发现,如果要统计流式调用的token消耗,最好在流结束后单独取一次usage,或者用litellm的回调机制。
response = litellm.completion( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "写一段100字的产品介绍"}], stream=True ) full_content = "" for chunk in response: delta = chunk.choices[0].delta.content if delta: full_content += delta print(delta, end="", flush=True) print() # 换行提示:流式调用时,如果客户端断开连接,记得处理异常并释放资源。litellm底层用的是HTTP连接池,长时间不释放可能导致连接泄漏。
4. 多模型路由与降级策略的实战设计
4.1 基于任务类型的动态路由
实际项目里,不同任务对模型的要求不同。简单分类任务用便宜的小模型就够了,复杂推理才需要上大模型。litellm的model参数是运行时决定的,这为动态路由提供了天然支持。
我通常会在配置层定义一个路由表,把任务类型映射到模型列表,然后按优先级尝试:
ROUTING_TABLE = { "simple_classification": ["gpt-3.5-turbo", "claude-3-haiku-20240307"], "complex_reasoning": ["gpt-4", "claude-3-opus-20240229"], "code_generation": ["gpt-4", "claude-3-sonnet-20240229"], } def route_call(task_type, messages): models = ROUTING_TABLE.get(task_type, ["gpt-3.5-turbo"]) last_error = None for model in models: try: return litellm.completion(model=model, messages=messages) except Exception as e: last_error = e continue raise last_error这个模式的好处是,当主模型不可用或超时时,自动降级到备用模型。但要注意,不同模型的输出风格可能不一致,如果业务对输出格式有严格要求,降级后可能需要额外的后处理。
4.2 成本感知的模型选择
litellm提供了成本追踪功能,可以通过回调获取每次调用的费用估算。这对于需要控制预算的项目很实用。
from litellm import completion import litellm # 开启成本追踪 litellm.success_callback = ["cost"] response = completion( model="gpt-4", messages=[{"role": "user", "content": "测试"}] ) # 获取本次调用成本 cost = litellm.completion_cost(completion_response=response) print(f"本次调用成本: ${cost:.6f}")基于这个能力,可以做一个简单的成本感知路由:先估算prompt的token数,如果超过阈值就路由到更便宜的模型,或者先做摘要再调用大模型。
不过要注意,成本估算依赖litellm内置的价格表,这个表可能不是实时更新的。如果提供商调价了,估算值会有偏差。生产环境建议定期核对实际账单。
4.3 超时、重试与异常处理
多模型调用的异常处理比单模型复杂得多。不同提供商的超时行为、限流策略、错误码都不一样。litellm做了统一映射,但有些细节需要自己处理。
我的一般做法是:
- 设置合理的超时:通过
timeout参数控制,默认值可能偏长,建议根据业务SLA调整。 - 配置重试:litellm支持
num_retries参数,但重试策略要结合提供商特性。比如某些提供商的限流是短时间窗口,重试间隔太短没意义。 - 区分可重试和不可重试异常:认证失败、参数错误重试没用,限流和超时才值得重试。
from litellm import completion import litellm try: response = completion( model="gpt-4", messages=[{"role": "user", "content": "测试"}], timeout=30, num_retries=2 ) except litellm.RateLimitError as e: # 限流,可以降级或排队 print(f"限流: {e}") except litellm.AuthenticationError as e: # 认证失败,重试无意义 print(f"认证失败: {e}") except litellm.Timeout as e: # 超时,考虑降级 print(f"超时: {e}")注意:litellm的异常类型在不同版本间可能有调整,升级时留意changelog。我遇到过某个版本把
Timeout改成了APITimeoutError,导致捕获失效。
4.4 多模型并行调用的实现
有时候需要同时调用多个模型,然后对比结果或投票。litellm本身是同步的,但可以配合asyncio或线程池实现并行。
import asyncio from litellm import acompletion async def call_model(model, messages): try: return await acompletion(model=model, messages=messages) except Exception as e: return {"error": str(e), "model": model} async def parallel_calls(models, messages): tasks = [call_model(m, messages) for m in models] return await asyncio.gather(*tasks) # 使用 models = ["gpt-3.5-turbo", "claude-3-haiku-20240307"] results = asyncio.run(parallel_calls(models, [{"role": "user", "content": "你好"}]))并行调用要注意各提供商的并发限制。有些提供商的免费额度并发很低,同时发太多请求会触发限流。建议根据实际配额控制并发数。
5. 那些官方文档没细说的坑与排查过程
5.1 模型名称映射的隐式规则
litellm的模型名称解析有一套隐式规则,官方文档没有完整列出。我踩过的坑包括:
- 某些提供商的模型名需要加前缀,比如
azure/gpt-4、bedrock/anthropic.claude-v2,不加前缀会路由到错误的提供商。 - 同一模型在不同提供商可能有不同名称,比如Claude在Anthropic叫
claude-3-opus-20240229,在Bedrock叫anthropic.claude-3-opus-20240229-v1:0。 - 某些模型名有别名,比如
gpt-4可能指向最新版本,但具体指向哪个版本取决于litellm的映射表。
排查方法:如果调用报"model not found",先用litellm.get_model_info(model)查看litellm对这个模型的理解,确认它路由到了哪个提供商。
info = litellm.get_model_info("gpt-4") print(info) # 输出包含 provider、max_tokens、input_cost_per_token 等信息5.2 参数传递的兼容性陷阱
不同提供商支持的参数集不一样。比如top_p、frequency_penalty、presence_penalty这些参数,OpenAI支持,但某些提供商可能不支持或行为不同。litellm会尽量传递,但不支持的参数可能被静默忽略,或者导致报错。
我遇到过一个典型问题:给Anthropic传frequency_penalty,litellm没有报错,但实际请求中这个参数被丢弃了,导致输出效果和预期不符。排查时用litellm.set_verbose(True)打开详细日志,才看到实际发出的请求体里没有这个参数。
import litellm litellm.set_verbose = True # 打印详细请求日志 response = litellm.completion( model="claude-3-haiku-20240307", messages=[{"role": "user", "content": "测试"}], frequency_penalty=0.5 # 可能被忽略 )建议:对于关键参数,先在小规模测试中验证是否生效,不要假设所有提供商都支持全部参数。
5.3 流式调用中的usage统计问题
前面提到过流式调用的usage问题,这里展开说。OpenAI的流式返回中,usage信息通常在最后一个chunk,但需要设置stream_options={"include_usage": True}才会返回。Anthropic的流式返回结构不同,litellm做了归一化,但usage的填充时机可能不一致。
我的解决方案是:在流结束后,用litellm.completion_cost基于完整响应计算成本,或者自己维护一个token计数器。如果对成本统计精度要求高,建议关闭流式,用非流式调用获取准确的usage。
5.4 版本升级导致的接口变更
litellm迭代快,minor版本之间可能有breaking change。我遇到过两次:
一次是异常类型重命名,导致except litellm.Timeout失效。另一次是completion函数的某个参数默认值变了,导致行为不一致。
应对策略:
- 生产环境锁定版本,用
pip freeze或poetry.lock固定。 - 升级前在测试环境跑一遍核心用例。
- 关注项目的release notes,特别是标了"breaking"的条目。
5.5 自部署模型的接入细节
接入Ollama或vLLM这类自部署服务时,api_base的配置有讲究。Ollama的OpenAI兼容端点默认在http://localhost:11434/v1,注意要带/v1后缀。vLLM启动时如果指定了--api-key,调用时也要传对应的Key,即使本地服务通常不校验。
response = litellm.completion( model="ollama/llama2", messages=[{"role": "user", "content": "你好"}], api_base="http://localhost:11434" )另外,自部署模型的响应格式可能和OpenAI有细微差异,litellm会尽量兼容,但某些边缘字段可能缺失。如果业务依赖这些字段,需要做兼容处理。
6. 把litellm用稳的几个工程化建议
6.1 配置集中管理与环境隔离
不要把API Key散落在代码里。我习惯用一个统一的配置模块,从环境变量读取,并做启动时校验。
# config.py import os class LLMConfig: OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "gpt-3.5-turbo") TIMEOUT = int(os.getenv("LLM_TIMEOUT", "30")) MAX_RETRIES = int(os.getenv("LLM_MAX_RETRIES", "2")) @classmethod def validate(cls): missing = [] if not cls.OPENAI_API_KEY: missing.append("OPENAI_API_KEY") if missing: raise ValueError(f"缺少环境变量: {', '.join(missing)}")不同环境(开发、测试、生产)用不同的.env文件,通过ENV变量切换。这样本地开发可以用便宜的模型,生产环境用稳定的模型。
6.2 日志与可观测性
多模型调用的日志比单模型重要得多。我建议至少记录:请求的模型、prompt长度、响应时间、token消耗、是否降级、错误信息。
litellm支持回调机制,可以挂载自定义的日志处理器:
import litellm import logging logger = logging.getLogger("llm_calls") def log_callback(kwargs, completion_response, start_time, end_time): logger.info({ "model": kwargs.get("model"), "duration": (end_time - start_time).total_seconds(), "usage": completion_response.usage.dict() if completion_response else None }) litellm.success_callback = [log_callback] litellm.failure_callback = [log_callback]这些日志在排查问题时非常有用,尤其是当某个提供商的响应变慢或错误率上升时,能快速定位。
6.3 缓存策略减少重复调用
对于重复的prompt,可以用缓存避免重复调用。litellm内置了缓存支持,可以配置Redis或本地缓存。
import litellm from litellm.caching import Cache litellm.cache = Cache(type="redis", host="localhost", port=6379) response = litellm.completion( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "固定问题"}], caching=True )缓存对开发调试特别有用,同样的prompt不用反复花钱。但要注意,缓存key通常基于prompt和参数,如果模型版本更新了,缓存可能返回旧结果。建议设置合理的TTL,并在模型切换时清理缓存。
6.4 测试策略:mock与真实调用分离
单元测试不应该依赖真实API调用。litellm支持mock模式,可以返回预设响应。
import litellm from litellm import completion # 开启mock litellm.mock_completion = True response = completion( model="gpt-4", messages=[{"role": "user", "content": "测试"}] ) # 返回mock响应,不实际调用API对于集成测试,建议用一个专门的测试账号,设置较低的配额,避免测试消耗太多预算。同时,测试用例要覆盖降级路径,确保主模型不可用时备用模型能正常工作。
6.5 成本控制的实际手段
除了前面提到的成本追踪,还有几个实用手段:
- 设置预算上限:在应用层维护一个累计成本计数器,超过阈值就拒绝新请求或降级到便宜模型。
- prompt压缩:调用前对长prompt做摘要或截断,减少token消耗。
- 结果缓存:前面提到的缓存策略,对重复查询效果显著。
- 模型分级:非关键路径用便宜模型,关键路径才用大模型。
我在一个项目里做过统计,通过模型分级和缓存,整体成本降低了约60%,而用户体验没有明显下降。关键是要先摸清哪些调用是"必须用大模型"的,哪些是"可以用小模型凑合"的。
6.6 团队协作中的接口约定
如果团队多人开发,建议约定好模型调用的统一入口,不要各自直接调litellm。可以封装一个内部SDK,统一处理认证、重试、日志、成本追踪,业务代码只调用这个封装层。
# internal_llm.py from litellm import completion from config import LLMConfig def call_llm(messages, task_type="default", **kwargs): model = select_model(task_type) try: return completion( model=model, messages=messages, timeout=LLMConfig.TIMEOUT, num_retries=LLMConfig.MAX_RETRIES, **kwargs ) except Exception as e: log_error(e, model, messages) raise这样做的另一个好处是,将来如果要换掉litellm或升级版本,只需要改这一个文件,业务代码不受影响。
提示:封装层不要过度设计,保持接口简单。我见过把litellm封装了七八层的项目,最后调试时得一层层扒代码,反而降低了效率。两三层封装足够了。
7. 一些零散但实用的经验碎片
关于litellm的drop_params参数。这个参数设为True时,litellm会自动丢弃目标提供商不支持的参数,而不是报错。这在多模型路由时很有用,但也会掩盖问题。我的建议是:开发阶段设为False,让不兼容的参数暴露出来;生产环境设为True,保证调用不中断。
关于token计数。litellm提供了token_counter函数,可以在调用前估算token数。但不同提供商的tokenizer不一样,估算值可能有偏差。对于成本敏感的场景,建议用实际调用后的usage为准。
from litellm import token_counter count = token_counter(model="gpt-4", messages=[{"role": "user", "content": "测试文本"}]) print(f"估算token数: {count}")关于代理配置。如果你的网络环境需要走代理,litellm支持通过环境变量或参数配置。但要注意,某些提供商的SDK可能有自己的代理逻辑,需要单独配置。这块容易出问题,建议先用curl测试网络连通性,再排查litellm的配置。
关于异步支持。litellm提供了acompletion异步接口,适合在高并发场景使用。但异步调用的异常处理和同步略有不同,需要配合asyncio的异常机制。如果团队对异步不熟悉,建议先用同步接口,性能不够再考虑异步。
关于模型弃用。提供商会定期弃用旧模型,litellm的映射表更新可能滞后。如果发现某个模型突然报"not found",先查提供商的官方公告,确认模型是否已下线,然后更新到新模型名。
关于多租户场景。如果每个租户用自己的API Key,需要在调用时显式传api_key参数,而不是依赖环境变量。同时要注意Key的存储安全,不要明文存在数据库里。
关于响应格式的稳定性。不同提供商的响应格式可能有细微差异,比如finish_reason的取值、usage字段的完整性。如果业务依赖这些字段,建议做一层兼容处理,不要假设所有提供商都返回完全一致的结构。
关于版本选择。litellm的稳定版和最新版之间可能有功能差异。我的经验是,生产环境用稳定版,新功能先在测试环境验证。如果遇到bug,先查GitHub issues,看看是否已有修复或workaround。
关于社区资源。litellm的文档覆盖了主要功能,但细节问题往往在GitHub issues和讨论区能找到答案。遇到问题时,先用set_verbose(True)打印详细日志,然后带着日志去搜索,效率会高很多。
关于性能优化。如果发现litellm的转换开销成为瓶颈,可以考虑几个方向:复用HTTP连接、减少不必要的参数转换、对高频调用的模型做本地缓存。但大多数场景下,网络延迟才是主要瓶颈,litellm的开销可以忽略。
关于错误重试的退避策略。简单的固定间隔重试在限流场景下效果不好。建议用指数退避,并且区分错误类型。litellm的num_retries用的是内置策略,如果需要更精细的控制,可以自己实现重试逻辑。
关于测试覆盖率。多模型调用的测试要覆盖:正常调用、超时、限流、认证失败、模型不存在、参数不兼容。这些场景在真实环境中都会遇到,提前测试能避免上线后手忙脚乱。
关于文档维护。如果团队多人使用,建议维护一份内部文档,记录:支持的模型列表、各模型的适用场景、已知的坑和workaround、成本参考。这份文档比官方文档更贴合团队实际,新人上手会快很多。
关于监控告警。建议对LLM调用的成功率、延迟、成本设置告警阈值。当某个提供商的错误率上升或延迟增加时,能及时发现并切换。这块可以和现有的APM系统集成,不用单独造轮子。
关于数据安全。调用外部API时,注意不要发送敏感数据。如果业务涉及用户隐私,建议在调用前做脱敏处理,或者用自部署模型。litellm本身不存储数据,但数据会经过提供商的服务器,这一点要清楚。
关于合规性。不同提供商的数据处理政策不同,有些会用于模型训练,有些不会。如果业务对数据合规有要求,选择提供商时要仔细阅读条款。litellm不改变这些政策,只是调用层。
关于长期维护。litellm是一个活跃的开源项目,但依赖单一项目总有风险。如果业务高度依赖,建议关注项目的维护状态,必要时准备备选方案。不过从目前来看,这个项目的社区活跃度和更新频率都还不错。
我在实际使用中最大的体会是:litellm解决的是"多模型调用"这个具体问题,它不解决模型效果、不解决prompt工程、不解决业务逻辑。把它放在合适的位置,作为基础设施层的一个组件,它能发挥很大价值。但如果指望它解决所有LLM应用的问题,那会失望。工具就是工具,用对场景最重要。