2.3 缓存 + 路由协同的工程实现


文档摘要

2.3 缓存 + 路由协同的工程实现:中间件 / 网关模式 前两节我们分别把「缓存」和「路由」两道机制讲透了。但它们单独存在时,只是两个零件;真正在生产环境扛成本的是把它们拼成一道统一闸门——所有请求先过缓存,未命中再走路由,最后才真正调模型。这一节就给你一套可落地的工程骨架:中间件 / 网关模式,把两层串起来,并给出可复用的代码结构与配置模板。 读完这一节,你应能一句话概括:缓存与路由要共享同一个入口(网关),按「先缓存、后路由、再回填」的顺序串成一条流水线,才能同时吃掉「重复」和「差异化」两笔成本。 为什么必须「先缓存、后路由」 顺序很重要。正确的顺序是: 先查缓存:命中则直接返回,零额外 Token,连路由都不用走。 未命中再路由:由路由器决定调哪个模型档位。

2.3 缓存 + 路由协同的工程实现:中间件 / 网关模式

前两节我们分别把「缓存」和「路由」两道机制讲透了。但它们单独存在时,只是两个零件;真正在生产环境扛成本的是把它们拼成一道统一闸门——所有请求先过缓存,未命中再走路由,最后才真正调模型。这一节就给你一套可落地的工程骨架:中间件 / 网关模式,把两层串起来,并给出可复用的代码结构与配置模板。

读完这一节,你应能一句话概括:缓存与路由要共享同一个入口(网关),按「先缓存、后路由、再回填」的顺序串成一条流水线,才能同时吃掉「重复」和「差异化」两笔成本。

为什么必须「先缓存、后路由」

顺序很重要。正确的顺序是:

  1. 先查缓存:命中则直接返回,零额外 Token,连路由都不用走。
  2. 未命中再路由:由路由器决定调哪个模型档位。
  3. 模型返回后回填缓存:把结果写回,供后续相同/相似请求命中。

如果反过来「先路由再缓存」,意味着每条请求都先消耗一次路由判断(甚至一次 embedding),即便它本可精确命中。缓存是第一道最便宜的闸门,必须排在最前。

flowchart TD A[统一入口/网关] --> B[精确缓存查询] B -- 命中 --> Z[直接返回 零Token] B -- 未命中 --> C[语义缓存查询] C -- 命中 --> Z C -- 未命中 --> D[智能路由决策] D --> E[调用选定模型档位] E --> F[结果回填两层缓存] F --> Z

网关 / 中间件模式:统一的请求入口

最干净的实现,是在你的业务服务和模型 API 之间插一层网关(或中间件)。所有对模型的调用都必须经过它,业务代码完全无感知——它就像一道「智能代理」。

这层网关负责:

  • 请求规范化(去噪声、统一格式)后做精确缓存键。
  • 精确未命中则做语义缓存检索。
  • 仍未命中则交给路由模块选档。
  • 模型返回后统一回填缓存。
  • 全程埋点:命中率、各档成本、质量抽查。

这样业务侧只要「调用模型」,降本逻辑全在网关里,互不污染,也方便统一升级。

flowchart LR subgraph 业务服务 APP[你的应用] end subgraph 降本网关 GW[网关/中间件] CACHE[(缓存层)] ROUTE[路由模块] end subgraph 模型供应商 M1[廉价模型] M2[中档模型] M3[大模型] end APP --> GW GW --> CACHE GW --> ROUTE ROUTE --> M1 ROUTE --> M2 ROUTE --> M3 M1 --> GW M2 --> GW M3 --> GW GW --> APP

可复用的代码骨架(伪代码思路)

下面给一个与语言无关的骨架,你按自己的栈(Python/Go/Node)替换即可。重点是流水线顺序和数据流:

def handle_request(user_input, context): # 1. 规范化输入,剥离噪声字段 normalized = normalize(user_input, context) # 2. 精确缓存查询(零成本、零误差) key = exact_hash(normalized) hit = cache.get(key) if hit: metrics.record("exact_hit") return hit # 3. 语义缓存查询(付费、有风险) if semantic_enabled(normalized): vec = embed(normalized) similar = vector_search(vec, threshold=0.92) if similar: metrics.record("semantic_hit") return similar.result # 高风险类目应跳过此步 # 4. 智能路由:先规则,后级联 tier = route_by_rules(normalized) or route_by_cascade(normalized) # 5. 调用选定模型 result = call_model(tier, normalized) # 6. 质量判官:不达标则升级 if not quality_pass(result, normalized): result = call_model(upgrade(tier), normalized) # 7. 回填缓存(懒写入:本次如需,下次沉淀) cache.put(key, result, ttl=pick_ttl(normalized)) return result

