2929 字
约 9 分钟
18
S20-X15 · 多模型路由与成本控制

一个客服问答接口,绝大多数流量是"我的订单到哪了",少部分是三百字的投诉。全走最强模型,账单像开了水龙头;全走小模型,投诉那一类答得一塌糊涂。多模型不是"多接几个 API",而是在请求进入模型之前,先决定这次该花多少钱。

成本不是事后看账单能控制的,它由四个变量在请求发生的那一刻就锁定了:模型单价、输入长度、输出长度、调用次数。


1. 为什么需要多模型

动因 表现 工程含义
能力梯度 简单分类小模型就够,复杂推理要强模型 按难度分流,别一律顶配
成本梯度 单价可差一个数量级 分流即省钱,量级按各家价目核对
延迟差异 小模型首 token 明显更快 交互场景优先小模型
可用性冗余 单一供应商限流或故障 主备切换,接口契约要统一
合规与地域 数据不能出境、需本地部署 按租户/地域路由到不同部署

最后一条常被忽略:某些租户的请求必须走境内节点,这不是成本问题,是路由的硬约束,优先级高于一切成本优化。


2. 路由的四种依据

依据 判定手段 优点 风险
任务类型 意图分类(小模型或规则) 稳定、可解释 分类错则整条链路错
输入长度/复杂度 token 数、实体数、是否含代码 零成本、即时 长度≠难度
置信度/自评 小模型输出概率或自评分数 动态 自评本身要花一次调用
成本预算 剩余额度、租户等级 直接控钱 额度耗尽时的降级要设计
def route(req, tenant):
    if tenant.region == "cn":                    # 硬约束优先
        return "model-cn-small" if req.tokens < 2000 else "model-cn-large"
    if req.tokens > 8000 or req.has_code:
        return "model-large"
    if req.intent in {"refund_policy", "complaint"}:
        return "model-large"
    return "model-small"

反模式:用"让模型自评难度"来做路由判据。自评要调用一次模型,如果用小模型自评、再由大模型作答,你付了两次钱;用大模型自评,等于每次都先付大模型的钱。自评只在"答错代价极高"的场景成立。


3. 级联策略与成本结构

级联是"小模型先答,不合格再升级"。它的成本不是两段相加,而是加权:

总成本 ≈ c1 + (1 - h) · c2
其中 c1 = 小模型单次成本,c2 = 大模型单次成本,h = 小模型命中率(一次通过率)
h(命中率) c2 = 5·c1 时总成本 c2 = 20·c1 时总成本
0.5 3.5·c1 10.5·c1
0.7 2.2·c1 6.7·c1
0.9 1.4·c1 2.9·c1

读法:升级代价越大,命中率的价值越高。h 从 0.7 提到 0.9,在 20 倍单价差下成本降一半以上;若 h 只有 0.5,级联几乎等于全走大模型还多付一次小模型的钱。

def cascade(prompt, checker):
    out = call("model-small", prompt)
    ok, reason = checker(out)            # 规则校验优先,别用模型判
    if ok:
        return out, {"tier": 1, "reason": None}
    escalated = call("model-large", prompt + "\n\n上次回答的问题:" + reason)
    return escalated, {"tier": 2, "reason": reason}

命中率靠可判定的检查器提升:格式校验、必填字段、引用是否命中检索片段、是否触发拒答词表。这些判定便宜且确定;用模型判"答得好不好"会把省下的钱又花回去。


4. 成本的可控项

可控项 手段 效果量级 副作用
输入长度 检索片段去重、历史压缩 直接线性 信息损失
输出上限 max_tokens 硬限 防跑飞 长回答被截断
缓存命中 精确/语义/前缀缓存 命中即省全额 一致性风险
批处理 离线任务合并提交 通常有折扣 延迟上升
减少重试 首次就带足上下文与示例 少一次即省一半 提示词变长
工具收窄 按阶段暴露少量工具 减少无效轮次 灵活性下降

max_tokens 是最便宜的一道闸:不设上限时,模型偶尔会输出一段冗长复述,一次就把预算吃掉。设上限的代价是可能截断,所以要配合"输出结构简单化"。


5. 缓存的三层

层 键 命中条件 风险
精确匹配 规范化后的完整 prompt 哈希 逐字节一致 命中率低(温度、时间戳都会变)
语义相似 embedding + 阈值 语义近 错配:近义不同问,答案不可互换
前缀缓存 prompt 前缀 前缀一致 取决于供应商支持与计费方式
import hashlib, json

def cache_key(model, version, system, user, params):
    raw = json.dumps({"m": model, "v": version, "s": system, "u": user,
                      "p": {"t": params.get("temperature"), "mt": params.get("max_tokens")}},
                     sort_keys=True, ensure_ascii=False)
    return "llm:" + hashlib.sha256(raw.encode()).hexdigest()

缓存键必须含模型名与版本。换模型后键不变,就会把旧模型的答案发给新模型的用户——这类 bug 极难发现,因为"答案看起来是对的"。

