
前阵子做一个多模型接入的小项目被各家大模型API的格式差异折腾得够呛。厂商A的请求体长这样厂商B的请求体又换了一套字段好不容易把两家的都跑通产品经理又说要接第三家而且第三家的错误码结构跟前两家完全不一样。那段时间我一半的精力都花在写适配层上真正该做的业务反而没什么进展。后来我接触到litellm这个开源项目它把这件事彻底简化了所有模型服务商统一成同一种调用格式我只需要维护一套代码想换哪家换哪家。这篇文章就把我实际使用litellm的完整经验整理一遍从安装、配置、路由、成本追踪到各种坑尽量讲清楚每个选择背后的原因适合正在做多模型接入、或者想把模型调度能力收拢到一个统一层里的开发者参考。1. 一个BaseURL接所有模型litellm到底在解决什么1.1 从一次三方接入事故说起事情是这样的。我当时要在一个应用里同时接三家模型服务商一家做文本生成一家做向量化还有一家做长上下文理解。每个厂商都有自己的SDK请求参数、认证方式、返回字段、错误码全都不一样。我最初的做法是每个厂商封装一个模块然后在上层做工厂方法。看起来挺干净但实际情况是厂商A的响应里token用量字段叫usage.total_tokens厂商B叫usage.completion_tokens加prompt_tokens厂商C则是把计量信息放在meta里。我做一层适配还行做到第三层的时候已经想骂人了。这还不算完。某天厂商B悄悄更新了某个字段的格式我们线上服务直接解析报错厂商A的限流策略改了之前设置的retry逻辑全部失效。我意识到靠业务代码去追每个上游的变化这条路是走不远的。1.2 litellm做的事把SDK差异吞掉的一层网关litellm的定位就是一个AI网关或者叫统一接入层。它把你对各个模型服务商的所有调用都收口到一个入口你的业务代码不直接对接任何一家厂商而是对接litellm。它在内部做了几个关键的事请求格式统一不管上游需要什么样的消息结构、系统提示词格式、采样参数命名litellm都帮你转成目标服务商要求的形态。响应格式统一所有返回结果都整理成同一套结构包括内容、角色、token用量、结束原因等。错误码统一不同厂商的限流、鉴权失败、模型不存在等错误litellm会翻译成统一的异常类型业务侧只需要处理一套异常。模型名映射你可以给每个模型起一个内部的逻辑名比如fast-llm、big-llm实际请求转发到谁由网关配置决定。换句话说litellm对我来说就是一个模型流量的交换机。应用不需要知道下一刻流量被转给谁只要把请求交给交换机就行。后续要加新模型改配置、加一个上游条目就完事业务代码一行不动。1.3 和编排框架的区别它更靠近传输层很多人会把litellm和那些AI编排框架混淆。我个人的理解是编排框架解决的是“如何把模型调用组合成一条链”比如先做意图识别、再调检索、最后生成回答而litellm解决的是“如何把一次模型调用稳定地发出去、发出去之后账怎么算、出错了怎么兜底”。编排框架可以集成litellmlitellm也可以单独使用。我在项目里的做法是业务层直接用litellm发请求不引入太重的东西等确实需要复杂编排再考虑上层框架。先把通路和账本管好比什么都实在。2. 十分钟把litellm跑起来安装、最小配置和第一次转发2.1 安装与环境准备litellm是Python写的安装非常简单pip install litellm建议Python版本在3.8以上我用的环境是3.10运行很稳定。安装完之后你用python -c import litellm; print(litellm.__version__)确认一下导入没问题。如果是在公司内网环境可能需要配置内部pip源这个取决于各自的基础设施。我装好之后习惯先把默认的debug日志开起来方便观察请求从进入到返回的完整链路export LITELLM_LOGDEBUG第一次跑建议开着DEBUG能清楚看到litellm把请求重写成了什么样子、实际发给了哪个上游地址这对理解它的工作方式非常有帮助。2.2 最小配置文件model_list是核心使用litellm的网关模式核心是一个YAML配置文件。我习惯先建一个config.yaml内容长这样model_list: - model_name: fast-llm litellm_params: model: vendor_a/gpt-4o-mini api_key: os.environ/A_API_KEY api_base: https://api.vendor-a.example.com/v1 - model_name: big-llm litellm_params: model: vendor_b/claude-3.5-sonnet api_key: os.environ/B_API_KEY api_base: https://api.vendor-b.example.com/v1这里有几个关键点model_name是你自己定义的名字业务侧只认这个名字。litellm_params里的model字段前半部分是提供商标识后半部分是实际模型名。api_key可以用os.environ/环境变量名这种写法litellm启动时会自动去环境变量里取避免密钥写死在配置文件里。api_base显式指定上游地址防止它自动推断到你不想访问的默认地址。为什么强调model_name要自定义因为这样做业务代码和上游解耦。今天fast-llm指向厂商A的模型明天想换成厂商B的改配置就行业务完全无感。我后面的多模型路由也是基于这一层别名的。2.3 启动网关三种方式调用配置文件准备好后一条命令启动litellm --config config.yaml --port 8000看到Uvicorn running on的日志就说明网关起来了。默认端口是8000如果被占用改个端口即可。启动之后我一般会先验证三条路第一条curl直连最直观的探活方式curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: fast-llm, messages: [{role: user, content: 你好做个自我介绍}] }只要返回里带choices[0].message.content就说明网关转发链路完全打通。这一步的核心价值是先证明通路没问题之后再加参数、加策略排错范围就小很多。第二条从Python代码里直接调用from litellm import completion response completion( modelfast-llm, messages[{role: user, content: 你好做个自我介绍}], api_basehttp://localhost:8000, ) print(response.choices[0].message.content)注意这里我传了api_basehttp://localhost:8000意思是不走litellm的直连模式而是把请求发给自己起的这个本地网关。这种做法适合在业务服务里使用业务服务只依赖一个固定的网关地址。第三条用官方SDK兼容格式接进来这是最有价值的一点。litellm启动后暴露的HTTP接口风格和不少团队已经用过的那个主流接口格式高度一致。也就是说你把原来的base_url从真实厂商地址改成http://localhost:8000其他代码一行都不用动。这对存量项目的迁移极其友好不需要动业务代码只需要改环境变量里的地址。2.4 为什么端口和入口设计得这么“常规”一些人可能觉得litellm的接口长得太“普通”了。恰恰是这个“普通”让它好用。它在设计上没有发明新协议而是选择兼容大家已经写烂了的客户端代码让迁移成本降到最低。很多人在生产环境里纠结“要不要换网关”本质上纠结的是“我现有代码要不要大改”litellm把这个问题的答案变成了“不用”。我实测下来从直连上游改成走litellm一个核心服务只改了配置文件里的base_url剩下的测试全部通过。这份兼容性带来的迁移红利比任何炫酷的新功能都值钱。3. 把多个模型变成一个池子Router的路由、负载均衡与故障转移3.1 为什么需要Router单点接入不够要的是调度能力如果litellm只是把请求转发给一个固定的模型那叫“统一网关”但称不上“调度”。生产环境真正需要的是同一个模型配置多个上游Key甚至配置多家厂商的同级别模型当其中一个不可用、限流或者变慢时系统能自动把流量导到另一个。litellm的Router组件就是干这个的。它负责在三层做事情同模型多Key负载均衡把请求均匀或按权重分发到配置的多个上游Key上降低单Key被限流的概率。不同厂商的故障转移上游A挂了能自动切到上游B。请求级别的重试与冷却某个上游连续失败会暂时把它踢出调度池等冷却期过了再放回来。3.2 model_list的配置手法同一个别名挂多个真实目标Router的配置方式和之前的config.yaml基本一致区别在于同一个model_name可以对应多个litellm_params条目model_list: - model_name: fast-llm litellm_params: model: vendor_a/gpt-4o-mini api_key: os.environ/A_API_KEY_1 api_base: https://api.vendor-a.example.com/v1 - model_name: fast-llm litellm_params: model: vendor_a/gpt-4o-mini api_key: os.environ/A_API_KEY_2 api_base: https://api.vendor-a.example.com/v1 - model_name: fast-llm litellm_params: model: vendor_b/claude-3-5-haiku api_key: os.environ/B_API_KEY_1 api_base: https://api.vendor-b.example.com/v1配置里三个条目共用一个别名fast-llm而实际上游分布在两个厂商。从业务侧看来它仍然只知道“我调用fast-llm”但流量实际被Router按策略分发。在代码里这样用from litellm import Router router Router(model_list[ # 这里放和上面yaml等价的模型列表 ]) response router.completion( modelfast-llm, messages[{role: user, content: 你好}], ) print(response.choices[0].message.content)Router内部会自动记录每个上游的成功率、延迟、限流状态然后决定下一次请求发给谁。你不需要自己维护“可用清单”它替你做。3.3 权重、冷却时间和Fallback的配合Router的调度不是随机抢单它支持几套可调参数。我常用的几个参数作用我的建议routing_strategy选路由策略如simple-shuffle、least-busy、usage-based-routing一般用默认多Key场景可以考虑least-busycooldown_time上游失败后冷却多少秒我习惯设60秒太短会导致病节点反复被请求max_fallbacks单个请求最多尝试多少个备用上游设2就够了尝试太多会放大整体延迟allowed_fails连续失败多少次才触发冷却默认3次比较合适举个例子cooldown_time: 60的意思是某个上游连续失败达到阈值后Router会在60秒内不再把请求发给它而不是每次都顺手试一下。这个机制很像电路里的熔断器防止病节点把整个池子的成功率拖垮。Fallback则是另一个层面。比如配置了两个厂商vendor_a片全部不可用Router会直接把请求转给vendor_b业务侧只感觉到响应慢了一点点不会收到“上游挂了”的报错。对于追求稳定的线上服务这个是保命功能。3.4 实测体验一次人为故障演练我曾经做过一次测试把厂商A的两个Key全部停掉然后持续向Router发请求观察行为。第一波请求回来时Router返回了厂商A的限流错误。设置好allowed_fails和cooldown_time之后几秒内Router识别出该厂商的所有目标都不可达开始把流量全部切到厂商B。切换过程中我没有看到业务侧出现明显的5xx比例上升只有个别请求延迟从几百毫秒涨到一两秒。那次测试给我的启发是真正决定故障恢复速度的是冷却时间和失败阈值的配合。阈值设得太小偶发抖动就会触发切换反而影响稳定性设得太大故障期间用户会一直打到坏节点上。这个参数没有绝对标准建议按线上真实流量逐步调整。4. 生产环境要有一本账虚拟Key、配额和成本追踪4.1 虚拟Key体系把上游Key藏起来给团队发“分Key”直接让每个开发人员拿着厂商的上游Key开发是件危险的事。Key的权限边界很难控制离职人员带走的Key也可能没有及时回收。litellm提供了虚拟Key的机制它相当于在网关层再签发一拨“分Key”这些Key只对litellm生效不直接对应上游任何账号。你可以在启动网关后调用管理接口生成一个虚拟Keycurl -X POST http://localhost:8000/key/generate \ -H Authorization: Bearer sk-1234 \ -H Content-Type: application/json \ -d { max_budget: 20, max_parallel_requests: 10, models: [fast-llm, big-llm] }这里sk-1234是管理员的master key在启动网关时配好的。返回的JSON里会有一个新的key字段把这个key分发给团队里的人使用。每个人用自己的虚拟Key访问网关上游的真实Key始终握在管理员手里。4.2 预算与配额超了不让用而不是事后吓人虚拟Key支持max_budget和max_parallel_requests这样的限制。max_budget是总预算上限以美元计超过之后这个Key会被自动停用max_parallel_requests则是并发上限防止某个项目把整个网关流量占满。我通常这样分配测试环境的Key预算给5到10美元够用又不至于失控。线上某个业务线的Key按预估月消耗设上限并且配上并发限制。给外部合作方开Key时只开放特定模型限制预算避免被拿去做无关调用。这套做法的好处是控制成本的动作前置到了网关层而不是等月账单出来之后再去追查是哪个项目超了。实际效果非常明显我们接入多模型之后再也没出现过“某个月某项目突然跑了几百美元”的惊吓。4.3 成本与Token追踪一次请求花了多少钱明明白白除了预算还要知道钱具体花在哪。litellm支持在请求完成之后触发回调把用量和费用信息吐给你自己的系统。我习惯在服务启动时挂一个回调函数import litellm def track_cost(kwargs): response kwargs.get(response) if not response: return usage response.get(usage, {}) cost kwargs.get(response_cost, 0) model kwargs.get(model, ) print(fmodel{model}, tokens{usage}, cost${cost:.4f}) litellm.success_callback [track_cost]挂上这个回调之后每完成一次请求我都能看到模型名、token消耗、估算成本。长期累积下来我就有了一个按模型、按应用维度的成本明细。这个数据对后续做容量规划、模型选型极其有用——比如把某类任务从大模型切换到小模型到底能省多少钱不用拍脑袋看几天的调用统计就明白了。4.4 账本之外日志和可观测性的延伸litellm本身也支持把日志输出到各种可观测性平台但在内网环境里我更倾向于用回调自建一套轻量记录把关键字段落到日志系统或数据库里。我做成本追踪时的经验是不要只采集成功请求失败请求也要留记录。很多成本异常来自重试——一次请求失败上层自动重试三次账单上就记了四次调用。如果只看成功率永远发现不了这个问题。把失败和重试都计入成本统计才能看到真实消耗。5. 实测踩坑记录几个让我加班到深夜的问题5.1 坑一环境变量写法不对Key一直读不到litellm在配置里支持os.environ/A_API_KEY这种语法它启动时会从环境变量里读取。但我第一次写配置时把api_key的值直接写成了os.environ/A_API_KEY并且用双引号包住。结果litellm没去解析这个特殊语法而是把它当成一个普通的字符串Key发出去了上游直接返回401鉴权失败。排查过程我印象很深先检查了环境变量本身确认在shell里能打印出来又检查了配置文件路径确认启动时加载的是同一个文件最后对比了官方文档里的示例才发现写法上少了“裸写为YAML的标量”这个前提。正确的姿势是不加引号litellm_params: model: vendor_a/gpt-4o-mini api_key: os.environ/A_API_KEY这个坑的本质是YAML里引号改变了类型解析。之后我的规则是配置文件里的特殊语法一律不戴引号普通字符串才用引号。5.2 坑二流式输出时响应迟迟不结束某个功能需要流式返回我在调用里加了streamTrue奇怪的是网关这边日志显示已经向上游拿全了数据但应用侧就是迟迟拿不到完整内容或者打印到一半就断了。后来抓包看litellm转发流式响应时用的是SSE格式也就是一段一段以data:前缀开头的数据流。我的应用代码用普通JSON解析方式去读读到一半就报错或者因为HTTP连接没有正常关闭而一直等。解决方式是在应用侧实现标准的SSE读取逻辑import requests resp requests.post( http://localhost:8000/v1/chat/completions, json{ model: fast-llm, messages: [{role: user, content: 讲个故事}], stream: True, }, streamTrue, ) for line in resp.iter_lines(): if not line: continue text line.decode(utf-8).strip() if not text.startswith(data:): continue data text[5:].strip() if data [DONE]: break # 解析JSON逐段处理内容实际做流式接入建议直接用支持SSE的HTTP客户端库或者自己封装上面这套解析。遇到流式响应先不要怀疑litellm先确认你的解析逻辑是按SSE规范写的。5.3 坑三model_name和上游模型名混在一起用我一开始配置Router时图省事model_name直接写成上游的真实模型名比如(fast-llm别名为vendor_a/gpt-4o-mini然后我在业务代码里把model参数传成了vendor_a/gpt-4o-mini甚至加了api_base绕过别名直接用上游参数。这种写法在单模型场景下跑得通但治不好多模型调度。model_name和litellm_params.model是两回事前者是给业务看的别名后者是给上游看的真实值。业务代码应该只认前者。打比方说别名是“客服组电话”真实模型是“具体某个客服工号”。业务部门只需要拨客服组电话由总机决定今天谁接如果业务直接拨工号那总机的调度、备份、负载均衡全都白配了。5.4 坑四默认超时在某些场景下不够用litellm默认的请求超时设置对于大多数场景是够的但遇到长上下文模型或者高峰期排队就会出现read timeout。而且超时并不是“只影响那一个请求”那么简单——客户端超时后可能重试重试又挤占了上游的排队资源形成恶性循环。我给慢模型单独设了更宽松的超时参数litellm_params: model: vendor_b/long-context-model api_key: os.environ/B_API_KEY timeout: 600 request_timeout: 600同时提醒一点不同的litellm版本里超时参数名可能有微小差异我用的这个版本是认timeout和request_timeout建议升级版本之后顺手看一眼更新文档。超时设得太小省不了成本反而制造重复请求设得太大又会占用更多连接资源需要按模型特性分别对待。5.5 坑五网关模式和直连模式混用回调不生效有段时间我在同一个服务里既用litellm的网关地址调模型又直接用litellm的Python包直连上游。我以为success_callback能同时覆盖这两种调用结果只有直连模式触发了回调走网关的请求完全没有成本统计。原因是走网关时真正执行请求的是那个独立跑着的网关进程它跟你当前这个Python进程是完全隔离的你在业务进程里挂的success_callback当然管不到它。网关自己只按它加载的配置工作而成本统计回调需要写在网关进程那边。这个认知很重要。用网关模式就是放权给网关进程所有策略配置、日志回调都要在网关的配置文件里声明不能指望业务进程的局部变量对它生效。理顺这一点很多“为什么我的回调没执行”的疑问都会消失。我在实际使用中慢慢形成的固定做法是网关层管统一入口、路由和账户体系业务层只管传参数、收结果。所有跨模型的策略尽量下沉到网关层业务代码保持简单。这样既能让不同团队共用一套模型出口也能把成本、权限、稳定性这些横切关注点集中在一处治理。如果你也在为多模型接入头疼不妨先起一个本地网关把现有的一个模型服务切过去试一两天亲身感受一下“业务代码完全不用改”的迁移过程再决定要不要全面铺开。