前 5 篇我们把 ChatMemory 从内存玩到了 MySQL、Redis,又做了性能对比和分布式部署。
但有个评论一直在我心里:“会多轮对话,它就能当客服了吗?”
不能。因为多轮对话只是"记得你刚才说了啥",它并不知道你公司的业务规则——退货几天、运费多少、会员几折。这些信息在知识库里,不在模型脑子里。
所以这一篇,我们把ChatMemory(记忆)和RAG(检索增强)组合起来。一个管"对话上下文",一个管"业务知识",合体之后,才是真正能用的 AI 助手。
一、ChatMemory 和 RAG,到底是什么关系?
很多人以为这是二选一,其实它们是互补的。
| 维度 | ChatMemory(对话记忆) | RAG(检索增强) |
|---|---|---|
| 解决的问题 | “你上一轮说了什么” | “公司手册里写了什么” |
| 数据来源 | 本次会话的历史消息 | 外部知识库(文档/FAQ/数据库) |
| 生命周期 | 会话级(可持久化) | 长期静态知识 |
| 类比 | 短期记忆 | 长期记忆 / 案头手册 |
| Spring AI 组件 | MessageChatMemoryAdvisor | QuestionAnswerAdvisor+VectorStore |
一句话总结:
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 管理 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-vector-store</artifactId></dependency><!-- RAG Advisor:QuestionAnswerAdvisor 所在模块(独立模块,openai starter 不传递引入) --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-advisors-vector-store</artifactId></dependency>⚠️重要:Spring AI 1.0 的
SimpleVectorStore是纯内存实现,官方明确说明"仅用于测试/演示,不能上生产"。它启动就把知识库全部向量化加载到内存,重启即丢。生产请看第六节换成 pgvector / Redis / Chroma。
Step 2:配 Embedding
application.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){SimpleVectorStorestore=SimpleVectorStore.builder(embeddingModel).build();// 启动时把知识库向量化并写入内存(演示用 5 条客服 FAQ)store.accept(knowledgeBase());returnstore;}/** * 演示知识库:公司客服 FAQ。 * 真实项目可改为从数据库 / 文件 / 网页批量加载,并用 metadata 打标签便于过滤。 */privateList<Document>knowledgeBase(){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、没有记忆的版本,方便对比:
@RestController@RequestMapping("/rag")publicclassRagController{privatefinalChatClientragOnlyClient;// 纯 RAG:只检索知识库publicRagController(ChatClient.Builderbuilder,VectorStorevectorStore){// 纯 RAG:只有 QuestionAnswerAdvisor,不带记忆this.ragOnlyClient=builder.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 的上下文位置 → 模型基于"知识库 + 问题"作答
测一下:
curl"http://localhost:8080/rag/ask?question=你们运费怎么算?"# => 单笔订单满 99 元包邮,不满则收取 10 元运费。效果不错!但如果你紧接着问**“那金卡会员呢?”**——它不知道你上一句在聊运费,容易答偏。
五、重点:ChatMemory + RAG 结合 ⭐
把上一篇的MessageChatMemoryAdvisor加进来,两个 Advisor 共存于同一个ChatClient,记忆和检索一起生效:
@RestController@RequestMapping("/rag")publicclassRagController{privatefinalChatClientragOnlyClient;// 纯 RAGprivatefinalChatClientragChatClient;// RAG + ChatMemory 结合privatefinalChatMemorychatMemory;publicRagController(ChatClient.Builderbuilder,VectorStorevectorStore,ChatMemorychatMemory){this.chatMemory=chatMemory;// 纯 RAG(同上,省略)this.ragOnlyClient=builder.defaultSystem("你是公司客服助手,只依据【知识库】内容回答,不要编造;知识库没有就如实说不知道。").defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().topK(4).similarityThreshold(0.5).build()).build()).build();// 结合:RAG 提供外部知识 + ChatMemory 提供对话历史// 顺序:MessageChatMemoryAdvisor 在前(先把历史拼进 prompt),QuestionAnswerAdvisor 在后(再注入检索到的知识)this.ragChatClient=builder.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检索知识库,把相关文档拼进上下文(让模型"知道业务规定") - 模型拿到历史 + 知识 + 当前问题,给出连贯又准确的回答
测一下多轮:
# 第一轮curl"http://localhost:8080/rag/chat?sessionId=u1&question=运费怎么算?"# => 单笔订单满 99 元包邮,不满则收取 10 元运费。# 第二轮(指代消解:"那金卡呢"依赖上一轮的"运费")curl"http://localhost:8080/rag/chat?sessionId=u1&question=那金卡会员运费有优惠吗?"# => 金卡会员享全场 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 的maxMessages=20(前几篇配的)+ RAG 的topK文档,都会塞进上下文。两者叠加可能撑爆窗口、推高费用。经验值:
- 对话类场景:
maxMessages=10~20,topK=3~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把两者解耦:
MessageChatMemoryAdvisor+QuestionAnswerAdvisor,一个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,关注我,一起走。有问题评论区聊,我会逐条回复。
如果这篇文章帮到了你,点个赞就是对我最大的鼓励❤️