聊天记忆
约 1532 字大约 5 分钟
2026-09-15
📌 原文地址:https://docs.spring.io/java-ai/reference/api/chat-memory.html 📌 原文版本:Java AI 2.0.1
大语言模型(LLM)本身是无状态的——它不会"记住"上一句说过什么。当你要做多轮对话时,需要把历史消息攒起来,每次提问时一起塞回给模型。
白话解读
LLM 是金鱼脑,七秒就忘。所以"聊天记忆"就是一个"记事本",每次问之前先把之前的对话抄一遍贴上去。
Java AI 把聊天记忆拆成两层抽象:
ChatMemory—— 决定保留哪些消息、什么时候丢弃(策略)。ChatMemoryRepository—— 单纯负责把消息存起来、取出来(存储)。
Chat Memory ≠ Chat History
- Chat Memory(聊天记忆):LLM 用来维持上下文感知的信息(可能是压缩/截断后的)。
- Chat History(聊天历史):完整、未删减的全部对话记录。
ChatMemory 接口设计目标是"喂给模型用的记忆",不是"审计用的完整历史"。如果要做审计/合规留痕,请用 Spring Data 单独存一份。
1. 快速开始
Spring Boot 自动配置会注册一个默认的 ChatMemory bean(基于内存仓库 + 消息窗口策略):
@Autowired
ChatMemory chatMemory;直接用就行。
2. 会话 ID(Conversation ID)
ChatMemory 的所有操作都以会话 ID 为键——多用户系统里必须保证每用户每对话一个独立 ID,否则消息会串。
String conversationId = currentUser.getId() + ":" + httpSession.getId();
chatClient.prompt()
.user(userInput)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();三个最佳实践
- 每个用户一个 ID;一个用户多个对话时,每个对话一个 ID。
- ID 在服务器端从用户/会话派生,不要让客户端随便传。
- 列举/删除对话时,只动当前用户的 ID,不要跨用户操作。
3. 记忆类型
3.1 MessageWindowChatMemory(消息窗口,默认)
维护一个滑动窗口,最多保留 N 条消息。超出时丢最旧的,但 SystemMessage 永远保留。
ChatMemory memory = MessageWindowChatMemory.builder()
.maxMessages(10)
.build();默认窗口大小 20。
回合边界驱逐(Turn-Boundary Eviction)
需要丢消息时,永远以"用户消息"为边界丢弃——保留的窗口里不会出现半个回合(不会把助手回复截断在工具调用中间)。
maxMessages 是上限不是精确值
当需要对齐到下一个回合边界时,实际消息数可能略小于 maxMessages。此外,maxMessages 要至少能装下一个完整的多步骤工具调用回合,否则会被清空到只剩系统消息。
4. 记忆存储(ChatMemoryRepository)
4.1 InMemoryChatMemoryRepository(默认)
@Autowired
ChatMemoryRepository repo; // 自动配置的就是它适合本地开发/原型。重启即丢。
4.2 JdbcChatMemoryRepository
把消息存进关系数据库(PostgreSQL / MySQL / MariaDB / SQL Server / HSQLDB / Oracle)。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>@Autowired JdbcChatMemoryRepository repo;
ChatMemory memory = MessageWindowChatMemory.builder()
.chatMemoryRepository(repo)
.maxMessages(10)
.build();要点:
- 消息按
sequence_id升序返回(LLM 需要的顺序)。 - 每条消息存了时间戳,可从
message.getMetadata().get(JdbcChatMemoryRepository.CONVERSATION_TS)拿到Instant。 - 方言自动检测(按 JDBC URL),也支持自定义方言。
不支持工具调用消息
JDBC 仓库会把包含工具调用的 AssistantMessage 和 ToolResponseMessage 静默过滤掉——保存了不报错,但加载时取不到。如果业务里有工具调用,建议改用 Java AI Session 的 JDBC session store。
| 属性 | 说明 | 默认 |
|---|---|---|
spring.ai.chat.memory.repository.jdbc.initialize-schema | embedded / always / never | embedded |
spring.ai.chat.memory.repository.jdbc.schema | 自定义建表脚本 | classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-@@platform@@.sql |
spring.ai.chat.memory.repository.jdbc.platform | 用 @@platform@@ 占位符时指定 | 自动检测 |
4.3 CassandraChatMemoryRepository
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-cassandra</artifactId>
</dependency>时间序列架构,按时间戳升序取消息。TTL 支持,适合"三年留痕"这种合规场景。同样不支持工具调用消息。
| 属性 | 说明 | 默认 |
|---|---|---|
spring.cassandra.contactPoints | Cassandra 主机 | 127.0.0.1 |
spring.cassandra.port | 端口 | 9042 |
spring.cassandra.localDatacenter | 数据中心 | datacenter1 |
spring.ai.chat.memory.cassandra.time-to-live | 消息 TTL | - |
spring.ai.chat.memory.cassandra.keyspace | keyspace | springframework |
spring.ai.chat.memory.cassandra.messages-column | 消息列名 | springframework |
spring.ai.chat.memory.cassandra.table | 表名 | ai_chat_memory |
spring.ai.chat.memory.cassandra.initialize-schema | 是否自动建表 | true |
4.4 Neo4jChatMemoryRepository
存成属性图(节点 + 关系)。可以充分利用 Neo4j 的图查询能力做关系分析。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-neo4j</artifactId>
</dependency>支持节点标签自定义:Session / Message / ToolCall / Metadata / ToolResponse / Media。会自动为会话 ID 和消息索引建索引。
4.5 CosmosDBChatMemoryRepository
外部模块,由 Azure Cosmos DB 团队维护。见官方相关文档。
4.6 MongoChatMemoryRepository
文档型存储。不支持工具调用消息(过滤掉)。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-mongodb</artifactId>
</dependency>| 属性 | 说明 | 默认 |
|---|---|---|
spring.ai.chat.memory.repository.mongo.create-indices | 启动时建索引 | false |
spring.ai.chat.memory.repository.mongo.ttl | 消息 TTL(秒) | 0(永不过期) |
4.7 RedisChatMemoryRepository
需要 Redis Stack 7.0+(带 RedisJSON + Redis Query Engine)。支持 TTL 和高级查询(按内容、类型、时间范围、元数据)。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-redis</artifactId>
</dependency>RedisClient jedis = RedisClient.builder().hostAndPort("localhost", 6379).build();
ChatMemoryRepository repo = RedisChatMemoryRepository.builder()
.jedisClient(jedis)
.indexName("my-chat-idx")
.keyPrefix("my-chat:")
.timeToLive(Duration.ofHours(24))
.build();高级查询(AdvancedRedisChatMemoryRepository):
AdvancedRedisChatMemoryRepository adv = (AdvancedRedisChatMemoryRepository) repo;
List<MessageWithConversation> userMsgs = adv.findByType(MessageType.USER, 100);
List<MessageWithConversation> hits = adv.findByContent("Spring AI", 50);
List<MessageWithConversation> recent = adv.findByTimeRange(convId,
Instant.now().minus(Duration.ofHours(1)), Instant.now(), 100);5. 在 ChatClient 中使用(Advisor 方式)
Java AI 提供两个开箱即用的 Advisor:
MessageChatMemoryAdvisor:把历史作为消息集合塞进提示。VectorStoreChatMemoryAdvisor:从向量库里检索相关历史,作为纯文本追加到系统消息。
ChatMemory memory = MessageWindowChatMemory.builder().build();
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build())
.build();
String conversationId = "007";
client.prompt()
.user("Do I have license to code?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();必须传 CONVERSATION_ID
所有记忆 Advisor 都要求设置 ChatMemory.CONVERSATION_ID 参数。漏传会抛 IllegalArgumentException——没有"默认 ID"这种东西。
当前限制
执行工具调用时与 LLM 交换的"中间消息"暂时不存进记忆——会在后续版本解决。要存这些消息请用"用户控制工具执行"。
6. 在 ChatModel 中使用(手动方式)
直接用 ChatModel 时,自己手动维护记忆:
ChatMemory memory = MessageWindowChatMemory.builder().build();
String conversationId = "007";
// 第一轮
UserMessage u1 = new UserMessage("My name is James Bond");
memory.add(conversationId, u1);
ChatResponse r1 = chatModel.call(new Prompt(memory.get(conversationId)));
memory.add(conversationId, r1.getResult().getOutput());
// 第二轮
UserMessage u2 = new UserMessage("What is my name?");
memory.add(conversationId, u2);
ChatResponse r2 = chatModel.call(new Prompt(memory.get(conversationId)));
memory.add(conversationId, r2.getResult().getOutput());
// r2 会回答 "James Bond"