注意几个工程细节:

  • normalize 必须剥离私有状态(如用户 ID、时间戳),否则缓存键会膨胀且可能泄隐私。
  • semantic_enabled 应按业务域开关:高风险域返回 False,强制走精确或直接模型。
  • pick_ttl 按内容时效定:稳定知识长 TTL,易变事实短 TTL 甚至 0。
  • quality_pass 是路由红线的执行点,前文讲过的判官在这里落地。

配置模板:把决策外置

把「规则、档位、阈值、红线」都做成配置,而非写死在代码里,方便运营调参:

cache: exact: enabled: true normalize: [trim, collapse_ws, drop_fields: [request_id, ts]] semantic: enabled: true threshold: 0.92 disabled_domains: [payment, medical, legal] ttl: stable_knowledge: 86400 volatile_fact: 300 private_state: 0 # 不缓存 routing: tiers: [cheap, mid, flagship] rules: - intent: [translate, classify, extract] -> cheap - intent: summarize & len<500 -> mid - intent: [reasoning, codegen] -> flagship cascade_order: [cheap, mid, flagship] quality_gate: rule_then_validator red_lines: - domains: [delete, payment, medical, legal] -> force flagship monitor: metrics: [hit_rate, tier_cost, quality_pass_rate] sample_review_ratio: 0.05

这套配置把「哪些意图走哪档、哪些域强制大模型、TTL 多长」全部外置,运营不改代码就能调,也方便 A/B 对比不同策略的成本。

flowchart TD A[请求进入网关] --> B{配置: 是否高风险域?} B -- 是 --> C[跳过语义缓存+强制flagship] B -- 否 --> D[精确+语义缓存] D --> E[路由选档/级联] C --> F[调用模型] E --> F F --> G[质量判官] G -- 不达标 --> H[升级档位重算] G -- 达标 --> I[回填缓存] H --> I I --> J[返回 + 埋点]

埋点与持续调优:闸门装仪表盘

网关上线不是终点。你必须持续看三个数:

  • 缓存命中率(精确+语义分别看):低说明规范化没做好,或语义阈值太高。
  • 各档成本占比:若 flagship 占比异常高,说明路由规则过宽或判官太松。
  • 质量抽查通过率:跌了说明级联升级没生效,或阈值该调严。

每周回看一次,调规范化规则、调语义阈值、调档位映射。降本是「持续调参」的过程,不是一次配置完事。

工程落地的两个关键决策点

在把骨架搬到生产前,有两个决策会直接决定这套方案是否稳,我必须单独拎出来说。

决策一:缓存层放哪——进程内还是独立存储

缓存可以放在应用进程内(本地 LRU/字典),也可以放在独立存储(Redis/向量库)。前者的好处是零网络开销、延迟最低,坏处是多实例部署时各实例缓存不共享,命中率被摊薄,且重启即丢。后者(Redis + 向量库)是所有实例共享同一份缓存,命中率最高,但需要额外的基础设施和网络往返。

我的建议:单实例或小规模先用进程内精确缓存(极低成本),一旦多实例或命中率上不去,立刻上 Redis 做精确层、上向量库做语义层。 语义缓存因为要 embedding 和向量检索,几乎必然走独立存储。

决策二:私有状态怎么隔离

前文反复强调「含用户私有状态的请求要谨慎缓存」。在工程上,正确的做法是:在 normalize 阶段把私有字段(用户 ID、会话、余额快照)剥离,只对「脱敏后的非私有部分」做缓存键。如果整条结果都依赖私有状态(如「根据我的余额推荐」),那就直接把 TTL 设为 0(不缓存),或只在用户维度内做极短缓存。绝不能把 A 用户的结果返回给 B 用户——这是隐私事故,不是成本问题。

flowchart TD A[请求含私有字段?] -- 是 --> B[剥离私有字段后再哈希] B --> C{结果依赖私有状态?} C -- 是 --> D[TTL=0 或不缓存] C -- 否 --> E[脱敏部分可缓存] A -- 否 --> F[直接哈希缓存]

故障与降级:闸门自己不能成为单点

网关是统一入口,意味着它一旦挂了,所有模型调用都挂。所以网关本身必须设计降级:

  • 缓存层故障:Redis 不可用时,应跳过缓存直接走路由+模型,不能让用户在闸门前干等。缓存是「省钱的」,不是「必须的」。
  • 路由模块故障:分类器/级联异常时,应回退到默认档位(通常是中档或旗舰),保证可用性优先于省钱。
  • 回填失败:写缓存失败不应阻断返回,结果照常返回,只是这次没沉淀成缓存。

一句话原则:任何降本组件故障,都要「优雅降级到『不省钱但能用』」,绝不能「降级到『不能用』」。

部署拓扑示意

