承上:上一篇我们接入了社区MCP Server,AI能读文件、操作GitHub,工具能力已经拉满了。但每次对话都像在跟一个失忆的人聊天------上一句刚说完我叫张三,下一句它就问"您好,请问怎么称呼?"今天,我们要给AI装上"海马体"。
1. 问题场景:每句话都是"初次见面"
先看看我们现在的对话:

血压上来了吗?明明上一句刚说过,转头就忘。
原因很简单 :每次调用ChatClient,都是一个独立的会话。AI看不到上一轮说了什么,它只能看到你当前这一句话。
后端老鸟的类比:这不就是HTTP的无状态问题吗? HTTP每次请求独立,我们用Session/Cookie来保持状态。AI对话也需要一个"Session"------这就是 ChatMemory(对话记忆) 。
2. 核心概念:ChatMemory是什么?
Spring AI的 ChatMemory 接口,本质上就是对话历史记录的管理器:
less
用户:你好AI,我是"第一行代码HW"。 → 存入记忆: [用户消息: "你好AI,我是"第一行代码HW"。"]
AI:你好AI,第一行代码HW。 → 存入记忆: [AI回复: "你好AI,第一行代码HW。"]
用户:我是谁呀? → 存入记忆: [用户消息: "我是谁呀?"]
AI:你是"第一行代码HW"。 → 存入记忆: [AI回复: "你是第一行代码HW"]
用户:知道我为么起个名字吗? → 把以上4条记忆一起发给AI → AI知道上下文了!
每一轮对话的消息都会被保存,下一次调用时,Spring AI自动把历史消息和当前消息拼接在一起发给AI。
Spring AI提供了三种ChatMemory实现,对应不同的存储后端和适用场景:
| 实现方式 | 存储位置 | 持久化 | 适用场景 |
|---|---|---|---|
InMemoryChatMemory |
JVM堆内存 | ❌ 重启丢失 | 开发测试、单机演示 |
JdbcChatMemory |
关系型数据库 | ✅ | 已有数据库的中小型项目 |
RedisChatMemory |
Redis | ✅ | 分布式系统、高并发生产环境 |
实现方式对比:
| 维度 | InMemory | JDBC | Redis |
|---|---|---|---|
| 性能 | 最高(内存读写) | 中(依赖DB性能) | 高(内存+持久化) |
| 持久化 | ❌ | ✅ | ✅ |
| 分布式支持 | ❌ | ✅(共享DB) | ✅(天然支持) |
| 过期管理 | 手动编码 | SQL定时清理 | 原生TTL支持 |
| 运维成本 | 零 | 利用现有DB | 需Redis集群 |
| 数据量上限 | 受JVM堆限制 | 受磁盘限制 | 受内存+磁盘限制 |
下面逐一实现,并分析各自的优势和适用场景。
3. 方式一:InMemoryChatMemory(开发首选)
3.1. 核心特点
- 存储位置 :
ConcurrentHashMap<String, List<Message>> - 生命周期:跟随JVM进程,应用重启数据全丢
- 性能:纯内存读写,微秒级延迟
- 容量限制:无内置上限,长会话可能OOM
3.1.1. 优势
- 零依赖:不需要数据库、Redis,开箱即用
- 极速响应:纯内存操作,不产生网络IO
- 调试友好:断点直接看内存数据,排查问题方便
- 无运维成本:不需要额外中间件
3.1.2. 劣势
- 重启丢失:发版、宕机后所有对话记忆消失
- 单机限制:无法在多个实例间共享
- 内存泄漏风险:没有自动过期机制,需手动清理
- 不可扩展:受单机堆内存限制
3.1.3. 适用场景
- 本地开发和单元测试
- 演示环境(Demo、POC)
- 对对话连续性要求不高的单机应用
- 短期会话(如客服咨询后立即结束)
3.2. 代码实现
kotlin
package com.yunxi.ai.config;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class InMemoryChatMemoryConfig {
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
// 明确使用 InMemoryChatMemoryRepository(不配置也默认是这个)
.chatMemoryRepository(new InMemoryChatMemoryRepository())
// 自定义最大消息数,默认为 20[citation:2]
.maxMessages(30)
.build();
}
}
MessageWindowChatMemory 底层是一个 ConcurrentHashMap<会话ID, 消息列表>。重启就丢,但开发阶段够用了。
kotlin
package com.yunxi.ai.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
public String chat(String message) {
return chatClient.prompt()
.system("你是一个专业的助手,可以回答用户的问题")
.user(message)
.call()
.content();
}
}
核心代码就一行:
less
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
MessageChatMemoryAdvisor 做了什么?
- 调用前 :从
chatMemory取出历史消息,拼接到请求里 - 调用后 :将本次的用户消息和AI回复存入
chatMemory
Advisor模式是Spring AI的一个设计亮点,类似AOP的环绕通知。它可以在Prompt发给AI之前和收到响应之后做拦截处理。记忆管理就是通过Advisor实现的。
3.3. 测试结果

