2.1 请求缓存层设计:精确缓存、语义缓存、TTL 与失效 上一章我们算清楚了一笔账:在两成业务里,真正烧钱的是「重复又选错」。这一节我们动手解决「重复」这一半——请求缓存层。读完这一节,你应该能用一句话说清楚自己学到了什么:缓存不是把东西存起来那么简单,选对缓存策略、管好它的失效边界,才真正把 Token 拦在模型门外。 一句话先定调:缓存层到底在拦什么 缓存层的唯一使命,是把「本可以不调用模型就能回答的请求」拦截在模型之外。每一次命中,省下的不只是这一次返回结果所消耗的 Token,还有背后那一次完整的推理算力。对高频重复场景(比如客服话术、代码模板、固定问答),缓存命中率做到六成以上,整体的模型调用量往往能砍掉一半。
上一章我们算清楚了一笔账:在两成业务里,真正烧钱的是「重复又选错」。这一节我们动手解决「重复」这一半——请求缓存层。读完这一节,你应该能用一句话说清楚自己学到了什么:缓存不是把东西存起来那么简单,选对缓存策略、管好它的失效边界,才真正把 Token 拦在模型门外。
缓存层的唯一使命,是把「本可以不调用模型就能回答的请求」拦截在模型之外。每一次命中,省下的不只是这一次返回结果所消耗的 Token,还有背后那一次完整的推理算力。对高频重复场景(比如客服话术、代码模板、固定问答),缓存命中率做到六成以上,整体的模型调用量往往能砍掉一半。
但缓存有代价:它引入了一层「可能过时、可能错误、可能泄露隐私」的状态。做不好,省下的钱远不够填事故的坑。所以这一节我不只讲「怎么存」,更要讲「什么时候不能存」「什么时候必须失效」。
精确缓存(Exact Cache)的逻辑直白到近乎无聊:把请求的完整输入(prompt + 参数)做哈希,作为键;命中则直接返回上次结果。它适合「同一句话会被反复问」的场景——比如固定的产品问答、固定的内部知识检索、批处理任务里的重复样本。
第一,零语义误差。既然键是逐字节相同的输入,返回的就是完全相同的输出,不存在「近似」带来的质量漂移。第二,实现成本极低。一个 Redis 或本地 LRU 就能扛住,不需要任何模型参与判断。第三,可解释、可审计。出问题能直接比对输入是否真的一致。
最大的短板是键太「脆」。用户输入里但凡多一个空格、换一行、时间戳变了、会话 ID 不同,哈希就不一样,直接未命中。真实业务里,用户的提问经常带着微小变动(「帮我写个 Python 读取 csv 的函数」「用 python 读 csv 怎么写」),精确缓存对这些「同义不同形」的请求完全无能为力。
request_id、时间戳、session_id)剥离后再哈希,避免无意义的键膨胀。语义缓存(Semantic Cache)是对精确缓存的直接补强。它不再比「字面上相不相同」,而是比「意思上近不近」:把请求向量化(embedding),在向量库里检索是否有「语义相似」的历史请求;相似度超过阈值就认为命中,直接复用(或微调后复用)历史结果。
它解决了精确缓存最痛的「同义不同形」问题。用户的自然语言千变万化,但意图高度集中。语义缓存能把这些变体合并到同一份答案上,命中率可以比精确缓存高出一个数量级,尤其适合开放问答、客服、搜索式查询。
第一,相似 ≠ 相同,复用有质量风险。两个看起来像的问题,答案可能差之千里。比如「怎么删除数据库表」和「怎么清空数据库表」,一字之差,操作破坏性天差地别。若直接复用,可能给出危险建议。
第二,embedding 本身有成本和延迟。每次请求都要先调一次 embedding 模型,再查向量库——这本身也是一次调用,且要钱。当你的缓存命中率不够高时,反而会「为缓存多花一笔 embedding 的钱」。
第三,阈值难调。阈值太高,退化成近似精确缓存,命中率上不去;阈值太低,开始乱复用,质量塌方。
很多人以为「缓存设个永不过期最省事」,这是生产事故的典型起点。缓存必须有寿命,因为模型背后的世界在变。
所以 TTL(Time To Live)是缓存的「保质期」。读多写少、事实稳定的内容(如语法知识、固定模板)可以设较长 TTL(小时到天级);事实敏感、易变内容(如实时报价、用户个性化结果)必须短 TTL 甚至禁用缓存。
TTL 是「到点再说」,但很多变更是突发且重要的(运营改了活动规则、安全补丁发布)。这时需要主动失效:
cache:faq:、cache:price:),变更时清整个桶,避免漏删。model_version 或 prompt_version,模型升级后旧键自然全部失效,无需手动清。我曾见过一个团队把「根据用户余额推荐理财产品」的结果也做了缓存,还设了 24 小时 TTL。结果某用户上午充值后,下午问的推荐还是基于上午余额给的——直接给了错配建议,差点引发投诉。教训很硬:凡结果依赖「用户私有状态」或「实时数据」,要么不缓存,要么键里必须包含这些状态的最小快照,且 TTL 极短。 涉及隐私(PII)的内容,更要谨慎,很多合规要求「不缓存用户身份信息」,这类必须脱敏甚至禁止缓存。
讲了这么多,很多人还是纠结「到底上哪个」。我把两者的关键差异摊在一张表里,你照着对号入座:
| 维度 | 精确缓存 | 语义缓存 |
|---|---|---|
| 命中判断依据 | 输入逐字节相同(哈希) | 语义向量相似度 |
| 额外成本 | 无(纯计算哈希) | 每次需调 embedding 模型 |
| 质量风险 | 零(完全相同才复用) | 有(相似≠相同,可能复用错答案) |
| 抗输入扰动 | 弱(多空格即未命中) | 强(同义不同形也能命中) |
| 实现复杂度 | 极低 | 中(需向量库+embedding) |
| 适用场景 | 标准化、批处理、固定问答 | 自然语言多、意图集中、事实稳定 |
| 典型命中率 | 取决于输入规范性 | 通常显著高于精确缓存 |
我的建议永远是串联而非二选一:先精确(免费、零误差),未命中再语义(付费、有风险但补长尾)。这样高频请求走精确通道几乎零成本,只有长尾才消耗 embedding。
缓存被命中是省钱,但「写入缓存」本身也有讲究——尤其是回填时机。常见两种策略:
我更偏向懒写入:对长尾请求,它不会污染缓存;对真正高频的请求,第二次就沉淀成缓存。配合 TTL,缓存里留下的都是「活得久、问得多」的干货。
另一个坑是一致性:当业务数据更新,缓存若没跟着失效,就会返回旧答案。这在前文 TTL 与主动失效已讲过,但特别强调一点——回填时务必带上「数据版本号」或「来源时间戳」,这样即便 TTL 没到,也能通过版本比对发现脏数据。
假设你每天有 10 万次请求,平均每次输出 500 Token、单价折算每千 Token 0.01 元。若不做缓存,每天模型输出成本约 10 万 × 500 / 1000 × 0.01 = 50 元(仅输出侧,输入侧另算)。若缓存命中率做到 60%,其中精确命中占四成、语义五成、其余长尾——那么约 6 万次请求直接命中,省下约 30 元/天,一个月近 900 元,一年过万。这还没算输入 Token 和推理算力的节省。对中等规模业务,缓存层往往是「投入半天、回本一周」的划算买卖。
作为作者,我替你做个取舍——不要三个都上,按业务形态选主角:
回头看开头那句话——缓存层是「把本可以不调模型的请求拦在门外」。要做到这一点,你至少要做出三个决定:用精确还是语义(或串联)、TTL 设多长、哪些内容根本不能缓存。这三个决定比「用什么中间件」重要得多。下一节我们讲「没被缓存拦下的请求」该怎么办——那就是智能路由的战场。
提示:缓存层只解决「重复」。剩下大量一次性、差异化的请求,才是路由要接管的;同时,缓存写回(回填)的时机,也要和路由配合——这一点我们在 2.3 一起串起来讲。