小模型+大模型级联:简单问题用便宜的,难的再上贵的
小模型+大模型级联:简单问题用便宜的,难的再上贵的
"大多数 AI 应用的账单里,有 70% 的请求其实不配用旗舰模型——它们只是恰好走了一条不打折的通道。"
🔥 摘要:级联(Cascade)不是"用小模型替代大模型",而是给每一次请求做难度分级:先用便宜的小模型(SLM)冲一轮,只有它"没把握"时才升级到旗舰大模型(LLM)。本文讲清级联为什么省钱、三种路由策略怎么选(规则 / 分类器 / 自一致性置信度)、难度打分怎么落地,并给出一份可直接运行的 Python 级联路由器(含成本账本与超时兜底)。最后给出成本模型、7 个踩坑与 3 个练手项目。
🎯 阅读收益:① 看懂级联降本的真实杠杆点;② 拿到三种路由策略的选型对比表;③ 学会用"自一致性投票"给小模型输出打置信度;④ 复制一份可运行的级联路由器代码;⑤ 掌握升级率与漏升级率的调法;⑥ 避开最常见的 7 个坑。
⚠️ 说明:文中模型单价、成本比例、升级率均为示例量级,不代表任何厂商实际定价;各家模型价格与能力迭代极快,动手前请以官网实时报价为准,并在自己的流量上重测。
一、你的钱到底花在哪了
先把账算明白,再谈架构。
一个典型的 LLM 应用,单次请求成本可以拆成:
$$Cost = \frac{P_{in} \times T_{in} + P_{out} \times T_{out}}{10^6}$$
其中 $P$ 是每百万 token 单价,$T$ 是 token 数——一旦乘上日请求量,差异非常刺眼。假设一个日均 3 万次请求的知识助手,平均输入 1200 token、输出 300 token,全量走旗舰模型,按示例单价(输入 15 / 输出 60 元每百万)估算:单次 ≈ $0.036$ 元,单月 ≈ 3.2 万元。
而这 3 万次请求里,真实分布通常是这样的:
| 请求类型 | 典型占比 | 是否真的需要旗舰模型 |
|---|---|---|
| 格式转换、字段抽取、意图分类 | 30%~40% | 不需要,1B~3B 模型绰绰有余 |
| FAQ 直答、简单摘要、改写润色 | 25%~35% | 大部分不需要 |
| 多步推理、长文档综合、代码生成 | 15%~25% | 需要 |
| 边界模糊、高风险、用户明确要求"仔细想" | 5%~10% | 必须 |
也就是说,有六到七成的请求本来就用不到旗舰模型。级联要做的,就是把这六到七成拦下来。
这里有个容易忽略的点:降本不只是"换便宜模型",还包括减少不必要的输入 token(提示压缩、缓存命中)和减少输出 token(约束输出长度、强制 JSON Schema)。级联和这两招是叠加关系,不是互斥关系。
二、级联的三种路由策略
路由是级联系统的心脏。目前工程上主流的有三种,复杂度与收益依次递增。