**下面我们根据日志分析交互流程:
**第一次请求:
css
request: ChatClientRequest[prompt=Prompt{messages=[ SystemMessage{...你是一个专业的助手...}, UserMessage{content='你好AI,我是"第一行代码HW"'}], ...}
- 请求中只包含 系统提示词 + 用户消息
- 没有历史消息(因为是第一轮对话)
第二次请求:
css
request: ChatClientRequest[prompt=Prompt{messages=[ UserMessage{content='你好AI,我是"第一行代码HW"'}, ← 第一轮的历史 AssistantMessage{textContent=你好!欢迎...}, ← 第一轮的回复 SystemMessage{...你是一个专业的助手...}, UserMessage{content='还知道我是谁吗?'} ← 当前提问], ...}
- 历史消息已被自动注入!包含:
-
- 第一轮用户提问
- 第一轮 AI 回复
- 系统提示词
- 当前用户提问
AI 的回复:
arduino
当然记得!你是"第一行代码HW"------上一次你说过的名字。
4. 方式二:JdbcChatMemory(企业级持久化)
4.1. 核心特点
- 存储位置:MySQL/PostgreSQL等关系型数据库
- 持久化:数据不丢失,应用重启后对话依然可用
- 事务支持:可利用数据库事务保证一致性
- 查询能力:可用SQL分析对话历史、用户行为
4.1.1. 优势
- 利用现有基础设施:大多数项目已有数据库,零额外成本
- 持久化可靠:ACID事务保证,不怕断电宕机
- SQL查询灵活:可做对话分析、统计用户提问频率、热点问题挖掘
- 备份恢复简单:随数据库一起备份,运维成熟
- 权限管理:可复用数据库的权限体系
4.1.2. 劣势
- 性能瓶颈:高并发下数据库压力大,需做好索引优化
- 存储成本:大量对话历史占用磁盘空间,需定期清理
- 延迟较高:相比内存/Redis,磁盘IO延迟高一个数量级
- Schema依赖:需提前建表,跨项目复用需统一表结构
4.1.3. 适用场景
- 已有MySQL/PostgreSQL的中小型项目
- 需要对话历史审计、分析的场景
- 不想引入新中间件的保守型架构
- 用户对话量不大(日活<10万)的系统
4.2. 代码实现
xml
<!-- Spring AI JDBC 聊天记忆持久化 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/ai_test?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
ai:
memory:
repository:
jdbc:
# 表初始化策略:开发环境用 always,生产环境用 never
initialize-schema: always
sql
-- ai_test.SPRING_AI_CHAT_MEMORY definition
CREATE TABLE `SPRING_AI_CHAT_MEMORY` (
`conversation_id` varchar(36) NOT NULL,
`content` text NOT NULL,
`type` varchar(10) NOT NULL,
`timestamp` timestamp NOT NULL,
CONSTRAINT `TYPE_CHECK` CHECK ((`type` in (_utf8mb4'USER',_utf8mb4'ASSISTANT',_utf8mb4'SYSTEM',_utf8mb4'TOOL')))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
4.3. 测试结果


5. 方式三:RedisChatMemory(高性能生产方案)
5.1. 核心特点
- 存储位置:Redis(内存+持久化)
- 数据结构 :
List<Message>,按conversationId组织 - 过期机制:原生TTL支持,自动清理
- 分布式支持:多实例共享,天然适配微服务
5.1.1. 优势
- 高性能:内存级读写,微秒级延迟,支撑高并发
- 持久化:RDB快照 + AOF日志,兼顾性能和数据安全
- 自动过期:原生TTL,无需手动写清理任务
- 分布式共享:所有应用实例共享同一份记忆,用户切换节点无感知
- 数据结构丰富:可扩展到Sorted Set(按时间排序)、Hash(存储会话元数据)
- 运维成熟:哨兵模式、集群模式,高可用方案完善
5.1.2. 劣势
- 额外中间件:需要部署和维护Redis
- 内存成本:大量对话历史会占用Redis内存,需合理设置TTL
- 数据一致性:Redis和DB的数据一致性需额外处理(如果需要双写)
- 序列化开销:复杂对象序列化/反序列化有性能损耗
5.1.3. 适用场景
- 分布式微服务架构
- 高并发对话系统(日活>10万)
- 需要多实例共享会话状态
- 对对话响应速度有极致要求
- 会话有明确生命周期(如客服对话结束后7天自动清理)
5.2. 代码实现
spring-ai更新很快,我写这篇文章的时间是:2027-07-15,所以大家在学习的时候最好看一下博客的编写时间。
注意:必须是spring AI 2.0.0
Spring AI 2.0.0 要求 Spring Boot 4.0/4.1
这一块我在开发过程中遇到很多小问题,还没整理完!!
6. 什么是 CONVERSATION_ID?
CONVERSATION_ID 是对话记忆的唯一标识符,就像数据库的主键、HTTP Session的SessionId。它决定了哪些消息属于同一个对话上下文。
makefile
conversationId = "user_001"
├── 用户: "我叫张三"
├── AI: "好的张三"
├── 用户: "查订单ORD001"
└── AI: "已发货..."
conversationId = "user_002"
├── 用户: "今天天气怎么样"
└── AI: "北京今天晴,25°C"
不同的 conversationId,完全隔离的记忆空间。
核心代码:
less
.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, chatId))
7. 本篇小结
这一篇我们深入对比了ChatMemory的三种实现:
| 实现 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
InMemory |
零依赖、极速 | 重启丢失 | 开发测试 |
Jdbc |
利用现有DB、可审计 | 性能一般 | 中小项目 |
Redis |
高性能、分布式、自动过期 | 需额外中间件 | 生产高并发 |
现在我们的AI已经是一个"有记忆的打工人"了------能记住上下文、能调工具、能流式输出、能通过MCP接入外部生态。
但它还有一个致命短板:老板问"这个月谁业绩最好",它回答不了。 因为AI根本看不到你数据库里的销售表。
下一篇,我们要解决这个问题------Text-to-SQL,让AI直接写SQL查业务数据库。
本文与DeepSeek协作完成