VoiceStudio本地语音工作流:Electron+Python架构实践
2026/10/27 2:30:32
用户问题 → embed 成向量 → 缓存库做相似度检索(按租户/用户过滤) ├─ 命中(score ≥ 阈值):直接返回缓存答案 ← 毫秒级,无模型调用 └─ 未命中:调模型生成 → 把(问题向量,原文,答案)回填缓存 → 返回userId做过滤(和长期记忆那套一模一样),或者干脆不缓存;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 routingModel = new RoutingChatModel(flash, pro); ChatModel cachingModel = new SemanticCacheChatModel( routingModel, embeddingModel, cacheStore, 0.90, "default"); Assistant assistant = AiServices.builder(Assistant.class) .chatModel(cachingModel) .build();