策略一:规则路由(Rule-based)
最朴素,也最不该被轻视。用一组可解释的规则决定走哪条路:
- 输入 token 数 < 300 且不含代码块 → 小模型
- 命中"总结/翻译/改写/提取/分类"等意图词 → 小模型
- 命中"推导/证明/一步步/写代码/对比分析" → 大模型
- 用户显式指定模型 → 直接路由
优点:零额外开销、可解释、可审计、出问题时一句话就能改。
缺点:规则永远追不上用户表达的多样性,长尾请求容易误判。
适合:冷启动阶段,或者业务请求形态高度固定的场景(例如客服 FAQ、表单抽取)。我的建议是规则路由一定要保留,哪怕后面上了分类器,也要让规则拥有"一票否决"的最高优先级——这是你线上最后一道可解释的护栏。
策略二:分类器路由(Router Model)
训练(或微调)一个轻量打分器,输入用户 query,输出"难度分"或"该走哪个模型"的标签。常见做法:
- 用一个 0.5B~3B 的小模型做二分类/多分类微调;
- 或者直接用 embedding + 轻量 MLP 头,输入 query 向量,输出难度分;
- 训练标签来自历史日志:把"小模型答对、大模型也答对"的样本标为 0(可降级),把"小模型答错、大模型答对"的标为 1(必须升级)。
优点:能捕捉规则覆盖不到的语义难度信号,准确率通常显著高于纯规则。
缺点:需要标注数据和维护成本;分类器本身也会漂移,业务请求分布一变,判定就失真。
关键技巧:不要直接让分类器输出 0/1 硬标签,而是输出概率,然后你去调阈值。因为"漏升级"(本该上大模型却给了小模型,答错了)和"过度升级"(简单题也上大模型,多花钱)的代价是不对称的——绝大多数业务里,漏升级的代价比多花几分钱高得多。
策略三:自一致性路由(Self-Consistency Routing)
这是我最推荐作为主策略的一种:让小模型先答,然后判断它"答得稳不稳"。
具体做法(后面代码里会实现):
- 小模型用温度 $T > 0$(例如 0.8)采样 $k$ 次(通常 $k=3$);
- 对 $k$ 个答案做归一化后比对,统计一致率;
- 一致率高 → 说明模型对这个问题是"有把握"的,直接返回;
- 一致率低 → 说明模型在犹豫,升级到大模型。
为什么有效:模型采样结果的分歧度,和它答对的概率存在显著相关性。模型"拿不准"的时候,多次采样会给出五花八门的答案;"拿得准"的时候,几次采样高度收敛。这个信号是免费的——你已经花了那一次推理,只是多花两次采样,成本仍然远低于直接上大模型。
代价:小模型侧耗时变成约 3 倍(但可以并行发起),且需要任务是可判定的(答案能比较)。对于开放式写作类任务,改为让小模型自己输出一个 0~100 的置信度分,或者用第二个小模型做"评审"(LLM-as-judge),也能达到类似效果。
三种策略横向对比
| 维度 | 规则路由 | 分类器路由 | 自一致性路由 |
|---|---|---|---|
| 额外推理开销 | 无 | 一次小模型/向量打分 | $k-1$ 次小模型采样 |
| 准确率 | 中 | 中高 | 高(对可判定任务) |
| 可解释性 | 强 | 弱 | 中(可以给"分歧原因") |
| 维护成本 | 中(规则会膨胀) | 高(需重训) | 低(无需训练) |
| 冷启动难度 | 低 | 高(需日志积累) | 低 |
| 推荐用法 | 一票否决护栏 | 主策略(有数据后) | 主策略(冷启动) |
工程上成熟的形态通常是三层叠加:规则先做硬性拦截(例如涉法涉医必须大模型)→ 分类器给一个先验难度分 → 小模型自一致性校验做最终闸门。
三、架构与数据流
一次请求的完整生命周期:统一入口(OpenAI 兼容协议,业务方无感知)→ 规则闸门(命中即路由,零额外推理)→ 小模型执行(顺带取 logprobs,那是免费的置信度信号)→ 置信度判定(自一致性投票 / logprobs 均值 / 评审打分,三选一或组合)→ 升级或返回(低于阈值调大模型,并写入升级原因)→ 账本与复盘(记录模型、token、耗时、成本、是否升级)。
最后一步最容易被砍掉,但它决定了这套系统是"一次性降本"还是"持续降本"。
四、一个可直接运行的级联路由器
下面这份代码是完整可跑的,只依赖 httpx,对接任何 OpenAI 兼容端点(vLLM、One-API、New API、官方 API 均可)。
# -*- coding: utf-8 -*-
"""cascade_router.py —— 小模型优先、不达标再升级到大模型
依赖: pip install httpx 环境变量: OPENAI_BASE_URL / OPENAI_API_KEY"""
import os, re, time, json, asyncio
from collections import Counter
import httpx
BASE = os.getenv("OPENAI_BASE_URL", "http://localhost:8000/v1")
KEY = os.getenv("OPENAI_API_KEY", "sk-placeholder")
SMALL = os.getenv("SMALL_MODEL", "Qwen/Qwen2.5-3B-Instruct")
LARGE = os.getenv("LARGE_MODEL", "Qwen/Qwen2.5-72B-Instruct")
PRICE = {SMALL: {"in": 0.6, "out": 1.8}, LARGE: {"in": 4.0, "out": 12.0}} # 元/百万token
FORCE_LARGE = [r"推导", r"证明", r"一步步", r"逐步推理", r"写代码", r"对比分析"]
FORCE_SMALL = [r"^翻译[::]", r"^改写[::]", r"^提取", r"^总结为?\d*字"]
MAX_SMALL_INPUT, CONF_THRESHOLD, SAMPLES = 1500, 0.66, 3
_c = None
def client():
global _c
if _c is None:
_c = httpx.AsyncClient(base_url=BASE,
headers={"Authorization": f"Bearer {KEY}"}, timeout=60.0)
return _c
async def chat(model, messages, temperature=0.7, max_tokens=512):
r = await client().post("/chat/completions", json={
"model": model, "messages": messages,
"temperature": temperature, "max_tokens": max_tokens})
r.raise_for_status()
d = r.json()
return d["choices"][0]["message"]["content"], d.get("usage", {})
def cost(model, usage):
p = PRICE.get(model, {"in": 0, "out": 0})
return (p["in"] * usage.get("prompt_tokens", 0)
+ p["out"] * usage.get("completion_tokens", 0)) / 1_000_000
def norm(s): # 归一化:去空白/标点/大小写
return re.sub(r"[\s\W_]+", "", s).lower()
def rule_route(q): # 'large' / 'small' / None(交给置信度判定)
if any(re.search(p, q) for p in FORCE_LARGE) or len(q) > MAX_SMALL_INPUT:
return "large"
return "small" if any(re.search(p, q) for p in FORCE_SMALL) else None
async def self_consistency(messages, k=SAMPLES):
"""采样 k 次投票 -> (最一致答案, 置信度, 累计用量)"""
res = await asyncio.gather(*[chat(SMALL, messages, temperature=0.9) for _ in range(k)])
ans = [norm(a) for a, _ in res]
usage = {"prompt_tokens": sum(u.get("prompt_tokens", 0) for _, u in res),
"completion_tokens": sum(u.get("completion_tokens", 0) for _, u in res)}
top, cnt = Counter(ans).most_common(1)[0]
return res[ans.index(top)][0], cnt / k, usage
async def route(query, system="你是一个严谨的助手,回答简洁准确。"):
messages = [{"role": "system", "content": system}, {"role": "user", "content": query}]
led = {"small_calls": 0, "large_calls": 0, "cost": 0.0,
"upgraded": False, "reason": "small_confident"}
t0 = time.time()
forced = rule_route(query)
if forced == "large":
ans, u = await chat(LARGE, messages, temperature=0.3)
led.update(large_calls=1, upgraded=True, reason="rule_forced_large", cost=cost(LARGE, u))
elif forced == "small":
ans, u = await chat(SMALL, messages, temperature=0.3)
led.update(small_calls=1, reason="rule_forced_small", cost=cost(SMALL, u))
else:
ans, conf, u = await self_consistency(messages)
led.update(small_calls=SAMPLES, confidence=round(conf, 3), cost=cost(SMALL, u))
if conf < CONF_THRESHOLD: # 没把握 -> 升级
ans, u2 = await chat(LARGE, messages, temperature=0.3)
led.update(large_calls=1, upgraded=True, reason=f"low_confidence_{conf:.2f}",
cost=led["cost"] + cost(LARGE, u2))
led["latency_s"] = round(time.time() - t0, 2)
return ans, led
if __name__ == "__main__":
async def demo():
total = 0.0
for q in ["把下面这段话翻译成英文:今天天气不错,适合出门散步。",
"水池甲管 6 小时注满、乙管 4 小时注满,两管同开多久注满?请一步步推导。",
"帮我提取这段文本里的联系人姓名和手机号。"]:
ans, led = await route(q)
total += led["cost"]
print(json.dumps(led, ensure_ascii=False), "|", ans[:60].replace("\n", " "))
print(f"总成本: {total:.6f} 元")
asyncio.run(demo())
运行:
pip install httpx
export OPENAI_BASE_URL="http://localhost:8000/v1"
export OPENAI_API_KEY="sk-placeholder"
python cascade_router.py
这段代码已经包含了级联的所有关键要素:规则闸门、自一致性投票、阈值升级、成本账本、升级原因留痕。你只要把 PRICE 换成真实单价、把 FORCE_LARGE / FORCE_SMALL 换成你业务的关键词表,就能直接上灰度。
五、成本能降多少:把账算清楚
级联到底能省多少,取决于一个数字:升级率 $r$(走大模型的请求占比)。
设全量走大模型的单请求平均成本为 $C_L$,全量走小模型为 $C_S$,自一致性带来的额外采样倍率为 $k$,则级联后的单请求成本约为:
$$C_{cascade} \approx k \cdot C_S \cdot (1-r) + (k \cdot C_S + C_L) \cdot r
= k \cdot C_S + r \cdot C_L$$
也就是说,成本 = 小模型采样开销(固定支出)+ 升级率 × 大模型单价(浮动支出)。这个公式极其有用,它告诉我们两件事:
- 升级率 $r$ 是唯一的浮动杠杆,每降 10 个点,成本就线性下降;
- 小模型的采样开销是"底座",如果 $k$ 太大(比如采样 5 次),底座会抬高,抵消升级率下降的收益。所以 $k=3$ 通常是性价比拐点。
带入前文示例量级($C_L{in}=15$、$C_L=60$;$C_S{in}=0.6$、$C_S=1.8$,输入 1200 / 输出 300 token):
| 策略 | 相对成本 | 说明 |
|---|---|---|
| 全量旗舰大模型 | 100% | 基线,质量最好但最贵 |
| 全量小模型 | 约 4% | 便宜,但复杂问题质量不可接受 |
| 级联(升级率 30%) | 约 40% | 保守阈值,质量接近全量旗舰 |
| 级联(升级率 18%) | 约 25% | 调优后的常见区间 |
| 级联 + 前缀缓存 + 提示压缩 | 约 13%~18% | 三招叠加,缓存命中率高时更明显 |

