
双层缓存架构传统 Redis 精确匹配 向量近似检索的最佳协同路径每次大促前做架构评审调用外部大模型接口的账单总会让团队倒吸一口凉气。在日均数千万次咨询的客服场景中大模型单次调用的成本哪怕只有几厘钱放大到大促峰值也是一笔不小的开支。更致命的是响应延迟大模型推理的 TTFTTime to First Token通常在 500ms 到 2s 之间而前端用户对常见问题的心理预期是“秒回”。仔细分析线上全量日志后会发现大促期间超过 60% 的用户咨询其实都在询问极度集中的常规规则“今年满减跨店怎么算”、“活动优惠券能和红包叠加吗”、“退货包运费险规则是什么”。如果我们对每一个问题都发起完整的 Embedding 计算和模型推理既浪费算力又拖慢响应。为了在降本、低延迟和高准确率之间找到平衡点我们设计并落地了一套双层缓存架构第一层采用经过文本规范化的传统 Redis 精确哈希匹配第二层采用向量数据库近似检索ANN两层均未命中才穿透给大模型。为什么不能单靠向量检索做语义缓存现在很多开源框架如 GPTCache主打语义缓存Semantic Cache提倡将每一次用户的提问转化成 Embedding 向量然后去向量数据库里检索余弦相似度Cosine Similarity。很多同学以为只要引入向量检索就能包治百病但在高并发生产场景下纯向量检索有两大致命短板计算与检索延迟无法忽略计算一段文本的 Embedding 向量调用内网自建的小模型通常需要 10ms 到 30ms再在向量索引中进行 HNSW 检索耗时大约 5ms 到 15ms。整套流程下来缓存命中的耗时也达到了 20ms 以上。如果 QPS 冲到数万单纯为了算 Embedding 就会把向量推理服务压垮。相似度阈值的边界效应相似度阈值设得太高如 0.95许多意思完全相同但表达略有差异的问题无法命中设得稍低如 0.88又经常出现致命误判。比如“我想要退货”与“我不想要退货”在某些向量空间里的相似度高达 0.91一旦误判返回直接导致业务事故。相反传统 Redis 的 Key-Value 精确匹配单次查询通常在 0.5ms 到 1ms 之间内存吞吐量极高。对于高频点击的“猜你想问”快捷按钮或者完全相同的输入精确匹配具有最高的性价比和确定性。双层缓存的流转拓扑设计我们将缓存拦截设计为三级漏斗[用户提问输入] │ ▼ [文本标准化管道] ─── (小写转换、去空格、去特殊标点、分词排序) │ ▼ 【L1 传统 Redis 精确缓存】 ──(命中: 0.8ms)── [直接返回预存结果] │ (未命中) ▼ 【L2 向量语义近似检索】 ──(相似度 0.93)── [返回高可信语义结果] │ (未命中) ▼ 【L3 穿透至大模型推理】 ─── [生成最新回答] │ └──── 异步回填 L1(精确Key) 与 L2(向量库)核心实现细节与代码落地1. 文本规范化清洗用户输入的标点符号、全角半角、末尾空格极易导致传统哈希缓存失效。在生成 L1 缓存 Key 之前必须经过确定性的清洗通道package com.yali.ai.cache; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class TextNormalizer { /** * 过滤无关标点符号、统一转小写、合并连续空格 */ public static String normalize(String text) { if (text null || text.isBlank()) { return ; } // 去除首尾空格并转小写 String cleaned text.trim().toLowerCase(); // 移除非文字/数字字符仅保留中文、英文、数字 cleaned cleaned.replaceAll([\\p{Punct}\\p{Space}], ); return cleaned; } /** * 计算 SHA-256 指纹作为 L1 Redis Key */ public static String computeDigest(String rawText) { String normalized normalize(rawText); try { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] hash digest.digest(normalized.getBytes(StandardCharsets.UTF_8)); StringBuilder hexString new StringBuilder(); for (byte b : hash) { String hex Integer.toHexString(0xff b); if (hex.length() 1) hexString.append(0); hexString.append(hex); } return hexString.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(SHA-256 不可用, e); } } }2. 双层协同服务骨架在业务入口层我们通过统一的门面类协调 L1、L2 与 L3 的流转。未命中时引入分布式互斥锁防止高并发下同一冷门问题瞬间击穿到大模型。package com.yali.ai.cache; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.time.Duration; import java.util.List; import java.util.concurrent.TimeUnit; Service public class DualLayerAiCacheService { private final StringRedisTemplate redisTemplate; private final VectorSearchEngine vectorSearchEngine; private final EmbeddingModel embeddingModel; private final ChatClient chatClient; private final RedissonClient redissonClient; private static final String L1_PREFIX ai:cache:l1:; private static final String LOCK_PREFIX ai:lock:query:; private static final float SIMILARITY_THRESHOLD 0.93f; public DualLayerAiCacheService(StringRedisTemplate redisTemplate, VectorSearchEngine vectorSearchEngine, EmbeddingModel embeddingModel, ChatClient chatClient, RedissonClient redissonClient) { this.redisTemplate redisTemplate; this.vectorSearchEngine vectorSearchEngine; this.embeddingModel embeddingModel; this.chatClient chatClient; this.redissonClient redissonClient; } public String query(String rawPrompt) { // 1. L1 精确匹配查询 String digest TextNormalizer.computeDigest(rawPrompt); String l1Key L1_PREFIX digest; String cachedAnswer redisTemplate.opsForValue().get(l1Key); if (cachedAnswer ! null) { return cachedAnswer; } // 2. L2 向量语义近似匹配 ListFloat promptVector embeddingModel.embed(rawPrompt); SearchResult vectorMatch vectorSearchEngine.findNearest(promptVector); if (vectorMatch ! null vectorMatch.getScore() SIMILARITY_THRESHOLD) { // 向量命中后顺手回填 L1 缓存加速后续完全相同的提问 redisTemplate.opsForValue().set(l1Key, vectorMatch.getContent(), Duration.ofHours(6)); return vectorMatch.getContent(); } // 3. L3 穿透至大模型防击穿互斥锁 String lockKey LOCK_PREFIX digest; RLock lock redissonClient.getLock(lockKey); try { // 尝试获取锁等待最多 3 秒锁定 15 秒 if (lock.tryLock(3, 15, TimeUnit.SECONDS)) { try { // Double Check L1 缓存 String doubleCheck redisTemplate.opsForValue().get(l1Key); if (doubleCheck ! null) { return doubleCheck; } // 调用大模型推理 String answer chatClient.prompt() .user(rawPrompt) .call() .content(); // 异步或同步双写回填缓存 redisTemplate.opsForValue().set(l1Key, answer, Duration.ofHours(12)); vectorSearchEngine.save(promptVector, rawPrompt, answer); return answer; } finally { lock.unlock(); } } else { // 未抢到锁的请求稍作休眠重试读取 L1 Thread.sleep(200); String retryAnswer redisTemplate.opsForValue().get(l1Key); return retryAnswer ! null ? retryAnswer : 系统正在处理中请稍候再试。; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(请求被打断, e); } } }生产运行的关键防护点1. 缓存一致性与主动失效很多团队在搞大模型缓存时最怕业务规则变动。例如晚上八点运营突然修改了“满 300 减 50”的凑单门槛如果缓存里依然保留旧的解答客服机器人就会给出错误的回答引发投诉。针对这种时效性强的信息我们遵循两项原则分类打标与 Tag 级联失效在向向量库写入文档以及生成 L1 缓存时将该问答所属的业务域如PROMOTION_2026_DOUBLE11作为 Tag 存入元数据。当运营后台更新规则发布广播消息时通过消费 MQ 批量执行清空指定 Tag 的 L1 缓存与向量软删除。动态 TTL 与衰减因子大促白天的缓存 TTL 不宜设置过长推荐 4 到 8 小时夜间业务相对静止时可适当延长避免死缓存长期滞留。2. 区分用户个性化状态绝对不能将包含用户隐私如“我昨天买的这双鞋发货了吗”直接落入全局公共缓存池中。在进入缓存流水线前需要通过轻量级正则或意图识别判断该问题是否包含“上下文绑定代词”我、我的订单、这个地址。一旦识别为个人状态查询立刻绕过公共语义缓存直接流转至业务 RPC 或带有专属上下文的 Agent 处理。收益总结通过这套“L1 精确指纹 L2 向量近似”的双层漏斗架构在日常巡检中L1 精确匹配贡献了约 42% 的命中率平均响应耗时 1.1msL2 向量近似检索进一步捕获了 26% 的泛化表达问题平均响应耗时 24ms最终只有约 32% 的长尾复杂问题真正穿透到大模型生成环节。不仅整体 P99 响应延迟从原本的 1800ms 压降到了 35ms 以内大模型 Token 开销也直接砍掉了将近七成。在架构设计中永远没有唯一的银弹把最朴素的高性能组件与最前沿的 AI 技术拼接在合适的位置才是资深工程师最有价值的基本功。