ChatMemory + RAG 双剑合璧:让 AI 既记得你,又懂业务
前 5 篇我们把 ChatMemory 从内存玩到了 MySQL、Redis,又做了性能对比和分布式部署。但有个评论一直在我心里:“会多轮对话,它就能当客服了吗?”不能。因为多轮对话只是记得你刚才说了啥,它并不知道你公司的业务规则——退货几天、运费多少、会员几折。这些信息在知识库里,不在模型脑子里。所以这一篇,我们把ChatMemory(记忆)和RAG(检索增强)组合起来。一个管对话上下文,一个管业务知识,合体之后,才是真正能用的 AI 助手。一、ChatMemory 和 RAG,到底是什么关系?很多人以为这是二选一,其实它们是互补的。维度ChatMemory(对话记忆)RAG(检索增强)解决的问题“你上一轮说了什么”“公司手册里写了什么”数据来源本次会话的历史消息外部知识库(文档/FAQ/数据库)生命周期会话级(可持久化)长期静态知识类比短期记忆长期记忆 / 案头手册Spring AI 组件MessageChatMemoryAdvisorQuestionAnswerAdvisorVectorStore一句话总结:ChatMemory 让 AI记得你,RAG 让 AI懂业务。只上 ChatMemory:AI 记得聊天,但一问公司规定就胡说。只上 RAG:AI 答得准,但每轮都像失忆,追问那运费呢?它就懵了。两个一起上:既准又有连续性。二、Spring AI 怎么做 RAG?RAG 的核心链路:文档 → 切片 → 向量化(Embedding) → 存入向量库(VectorStore) → 用户提问时检索相似片段 → 拼进 Prompt。Spring AI 1.0 把检索 拼 Prompt封装成了一个开箱即用的 Advisor:QuestionAnswerAdvisor。我们只要准备两样东西:一个EmbeddingModel(把文本变成向量)一个VectorStore(存向量、做相似检索)本项目一直用智谱 GLM通过 OpenAI 兼容接口接入,所以 Embedding 直接复用同一个 starter,换一个embedding-3模型即可。Step 1:加依赖pom.xml:!-- RAG 向量库:SimpleVectorStore(内存版,仅演示/测试用) --!-- 注意:Spring AI 1.0 没有 spring-ai-starter-vector-store-simple 这个 starter, SimpleVectorStore 在核心模块 spring-ai-vector-store 里,版本由 spring-ai-bom 管理 --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-vector-store/artifactId/dependency!-- RAG Advisor:QuestionAnswerAdvisor 所在模块(独立模块,openai starter 不传递引入) --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-advisors-vector-store/artifactId/dependency⚠️重要:Spring AI 1.0 的SimpleVectorStore是纯内存实现,官方明确说明仅用于测试/演示,不能上生产。它启动就把知识库全部向量化加载到内存,重启即丢。生产请看第六节换成 pgvector / Redis / Chroma。Step 2:配 Embeddingapplication.yml(在已有的spring.ai.openai下加一段):spring:ai:openai:api-key:${ZHIPU_API_KEY}base-url:https://open.bigmodel.cn/api/paas/v4# 嵌入模型:复用智谱 OpenAI 兼容接口(embedding-3),供 SimpleVectorStore 向量化知识库embedding:base-url:https://open.bigmodel.cn/api/paas/v4# 关键:Spring AI 默认 embedding 路径是 /v1/embeddings,智谱 endpoint 是 /embeddings(无 /v1 前缀),# 不覆盖会被拼成 .../v4/v1/embeddings → 404。chat 靠 completions-path 覆盖,这里同理。embeddings-path:/embeddingsoptions:model:embedding-3chat:completions-path:/chat/completionsoptions:model:glm-4.7-flashtemperature:0.7加了这段,Spring AI 会自动配好一个EmbeddingModelBean(智谱embedding-3),后面的VectorStore直接注入就能用。三、先把知识库建起来新建RagConfig.java,用SimpleVectorStore承载知识库,启动时把几条客服 FAQ 向量化写进内存:ConfigurationpublicclassRagConfig{BeanpublicVectorStorevectorStore(EmbeddingModelembeddingModel){SimpleVectorStorestoreSimpleVectorStore.builder(embeddingModel).build();// 启动时把知识库向量化并写入内存(演示用 5 条客服 FAQ)store.accept(knowledgeBase());returnstore;}/** * 演示知识库:公司客服 FAQ。 * 真实项目可改为从数据库 / 文件 / 网页批量加载,并用 metadata 打标签便于过滤。 */privateListDocumentknowledgeBase(){returnList.of(newDocument(退货政策:商品签收后 7 天内,未拆封、不影响二次销售的可无理由退货。,Map.of(category,policy,title,退货政策)),newDocument(运费规则:单笔订单满 99 元包邮,不满则收取 10 元运费。,Map.of(category,policy,title,运费规则)),newDocument(会员等级:分为普通会员、银卡、金卡;金卡会员享全场 9 折优惠。,Map.of(category,member,title,会员等级)),newDocument(发票说明:支持开具电子普通发票,下单时填写抬头,1 个工作日内开具。,Map.of(category,policy,title,发票说明)),newDocument(客服时间:人工客服每日 9:00-21:00,智能助手 7x24 小时在线。,Map.of(category,service,title,客服时间)));}}关键点:Document第二个参数是metadata(键值对),后面可以用过滤表达式按category精准检索。store.accept(docs)会调用EmbeddingModel把每条文档变成向量存起来。真实项目里,这一步通常是定时从数据库/文档站同步知识库,而不是写死 5 条。四、纯 RAG:先单独跑通新建RagController.java,先来一个只有 RAG、没有记忆的版本,方便对比:RestControllerRequestMapping(/rag)publicclassRagController{privatefinalChatClientragOnlyClient;// 纯 RAG:只检索知识库publicRagController(ChatClient.Builderbuilder,VectorStorevectorStore){// 纯 RAG:只有 QuestionAnswerAdvisor,不带记忆this.ragOnlyClientbuilder.defaultSystem(你是公司客服助手,只依据【知识库】内容回答,不要编造;知识库没有就如实说不知道。).defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().topK(4).similarityThreshold(0.5).build()).build()).build();}/** 纯 RAG 问答:每轮独立,不知道上一轮聊了什么 */GetMapping(/ask)publicStringask(RequestParamStringquestion){returnragOnlyClient.prompt().user(question).call().content();}}QuestionAnswerAdvisor.builder(vectorStore)就是 RAG 的全部:用户提问 → Advisor 用同样的EmbeddingModel把问题向量化 → 去VectorStore里找最相似的topK条文档(相似度 ≥similarityThreshold)把检索到的文档拼进 Prompt 的上下文位置 → 模型基于知识库 问题作答测一下:curlhttp://localhost:8080/rag/ask?question你们运费怎么算?# 单笔订单满 99 元包邮,不满则收取 10 元运费。效果不错!但如果你紧接着问**“那金卡会员呢?”**——它不知道你上一句在聊运费,容易答偏。五、重点:ChatMemory RAG 结合 ⭐把上一篇的MessageChatMemoryAdvisor加进来,两个 Advisor 共存于同一个ChatClient,记忆和检索一起生效:RestControllerRequestMapping(/rag)publicclassRagController{privatefinalChatClientragOnlyClient;// 纯 RAGprivatefinalChatClientragChatClient;// RAG ChatMemory 结合privatefinalChatMemorychatMemory;publicRagController(ChatClient.Builderbuilder,VectorStorevectorStore,ChatMemorychatMemory){this.chatMemorychatMemory;// 纯 RAG(同上,省略)this.ragOnlyClientbuilder.defaultSystem(你是公司客服助手,只依据【知识库】内容回答,不要编造;知识库没有就如实说不知道。).defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().topK(4).similarityThreshold(0.5).build()).build()).build();// 结合:RAG 提供外部知识 ChatMemory 提供对话历史// 顺序:MessageChatMemoryAdvisor 在前(先把历史拼进 prompt),QuestionAnswerAdvisor 在后(再注入检索到的知识)this.ragChatClientbuilder.defaultSystem(你是公司客服助手,结合【知识库】与【对话历史】回答用户问题。).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build(),QuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().topK(4).similarityThreshold(0.5).build()).build()).build();}GetMapping(/ask)publicStringask(RequestParamStringquestion){returnragOnlyClient.prompt().user(question).call().content();}/** RAG ChatMemory 结合:既有知识库,又有多轮记忆 */GetMapping(/chat)publicStringchat(RequestParamStringsessionId,RequestParamStringquestion){returnragChatClient.prompt().user(question).advisors(a-a.param(ChatMemory.CONVERSATION_ID,sessionId)).call().content();}}就这一行差别:.defaultAdvisors(MessageChatMemoryAdvisor..., QuestionAnswerAdvisor...)。Spring AI 的 Advisor 是责任链:请求进来,MessageChatMemoryAdvisor先把该会话的历史消息读出来,拼进 Prompt(让模型记得前面聊了啥)接着QuestionAnswerAdvisor检索知识库,把相关文档拼进上下文(让模型知道业务规定)模型拿到历史 知识 当前问题,给出连贯又准确的回答测一下多轮:# 第一轮curlhttp://localhost:8080/rag/chat?sessionIdu1question运费怎么算?# 单笔订单满 99 元包邮,不满则收取 10 元运费。# 第二轮(指代消解:那金卡呢依赖上一轮的运费)curlhttp://localhost:8080/rag/chat?sessionIdu1question那金卡会员运费有优惠吗?# 金卡会员享全场 9 折;运费规则本身不变(满 99 包邮,不满收 10 元),# 但金卡折扣同样适用于运费部分。看到了吗?第二轮它能正确理解那金卡呢指的是运费优惠,这就是 ChatMemory 的功劳;而9 折满 99 包邮这些硬知识来自 RAG。两者缺一不可。六、生产落地,这 5 个坑别踩1. SimpleVectorStore 不能上生产内存版重启即丢、无法多实例共享。换持久化向量库,代价极小——只要换依赖和 Bean:// 生产推荐:pgvector(PostgreSQL 插件,SQL 友好、事务一致)BeanpublicVectorStorevectorStore(EmbeddingModelembeddingModel,JdbcTemplatejdbcTemplate){returnPgVectorStore.builder(jdbcTemplate,embeddingModel).dimensions(1024)// 与 embedding-3 维度一致.build();}对应依赖:spring-ai-starter-vector-store-pgvector(Redis/Chroma 同理,换成对应 starter 即可)。Advisor 那层代码一行不用改,这就是面向接口编程的好处。2. Advisor 顺序有讲究记忆 Advisor 放前面、RAG Advisor 放后面是常规写法。顺序影响最终 Prompt 的拼装结构,建议记忆在前、知识在后,并在 system 里写清先参考对话历史,再结合知识库。3. 注意 Token 预算ChatMemory 的maxMessages20(前几篇配的) RAG 的topK文档,都会塞进上下文。两者叠加可能撑爆窗口、推高费用。经验值:对话类场景:maxMessages10~20,topK3~5知识密集场景:maxMessages调小,topK调大4. 相似度阈值要调similarityThreshold太高(如 0.85)会漏召回,太低(如 0.3)会召回无关文档。中文 FAQ 场景一般 0.4~0.6 起步,用真实问题跑一遍校准。5. 用 metadata 过滤,而不是什么都塞进去QuestionAnswerAdvisor支持运行时过滤表达式,按业务隔离知识:// 只检索政策类文档chatClient.prompt().user(question).advisors(a-a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION,category policy)).call().content();多租户、多业务线时,这个过滤能避免 A 客户的问题召回 B 客户的内部文档。七、总结核心结论:ChatMemory 和 RAG不是二选一,而是标配组合——一个管记得你,一个管懂业务。Spring AI 用Advisor把两者解耦:MessageChatMemoryAdvisorQuestionAnswerAdvisor,一个ChatClient全搞定。组合成本极低:加一个 Advisor 而已,无需改模型、无需改 Prompt 结构。SimpleVectorStore仅演示,生产换 pgvector / Redis / Chroma,Advisor 层零改动。整体架构:用户问题 │ ├─[MessageChatMemoryAdvisor]── 读历史消息 ──┐ │ ├─ 拼成完整 Prompt ─ GLM ─ 回答 └─[QuestionAnswerAdvisor]── 检索知识库 ─────┘本篇代码清单:pom.xml:新增spring-ai-vector-store(核心模块,提供内存版 SimpleVectorStore)application.yml:新增spring.ai.openai.embedding(智谱 embedding-3)RagConfig.java:VectorStore 知识库加载RagController.java:纯 RAG 接口 RAG/ChatMemory 结合接口下一篇预告:ChatMemory 监控与排查(怎么知道记忆丢了、RAG 没召回?)写在最后我是一名8 年 Java 后端,正在转型 AI 应用开发。Spring AI 系列会持续更新,从 hello world 到 RAG 到 Agent,一路踩坑一路写。如果你也在转型 AI,关注我,一起走。有问题评论区聊,我会逐条回复。如果这篇文章帮到了你,点个赞就是对我最大的鼓励❤️