
























摘要:多轮对话是大模型应用的核心需求。本文讲解 Spring AI 如何通过 ChatMemory 和 Advisor 机制实现对话短期记忆,并支持 JDBC 持久化。内容包含:短期记忆概念、Advisor 原理、内存/JDBC 两种实现、生产环境配置。

短期记忆(Short-term Memory) 是指 AI 在单次对话会话中记住上下文的能力。
大模型本身是无状态的,每次请求都是独立的。如果没有短期记忆:
用户:我想去北京玩
AI:北京有很多景点,推荐故宫、长城...
用户:预算3000够吗?
AI:❓ 什么预算?(不记得前面说的是北京旅游)
有了短期记忆:
用户:我想去北京玩
AI:北京有很多景点,推荐故宫、长城...
用户:预算3000够吗?
AI:✅ 3000元预算去北京玩3-4天是够的...(记得是北京旅游)
| 类型 | 作用域 | 存储内容 | 生命周期 |
|---|---|---|---|
| 短期记忆 | 单次会话 | 对话历史 | 会话结束或清空 |
| 长期记忆 | 跨会话 | 用户偏好、关键信息 | 永久保存 |
本文聚焦短期记忆的实现。
Advisor(顾问/拦截器) 是 Spring AI ChatClient 的核心扩展机制,类似于 Spring 的拦截器或 AOP。
Advisor 可以在请求发送前后进行拦截,实现各种增强功能:
graph LR A[用户请求] --> B[Advisor 1] B --> C[Advisor 2] C --> D[ChatModel] D --> E[Advisor 2] E --> F[Advisor 1] F --> G[返回响应]
| Advisor | 功能 | 场景 |
|---|---|---|
| MessageChatMemoryAdvisor | 自动管理对话历史 | 多轮对话 |
| SimpleLoggerAdvisor | 打印请求/响应日志 | 开发调试 |
| VectorStoreAdvisor | RAG 向量检索 | 知识库问答 |
graph TD A[用户消息] --> B[MessageChatMemoryAdvisor] B --> C[从 ChatMemory 获取历史] C --> D[合并历史 + 当前消息] D --> E[发送给 ChatModel] E --> F[AI 响应] F --> G[保存到 ChatMemory] G --> H[返回给用户]
关键点:
假设你想让 AI 帮你策划产品发布会:
你:我们下个月要发布一款智能手表
AI:好的,智能手表发布会需要考虑场地、流程、媒体邀请...
你:预算大概50万,场地选在哪里合适?
AI:???(什么预算?)
问题:每次对话 AI 都像失忆了一样!
原因:直接使用 ChatModel 需要手动维护消息列表:
@GetMapping("/planProductLaunch")
public String planProductLaunch(String message) {
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个资深产品策划专家"));
messages.add(new UserMessage("我们下个月要发布一款智能手表"));
ChatResponse response = dashScopeChatModel.call(new Prompt(messages));
// 第二轮需要手动添加历史
messages.add(new AssistantMessage(response.getResult().getOutput().getText()));
messages.add(new UserMessage("预算大概50万,场地选在哪里合适?"));
// ... 代码冗长,无法跨请求保持上下文
return dashScopeChatModel.call(new Prompt(messages)).getResult().getOutput().getText();
}
缺点:
List<Message><!-- Spring AI 核心 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-starter</artifactId>
</dependency>
<!-- 通义千问模型 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
</dependency>
<!-- JDBC 持久化(生产环境) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
核心类说明:
org.springframework.ai.chat.client.ChatClient:对话客户端org.springframework.ai.chat.memory.ChatMemory:记忆接口org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor:记忆拦截器org.springframework.ai.chat.model.ChatModel:模型接口@RestController
@RequestMapping("/memory")
public class ChatMemoryController implements InitializingBean {
@Autowired
private ChatModel dashScopeChatModel;
@Autowired
private ChatMemory chatMemory;
private ChatClient chatClient;
@Override
public void afterPropertiesSet() throws Exception {
this.chatClient = ChatClient.builder(dashScopeChatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
new SimpleLoggerAdvisor()
)
.defaultOptions(
DashScopeChatOptions.builder()
.withTopP(0.7)
.build()
)
.build();
}
@GetMapping("/continuousChat")
public Flux<String> continuousChat(String message, String chatId, HttpServletResponse httpServletResponse) {
httpServletResponse.setCharacterEncoding("UTF-8");
return chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
.stream()
.content();
}
}
关键点:
MessageChatMemoryAdvisor:自动处理历史消息的加载和保存chatId:会话唯一标识,同一个 chatId 共享历史InitializingBean:确保 ChatClient 在应用启动时创建,全局复用# 第一轮对话
curl "http://localhost:8080/memory/continuousChat?message=我们下个月要发布一款智能手表&chatId=launch-001"
# 第二轮对话 - AI 记得前面说过发布会
curl "http://localhost:8080/memory/continuousChat?message=预算大概50万,场地选在哪里合适?&chatId=launch-001"
# 第三轮对话 - AI 依然记得上下文
curl "http://localhost:8080/memory/continuousChat?message=希望能邀请科技博主来做现场体验&chatId=launch-001"
效果:AI 现在能够记住整个对话历史,给出连贯的回复!
@Configuration
public class ChatMemoryConfiguration {
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
}
}
特点:
CREATE TABLE `spring_ai_chat_memory` (
`conversation_id` varchar(36) NOT NULL,
`content` text NOT NULL,
`type` enum('USER','ASSISTANT','SYSTEM','TOOL') NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`,`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_ai?useUnicode=true&characterEncoding=UTF-8
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
ai:
chat:
memory:
repository:
jdbc:
enabled: true
platform: mysql
initialize-schema: always
@Configuration
public class JdbcChatMemoryConfiguration {
@Bean
public ChatMemory jdbcChatMemory(JdbcChatMemoryRepository jdbcChatMemoryRepository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(jdbcChatMemoryRepository)
.maxMessages(20)
.build();
}
}
@RestController
@RequestMapping("/jdbc/memory")
public class JdbcChatMemoryController implements InitializingBean {
@Autowired
private ChatModel dashScopeChatModel;
@Autowired
private ChatMemory jdbcChatMemory;
private ChatClient chatClient;
@GetMapping("/persistentChat")
public Flux<String> persistentChat(String message, String chatId, HttpServletResponse response) {
response.setCharacterEncoding("UTF-8");
return chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
.stream()
.content();
}
@Override
public void afterPropertiesSet() {
this.chatClient = ChatClient.builder(dashScopeChatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(jdbcChatMemory).build(),
new SimpleLoggerAdvisor()
)
.defaultOptions(DashScopeChatOptions.builder().withTopP(0.7).build())
.build();
}
}
关键差异:注入的是 jdbcChatMemory Bean,其他代码与内存版本完全一致!
# 第一次对话
curl "http://localhost:8000/jdbc/memory/persistentChat?message=我们要发布一款智能手表&chatId=product-001"
# 第二次对话(同一会话)
curl "http://localhost:8000/jdbc/memory/persistentChat?message=预算50万,场地怎么选?&chatId=product-001"
# 重启应用后继续对话
curl "http://localhost:8000/jdbc/memory/persistentChat?message=希望能邀请科技博主&chatId=product-001"
效果:AI 依然记得之前的对话,因为数据已持久化到数据库!
| 特性 | 内存存储 | JDBC 存储 |
|---|---|---|
| 持久化 | ❌ 重启丢失 | ✅ 永久保存 |
| 性能 | ⚡ 极快 | ⚡ 快(有索引) |
| 配置 | 简单 | 需要数据库 |
| 适用场景 | 开发测试 | 生产环境 |
MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
建议:一般场景 10-20 条,长对话 30-50 条,注意模型的 token 限制。
连接池配置:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
定期清理过期数据:
DELETE FROM spring_ai_chat_memory
WHERE timestamp < DATE_SUB(NOW(), INTERVAL 30 DAY);
@GetMapping("/initiateSession")
public String initiateSession() {
return UUID.randomUUID().toString();
}
不会。使用 MessageWindowChatMemory 可以限制消息数量,超过限制后最早的消息会被自动删除。
通过不同的 chatId:
chatClient.prompt()
.user(message)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, "user-A"))
.stream();
chatMemory.clear(chatId);
Spring AI 的 ChatMemory + Advisor 机制为多轮对话提供了优雅的解决方案:
// 1. 注入 ChatMemory
@Autowired
private ChatMemory chatMemory;
// 2. 配置 ChatClient
chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
// 3. 调用时传入 chatId
chatClient.prompt()
.user(message)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
.stream()
.content();
通过 ChatMemory,我们可以轻松实现有"记忆力"的 AI 对话,让交互更加自然流畅!
参考资源:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。