ARTICLE / 7 MIN READ
高并发下的成本与延迟博弈:模型路由与多级缓存
大促把客服 Agent 的 QPS 从 50 顶到 5000,全调大模型烧钱、全调小模型答错。用网关层毫秒级路由、语义缓存和降级兜底链,在成本、延迟和正确率之间找平衡。
大促零点,客服 Agent 的 QPS 从平时的 50 直接顶到 5000。如果每一条都甩给 GPT-4o,账单一小时就够我解释半年;如果为了省钱全换小模型,退货政策、订单状态这类需要多步推理的问题就开始胡说,投诉又压回来。这不是“选一个模型”的题,而是“让每一条请求走它配得上的那条路”的题。我的做法是两件事叠在一起:网关层做毫秒级路由,把请求按难度分档;前面再垫一层多级缓存,让大量重复问题根本走不到模型。
路由判断不能用大模型来做
第一个反直觉的点:判断“这个问题该给谁”的分类器,本身不能是大模型。用 GPT-4o 去判断该不该用 GPT-4o,等于每条请求先交一次全额延迟和成本的“过路费”,路由就失去了意义。网关层的判断必须控制在个位数毫秒,所以只能用三种轻量手段的组合:
route(query):
# 1. 规则前置:命中即短路,最快
if matches_faq_regex(query): return L_FAQ # ~0.1ms
if has_sensitive_entity(query): return L_HUMAN # 涉资金/投诉直转人工
# 2. embedding 最近邻分类:拿意图中心向量比余弦
emb = embed(query) # 本地小模型 ~2-5ms
intent, score = nn_search(emb, intent_centroids)
if score < 0.6: return L_LARGE # 不像任何已知意图,保守走大模型
# 3. 按意图档位路由
return TIER_BY_INTENT[intent]
embed 用的是本地部署的小 embedding 模型(几十毫秒不可接受,要压到 2-5ms,靠 GPU 常驻 + 批处理)。intent_centroids 是离线用历史问题聚类算出来的每个意图的中心向量,上线前固化。这一层的产出只有一个:意图标签。真正的分档在下一张表。
三档分流:让简单问题永远碰不到大模型
| 意图档位 | 典型问题 | 目标 | 延迟预算 |
|---|---|---|---|
| L_FAQ | “运费多少”“几点发货” | 直接返回 FAQ 模板 | < 5ms |
| L_SMALL | “我的订单到哪了”“怎么改地址” | 小模型 + 工具调用 | < 800ms |
| L_LARGE | “这个退货政策适用我这单吗” | 大模型多步推理 | < 4s |
| L_HUMAN | 投诉、退款争议、涉资金 | 人工队列 | — |
关键在于 L_FAQ 和 L_SMALL 要尽可能宽:大促期间 70% 以上的问题是物流和订单状态查询,这些根本不需要 GPT-4o。把它们拦在前两档,大模型的实际 QPS 可能只有总量的 10%,成本曲线立刻塌下来。分档阈值(比如 score < 0.6 走保守路径)我会按线上误分类的代价来调——把“该走大模型却走了小模型”的漏判看得比“多花一次大模型钱”更重,因为前者直接变成错误答案。
多级缓存:三层各挡一类流量
路由之外,缓存是第二个成本杀手。我分三层,命中一层就返回:
- L1 精确缓存:
key = hash(normalized_query + tenant + tool_context)。归一化包括去空格、转小写、统一标点、剥掉无意义语气词。命中率不高,但延迟接近 0,先挡一遍完全重复的问法。 - L2 语义缓存:这是重点,下一节单独讲。
- L3 结果/工具缓存:把工具调用结果(如“订单 123 的物流状态”)单独缓存,TTL 按数据新鲜度设,物流状态给 30s,退货政策给 1h。同一订单被反复问时,模型每次都重新推理但工具结果直接复用。
语义缓存 Key:绝不用原文当 Key
“我的包裹到哪了”和“快递到哪儿了”字面完全不同,语义完全相同。如果拿原文做 key,这两条永远各存一份,缓存命中率上不去,等于缓存穿透——每条都当新问题打到模型。正确做法是不拿字面做 key,而是拿向量做近邻匹配:
semantic_lookup(query):
emb = embed(normalize(query))
hit, sim = vector_index.nearest(emb) # 向量库近邻检索
if sim >= 0.95: # 阈值命中
if hit.intent == cur_intent and \
hit.tenant == cur_tenant and \
not entity_conflict(query, hit): # 二次校验:意图/租户/关键实体一致
return hit.answer
return MISS
阈值 0.95 是权衡点:调低命中率高但会把“退运费”和“退货”这种近义但不同的问题混淆,返回错答案;调高则退化成精确匹配。所以命中向量后必须做二次校验——比对意图标签、租户、以及问题里的关键实体(订单号、城市、金额),任一冲突就判 MISS。缓存穿透和雪崩还要另外防:对于查不到的问题缓存一个短 TTL 的空标记,避免同一冷门问题反复打模型;给热点缓存的 TTL 加随机抖动,避免同一时刻集中失效把流量瞬间灌给模型。
限流降级:一条从大到小、从机器到人的兜底链
大模型 API 突然 429 限流,是大促必然发生的事。不能让它直接变成用户端的报错,要有一条明确的降级链,每一跳都比上一跳更廉价、更保命:
大模型 (GPT-4o)
│ 429 / 超时 / 熔断打开
▼
小模型 (本地或廉价 API) ← 质量降级但仍能答大部分
│ 仍失败 / 仍限流
▼
FAQ 模板 / 规则应答 ← 只能覆盖高频问题,但保证有回复
│ 覆盖不到
▼
人工兜底队列 ← 入队 + 告知"稍后回复",绝不静默失败
配套三个机制才能让这条链真正生效。熔断:对每个上游模型统计错误率,超阈值就打开熔断器,后续请求直接跳到下一跳,不再往已经限流的上游硬打,半开态再探活。超时预算:整条请求给定总预算(比如 4s),每一跳减去已耗时向下传,绝不出现“大模型卡了 3.9s 才失败,降级后小模型没时间跑完”的情况。并发控制:用信号量或令牌桶给每个上游设并发上限,主动把超出容量的请求提前降级,而不是等上游把 429 甩回来——主动降级永远比被动接错误快。
边界
这套设计不是免费的。路由分类器会误判,intent_centroids 需要定期用新数据重训,否则大促出现的新问法会被误分到旧意图。语义缓存的 0.95 阈值和二次校验规则要按业务持续调,价格、库存、权限这类强时效问题我会直接标记为不可缓存,宁可每次打模型也不能返回过期答案。降级链保证了“有回复”,但降级后的回复质量是真的会降,这需要在产品侧诚实告知,而不是假装无事发生。路由和缓存能把成本压下来,但压不掉“业务本身要求高质量推理”的那部分刚性成本——那部分只能靠把大模型用在刀刃上。
参考资料:GPTCache 语义缓存实现。
GITHUB DISCUSSION
评论与回复
评论保存在 GitHub Discussions,登录 GitHub 后即可参与,发布和回复都在本页完成。
评论区进入视口后自动加载。