语义缓存要用在"答案与措辞无关"的场景(政策条款、FAQ),不能用在"差一个字就不同答案"的场景(金额、日期、订单号)。判别方法:把两个语义相近但关键实体不同的 query 拿去比,若缓存判为同一,这个阈值就不能用。


6. 预算与配额

维度 计量 超限动作
用户 每用户每日 token 降级到小模型
租户 每月费用 拒绝非关键接口、告警
接口 每接口 QPS × 单价 限流、排队
全局 日预算 熔断到缓存/模板回答
LIMITS = {"user_daily_tokens": 200_000, "tenant_monthly_cost": 5_000}

def guard(tenant, user, est_tokens):
    if daily_used(user) + est_tokens > LIMITS["user_daily_tokens"]:
        return "downgrade"          # 降级,不拒绝
    if monthly_cost(tenant) > LIMITS["tenant_monthly_cost"]:
        return "reject_non_critical"
    return "allow"

降级顺序要写死并演练:先降模型档位 → 再减检索片段数 → 再缩短输出上限 → 最后命中缓存或返回模板。顺序反了(先砍检索)会让答案质量断崖,而成本只省一点。

预算只做告警不做拦截,等于没有预算——账单是在超限之后才看到的。


7. 能力评测

同一批用例跑多个模型,比三条线:准确率、延迟、成本。缺一条都会误判。

CASES = load_cases("eval/routing.jsonl")     # 每行: {"input":..., "expect":...}

def bench(model):
    rows = []
    for c in CASES:
        t0 = time.perf_counter()
        out = call(model, c["input"])
        rows.append({"ok": score(out, c["expect"]),
                     "lat": time.perf_counter() - t0,
                     "cost": tokens(out) * PRICE[model]})
    return {"model": model,
            "acc": sum(r["ok"] for r in rows) / len(rows),
            "p95_lat": percentile([r["lat"] for r in rows], 95),
            "cost_per_case": sum(r["cost"] for r in rows) / len(rows)}
维度 看什么 决定
准确率 分任务类型统计,不只看总分 哪些任务能交给小模型
延迟 p50 与 p95,不只看均值 交互场景是否可用
成本 单用例成本 × 预估日调用量 预算是否成立
稳定性 重跑两次的一致率 能否用于自动化链路

按任务类型分开统计是关键:好看的总分往往由「简单题几乎全对、难题大面积答错」构成,而简单题恰好占绝大多数流量。这个分布才决定路由阈值。


8. 模型替换的兼容

维度 检查项 不兼容的后果
输出格式 JSON mode / 结构化输出是否支持 解析层整体失效
工具调用 是否支持并行、是否支持严格 schema 多步 Agent 退化
上下文窗口 实际可用长度 长文档任务被截断
特殊 token 是否支持 system 角色、是否有专属控制符 提示词行为漂移
计费口径 按输入/输出分别计价还是合并 成本估算失真
限流 RPM/TPM 配额 高峰期大面积超时

替换前必须做的:拿同一套评测集跑一遍,对齐输出格式与工具调用语义;把"模型能力假设"从代码里抽出来做成配置(是否支持并行工具、最大窗口、是否支持 JSON schema),而不是散落在 if 里。


9. 常见坑

坑 后果 改法
只用最强模型 成本失控 按任务分流 + 级联
路由判据用模型自评 两级都贵 改规则/长度/意图分类
缓存键不含模型版本 换模型后返回旧答案 键里加 model + version
预算只告警不拦截 超限才发现 硬拦截 + 降级顺序
语义缓存阈值过松 近义不同问被合并 用实体差异样本校准
升级重试不带失败原因 大模型重复同样的错 把失败原因拼进升级请求

10. 排错

现象 先查 处理
账单突然翻倍 单请求 token 分布 找超长输入/无上限输出
降级后质量断崖 降级顺序 先降档位再减片段
同一问题答案不一致 缓存命中情况 缩短 TTL,键加参数
高峰期大量超时 限流配额与并发 排队 + 主备切换
换模型后解析失败率上升 输出格式支持 降级 JSON mode + 校验
某租户费用异常 是否绕过配额校验 配额校验前置到网关

11. 边界

场景 更合适 理由
单租户、低流量 单一模型 + 缓存 路由复杂度不划算
强合规要求 固定区域部署,不做成本路由 合规优先于省钱
离线批处理 用便宜模型 + 人工抽检 延迟不敏感
答案必须可复现 固定模型版本 + 温度 0 路由会引入不确定性
极短请求(分类打标) 专用小模型或规则 通用大模型是浪费

12. 自测

  1. 级联的总成本为什么不是 c1 + c2,命中率 h 在其中起什么作用?
  2. 为什么"让模型自评难度再路由"通常不省钱?
  3. 缓存键里为什么必须包含模型名与版本?
  4. 语义缓存在什么类型的问答上不能用,怎么校准阈值?
  5. 预算超限时的降级顺序为什么要把"减检索片段"排在"降模型档位"之后?
  6. 评测只看总分会导致什么误判,应该怎么拆?
S20-X15 · 多模型路由与成本控制
https://www.clxhxhhr.top/posts/5893/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。