前 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: /embeddings
options:
model: embedding-3
chat:
completions-path: /chat/completions
options:
model: glm-4.7-flash
temperature: 0.7
加了这段,Spring AI 会自动配好一个 EmbeddingModel Bean(智谱 embedding-3),后面的 VectorStore 直接注入就能用。
三、先把知识库建起来
新建 RagConfig.java,用 SimpleVectorStore 承载知识库,启动时把几条客服 FAQ 向量化写进内存:
@Configuration
public class RagConfig {
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
SimpleVectorStore store = SimpleVectorStore.builder(embeddingModel).build();
// 启动时把知识库向量化并写入内存(演示用 5 条客服 FAQ)
store.accept(knowledgeBase());
return store;
}
/**
* 演示知识库:公司客服 FAQ。
* 真实项目可改为从数据库 / 文件 / 网页批量加载,并用 metadata 打标签便于过滤。
*/
private List<Document> knowledgeBase() {
return List.of(
new Document("退货政策:商品签收后 7 天内,未拆封、不影响二次销售的可无理由退货。",
Map.of("category", "policy", "title", "退货政策")),
new Document("运费规则:单笔订单满 99 元包邮,不满则收取 10 元运费。",
Map.of("category", "policy", "title", "运费规则")),
new Document("会员等级:分为普通会员、银卡、金卡;金卡会员享全场 9 折优惠。",
Map.of("category", "member", "title", "会员等级")),
new Document("发票说明:支持开具电子普通发票,下单时填写抬头,1 个工作日内开具。",
Map.of("category", "policy", "title", "发票说明")),
new Document("客服时间:人工客服每日 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")
public class RagController {
private final ChatClient ragOnlyClient; // 纯 RAG:只检索知识库
public RagController(ChatClient.Builder builder, VectorStore vectorStore) {
// 纯 RAG:只有 QuestionAnswerAdvisor,不带记忆
this.ragOnlyClient = builder
.defaultSystem("你是公司客服助手,只依据【知识库】内容回答,不要编造;知识库没有就如实说不知道。")
.defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(4).similarityThreshold(0.5).build())
.build())
.build();
}
/** 纯 RAG 问答:每轮独立,不知道上一轮聊了什么 */
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return ragOnlyClient.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")
public class RagController {
private final ChatClient ragOnlyClient; // 纯 RAG
private final ChatClient ragChatClient; // RAG + ChatMemory 结合
private final ChatMemory chatMemory;
public RagController(ChatClient.Builder builder, VectorStore vectorStore, ChatMemory chatMemory) {
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")
public String ask(@RequestParam String question) {
return ragOnlyClient.prompt().user(question).call().content();
}
/** RAG + ChatMemory 结合:既有知识库,又有多轮记忆 */
@GetMapping("/chat")
public String chat(@RequestParam String sessionId, @RequestParam String question) {
return ragChatClient.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 友好、事务一致)
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel, JdbcTemplate jdbcTemplate) {
return PgVectorStore.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,关注我,一起走。有问题评论区聊,我会逐条回复。
如果这篇文章帮到了你,点个赞就是对我最大的鼓励 ❤️