文章目录
- [1. 前言](#1. 前言)
- [2. 内存记忆](#2. 内存记忆)
-
- [2.1 构建 ChatMemory](#2.1 构建 ChatMemory)
- [2.2 构建记忆增强器](#2.2 构建记忆增强器)
- [2.3 构建对话客户端](#2.3 构建对话客户端)
- [2.4 会话 ID](#2.4 会话 ID)
-
- [2.4.1 重要性](#2.4.1 重要性)
- [2.4.2 多会话列表实现](#2.4.2 多会话列表实现)
- [2.5 多轮会话测试](#2.5 多轮会话测试)
- [3. 数据库记忆](#3. 数据库记忆)
-
- [3.1 引入依赖](#3.1 引入依赖)
- [3.2 属性配置](#3.2 属性配置)
- [3.3 数据库结构初始化](#3.3 数据库结构初始化)
- [3.4 方言支持](#3.4 方言支持)
- [3.5 创建 JdbcChatMemoryRepository](#3.5 创建 JdbcChatMemoryRepository)
- [3.6 构建对话客户端](#3.6 构建对话客户端)
- [3.7 多轮会话测试](#3.7 多轮会话测试)
1. 前言
大模型的 "记忆" 本质是把历史信息塞进本次请求的上下文,而非模型真的记住了内容。
实现多轮对话的核心是维护一个 messages 数组。每一轮对话都需要将用户的最新提问和模型的回复追加到此数组中,并将其作为下一次请求的输入。
示例,首先第一轮对话添加用户消息:
java
UserMessage userMessage1 = UserMessage.builder().text("我叫张三,今年30岁,养了一只叫"可乐"的柯基犬,家住上海。").build();
String content1 = zhiPuAiChatClient.prompt()
.messages(userMessage1)
.call()
.content();
第二轮对话时,消息数组添加第一次对话用户消息、大模型回复内容、用户的最新提问:
java
AssistantMessage assistantMessage = AssistantMessage.builder().content(content1).build();
UserMessage userMessage2 = UserMessage.builder().text("你知道我叫什么名字吗?").build();
String content2 = zhiPuAiChatClient.prompt()
.messages(userMessage1, assistantMessage, userMessage2)
.call()
.content();
System.out.println(content1);
System.out.println(content2);
输出示例:

2. 内存记忆
基于 ConcurrentHashMap 存储消息,仅在应用内存中生效,应用重启后数据丢失,适用于无依赖、轻量、高性能,适合开发测试、临时演示场景。
2.1 构建 ChatMemory
Spring AI 只提供了一个 MessageWindowChatMemory (滑动窗口机制)实现,核心特点:
- 维持一个固定大小的消息窗口,当消息数量超过上限时,自动淘汰旧消息。
- 特殊处理
SystemMessage(系统消息):- 新增系统消息时,自动删除所有旧的系统消息(保证只有最新的系统指令生效)。
- 消息淘汰时,优先保留系统消息,只淘汰普通对话消息。
- 默认窗口大小:
20条消息,可自定义。
通过 builder() 方法构造:

java
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(10)
.build();
2.2 构建记忆增强器
记忆功能会由 MessageChatMemoryAdvisor 自动管理,两种聊天记忆 Advisor 核心对比:
| 特性 | MessageChatMemoryAdvisor | PromptChatMemoryAdvisor |
|---|---|---|
| 记忆方式 | 直接拼接消息列表 | 嵌入系统提示词文本 |
| 系统消息 | 强制置顶 保留原位置, | 内部嵌入记忆 |
| 灵活性 | 低(固定消息格式) | 高(自定义提示词模板) |
| 适用场景 | 通用场景,简单直接 | 提示词工程,精准控制上下文 |
| 实现复杂度 | 低 | 中(基于提示词工程) |
这里使用 MessageChatMemoryAdvisor 传递 MessageWindowChatMemory :
java
MessageChatMemoryAdvisor memoryAdvisor = MessageChatMemoryAdvisor
.builder(chatMemory)
.conversationId("default_conversationId") // 默认对话 `ID` 默认为 default
.build();
2.3 构建对话客户端
将 MessageChatMemoryAdvisor 传递给 ChatClient ,
java
ChatClient chatClient = ChatClient.builder(zhiPuAiChatModel)
.defaultSystem("你是一个智能助手")
.defaultAdvisors(memoryAdvisor)
.build();
2.4 会话 ID
2.4.1 重要性
会话 ID 是会话的唯一主键,用于将同一会话的所有消息(用户 / 助手 / 系统)归为一组,并与其他会话彻底隔离。
| 作用 | 说明 | 无会话 ID 的后果 |
|---|---|---|
| 会话隔离 | 区分不同用户 / 不同会话的对话历史,避免消息混乱 | 所有用户共享同一份记忆,回答相互干扰 |
| 上下文关联 | 保证同一会话内多轮对话的连贯性,AI 能基于历史生成连贯回答 | 每次请求都是 "单轮对话",AI 失忆 |
| 记忆寻址 | 让 ChatMemory 快速定位并加载当前会话的历史消息 | 无法加载历史,上下文丢失 |
| 多用户支持 | 支撑多用户并发场景,为每个用户维护独立对话状态 | 多用户会话互相串线,数据泄露 |
| 持久化标识 | 持久化记忆(如 Redis/MySQL)中,作为会话消息的外键关联 | 重启后会话记忆丢失,无法续聊 |
会话 ID 是 ChatMemory 接口的核心操作参数,所有记忆实现均围绕它展开:
java
public interface ChatMemory {
// 向指定会话添加消息
void add(String conversationId, List<Message> messages);
// 获取指定会话的历史消息
List<Message> get(String conversationId);
// 清空指定会话的记忆
void clear(String conversationId);
}
Spring AI 统一使用 ChatMemory.CONVERSATION_ID 作为上下文传递的键名:
java
String DEFAULT_CONVERSATION_ID = "default";
String CONVERSATION_ID = "chat_memory_conversation_id";
BaseChatMemoryAdvisor 中定义了获取会话 ID 的核心逻辑:
java
default String getConversationId(Map<String, Object> context, String defaultConversationId) {
// 1. 参数合法性校验
Assert.notNull(context, "context cannot be null");
Assert.noNullElements(context.keySet().toArray(), "context cannot contain null keys");
Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
// 2. 优先从上下文获取对话ID,无则使用默认值
return context.containsKey(ChatMemory.CONVERSATION_ID)
? context.get(ChatMemory.CONVERSATION_ID).toString()
: defaultConversationId;
}
2.4.2 多会话列表实现
例如豆包、元宝等 AI 助手中,左侧有多个对话窗口,使用同一个用户 UID + 不同的窗口会话 ID:
- 用户新建对话 → 后端生成一个新的会话
ID - 每个会话
ID对应一条独立的对话记录 - 切换对话窗口 → 切换会话
ID,加载对应历史 - 记忆完全隔离,互不干扰
定义一个简单的 ID 生成器:
java
/**
* 分步式会话 ID 生成器
* 流程:先生成随机ID → 传入用户ID+随机ID → 生成最终会话ID
*/
public final class ConversationIdGenerator {
// 会话ID前缀
private static final String CONVERSATION_PREFIX = "conv";
// 短ID长度
private static final int SHORT_ID_LENGTH = 8;
// 禁止实例化
private ConversationIdGenerator() {
}
/**
* 第一步:生成纯随机短ID(无任何前缀)
* @return 8位随机字符串
*/
public static String generateRandomShortId() {
return UUID.randomUUID().toString()
.replace("-", "")
.substring(0, SHORT_ID_LENGTH);
}
/**
* 第二步:根据 用户ID + 第一步生成的随机ID,生成最终会话ID
* @param userId 业务用户ID(必填)
* @param randomId 第一步生成的随机ID(必填)
* @return 标准会话ID:conv:userId:randomId
*/
public static String generateConversationId(String userId, String randomId) {
if (userId == null || userId.isBlank()) {
throw new IllegalArgumentException("用户ID不能为空");
}
if (randomId == null || randomId.isBlank()) {
throw new IllegalArgumentException("随机ID不能为空");
}
return String.format("%s:%s:%s", CONVERSATION_PREFIX, userId, randomId);
}
}
2.5 多轮会话测试
模拟多轮会话:
java
// 1. 业务用户ID(你的系统用户ID)
String userId = "user_10086";
// 2. 第一步:先生成独立随机ID(模拟新建一个会话窗口,同一个会话窗口中,都需要将这个ID 传过来)
String randomId = ConversationIdGenerator.generateRandomShortId();
System.out.println("生成的随机ID:" + randomId); // 示例:a7f29d3c
// 3. 第二步:传入 用户ID + 随机ID,生成最终会话ID
String conversationId = ConversationIdGenerator.generateConversationId(userId, randomId);
System.out.println("最终会话ID:" + conversationId); // 示例:conv:user_10086:a7f29d3c
// 4. 第一轮对话
String response1 = chatClient.prompt()
.user("我叫张三,今年30岁,养了一只叫"可乐"的柯基犬,家住上海。")
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
System.out.println("=== 第一轮对话 ===");
System.out.println(response1);
// 5. 第二轮对话
String response2 = chatClient.prompt()
.user("你知道我叫什么名字吗?")
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
System.out.println("\n=== 第二轮对话 ===");
System.out.println(response2);
控制台输出:
java
生成的随机ID:58990584
最终会话ID:conv:user_10086:58990584
=== 第一轮对话 ===
你好张三!很高兴认识你。30岁,住在上海,还养了一只可爱的柯基犬"可乐",听起来是很棒的生活!柯基犬以其短腿、大耳朵和友好性格而闻名,"可乐"真是个可爱的名字。
你平时和"可乐"一起在上海有哪些活动呢?比如喜欢去哪些公园散步,或者有什么特别的养狗心得可以分享?
=== 第二轮对话 ===
是的,我记得你叫张三。你之前告诉我你今年30岁,养了一只叫"可乐"的柯基犬,并且住在上海。
在 ChatMemory 可以看到内存中存储的数据:

3. 数据库记忆
JdbcChatMemoryRepository 是一个内置实现,使用 JDBC 将消息存储在关系数据库中。它开箱即用支持多个数据库,适合需要持续存储聊天内存的应用。
3.1 引入依赖
需要引入 JDBC 存储记忆对应的依赖:
xml
<!-- Spring AI Chat Memory Repository JDBC -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
MySQL 驱动:
xml
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
3.2 属性配置
配置数据库连接:
yml
spring:
application:
name: ai-chat-demo
# MySQL 数据库配置(用于对话记忆持久化)
datasource:
url: jdbc:mysql://192.168.1.235:3306/admin?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
数据库记忆存储相关配置:
| 属性 | 描述 | 默认值 |
|---|---|---|
| spring.ai.chat.memory.repository.jdbc.initialize-schema | 控制数据库结构初始化时机,可选值:embedded、always、never | embedded |
| spring.ai.chat.memory.repository.jdbc.schema | 数据库初始化脚本路径,支持classpath路径与平台占位符 | classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-@@platform@@.sql |
| spring.ai.chat.memory.repository.jdbc.platform | 当使用@@platform@@占位符时的数据库平台 | 自动检测 |
3.3 数据库结构初始化
自动配置会在启动时自动创建 SPRING_AI_CHAT_MEMORY 表,使用对应数据库的 SQL 脚本。默认仅对嵌入式数据库(H2、HSQL、Derby 等)执行结构初始化。
可通过以下配置控制初始化行为:
yml
# 仅对嵌入式数据库执行(默认)
spring.ai.chat.memory.repository.jdbc.initialize-schema=embedded
# 始终执行初始化
spring.ai.chat.memory.repository.jdbc.initialize-schema=always
# 从不执行(配合Flyway/Liquibase使用)
spring.ai.chat.memory.repository.jdbc.initialize-schema=never
自定义初始化脚本路径:
java
spring.ai.chat.memory.repository.jdbc.schema=classpath:/custom/path/schema-mysql.sql
数据表执行脚本:
java
-- admin.SPRING_AI_CHAT_MEMORY definition
CREATE TABLE `SPRING_AI_CHAT_MEMORY` (
`conversation_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL,
`content` text COLLATE utf8mb4_unicode_ci NOT NULL,
`type` enum('USER','ASSISTANT','SYSTEM','TOOL') COLLATE utf8mb4_unicode_ci NOT NULL,
`timestamp` timestamp NOT NULL,
KEY `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`,`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3.4 方言支持
Spring AI 会从你的 jdbc:mysql:// jdbc:postgresql:// URL 中自动识别数据库,无需配置。
支持通过方言抽象处理多个关系数据库,以下数据库开箱即用支持:
PostgreSQLMySQL/MariaDBSQL ServerHSQLDBOracle
使用 JdbcChatMemoryRepositoryDialect.from(DataSource) 时,框架会自动从 JDBC URL 中检测出正确的数据库方言。
示例:
java
import org.springframework.ai.chat.memory.JdbcChatMemory;
import org.springframework.ai.chat.memory.JdbcChatMemoryRepositoryDialect;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
// 1. 注入 Spring 数据源 (DataSource)
private final DataSource dataSource;
// 2. 自动检测数据库方言(核心代码)
JdbcChatMemoryRepositoryDialect dialect = JdbcChatMemoryRepositoryDialect.from(dataSource);
// 3. 构建 JdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
// 4. 构建 数据库版聊天记忆(持久化会话ID、对话历史)
JdbcChatMemory jdbcChatMemory = new JdbcChatMemory(jdbcTemplate, dialect);
如果你的数据库不在自动检测支持列表(如达梦、人大金仓、OceanBase),手动实现接口即可。
实现方言接口:
java
import org.springframework.ai.chat.memory.JdbcChatMemoryRepositoryDialect;
/**
* 自定义数据库方言(示例:适配达梦数据库)
*/
public class DmChatMemoryDialect implements JdbcChatMemoryRepositoryDialect {
// 返回建表SQL(存储对话历史的表)
@Override
public String getCreateChatMemoryTableSql() {
return """
CREATE TABLE IF NOT EXISTS AI_CHAT_MEMORY (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
conversation_id VARCHAR(255) NOT NULL,
message_type VARCHAR(50) NOT NULL,
content TEXT NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""";
}
// 插入消息SQL
@Override
public String getInsertMessageSql() {
return "INSERT INTO AI_CHAT_MEMORY (conversation_id, message_type, content) VALUES (?, ?, ?)";
}
// 查询历史消息SQL
@Override
public String getSelectMessagesByConversationIdSql() {
return "SELECT message_type, content FROM AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY create_time ASC";
}
// 清空会话SQL
@Override
public String getDeleteMessagesByConversationIdSql() {
return "DELETE FROM AI_CHAT_MEMORY WHERE conversation_id = ?";
}
}
使用自定义方言:
java
// 手动指定自定义方言
JdbcChatMemoryRepositoryDialect dialect = new DmChatMemoryDialect();
// 构建持久化聊天记忆
JdbcChatMemory jdbcChatMemory = new JdbcChatMemory(jdbcTemplate, dialect);
3.5 创建 JdbcChatMemoryRepository
Spring AI 提供自动配置 JdbcChatMemoryRepository,可以直接使用:
java
@Autowired
JdbcChatMemoryRepository chatMemoryRepository;
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(10)
.build();
也可以手动创建:
java
ChatMemoryRepository chatMemoryRepository = JdbcChatMemoryRepository.builder()
.jdbcTemplate(jdbcTemplate)
.dialect(new PostgresChatMemoryRepositoryDialect())
.build();
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(10)
.build();
当前示例:
java
/**
* 手动配置 JdbcChatMemoryRepository
*/
@Bean
public JdbcChatMemoryRepository jdbcChatMemoryRepository(JdbcTemplate jdbcTemplate) {
return JdbcChatMemoryRepository.builder()
.jdbcTemplate(jdbcTemplate)
.build();
}
/**
* 基于数据库持久化的对话记忆
*/
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(20) // 最多保留20条历史消息
.build();
}
3.6 构建对话客户端
将 PromptChatMemoryAdvisor 传递给 ChatClient ,
java
@Bean("zhiPuAiChatClient")
public ChatClient zhiPuAiChatClient(ZhiPuAiChatModel zhiPuAiChatModel, ChatMemory chatMemory) {
PromptChatMemoryAdvisor memoryAdvisor = PromptChatMemoryAdvisor
.builder(chatMemory)
.conversationId("default_conversationId") // 默认对话 `ID` 默认为 default
.build();
ChatClient chatClient = ChatClient.builder(zhiPuAiChatModel)
.defaultSystem("你是一个智能助手")
.defaultAdvisors(memoryAdvisor)
.build();
return chatClient;
}
3.7 多轮会话测试
模拟多轮会话:
java
// 1. 业务用户ID(你的系统用户ID)
String userId = "user_10086";
// 2. 第一步:先生成独立随机ID(模拟新建一个会话窗口,同一个会话窗口中,都需要将这个ID 传过来)
String randomId = ConversationIdGenerator.generateRandomShortId();
System.out.println("生成的随机ID:" + randomId); // 示例:a7f29d3c
// 3. 第二步:传入 用户ID + 随机ID,生成最终会话ID
String conversationId = ConversationIdGenerator.generateConversationId(userId, randomId);
System.out.println("最终会话ID:" + conversationId); // 示例:conv:user_10086:a7f29d3c
// 4. 第一轮对话
String response1 = chatClient.prompt()
.user("我叫张三,今年30岁,养了一只叫"可乐"的柯基犬,家住上海。")
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
System.out.println("=== 第一轮对话 ===");
System.out.println(response1);
// 5. 第二轮对话
String response2 = chatClient.prompt()
.user("你知道我叫什么名字吗?")
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
System.out.println("\n=== 第二轮对话 ===");
System.out.println(response2);
查看数据库,根据会话 ID 保存了每条记录,没有传会话 ID 的则使用默认的:
