Spring AI 语义缓存:重复提问不重复花钱

发布时间:2026/10/27 10:06:36
Spring AI 语义缓存:重复提问不重复花钱 你的客服系统里用户每天都在问怎么退款“退款流程是什么”“钱能退吗”——其实是同一个问题但系统每次都老老实实调一次大模型token 白烧、钱白花、回复还慢半拍。这篇文章用 Spring AI 2.0 的语义缓存Semantic Cache解决它把问题 答案存进 Redis 向量库新问题先用向量相似度匹配历史问题命中了直接返回缓存答案没命中才真调大模型。代码完整可运行读完就能复现。一、这个问题到底是什么先讲清楚语义缓存到底解决什么痛点。你肯定用过传统缓存比如 Redis 里存key用户IDvalue用户资料。它的规则很死key 必须一模一样才能命中。退款和申请退款是两条不同的 key缓存永远认不出来这俩是同一件事。但用户不会照着标准 key 说话。同一个怎么退款用户会问成如何退款“退款流程是啥”“钱能退吗”“我要退货退钱”甚至带几个错别字。传统缓存面对这些变体全部 miss全部重新调大模型。语义缓存Semantic Cache就是解决换种说法就 miss的问题。它不看字面是否一样而是看含义是否接近。用一个类比把关系说清楚传统缓存像图书馆按书名找书书名一个字不对就找不到。语义缓存像图书馆按内容主题找书你说想看怎么退钱的书它能找到《退款操作指南》哪怕书名不完全一样。所以语义缓存的核心价值是把重复问题从必须重新花一遍大模型的算力和钱变成直接返回上次算好的答案。带来三个直接好处省钱命中缓存的请求根本不调大模型token 费用为 0。提速省掉大模型那几秒到十几秒的生成时间命中后毫秒级返回。更稳少调大模型就少一次被限流、超时、报错的风险。一句话总结语义缓存让你只给真正的新问题付钱重复问题直接白嫖上次的答案。二、底层原理到底怎么回事要搞懂语义缓存得先理解两件事Embedding和向量相似度。Embedding嵌入是把一段文字变成一串数字。大模型内部不认识字它只认识数字。所以有个专门的 embedding 模型负责把怎么退款这种中文句子转换成一个固定长度的数字数组也叫向量。比如 OpenAI 的text-embedding-3-small会把任意文本变成 1536 个数字。这个转换有个神奇的性质含义相近的句子转出来的向量在空间里离得近含义无关的离得远。怎么退款和如何申请退款的向量几乎重叠怎么退款和今天天气不错的向量差得十万八千里。那离得近怎么量化用余弦相似度Cosine Similarity它算两个向量夹角的余弦值结果在 -1 到 1 之间。对我们来说只看 0 到 11 表示两个向量方向完全一致几乎同义。0.8 表示很接近基本是一个意思。0 表示完全不相关。Spring AI 的 Redis 向量库默认就用余弦相似度。**相似度阈值similarityThreshold**就是那条算不算同一个问题的分界线默认 0.8意思是新问题只要和历史问题相似度达到 0.8就当作同一个问题直接返回历史答案。接下来看语义缓存的完整流程分两步第一步写入第一次问缓存 miss 时用户问怎么退款缓存里没有。调用大模型生成答案拿到完整的ChatResponse。把问题怎么退款转成向量存进 Redis 向量索引同时把完整答案序列化成 JSON作为这个文档的一个元数据字段response存起来。第二步读取第二次问换种说法用户问如何申请退款。把这句话转成向量。在 Redis 里做向量最近邻搜索VECTOR_RANGE 查询找出相似度最高的历史文档。拿回那个文档看它的相似度是否 ≥ 0.8。达标 → 直接反序列化出上次存的答案返回给用户不调大模型不达标 → 走正常流程调大模型然后把这次的结果也写进缓存。这里有个容易被忽略但很重要的细节——上下文隔离contextHash。同一个你是谁对客服这个 system prompt 和对技术专家这个 system prompt答案应该不同。所以缓存会额外把 system prompt 的 SHA-256 哈希算进去只有 system prompt 也一样的相似问题才会命中缓存避免不同角色之间串答案。最后说底层存储。语义缓存用的是Redis Stack带 RediSearch 模块的 Redis不是普通 Redis。因为向量检索靠 RediSearch 提供的 HNSW 索引和FT.CREATE/FT.SEARCH命令普通 Redis 没有这些能力。文档以 JSON 格式存答案 JSON 就躺在文档的response字段里。三、实战手把手写代码下面这段用 Spring Boot 4.1.1 Spring AI 2.0.1都是当前最新 GA 版Java 21。你可以照着直接跑。3.1 依赖pom.xml三个关键依赖spring-ai-starter-model-openai提供聊天模型和 embedding 模型、spring-ai-redis-semantic-cache提供语义缓存实现、spring-boot-starter-web提供 REST 接口。?xml version1.0 encodingUTF-8?projectxmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion4.1.1/versionrelativePath//parentgroupIdcom.example/groupIdartifactIdsemantic-cache-demo/artifactIdversion0.0.1-SNAPSHOT/versionnamesemantic-cache-demo/namedescriptionSpring AI Semantic Cache Demo/descriptionpropertiesjava.version21/java.versionspring-ai.version2.0.1/spring-ai.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-openai/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-redis-semantic-cache/artifactId/dependency/dependenciesdependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion${spring-ai.version}/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagementbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project3.2 配置application.yml配置 OpenAI 的 API Key、聊天模型和 embedding 模型以及 Redis 的连接地址。spring:application:name:semantic-cache-demoai:openai:api-key:${OPENAI_API_KEY}chat:options:model:gpt-4o-miniembedding:options:model:text-embedding-3-smallapp:redis:host:${REDIS_HOST:localhost}port:${REDIS_PORT:6379}3.3 核心配置类SemanticCacheConfig这个类是重点。它做了四件事redisClient创建 Jedis 7 的RedisClient连你的 Redis Stack。semanticCache用DefaultSemanticCache.builder()把 RedisClient 和 EmbeddingModel 组装成语义缓存并设相似度阈值为 0.8。semanticCacheAdvisor把语义缓存包成 ChatClient 的 advisor拦截器。chatClient把 advisor 挂到 ChatClient 上之后每次对话都会先查缓存。关键点这里用jedisClient(...) embeddingModel(...)的方式让DefaultSemanticCache内部自己去建向量索引并自动声明好response、response_text、context_hash这几个元数据字段这点很重要第四节会展开讲为什么必须这样。packagecom.example.semanticcache;importorg.springframework.ai.chat.cache.semantic.SemanticCache;importorg.springframework.ai.chat.cache.semantic.SemanticCacheAdvisor;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.embedding.EmbeddingModel;importorg.springframework.ai.vectorstore.redis.cache.semantic.DefaultSemanticCache;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importredis.clients.jedis.RedisClient;ConfigurationpublicclassSemanticCacheConfig{BeanpublicRedisClientredisClient(Value(${app.redis.host})Stringhost,Value(${app.redis.port})intport){returnRedisClient.builder().hostAndPort(host,port).build();}BeanpublicSemanticCachesemanticCache(RedisClientredisClient,EmbeddingModelembeddingModel){returnDefaultSemanticCache.builder().jedisClient(redisClient).embeddingModel(embeddingModel).similarityThreshold(0.8).build();}BeanpublicSemanticCacheAdvisorsemanticCacheAdvisor(SemanticCachesemanticCache){returnnewSemanticCacheAdvisor(semanticCache);}BeanpublicChatClientchatClient(ChatClient.Builderbuilder,SemanticCacheAdvisoradvisor){returnbuilder.defaultAdvisors(advisor).build();}}3.4 对外接口 启动类SemanticCacheController暴露一个/ask接口接收question参数把问题交给挂了缓存的 ChatClient。packagecom.example.semanticcache;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;RestControllerpublicclassSemanticCacheController{privatefinalChatClientchatClient;publicSemanticCacheController(ChatClientchatClient){this.chatClientchatClient;}GetMapping(/ask)publicStringask(RequestParamStringquestion){returnchatClient.prompt().user(question).call().content();}}启动类packagecom.example.semanticcache;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;SpringBootApplicationpublicclassSemanticCacheApplication{publicstaticvoidmain(String[]args){SpringApplication.run(SemanticCacheApplication.class,args);}}3.5 跑起来验证先确保本地起了 Redis StackDocker 一行命令dockerrun-d--nameredis-stack-p6379:6379 redis/redis-stack-server:latest然后启动应用连续问两个同义不同字的问题curlhttp://localhost:8080/ask?question怎么退款# 第一次缓存 miss真的调大模型返回答案curlhttp://localhost:8080/ask?question如何申请退款# 第二次语义相似度 0.8命中缓存直接返回上次答案不再调大模型第二次请求时你去 OpenAI 后台看调用记录会发现这次根本没产生 token 消耗——这就是语义缓存省下的钱。四、踩坑经验和最佳实践坑一最隐蔽元数据字段没声明缓存永远 miss。这是最容易翻车的地方而且表现是缓存静默失效——不报错就是永远命中不了。原因在 Spring AI 的 RedisVectorStore 底层做向量搜索时FT.SEARCH的RETURN只返回你在建索引时声明过的字段。而语义缓存把答案存在元数据的response字段里如果建索引时没声明这个字段搜索结果的元数据里就取不到responseget()只能返回空缓存形同虚设。解决办法就是本文 3.3 里的写法用DefaultSemanticCache.builder().jedisClient(...).embeddingModel(...)让它内部自动声明response、response_text、context_hash字段。千万别图省事自己 new 一个 RedisVectorStore 丢进去却不配 metadataFields。坑二必须用 Redis Stack不是普通 Redis。语义缓存依赖 RediSearch 的向量检索能力。如果你连的是普通 Redis启动时执行FT.CREATE会直接报unknown command FT.CREATE。要么用redis/redis-stack-server镜像要么用带 RediSearch 模块的云 Redis。坑三阈值 0.8 不是万能值要按业务调。阈值越高越保守宁可多花点钱调模型也要答案准越低越激进省更多钱但可能答非所问。中文短句、口语化问题多的话0.8 可能偏严可以试 0.75 甚至 0.7但要配合线上抽检确认没出现牛头不对马嘴的缓存命中。坑四多角色不隔离会串答案。如果你的 ChatClient 有多个 system prompt比如客服角色和技术专家角色一定要确保 contextHash 生效。语义缓存默认会算 system prompt 的哈希做隔离但如果你在 advisor 链里动态改 system prompt要注意顺序别让隔离失效导致客服答案串给技术专家。坑五带时效性的答案要设 TTL。价格、库存、活动规则这类答案会过期。缓存层有带 TTL 的写入方法可以给这类条目设过期时间避免用户看到过期的退款政策或价格。最佳实践小结写入和读取必须用同一个 embedding 模型换了模型向量空间就不对齐相似度全乱。缓存命中率是你最该盯的指标命中率高说明省钱命中率低说明阈值太严或问题太发散。对命中返回的答案建议在关键业务金额、时效上做二次校验别 100% 无脑信任缓存。五、性能对比和技术选型一张表看清三种方案的区别方案命中条件换说法能命中吗成本延迟无缓存每次都调模型-最高最慢传统 key 缓存字面完全一致不能低快语义缓存含义相似 ≥ 阈值能低快命中时实测经验值参考客服、FAQ 这类问题重复度高的场景语义缓存命中率很容易做到 50% 以上意味着一半以上的请求直接省掉了大模型的 token 费用和生成延迟。对于高频问答、企业知识库问答、AI 客服这笔账非常划算。但语义缓存也不是银弹选型时认清它的边界适合FAQ、客服、知识库问答、固定的提示词任务——答案是相对稳定的、问题会重复出现的。不适合每次答案都必须最新实时行情、个性化推荐、用户问题极度长尾几乎不重复、答案高度依赖上下文多轮对话的后续轮次。一句话选型建议答案越稳定、问题越重复语义缓存越划算答案越实时、问题越长尾越没必要。六、总结语义缓存解决的是一个非常具体又非常费钱的问题用户换着说法问同一个问题每次都要重新付一遍大模型的账。它用 embedding 把含义变成可比较的向量用相似度阈值判断算不算同一个问题命中了就直接返回历史答案一分钱 token 都不花。落地要点回顾依赖用spring-ai-redis-semantic-cache版本对齐 Spring AI 2.0.1当前最新 GA。用DefaultSemanticCache.builder().jedisClient(...).embeddingModel(...)建缓存让它自动声明response等元数据字段——这是避免缓存永远 miss的关键。后端必须是 Redis Stack带 RediSearch 模块。阈值默认 0.8按业务调多角色靠 contextHash 隔离时效性答案设 TTL。语义缓存的本质是用一点点向量检索的算力换掉大模型重复生成的大笔费用。对于重复问题占比高的场景这是性价比极高的优化而且接入成本低——本文的代码改改配置就能上。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询