☰
语义缓存基础
2026/10/27 1:46:41 网站建设 项目流程

语义缓存的概念

  • 语义缓存(Semantic Cache):把用户问题先转成向量,在缓存库里找语义相似的历史问题,相似度超过阈值就直接返回当时存好的答案,不再调用模型;找不到才调模型,并把 “问题向量 → 答案” 回填缓存
  • 就是Redis呀,只不过key变成按语意匹配
用户问题 → embed 成向量 → 缓存库做相似度检索(按租户/用户过滤) ├─ 命中(score ≥ 阈值):直接返回缓存答案 ← 毫秒级,无模型调用 └─ 未命中:调模型生成 → 把(问题向量,原文,答案)回填缓存 → 返回

什么该缓存,什么不该

  • 全局共享缓存(命中率最高,存通用问答,如 “退款期限”" 客服时间 ");
  • 带个人数据的问题绝不能全局缓存:“我的订单到哪了”—— 问题向量几乎一样,可每个人的答案完全不同。正确做法是 metadata 里带userId做过滤(和长期记忆那套一模一样),或者干脆不缓存;
  • key 不只有问题本身:系统提示词版本、模型版本、业务线 / 租户不同,同一个问题答案也不同,这些都要进 key 或 metadata;
  • 不该缓存的:实时性数据(库存、价格、物流状态)、需要工具调用取数的、要求随机 / 个性化的、含敏感信息的

缓存怎么存,存哪里

  • 缓存存在一个向量库
  • 要么选Redis Stack(RediSearch 向量索引)
    • 普通 Redis(无 Redis Stack)做不了语义缓存—— 它只能按 key 精确查,KNN 需要向量索引,否则只能遍历全部 key 算余弦(O (N),不可行)。RediSearch 就是给 Redis 补上这个索引。
  • 要么还是存pgvector向量库,给他再开一个表
  • 怎么存?
  • 一条缓存 = 向量 + 问题原文 + 答案 + 元数据

手写的缓存

