如果你的服务每天要跑几十万次“没什么营养”的高频任务,比如给文本打标签、抽关键词、判断用户意图,那“模型分层路由”基本是今天的必选项。把便宜模型顶上高频任务,把旗舰模型留给真正复杂的推理任务,这件事LiteLLM恰好做得非常顺手。我最近把一个内容解析服务的调用链路整体改造成“默认走便宜模型、兜底走贵模型”的结构,账单掉了接近九成,延迟反而更稳定了。这篇文章把整个实战过程拆开讲,含配置文件、路由策略、踩坑记录和评估方法,适合正在做LLM应用成本治理、或者想给团队引入统一模型网关的开发者。
1. 成本账单教我的事:高频短任务为什么要单独走便宜模型
1.1 一次100万次调用的账
先说个我自己算过的账。当时服务里有个分类任务,输入一段短文本,输出一个类别标签。这种任务有几个特点:调用量极大、上下文很短、输出很短、模式基本固定。高峰期一天要跑几十万次,一个月下来轻松过百万。
我按写稿时比较常见的公开价格估算,拿一个旗舰模型和一个轻量模型对比,假设平均每次调用输入500 token、输出100 token:
| 项目 | 旗舰模型 | 轻量模型 |
|---|---|---|
| 输入价格(每1M token) | $2.50 | $0.15 |
| 输出价格(每1M token) | $10.00 | $0.60 |
| 100万次调用输入量 | 500M token | 500M token |
| 100万次调用输出量 | 100M token | 100M token |
| 输入费用 | $1,250 | $75 |
| 输出费用 | $1,000 | $60 |
| 合计 | $2,250 | $135 |
也就是说,同样的业务量,旗舰模型要烧2250美元,轻量模型只要135美元,差出16倍。如果任务本身对“世界知识”和“复杂推理”要求不高,用贵模型跑就是拿着高射炮打蚊子,打中了也是浪费。
你可能会说,几百块钱也不是大钱。但这类高频任务往往只是产品里一个小功能,如果每个小功能都这么烧,整体账单会非常难看。更关键的是,这种量级的调用会把旗舰模型的账号级TPM/RPM配额全部吃掉,等到真有一个复杂任务需要旗舰模型的时候,请求反而被限流堵在门口。
1.2 钱只是其中一个理由
除了成本,还有两个理由让我决定必须做分层。
第一是延迟。轻量模型在短输入场景下的首token延迟普遍更低,吞吐上限也更高。同样的QPS,用轻量模型可以少开实例,或者干脆不需要在应用层加复杂缓存。实测中,我们的P95延迟从800ms降到了400ms左右,体感非常明显。
第二是限流。旗舰模型在服务商那边有TPM和RPM限制,而且账号级的配额是共享的。高频任务全量涌入时,就算是旗舰模型也会被限流,频繁触发429。这时候你加再多重试都没用,因为问题出在流量结构上——太多不值得用旗舰模型的任务占着配额,真正重要的任务无路可走。
所以模型分层不是一个“省钱优化”,它本质上是算力资源的分配问题。让每一层模型匹配每一层任务,便宜的流量走便宜的路,复杂的问题才动用贵算力。这个思路在很多团队里其实早就有了,只是LLM应用普及之后,模型本身成了最大成本项,分层路由才显得如此重要。
2. LiteLLM在分层路由里扮演的角色:统一入口与模型组
2.1 两种落地形态:代理网关与进程内Router
LiteLLM有两种常见用法,我分别说下适用场景。
第一种是代理模式。启动一个独立的LiteLLM代理服务,它对外暴露OpenAI兼容接口,内部负责转发到各家模型服务商。业务代码只需要把base_url改成代理地址,其他一切照旧。这种模式的好处是运维集中、多服务共用,对业务代码侵入极小,适合一个团队有多个服务都在调LLM的场景。
第二种是进程内Router。直接在Python应用里引用litellm.Router,把模型列表配在代码里,应用进程内做路由。好处是不用多部署一个服务,适合改造面可控、规模不大的项目。
两种形态的核心逻辑是一样的,区别主要在部署方式:
| 维度 | 代理网关 | 进程内Router |
|---|---|---|
| 部署成本 | 需要维护一个独立服务 | 无额外服务,随应用启动 |
| 业务改造量 | 改base_url即可 | 需要改代码逻辑和依赖 |
| 故障域 | 代理挂了所有调用都中断 | 应用挂了才影响自己 |
| 集中治理 | 适合多服务统一管控 | 适合单服务内自管 |
| 适用规模 | 中大型团队、多业务线 | 小项目、快速验证 |
我这里选的是代理模式,因为团队里好几个服务都要接,统一走一个入口,后面做预算、配额、审计都方便。
2.2 Model Group:把不同厂商的模型挂在一个名字下面
LiteLLM里最核心的概念叫model_name,它不是一个具体的模型,而是一个逻辑组名。你可以把多个厂商的模型挂到同一个组名下,LiteLLM负责在组内做负载均衡和故障切换。
举个例子,我定义了两个组:
cheap-fast:放轻量模型,比如gpt-4o-mini、gemini-1.5-flash,承担高频简单任务。high-quality:放旗舰模型,比如gpt-4o、claude-3-5-sonnet,承担复杂推理、长文生成等任务。
业务代码里只写cheap-fast或high-quality,完全不感知底层是哪个厂商、哪个具体模型。哪天某个服务商涨价了,或者新出了一个性价比更高的模型,我只需要改代理配置文件,业务代码一行不用动。
这一点很重要。很多人一开始会直接按“模型名”来路由,比如代码里写死gpt-4o-mini,后面想切到别的模型就得全局搜索替换。用逻辑组名隔离之后,模型切换变成了配置操作,而不是代码操作。
3. 落地第一步:用config.yaml把模型分成两个梯队
3.1 最小可用的分组配置
LiteLLM代理的配置集中在一个YAML文件里。我直接贴一个能跑起来的最小配置:
model_list: - model_name: cheap-fast litellm_params: model: openai/gpt-4o-mini api_key: os.environ/OPENAI_API_KEY model_info: id: openai-gpt-4o-mini - model_name: cheap-fast litellm_params: model: gemini/gemini-1.5-flash api_key: os.environ/GEMINI_API_KEY model_info: id: gemini-flash - model_name: high-quality litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY model_info: id: openai-gpt-4o - model_name: high-quality litellm_params: model: anthropic/claude-3-5-sonnet api_key: os.environ/ANTHROPIC_API_KEY model_info: id: anthropic-claude-sonnet litellm_settings: router_settings: routing_strategy: simple-shuffle allowed_fails: 2 cooldown_time: 300这里有几个字段需要解释。
litellm_params.model前面带了前缀,比如openai/、gemini/、anthropic/。这是LiteLLM的provider标识,它看到前缀就知道该转换成哪家服务商的请求格式。api_key用os.environ/变量名的形式引用环境变量,不要把密钥直接写在配置文件里。
model_info.id是本组内该部署点的唯一标识。LiteLLM用它做日志、健康检查和故障归因。如果不写,它会自动生成,但建议手动指定一个可读的名字,排查问题的时候会舒服很多。
配置写好后,直接启动:
pip install 'litellm[proxy]' litellm --config config.yaml --port 4000启动完成后,用curl http://localhost:4000/health确认服务正常。接下来就可以用OpenAI SDK风格调用它了。
3.2 配额、熔断与预算:防止便宜模型顶不住
分层路由不是简单把便宜模型挂上去就完事。便宜模型也要面对限流、故障和预算问题,得给它们配好安全网。
先看限流。如果你在同一个组里挂了多个模型,但某个模型在服务商那边的配额很紧张,就可以给它单独设置每分钟Token数和每分钟请求数:
- model_name: cheap-fast litellm_params: model: openai/gpt-4o-mini api_key: os.environ/OPENAI_API_KEY rpm: 1000 tpm: 400000rpm和tpm分别限制每分钟请求数和每分钟Token数。LiteLLM会在组内分配流量时避开已经超限的模型,不至于硬顶着限流往上打。
再看熔断。LiteLLM支持allowed_fails和cooldown_time。我配置里的2和300表示:同一个模型连续失败两次后,把它临时移出路由池300秒。这个机制很实用。模型也会抽风,比如服务商某个区域集群抖动,如果继续把流量打过去,只会让错误率雪崩。移出冷却之后,流量会自动转到组内其他模型上。
最后是预算。代理模式下,每个调用方都可以分配一个虚拟Key,并绑定预算:
curl -X POST http://localhost:4000/key/generate \ -H "Authorization: Bearer sk-1234" \ -H "Content-Type: application/json" \ -d '{"models": ["cheap-fast", "high-quality"], "max_budget": 100}'这样每个业务线、每个项目都有自己的Key和预算上限,账单出来之后能直接知道钱花在谁头上。预算超了LiteLLM会直接拒绝请求,不会出现月底对账时才发现超支的情况。
4. 路由策略与回退设计:便宜模型顶得住才有价值
4.1 内置路由策略怎么选
LiteLLM本身带了多种路由策略,选型之前建议先把文档里的策略行为搞清楚。我实际用过的几种如下:
| 策略 | 行为特征 | 适合场景 |
|---|---|---|
| simple-shuffle | 在组内均匀打散流量,类似轮询 | 组内模型能力、价格接近 |
| least-busy | 按在途请求数选择,谁闲谁上 | 某个模型经常被限流 |
| latency-based | 按历史响应延迟加权,快者优先 | 对首token延迟敏感 |
| usage-based-routing | 结合预算与用量成本梯度选择 | 想强制控制成本上限 |
我一开始用的是simple-shuffle,因为两个便宜模型的能力和价格差不多,均匀分担最省心。后来发现某个服务商偶尔会抽风,就切到了latency-based,让响应快的模型多吃流量,慢的自动少拿。选型逻辑很简单:看你的首要诉求是稳定性、延迟还是成本,策略和诉求要对齐。
有一点需要注意,路由策略只能解决“组内怎么选模型”的问题,它不能替代业务分层。也就是说,LiteLLM负责在cheap-fast组内挑选当前最优的便宜模型,但业务方必须先把请求明确指定到cheap-fast。
4.2 回退不是万能药:fallback会带来延迟雪崩
很多人知道LiteLLM有fallback机制,就希望在cheap-fast组整体不可用时自动切到high-quality组。思路没问题,但实现时要非常小心。
默认的fallback逻辑是:先请求目标组,等它失败或超时后,再发起对备用组的请求。这意味着每个失败请求的代价是“目标组超时时间 + 备用组响应时间”。一旦目标组真的有问题,所有请求都会先饿着肚子等一趟,延迟直接翻倍甚至更多,这就是延迟雪崩。
我的做法是两层防护。
第一层,在配置里把allowed_fails调低,cooldown_time调长,让有问题的模型尽快冷却,而不是反复试探。第二层,在应用层针对关键请求做“双跑”:同时请求便宜模型和贵模型,谁先返回合理结果就用谁,另一路直接丢弃。
import asyncio from openai import AsyncOpenAI client = AsyncOpenAI( base_url="http://localhost:4000/v1", api_key="sk-your-virtual-key", ) async def double_run(messages): cheap_task = client.chat.completions.create( model="cheap-fast", messages=messages, timeout=3 ) heavy_task = client.chat.completions.create( model="high-quality", messages=messages, timeout=6 ) done, pending = await asyncio.wait( [cheap_task, heavy_task], return_when=asyncio.FIRST_COMPLETED, ) for task in pending: task.cancel() return done.pop().result()双跑看着浪费,实际只用在最核心的少量请求上。它的价值在于:便宜模型正常时几乎总是先返回,贵模型的请求其实多半还没发出去就会被取消;便宜模型异常时,贵模型也不会被前面的等待拖累。这比单纯的fallback可靠得多。
5. 应用侧改造:业务分层判断与无感切换
5.1 分层判断放在哪一层
LiteLLM把路由做得很漂亮,但“这个请求该走便宜还是走贵”的判断,通常要在应用层完成。我的经验是分层判断遵循一个从粗到细的顺序。
优先用规则判断。如果文本长度超长、包含多个互斥条件、要求多步推理,或者业务上判定了高风险,直接走high-quality。规则覆盖不了的,再让便宜模型先跑,同时对结果做一个简单的置信度检查,不达标再升级到贵模型。这种分级方式成本低、可解释性强,也容易灰度验证。
给个示意代码:
import os from openai import AsyncOpenAI client = AsyncOpenAI( base_url="http://localhost:4000/v1", api_key=os.environ.get("LITELLM_VIRTUAL_KEY"), ) def need_heavy(text: str) -> bool: # 规则前置:命中规则直接升级 if len(text) > 2000: return True if any(k in text for k in ["矛盾", "推理", "多轮"]): return True return False async def handle_task(text: str): group = "high-quality" if need_heavy(text) else "cheap-fast" resp = await client.chat.completions.create( model=group, messages=[{"role": "user", "content": text}], temperature=0, ) return resp.choices[0].message.content这个例子很简化,但核心思想是对的:默认便宜,按需升级。不要反过来,把所有请求先发给贵模型再降级,那样省不到钱。
5.2 从openai客户端切到LiteLLM代理只需要改base_url
我最喜欢LiteLLM代理的一点是,切换几乎是无痛的。只要业务代码里用的是OpenAI SDK,把base_url和api_key换成代理地址和虚拟Key,其他逻辑完全不变。
from openai import AsyncOpenAI client = AsyncOpenAI( base_url="http://localhost:4000/v1", api_key="sk-LiteLLM-virtual-key", )然后在配置文件中,把虚拟Key绑定到cheap-fast和high-quality这两个组,这个调用方的路由权限就落好了。
想验证某个模型是不是真的健康,LiteLLM支持mock_response,我测试时经常用它来验证链路通不通,不花真实token。它的思路是让某个模型返回预设的模拟响应,适合做联调和异常路径测试,上线前把整条链路先跑通。
代理模式还有一个好处:模型提供方的SDK升级或格式变化,都被隔离在LiteLLM这一层。业务代码永远只跟OpenAI兼容的协议打交道,底层怎么变都无所谓。
6. 实测数据与避坑记录:成本、延迟和那些隐蔽的坑
6.1 模拟压测结果
改造完成后,我在一个跟线上任务结构类似的仿真数据集上跑了压测。数据是模拟的,但评估方法可以照用:同一批请求跑两套链路,对比延迟、成本和错误率。
| 指标 | 改造前(全量旗舰) | 改造后(分层路由) |
|---|---|---|
| P50延迟 | 650ms | 320ms |
| P95延迟 | 2100ms | 780ms |
| 可支撑QPS | 150 | 600 |
| 月度成本(模拟估算) | 100% | 约14% |
| 错误率 | 0.8% | 1.1% |
成本下降最直观,直接打了一折多。延迟整体也降了,因为便宜模型在短任务上的首token延迟更低,而且不再被旗舰模型的限流拖住。错误率略有上升,主要来自便宜模型在少量边界样本上的误判,这部分我通过升级规则兜住了。
压测时我还发现一个规律:分层路由的效果跟“任务结构”强相关。如果你的任务里八九成都是短、模式化、低风险的任务,收益巨大;如果任务普遍复杂、需要深度推理,那分层能省的空间很有限,重点反而应该放在缓存的命中率上。
6.2 我踩过的几个坑
这几个坑是真实调试过程中出现的,每一条都花了不少时间。
第一个坑是模型间的Prompt和输出格式兼容问题。同一个任务在旗舰模型上表现很好,换到便宜模型后JSON格式偶尔会多出解释性文字,直接导致下游解析报错。解决办法是在系统提示里明确输出规范,同时在解析层做容错,不能默认模型一定会输出纯JSON。
第二个坑是上下文缓存被切碎。如果组内有多个厂商的模型,同一个用户会话可能这次走A厂商、下次走B厂商,各家缓存策略完全独立,结果就是缓存命中率很低,成本并没有表面上看起来那么低。我的做法是在配置里尽量让同一会话固定到同一模型,牺牲一点负载均衡来换缓存命中。
第三个坑是tpm限流造成的排队噪音。我给模型设置了tpm之后,LiteLLM会在本地排队等待配额,排队本身会拉高延迟,这时候再看latency-based路由选出来的结果其实是不准的。排查时才意识到,延迟数据里混进了排队时间,应该把“模型真实响应时间”和“本队列等待时间”分开看。
第四个坑比较低级但很常见:有人把cheap-fast这种组名当成模型名写死在客户端代码里,某个服务直接调model="cheap-fast"没问题,但想临时绕过代理直连某个模型验证问题时,就发现代码里根本没有真实模型名。教训是业务代码里永远只出现逻辑组名,真实模型名只在代理配置里出现一次。
6.3 想继续做的话:从成本分层到质量分层
分层路由做到后面,你会自然想从“成本分层”进化到“质量分层”。比如引入离线评测集,定期用便宜模型跑一遍,拿结果和旗舰模型对比打分,用分数决定哪些任务可以长期留在便宜层。也可以做任务复杂度预估模型,用一次便宜的预测判断该不该升级算力。
再往后还能加用户维度的分层:免费用户走便宜模型,付费用户默认走高质量模型;或者按时段路由,高峰期用便宜模型扛量,低峰期再跑贵模型做精排。这些方向都需要一个稳定的路由网关作为底座,LiteLLM这套结构已经能撑住这些玩法了。
我自己的体会是,模型分层路由最大的价值不在省下的那点账单,而在于它让你敢于拿便宜的算力去做大规模实验,让每一层资源真正用在刀刃上。最后分享一个小技巧:从改造第一天就把每个组名、每个模型ID的调用量和成本埋点数据记好,后面做任何路由策略调整,都要靠这些数据说话,不能拍脑袋。