
1. 从一个真实困境说起多模型调用的碎片化之痛如果你正在做AI应用开发大概率遇到过这样的场景项目初期用某家云服务商的API跑通了原型效果不错但老板突然说我们要支持国产模型成本降下来或者产品经理跑来说用户反馈某海外模型的回答质量更好能不能加个切换按钮。这时候你打开代码一看每个模型的调用方式都不一样——有的用OpenAI的SDK有的用自家封装的HTTP接口参数命名五花八门返回结构更是各说各话。改一处调用可能要动三四个文件测试一遍下来半天就没了。这就是litellm要解决的核心问题。它是一个用Python写的开源库目标非常直接用一套统一的OpenAI格式接口去调用市面上几乎所有主流的大语言模型。你只需要把原来代码里的openai.ChatCompletion.create换成litellm.completion然后在参数里加一个model某平台/某模型剩下的请求格式、响应解析、错误处理它帮你全部抹平。我第一次接触litellm是在一个需要同时对接四家模型供应商的内部工具项目上。当时团队评估过几种方案自己写一层适配器、用LangChain的LLM抽象、或者直接在每个业务模块里硬编码不同SDK。自己写适配器工作量太大LangChain又太重最后选了litellm原因很简单——它的接口设计几乎零学习成本任何用过OpenAI SDK的人十分钟就能上手。这篇文章适合三类人看第一类是做AI应用开发、需要频繁切换或对比不同模型的工程师第二类是想在自己的产品里给用户提供模型选择功能的开发者第三类是对LLM应用架构感兴趣、想了解统一调用层设计思路的技术人。我会从litellm的核心机制讲起拆解它的配置方式、路由策略、成本追踪、异常处理再结合我实际踩过的坑把那些官方文档里不会写的细节全部摊开来讲。2. litellm到底做了什么统一接口背后的映射逻辑2.1 核心设计思路把差异全部收进一个函数litellm最核心的入口就是litellm.completion()这个函数。它的参数签名和OpenAI的ChatCompletion.create几乎一模一样——model、messages、temperature、max_tokens、stream你原来怎么写的现在基本不用改。区别在于model这个字段的写法litellm用供应商前缀/模型名的格式来识别你要调用哪家的服务。比如你要调用某海外平台的模型写modelopenai/gpt-4o要调用某国内平台的模型写modeldeepseek/deepseek-chat要调用本地部署的Ollama写modelollama/llama3。litellm内部维护了一张巨大的映射表把每个供应商的API端点、认证方式、请求格式、响应结构全部登记在案。当你传入model参数时它做的第一件事就是解析前缀找到对应的Provider配置然后把你的统一请求翻译成该供应商能听懂的格式。这个翻译过程比想象中复杂。不同供应商的差异不只是URL不同还包括认证头是Authorization: Bearer还是x-api-keymessages里的role字段是否支持systemtemperature的取值范围是0到1还是0到2流式返回的chunk结构是delta.content还是text错误码的语义是否一致。litellm为每个Provider写了一个适配类把这些差异全部封装在内部。你作为调用方看到的永远是OpenAI格式的请求和响应。2.2 支持的模型范围从云端到本地从文本到多模态litellm目前支持的Provider数量已经超过100个覆盖了几乎所有你能想到的模型服务。我把它大致分成几类方便你快速判断自己的场景是否适用类别典型Provider适用场景海外商业APIOpenAI、Anthropic、Cohere、Mistral追求模型能力上限预算充足国内商业APIDeepSeek、智谱、Moonshot、百川成本敏感需要中文优化云平台托管Azure OpenAI、AWS Bedrock、Vertex AI企业已有云服务合同合规要求高本地部署Ollama、vLLM、HuggingFace TGI数据不出内网需要完全可控聚合平台OpenRouter、Together AI想用一个Key访问多家模型这个覆盖面意味着你可以在不改动业务代码的前提下把请求从一家切到另一家。我做过一个测试同一个completion调用只改model参数分别打到四家不同的服务上返回的响应结构完全一致业务层的解析代码一行都不用动。这种无感切换的能力在需要做模型对比评测或者灰度发布时特别有用。2.3 安装与最小可用示例安装litellm非常简单一条pip命令搞定pip install litellm如果你需要用到某些特定Provider的SDK比如AWS Bedrock需要boto3litellm会提示你安装对应的额外依赖。我建议一开始只装基础包遇到报错再按提示补装避免一次性引入太多不必要的依赖。最小可用示例大概长这样import litellm response litellm.completion( modeldeepseek/deepseek-chat, messages[ {role: system, content: 你是一个简洁的助手。}, {role: user, content: 用一句话解释什么是统一调用层。} ], api_key你的API Key ) print(response.choices[0].message.content)注意api_key这个参数。litellm支持两种传法一种是像上面这样直接传适合快速测试另一种是通过环境变量比如DEEPSEEK_API_KEY、OPENAI_API_KEYlitellm会自动读取。生产环境我强烈建议用环境变量避免Key硬编码在代码里。提示litellm的completion函数返回的对象结构和OpenAI SDK的返回完全一致response.choices[0].message.content、response.usage.prompt_tokens这些字段都可以直接用。如果你原来代码里有基于OpenAI响应写的解析逻辑迁移过来零成本。3. 配置体系与路由策略让多模型调用真正可控3.1 环境变量与配置文件两种管理方式的取舍litellm的配置管理有两种模式我分别说说适用场景。第一种是纯环境变量。每个Provider对应一组环境变量比如OPENAI_API_KEY、ANTHROPIC_API_KEY、AZURE_API_KEY加上AZURE_API_BASE和AZURE_API_VERSION。这种方式适合Provider数量少、部署环境简单的项目。优点是零配置文件容器化部署时直接注入环境变量就行。缺点是当Provider超过五六个之后环境变量列表会变得很长而且不同环境的Key管理容易混乱。第二种是配置文件加Router。litellm提供了一个Router类你可以用一个YAML或JSON文件定义所有模型列表、路由策略、重试规则、预算限制。这种方式适合Provider多、需要精细控制的场景。配置文件大概长这样model_list: - model_name: fast-model litellm_params: model: deepseek/deepseek-chat api_key: os.environ/DEEPSEEK_API_KEY - model_name: quality-model litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY router_settings: routing_strategy: latency-based-routing num_retries: 3 timeout: 30这里有个关键设计model_name是你自己起的别名业务代码里用这个别名调用而不是直接写具体的Provider模型名。这样做的好处是当你想把fast-model从DeepSeek换成另一个更便宜的模型时只需要改配置文件业务代码完全不动。我在项目里会把模型按用途分组比如fast-model用于简单分类任务quality-model用于复杂推理local-model用于敏感数据处理每个别名背后可以挂多个实际模型做负载均衡。3.2 路由策略延迟优先还是成本优先Router支持多种路由策略我重点讲三种最常用的。延迟优先latency-based-routinglitellm会记录每个模型最近的响应时间把请求优先发给当前最快的那个。这个策略适合对响应速度敏感的场景比如实时对话。但要注意延迟数据是滑动窗口统计的如果某个模型突然变慢需要几次请求之后才会被降权。成本优先cost-based-routing优先选择每Token成本最低的模型。适合大批量离线处理任务比如批量摘要、数据清洗。我在一个日志分析项目里用这个策略把大部分请求路由到了最便宜的模型上只有检测到复杂内容时才升级到高质量模型整体成本降了将近七成。权重分配weighted-routing你给每个模型配一个权重litellm按权重随机分配。这个策略适合做A/B测试或者灰度发布。比如新模型上线先给它10%的流量观察一周没问题再逐步加量。配置方式是在model_list里给同一个model_name配多个条目然后设置routing_strategy。这里有个细节如果你用的是延迟优先策略litellm默认会做冷却处理——某个模型连续失败几次后会被暂时移出候选列表过一段时间再重新尝试。这个冷却时间可以通过cooldown_time参数调整默认是60秒。我在实际使用中发现对于偶尔抽风的API这个机制能有效避免请求堆积在故障节点上。3.3 回退与重试让调用链更健壮多模型调用的一个核心价值就是容错。litellm的Router支持配置fallbacks当主模型调用失败时自动切换到备用模型。配置大概是这样router_settings: fallbacks: [{fast-model: [backup-model]}] num_retries: 2 retry_after: 1这段配置的意思是当fast-model调用失败时自动尝试backup-model每个模型最多重试2次重试间隔1秒。我建议把fallbacks设计成链式结构比如主模型失败切到备用模型备用模型再失败切到本地模型形成三级保险。但这里有个坑要注意不是所有错误都适合重试。比如认证失败401或者请求格式错误400重试多少次都没用反而浪费时间和配额。litellm默认会对所有异常做重试你需要在代码里判断异常类型对不可重试的错误直接抛出。我通常会在外层包一个try-except捕获litellm.exceptions.AuthenticationError和litellm.exceptions.BadRequestError这两类直接记录日志并告警不走重试逻辑。注意回退机制会增加整体延迟。如果主模型的超时设置是30秒回退模型也要30秒最坏情况下用户要等60秒才能拿到结果。我的做法是把主模型超时设短一些比如15秒回退模型设长一些同时在业务层加一个总超时控制。4. 成本追踪与预算控制把每一分钱都算清楚4.1 Token计数与成本计算的内置逻辑litellm内置了一个成本计算模块每次调用完成后它会根据返回的usage字段包含prompt_tokens和completion_tokens乘以对应模型的价格算出这次调用的花费。价格数据维护在litellm的源码里覆盖了大部分主流模型。你可以通过litellm.completion_cost(response)这个函数直接拿到本次调用的成本。这个功能看起来简单但实际用起来有几个细节值得注意。第一流式调用的成本计算。流式模式下usage字段可能不会在最后一个chunk里返回litellm需要自己累计Token数量。我实测下来大部分Provider的流式响应都能正确计算但个别Provider的chunk里不带usage信息这时候litellm会用一个估算值准确度会打折扣。如果你对成本精度要求高建议在流式场景下额外做一次非流式的Token计数校验。第二缓存命中的成本。有些Provider对缓存命中的Token收费更低litellm的成本计算会区分cache_creation_input_tokens和cache_read_input_tokens分别按不同单价计算。这个细节在官方文档里写得比较隐蔽但如果你用到了Prompt Caching功能一定要确认成本计算是否正确。第三自定义模型的价格。如果你用的是litellm官方价格表里没有的模型比如自己微调后部署的模型需要手动注册价格from litellm import register_model register_model({ my-custom-model: { input_cost_per_token: 0.000001, output_cost_per_token: 0.000002, litellm_provider: openai, mode: chat } })注册之后成本计算和预算控制就能正常工作了。4.2 预算限制与告警避免账单失控litellm的Router支持给每个模型或每个用户设置预算上限。配置方式是在model_list里加max_budget参数或者在Router初始化时设置budget_duration比如30d表示30天。当累计花费超过预算时litellm会抛出BudgetExceededError你可以捕获这个异常做降级处理。我在一个面向内部团队的AI工具里用了这个功能。每个团队每月有固定的Token预算超了之后自动切换到更便宜的模型同时给管理员发告警。实现方式是在Router配置里给每个团队建一个虚拟Key绑定预算和模型列表。litellm的虚拟Key机制支持细粒度的权限控制比如限制只能调用某几个模型、限制每分钟请求数、限制总Token数。这里有个经验预算检查是在请求发出前做的但成本是在请求完成后才计算的。这意味着如果预算刚好在边界上可能会出现最后一次调用超预算的情况。对于预算敏感的场景我建议把max_budget设得比实际预算低10%左右留出缓冲空间。4.3 成本数据的导出与分析litellm支持把每次调用的成本数据写入回调函数你可以对接自己的监控系统。常用的回调包括success_callback和failure_callback前者在调用成功时触发后者在失败时触发。回调函数接收完整的请求和响应对象你可以从中提取模型名、Token数、成本、延迟等指标写入数据库或时序数据库。我自己的做法是用success_callback把数据写到本地的SQLite里然后用一个简单的看板做可视化。关键指标包括每个模型的日均调用次数、平均延迟、平均成本、错误率。这些数据对于后续的模型选型和路由策略调整非常有价值。比如我发现某个模型虽然单价便宜但错误率高导致重试成本反而更贵就把它从主路由里移除了。5. 流式输出与异步调用性能优化的关键细节5.1 流式响应的统一处理流式输出是对话类应用的标配litellm对流的支持做得比较到位。你只需要在completion里加streamTrue返回的就是一个生成器每次迭代拿到一个chunk。chunk的结构和OpenAI一致chunk.choices[0].delta.content就是增量文本。但不同Provider的流式实现差异很大。有的Provider会在第一个chunk里返回role字段有的不会有的Provider的结束标志是finish_reasonstop有的是[DONE]标记有的Provider在流式模式下不返回usage信息。litellm在内部做了归一化处理把大部分差异抹平了但我在实际使用中还是遇到过两个问题。第一个问题是流式模式下的错误处理。如果请求发出后、流开始之前就出错了比如认证失败litellm会直接抛异常这个好处理。但如果流已经开始、中途出错比如网络中断异常会在迭代过程中抛出你需要用try-except包住整个迭代循环。我建议在流式场景下加一个心跳检测——如果超过N秒没有收到新chunk就主动断开并触发回退。第二个问题是流式模式下的Token计数。前面提到过部分Provider不返回usagelitellm会用估算值。如果你需要精确的Token计数可以在流结束后用litellm.token_counter函数重新计算一遍。这个函数支持传入model和messages返回精确的Token数。虽然多了一次计算但对于成本敏感的场景是值得的。5.2 异步并发用acompletion提升吞吐litellm提供了异步版本的acompletion配合asyncio可以大幅提升并发吞吐。我做过一个对比测试同样的100个请求同步串行执行耗时约120秒用asyncio.gather并发执行并发数20耗时不到8秒提升非常明显。异步调用的代码结构大概是这样import asyncio import litellm async def call_model(prompt): response await litellm.acompletion( modeldeepseek/deepseek-chat, messages[{role: user, content: prompt}] ) return response.choices[0].message.content async def main(): prompts [问题1, 问题2, 问题3] tasks [call_model(p) for p in prompts] results await asyncio.gather(*tasks) return results asyncio.run(main())这里有个坑并发数不是越高越好。大部分Provider都有速率限制Rate Limit并发太高会触发429错误。litellm的Router支持配置rpm每分钟请求数和tpm每分钟Token数限制超过限制的请求会自动排队。我建议根据Provider的实际限制来设置并发数一般从10开始试逐步往上加观察错误率。另外异步模式下如果某个请求失败asyncio.gather默认会抛出异常并取消其他任务。如果你希望部分失败不影响整体可以加return_exceptionsTrue参数这样失败的请求会返回异常对象而不是抛出。我在批量处理场景下会用这个模式最后统一统计成功和失败的数量。5.3 超时与重试的配合策略超时设置是异步调用里最容易出问题的地方。litellm的completion和acompletion都支持timeout参数单位是秒。但要注意这个超时是单次请求的超时如果配置了重试总耗时可能是timeout × (num_retries 1)。我的经验是分两层设置超时第一层是litellm的timeout设得短一些比如15秒让快速失败的请求尽快触发重试或回退第二层是业务层的总超时用asyncio.wait_for包住整个调用链设得长一些比如45秒防止无限等待。这样既能快速响应故障又能给重试留出空间。还有一个细节流式调用的超时行为和非流式不同。非流式调用是在整个响应返回后判断超时流式调用是在每个chunk之间判断。这意味着如果模型生成很慢但一直在输出流式调用不会超时而非流式调用可能已经超时了。对于长文本生成场景流式调用在超时控制上更友好。6. 踩坑实录那些文档里不会写的细节6.1 模型名映射的隐藏规则litellm的模型名解析有一套隐藏规则官方文档没有完整列出但实际使用中经常遇到。比如modelgpt-4o不带前缀时litellm会默认按OpenAI处理modelclaude-3-sonnet不带前缀时会按Anthropic处理。这个默认推断机制在简单场景下很方便但在多Provider环境下容易出问题。我遇到过一个典型案例项目里同时用了某国内平台的模型和某海外平台的模型两个模型的名称恰好有重叠。不带前缀调用时litellm推断错了Provider导致认证失败。排查了半天才发现是模型名解析的问题。从那以后我要求团队里所有litellm调用必须带Provider前缀不允许依赖默认推断。这个规则写进了代码规范虽然多打几个字符但避免了大量隐性Bug。另一个相关问题是模型名的版本后缀。有些Provider的模型名带日期后缀比如gpt-4-0613有些带版本号比如claude-3-5-sonnet-20241022。litellm的价格表和能力表是按具体版本维护的如果你用的版本号不在表里成本计算和能力判断可能会出错。我的做法是定期检查litellm的版本更新新模型发布后及时升级库版本。6.2 认证信息的传递陷阱litellm支持多种认证方式但不同Provider的认证参数名不一样。OpenAI用api_keyAzure用api_key加api_base和api_versionAWS Bedrock用aws_access_key_id和aws_secret_access_key。这些参数可以直接传给completion函数也可以通过环境变量。我踩过的一个坑是环境变量名冲突。项目里同时用了OpenAI和Azure OpenAI两者都需要api_key但环境变量名不同OPENAI_API_KEY和AZURE_API_KEY。litellm在解析时会根据model前缀选择对应的环境变量这个逻辑本身没问题。但有一次我在代码里手动传了api_key参数litellm优先使用了手动传的值导致Azure的请求用了OpenAI的Key报了个莫名其妙的认证错误。排查了很久才定位到是参数覆盖的问题。经验总结手动传参和环境变量不要混用。要么全部走环境变量要么全部手动传保持一致。如果确实需要混用一定要在代码注释里写清楚每个参数的来源避免后续维护时踩坑。6.3 流式与非流式返回结构的差异虽然litellm尽力统一了返回结构但流式和非流式之间还是有一些细微差异。非流式返回的response.choices[0].message是一个完整的消息对象包含content、role、tool_calls等字段。流式返回的每个chunk里delta对象可能只包含部分字段——第一个chunk可能有role中间的chunk只有content最后一个chunk有finish_reason。如果你写的解析代码假设每个chunk都有完整字段就会出问题。我见过有开发者用chunk.choices[0].delta.content直接拼接结果遇到第一个chunk只有role没有content时报NoneType错误。正确的做法是用getattr或者判断字段是否存在for chunk in response: delta chunk.choices[0].delta if hasattr(delta, content) and delta.content: print(delta.content, end)还有一个差异是工具调用Function Calling的流式返回。非流式模式下tool_calls是一个完整的数组流式模式下tool_calls是分片返回的需要自己拼接。litellm提供了一个辅助函数litellm.stream_chunk_builder来简化这个拼接过程但我在实际使用中发现它对某些Provider的兼容性不够好最后还是自己写了一个拼接逻辑。6.4 错误码的跨Provider差异不同Provider的错误码语义不完全一致。比如同样是429OpenAI表示速率限制某国内平台可能表示配额耗尽。litellm在内部做了一层错误码映射把常见的错误归类为RateLimitError、AuthenticationError、BadRequestError等。但这个映射不是100%准确个别Provider的特殊错误码可能被归类到APIError这个通用类别里。我的处理策略是对litellm的标准异常做通用处理对未识别的异常做兜底处理。具体来说捕获RateLimitError时做退避重试捕获AuthenticationError时告警并停止重试捕获APIError时记录完整的错误信息包括原始响应体方便后续分析。litellm的异常对象里有一个original_exception属性可以拿到Provider返回的原始错误信息这个在排查问题时非常有用。7. 生产环境部署的几点实战建议7.1 代理模式把litellm做成独立服务litellm除了作为Python库使用还提供了一个代理服务器模式。你可以用一条命令启动一个HTTP服务对外暴露OpenAI兼容的接口所有请求由这个服务转发到后端的不同Provider。这个模式适合多语言技术栈的团队——比如你的主业务是Java写的不想为了调用LLM专门引入Python依赖就可以用litellm代理做中间层。启动命令大概是这样litellm --config config.yaml --port 8000启动后任何支持OpenAI接口的客户端都可以把base_url指向这个服务。代理模式还自带了一个管理界面可以查看调用日志、成本统计、模型状态。我在一个多团队协作的项目里用了这个模式每个团队用自己的虚拟Key调用管理员在界面上统一管理预算和权限省去了很多沟通成本。代理模式的性能开销不大我实测下来单次请求的额外延迟在5到10毫秒左右对于大部分应用来说可以忽略。但如果你的QPS很高比如上千建议做水平扩展用Nginx做负载均衡。7.2 日志与可观测性生产环境一定要打开litellm的日志功能。litellm支持设置litellm.set_verbose True来输出详细日志但这个日志量很大只适合调试时用。生产环境建议用回调机制把关键指标写到自己的监控系统。我关注的指标包括每个模型的P50/P95/P99延迟、错误率、每分钟Token消耗量、成本趋势。这些指标可以帮助你判断某个模型是否在退化、路由策略是否需要调整、预算是否要重新分配。litellm的回调函数是异步执行的不会阻塞主请求但要注意回调函数本身不要做太重的操作否则会拖慢整体性能。还有一个细节日志里不要记录完整的请求和响应内容。LLM的请求和响应可能包含敏感信息记录到日志里有合规风险。我通常只记录Token数、模型名、延迟、状态码这些元数据内容本身不落盘。7.3 版本升级的注意事项litellm的迭代速度很快几乎每周都有新版本。新版本会加入新模型支持、修复Bug、调整价格表但也可能引入不兼容的变更。我的升级策略是小版本跟紧大版本谨慎。比如从1.30升级到1.31一般可以直接升从1.x升级到2.x先看Changelog里有没有Breaking Change然后在测试环境跑一遍回归测试。升级后重点检查三件事模型价格表是否更新影响成本计算、异常类型是否有变化影响错误处理逻辑、返回结构是否有调整影响解析代码。我一般会在升级后跑一个包含各种边界情况的测试用例集确认没问题再上生产。8. 一些个人体会litellm这个库我用了快一年最大的感受是它把多模型调用这件事的复杂度从业务层转移到了基础设施层。以前每接一个新模型都要写一套适配代码、测一遍边界情况现在只需要在配置里加一行业务代码完全不动。这种一次接入、随处切换的能力在模型快速迭代的当下特别有价值。但它也不是银弹。litellm的统一接口是最大公约数某些Provider的独有功能比如某家特有的结构化输出格式、某家特有的缓存机制可能无法通过litellm直接使用。遇到这种情况我的做法是通用功能走litellm独有功能直接调原生SDK两者共存。不要为了追求全部统一而放弃Provider的独有能力。另外litellm的文档虽然覆盖面广但细节深度不够很多参数的实际行为需要看源码或者自己测试才能确认。我建议在使用前先花半天时间把核心模块的源码翻一遍特别是litellm/main.py和litellm/router.py这两个文件看完之后对它的工作机制会有更清晰的认识遇到问题也能更快定位。最后分享一个小技巧如果你不确定某个模型是否被litellm支持可以用litellm.get_supported_openai_params(model模型名)这个函数查一下。它会返回该模型支持的OpenAI参数列表如果返回空列表或者报错说明litellm还没有适配这个模型需要自己写Provider适配或者等社区更新。这个函数在选型阶段特别有用能帮你快速判断技术可行性。