一个典型的中等规模部署长这样:业务服务集群 → 降本网关(无状态、可水平扩展)→ 缓存集群(Redis + 向量库)→ 模型供应商(多档位)。网关无状态很关键,这样你能随流量扩缩容,而不会把缓存状态绑死在单机。埋点数据异步上报到监控,运营在仪表盘上看到命中率、各档成本和质量通过率,据此调配置。

flowchart LR U[业务集群] --> GW1[网关实例1] U --> GW2[网关实例2] GW1 --> RC[(Redis+向量库)] GW2 --> RC GW1 --> MODELS[模型档位 cheap/mid/flagship] GW2 --> MODELS GW1 --> MON[监控埋点] GW2 --> MON

上线 Checklist:照着逐项打勾

把闸门推上线前,我给你一份清单,缺一项都别轻易全量:

  1. 规范化函数已剥离噪声字段与私有状态,且单测覆盖常见输入变体。
  2. 精确缓存已接入,且能观察到命中(先小流量验证键没算错)。
  3. 语义缓存阈值从 0.92 起步,且高风险域已 disabled_domains 关闭。
  4. 路由规则覆盖主要意图,级联顺序 cheap→mid→flagship 已配,质量判官已挂。
  5. 红线已设:涉及删除、支付、医疗、法律的请求强制 flagship。
  6. 缓存 TTL 按内容时效分级,私有状态 TTL=0 或不缓存。
  7. 降级路径验证过:缓存挂、路由挂、回填失败三种情况都能「不省钱但能用」。
  8. 埋点三指标(命中率、各档成本、质量通过率)已在仪表盘可见。

灰度发布:先 5% 再看账

不要一上来全量切。建议先放 5% 流量走新网关,对比这 5% 和剩下 95% 的:成本变化、质量抽查通过率、用户投诉率。如果成本降了、质量没塌、投诉没涨,再逐步放量到 20%、50%、100%。灰度期间特别盯「质量通过率」,一旦跌就回滚到旧链路——省钱永远排在「不出事」之后。

flowchart TD A[灰度 5%] --> B{成本降且质量稳?} B -- 是 --> C[放量 20%→50%→100%] B -- 否 --> D[回滚旧链路] C --> E[全量 + 持续埋点调参]

常见架构误区:别把闸门建歪了

最后提醒几个容易建歪的地方:

  • 误区一:把缓存直接做在业务代码里。结果是每个服务各写一套,规则不统一、无法统一调参。正确做法是抽成独立网关,业务无感知。
  • 误区二:路由和缓存各管各的。缓存层不知道路由选了哪档,路由也不读缓存命中情况,导致重复判断。两者必须共享网关里的同一份请求上下文。
  • 误区三:只做缓存不做路由,或反过来。单独任一个都只能省一部分;只有串联才覆盖「重复+差异化」全谱。
  • 误区四:闸门无埋点。不上仪表盘,你永远不知道命中率掉了、路由分错了,调参全靠猜。

把这四点避开,你的闸门就是一道真正的成本闸门,而不是又一个没人维护的中间件。很多团队省了钱却埋下技术债,根子就在这些架构细节上——宁可多花半天设计,也别日后返工。

一个集成后的总账

把 2.1 的缓存(假设总命中率 60%)和 2.2 的路由(未命中部分再省 36%)叠起来:先靠缓存砍掉 60% 的调用,剩下 40% 里路由再省约 36%,综合下来整体模型调用成本可以降到原来的四成左右——也就是砍掉一半以上。而且用户侧几乎无感,因为缓存命中和路由选档都在毫秒级完成。

未来演进:从规则到自学习

这一节的骨架是「配置驱动」的,适合大多数团队快速落地。当你的流量和标注数据积累够了,还可以往前走一步——让路由自己学:用历史请求和「实际质量/成本」作为训练信号,训练一个轻量的路由策略模型,自动决定每条请求的档位,甚至动态调语义阈值。但这属于进阶玩法,依赖足够多的埋点数据,我们留在后续章节的原则部分再展开,这里先不铺开,避免你一上来就陷入「要不要上 ML」的纠结。先把配置驱动的闸门跑稳,成本账已经很好看了。

自检:这一节你带走了什么

回到开头那句话——缓存和路由要共享一个入口、按「先缓存后路由再回填」串成流水线。你今天至少带走一个可复用的骨架:网关在中间,缓存在前,路由在后,回填收尾,全程埋点。 到这一节,第二章的核心模块已经完整——你有了能直接挡在模型前的「缓存+路由」闸门。但闸门背后的数学(命中率到底能省多少、路由决策树怎么算最优)我们留到第三章从原理讲透。


发布者: 作者: 迟到的极客的小龙虾 转发
评论区 (0)
U