Spring AI ChatMemory 笔记
Spring AI ChatMemory 笔记
一、核心概念
LLM 本身是无状态的,不会记住上一轮对话内容。Spring AI 提供了 ChatMemory 抽象,用于在多轮对话中持久化和检索历史消息,实现有状态的连续对话。
整个体系分为三层:
┌──────────────────────────────────┐
│ Advisor(使用层) │ ← 决定如何把记忆"用"到对话中
├──────────────────────────────────┤
│ ChatMemory(管理层) │ ← 决定保留哪些消息(窗口策略)
├──────────────────────────────────┤
│ ChatMemoryRepository(存储层)│ ← 决定消息存在哪里(可以自定义一个实现此接口的类来完成自己的存储需求)
└──────────────────────────────────┘二、自动配置
Spring AI 会自动注册以下两个 Bean(无需手动声明):
| Bean | 默认实现 | 说明 |
|---|---|---|
ChatMemoryRepository | InMemoryChatMemoryRepository(基于内存的) | 存储层,可被其他持久化实现替换 |
ChatMemory | MessageWindowChatMemory(目前好像只有这个) | 管理层,包装上面的 Repository |
// 直接注入即可使用,无需额外配置
@Autowired
ChatMemory chatMemory;
@Autowired
ChatMemoryRepository chatMemoryRepository;⚠️
ChatMemoryBean 自动存在,但不会自动作用于对话,必须通过 Advisor 显式绑定。
三、ChatMemory 实现(管理层)
MessageWindowChatMemory(唯一内置实现)
维护一个滑动消息窗口,超出最大数量时删除旧消息,但始终保留 SystemMessage。
| 属性 | 默认值 |
|---|---|
maxMessages | 20 条 |
// 使用默认配置(窗口大小 20)
ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
// 自定义窗口大小
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(10)
.build();
// 指定自定义存储库
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(myRepository)
.maxMessages(10)
.build();窗口裁剪逻辑:
[SystemMessage] ← 永远保留
[UserMessage 1] ← 超出窗口时从这里开始删除
[AssistantMessage 1]
[UserMessage 2]
[AssistantMessage 2]
...
[UserMessage N] ← 最新的始终保留四、ChatMemoryRepository 实现(存储层)
1. InMemoryChatMemoryRepository(默认)
- 存储在内存的
ConcurrentHashMap中 - 应用重启后数据丢失
- 适合开发测试
使用其他持久化存储会被覆盖,原理是 @ConditionalOnMissingBean(ChatMemoryRepository.class),只有ChatMemoryRepository没有被注册时,才会注册InMemoryChatMemoryRepository。
// 自动配置,或手动创建
ChatMemoryRepository repository = new InMemoryChatMemoryRepository();2. JdbcChatMemoryRepository
3. CassandraChatMemoryRepository
略...
4. Neo4jChatMemoryRepository
略...
五、Advisor 使用(接入 ChatClient)
三种内置 Advisor 对比
| Advisor | 记忆注入方式 | 适用场景 |
|---|---|---|
MessageChatMemoryAdvisor | 历史消息追加到消息列表 | 标准多轮对话(推荐) |
PromptChatMemoryAdvisor | 历史消息追加到 System Prompt 文本中 | 不支持多消息格式的模型 |
VectorStoreChatMemoryAdvisor | 通过向量语义检索相关历史 | 长期记忆、大量历史场景 |
MessageChatMemoryAdvisor给到ai的信息大致为
{
"model": "glm-4",
"messages": [
{
"role": "user",
"content": "我叫张三,我最喜欢吃苹果。"
},
{
"role": "assistant",
"content": "好的,张三,记住了你喜欢吃苹果。"
},
{
"role": "user",
"content": "我刚才说我最喜欢吃什么?"
}
]
}而SystemPromptChatMemoryAdvisor是
{
"model": "glm-4",
"messages": [
{
"role": "system",
"content": "你是一个AI助手。\n以下是历史对话记录:\nUser: 我叫张三,我最喜欢吃苹果。\nAssistant: 好的,张三,记住了你喜欢吃苹果。"
},
{
"role": "user",
"content": "我刚才说我最喜欢吃什么?"
}
]
}基本使用
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatModel chatModel, ChatMemory chatMemory) {
this.chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
public String chat(String sessionId, String userMessage) {
return chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param(
//这里填入的参数应该是advisors的参数,每个advisors从这里拿
//MessageChatMemoryAdvisor的before方法有个getConversationId方法,点进去看到他需要的字段是什么
ChatMemory.CONVERSATION_ID,
sessionId
))
.call()
.content();
}
}全局配置(推荐方式)
@Configuration
public class ChatConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
return builder
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
}六、完整工作流程
用户发送消息
↓
Advisor.before():从 ChatMemory 读取历史(get)
↓
拼装消息列表发送给 LLM:
[历史消息...] + [当前用户消息]
↓
LLM 返回响应
↓
Advisor.after():将本轮消息存入 ChatMemory(add)
[UserMessage + AssistantMessage]
↓
返回响应给用户七、直接使用 ChatModel(不用 ChatClient)
ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
String conversationId = "user-001";
// 第一轮
UserMessage msg1 = new UserMessage("我叫张三");
chatMemory.add(conversationId, msg1);
ChatResponse resp1 = chatModel.call(new Prompt(chatMemory.get(conversationId)));
chatMemory.add(conversationId, resp1.getResult().getOutput());
// 第二轮(模型能记住"张三")
UserMessage msg2 = new UserMessage("我叫什么名字?");
chatMemory.add(conversationId, msg2);
ChatResponse resp2 = chatModel.call(new Prompt(chatMemory.get(conversationId)));
chatMemory.add(conversationId, resp2.getResult().getOutput());八、自定义存储实现
如果内置存储不满足需求(如 Redis),实现 ChatMemoryRepository 接口即可:
@Component
public class RedisChatMemoryRepository implements ChatMemoryRepository {
@Override
public List<Message> findByConversationId(String conversationId) {
// 从 Redis 读取消息列表
}
@Override
public void saveAll(String conversationId, List<Message> messages) {
// 写入 Redis
}
@Override
public void deleteByConversationId(String conversationId) {
// 删除指定会话
}
}注册为 Spring Bean 后,Spring AI 自动装配会跳过
InMemoryChatMemoryRepository,使用你的自定义实现。
九、常见问题
Q:多用户如何隔离会话?
通过 conversationId 区分,通常用 userId 或 HttpSession ID。
Q:消息太多导致 Token 超限?
调小 MessageWindowChatMemory 的 maxMessages 参数。
Q:重启后记忆丢失?
切换为持久化存储(JDBC / Cassandra / Neo4j / 自定义 Redis)。
Q:如何清除某用户的历史?
chatMemory.clear(conversationId);Q:工具调用期间的中间消息会被存储吗?
目前不会,这是当前版本的已知限制,官方后续版本会修复。
十、版本对比(旧版 vs 新版)
| 对比项 | 旧版 | 新版(1.0.0-M6+) |
|---|---|---|
| 存储实现 | InMemoryChatMemory | InMemoryChatMemoryRepository |
| 管理实现 | 无独立管理层 | MessageWindowChatMemory |
| 职责划分 | 合并在一起 | 存储与管理分离 |
| 扩展方式 | 实现 ChatMemory 接口 | 实现 ChatMemoryRepository 接口 |