publicclassSemanticCacheHandwritten{/** 相似度阈值:宁严勿松,调它必须用问答对评测集回归(W3 方法)。 */privatestaticfinaldoubleTHRESHOLD=0.90;privatefinalChatModelmodel;privatefinalEmbeddingModelembeddingModel;privatefinalEmbeddingStore<TextSegment>cache;publicSemanticCacheHandwritten(ChatModelmodel,EmbeddingModelembeddingModel,EmbeddingStore<TextSegment>cache){this.model=model;this.embeddingModel=embeddingModel;this.cache=cache;}publicStringask(Stringquestion){longt0=System.currentTimeMillis();// ---- 查缓存:问题向量化 → 相似度检索 top1 ----EmbeddingquestionEmbedding=embeddingModel.embed(question).content();EmbeddingSearchRequestrequest=EmbeddingSearchRequest.builder().queryEmbedding(questionEmbedding).maxResults(1).minScore(THRESHOLD)// 低于阈值视为未命中.build();List<EmbeddingMatch<TextSegment>>matches=cache.search(request).matches();if(!matches.isEmpty()){EmbeddingMatch<TextSegment>hit=matches.get(0);Stringanswer=hit.embedded().metadata().getString("answer");System.out.printf("[缓存命中] score=%.3f 耗时=%dms(未调用模型)%n 原问题:%s%n",hit.score(),System.currentTimeMillis()-t0,hit.embedded().text());returnanswer;}// ---- 未命中:调模型生成 ----Stringanswer=model.chat(question);System.out.printf("[缓存未命中] 耗时=%dms(已真实调用模型,答案回填缓存)%n",System.currentTimeMillis()-t0);// ---- 回填:问题向量 + 问题原文 + 答案/版本/时间 ----Metadatametadata=newMetadata().put("answer",answer).put("model","deepseek-v4-flash").put("tenant","default").put("createdAt",Instant.now().toString());cache.add(questionEmbedding,TextSegment.from(question,metadata));returnanswer;}publicstaticvoidmain(String[]args){ChatModelmodel=OpenAiChatModel.builder().baseUrl("https://api.deepseek.com").modelName("deepseek-v4-flash").apiKey(System.getenv("DEEPSEEK_API_KEY")).temperature(0.1).build();EmbeddingModelembeddingModel=OpenAiEmbeddingModel.builder().baseUrl("https://open.bigmodel.cn/api/paas/v4").modelName("embedding-3").apiKey(System.getenv("EMBED_API_KEY")).build();SemanticCacheHandwrittencache=newSemanticCacheHandwritten(model,embeddingModel,newInMemoryEmbeddingStore<>());// 第一次:未命中,真实调模型并回填Stringq1="退款一般几天到账?";System.out.println("Q1: "+q1);Stringa1=cache.ask(q1);System.out.println("A1: "+abbreviate(a1,80));// 换个说法:语义等价,应命中 Q1 的缓存(答案与 A1 完全一致即为证据)Stringq2="下单后多久能退款到账?";System.out.println("\nQ2: "+q2);Stringa2=cache.ask(q2);System.out.println("A2(应等于A1): "+abbreviate(a2,80));System.out.println("两次答案一致:"+a1.equals(a2));// 不同意图:应未命中,调模型Stringq3="退款被拒绝了怎么办?";System.out.println("\nQ3: "+q3);Stringa3=cache.ask(q3);System.out.println("A3: "+abbreviate(a3,80));privatestaticStringabbreviate(Strings,intmax){s=s.replaceAll("\\s+"," ").trim();returns.length()<=max?s:s.substring(0,max)+"……(已截断)";}}
  • 就和处理缓存一个意思

框架下的缓存

publicclassSemanticCacheChatModelimplementsChatModel{privatefinalChatModeldelegate;privatefinalEmbeddingModelembeddingModel;privatefinalEmbeddingStore<TextSegment>cacheStore;privatefinaldoublethreshold;privatefinalStringtenant;publicSemanticCacheChatModel(ChatModeldelegate,EmbeddingModelembeddingModel,EmbeddingStore<TextSegment>cacheStore,doublethreshold,Stringtenant){this.delegate=delegate;this.embeddingModel=embeddingModel;this.cacheStore=cacheStore;this.threshold=threshold;this.tenant=tenant;}@OverridepublicChatResponsedoChat(ChatRequestrequest){Stringquestion=lastUserText(request);// 1) 查缓存EmbeddingqueryEmbedding=embeddingModel.embed(question).content();EmbeddingSearchRequestsearch=EmbeddingSearchRequest.builder().queryEmbedding(queryEmbedding).maxResults(1).minScore(threshold)// 多租户时这里要加 metadataKey("tenant").isEqualTo(tenant) 过滤.build();List<EmbeddingMatch<TextSegment>>matches=cacheStore.search(search).matches();// 2) 命中:直接构造响应返回,不调用 delegate(路由/模型全部跳过)if(!matches.isEmpty()){EmbeddingMatch<TextSegment>hit=matches.get(0);StringcachedAnswer=hit.embedded().metadata().getString("answer");System.out.printf("[语义缓存命中] score=%.3f -> 直接返回%n",hit.score());returnChatResponse.builder().aiMessage(dev.langchain4j.data.message.AiMessage.from(cachedAnswer)).build();}// 3) 未命中:放行给内层(路由模型或真实模型)ChatResponseresponse=delegate.chat(request);Stringanswer=response.aiMessage().text();// 4) 回填缓存(答案 + 版本 + 租户 + 时间)Metadatametadata=newMetadata().put("answer",answer).put("tenant",tenant).put("createdAt",Instant.now().toString());cacheStore.add(queryEmbedding,TextSegment.from(question,metadata));System.out.println("[语义缓存未命中] 已调用下游并回填");returnresponse;}privateStringlastUserText(ChatRequestrequest){List<ChatMessage>messages=request.messages();for(inti=messages.size()-1;i>=0;i--){if(messages.get(i)instanceofUserMessageum){returnum.singleText();}}return"";}}
  • 就是修饰ChatModel这个接口,自己写先查缓存,缓存没有查路由(这个方法要把路由注入进来)
  • 就是那个delegate委托的模型,构造器注入,外层持有内层
  • 就像这样组装起来
ChatModel routingModel = new RoutingChatModel(flash, pro); ChatModel cachingModel = new SemanticCacheChatModel( routingModel, embeddingModel, cacheStore, 0.90, "default"); Assistant assistant = AiServices.builder(Assistant.class) .chatModel(cachingModel) .build();

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询