注意这里的"质量"是有代价的。升级率从 30% 压到 18%,意味着有 12% 的请求从"大模型答"变成"小模型答",其中一部分会答错。所以调阈值时一定要盯着质量指标一起看,而不是只看账单。
六、质量护栏:别为了省钱把体验搞砸
级联最怕的不是多花钱,是漏升级——一个本来该上大模型的问题被小模型糊弄过去了,用户拿到一个一本正经的错误答案。这类事故比多花几块钱严重得多。
我的做法是加三道护栏:① 非对称阈值——漏升级代价远高于过度升级,阈值要偏向"宁可多升级",自一致性阈值一开始定 0.66(3 次采样 2 次一致),宁可多花也别错;② 高风险领域强制升级——医疗、法律、金融、人身安全相关一律走大模型,并在提示词里加"不确定时明确说不确定",这条是硬规则,不看置信度;③ 用户反馈闭环——前端放一个"这答案不对"按钮,点一次就把该请求连同上下文存进 bad case 库并强制升级重答,既是兜底,也是调优分类器最好的标注数据源。
评估效果时不要只看成本,至少同时盯这四个指标:
| 指标 | 定义 | 健康区间(参考) |
|---|---|---|
| 升级率 $r$ | 走大模型的请求占比 | 15%~35%,视业务难度 |
| 漏升级率 | 小模型答错且未升级的比例 | 越低越好,建议 < 2% |
| 过度升级率 | 小模型本来能答对却升级的比例 | 可接受 10%~25% |
| P95 延迟 | 包含升级路径的端到端耗时 | 不超过全量大模型的 1.2 倍 |
"过度升级率高"不是坏事,它是你为漏升级买的安全垫。真正要压制的是漏升级率。
七、什么时候不该用级联
四类场景我不建议上:① 请求形态单一且都很简单(95% 都是"提取手机号")→ 直接全用小模型;② 对延迟极度敏感的实时交互(语音字幕、输入法联想)→ 升级路径串行,尾部延迟翻倍;③ 输出不可比较的开放式创作 → 投票机制失效;④ 日均请求量太小(< 5000 次)→ 省的钱不够维护这套系统。
八、生产踩坑清单
- 阈值拍脑袋定死,上线后再也不看。请求分布会随季节、活动、产品改版漂移,必须把升级率做成仪表盘指标,月度复盘。
- 忘了给升级路径做超时兜底。级联是串行的,必须给大模型调用设独立超时(如 8 秒),超时就用小模型的最佳答案返回,并记录
fallback_timeout。 - 自一致性采样用了 temperature=0。温度为 0 时多次采样结果几乎一致,投票率恒等于 1,置信度信号直接失效——这是个非常隐蔽、会让你"以为级联很有效"的坑。采样温度必须 > 0(0.7~1.0)。
- 归一化函数写得太粗糙。只去空格会把"答案是 3"和"答案是 3。"判为不一致,升级率虚高;归一化过头又会误判不同答案为一致。归一化规则要针对任务单独设计并回归测试。
- 成本账本只统计 token,没统计失败重试。重试、流式中断、超时重发都产生真实费用。账本要挂在实际发出的请求上,否则成本模型系统性偏低。
- 大小模型部署在同一实例抢显存。级联意味着两个模型都常驻,小模型应单独部署(甚至可用 CPU 或消费级卡),大模型独占推理卡。
- 提示词没做区分。小模型需要更强的输出格式约束(JSON Schema、少样本示例、更短指令),大模型才适合开放式指令。同一份提示词下,小模型表现会被系统性低估。
九、练手项目
- 搭一个最小级联网关并压测。用 vLLM 起一个 3B 和一个 32B(或 API 版),套用本文代码,构造 300 条混合难度 query,测出不同阈值(0.5 / 0.66 / 0.9)下的升级率、准确率与总成本,画出"成本-准确率"帕累托前沿曲线。
- 训练一个难度分类器。跑两周级联日志,用"小模型答对/答错"自动打标,训一个 0.5B 二分类器,看能否把升级率再降 5~10 个点且漏升级率不升,并对比它与规则路由的差异。
- 做一次"漏升级"专项复盘。捞出所有被用户点"答案不对"的请求,人工分类:规则没拦住?置信度误判?还是小模型确实答不出来?输出根因分布图,针对性补规则。
结语
级联的本质不是"用小模型省钱",而是给每一次请求匹配它真正需要的算力——绝大多数团队的做法恰恰相反:为了省事,把所有请求都扔给最贵的模型,然后祈祷账单能扛住。
动手顺序建议:先上规则路由 + 账本(零成本,先看清请求分布)→ 再上自一致性校验(把难度判断自动化)→ 最后用日志训分类器(把升级率一点点压下来),全程盯着漏升级率,别让省钱变成事故。
最后提醒:所有成本数字都要在你自己的流量上重算一遍。本文公式是通用的,数字是示例的。你真正需要的不是抄一个阈值,而是建立"升级率—质量—成本"三者的量化关系,按自己的业务容忍度去调它。

浙公网安备 33010602011771号