对话记忆
大语言模型是无状态的,无法记住之前的对话内容。对话记忆(Chat Memory)解决了这个问题,让 AI 能够保持上下文连贯性。本章介绍 Spring AI 中的对话记忆功能。
为什么需要对话记忆?
┌─────────────────────────────────────────────────────────────┐
│ 无状态 vs 有状态 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 无状态对话(默认): │
│ 用户: "我叫张三" │
│ AI: "你好张三!" │
│ 用户: "我叫什么名字?" │
│ AI: "抱歉,我不知道您的名字。" ❌ │
│ │
│ 有状态对话(使用记忆): │
│ 用户: "我叫张三" │
│ AI: "你好张三!" │
│ 用户: "我叫什么名字?" │
│ AI: "您说您叫张三。" ✅ │
│ │
└─────────────────────────────────────────────────────────────┘
ChatMemory 接口
Spring AI 提供了 ChatMemory 抽象接口:
public interface ChatMemory {
// 添加消息到会话
void add(String conversationId, List<Message> messages);
// 获取会话的消息列表
List<Message> get(String conversationId, int lastN);
// 清除会话历史
void clear(String conversationId);
}
内置实现
Spring AI 提供了多种 ChatMemory 实现:
| 实现 | 说明 | 适用场景 |
|---|---|---|
InMemoryChatMemoryRepository | 内存存储 | 开发测试 |
JdbcChatMemoryRepository | 关系数据库 | 生产环境 |
CassandraChatMemoryRepository | Cassandra | 大规模分布式 |
Neo4jChatMemoryRepository | Neo4j 图数据库 | 复杂关系查询 |
MongoChatMemoryRepository | MongoDB | 文档存储 |
CosmosDBChatMemoryRepository | Azure Cosmos DB | 云原生应用 |
快速开始
使用默认配置
Spring AI 自动配置了一个基于内存的 ChatMemory:
@Service
public class ChatService {
@Autowired
private ChatMemory chatMemory;
public String chat(String conversationId, String userMessage) {
// 添加用户消息
chatMemory.add(conversationId, List.of(new UserMessage(userMessage)));
// 获取完整的对话历史
List<Message> history = chatMemory.get(conversationId);
// 调用模型...
return "response";
}
}
使用 MessageChatMemoryAdvisor
推荐的方式是使用 Advisor 来自动管理对话记忆:
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20) // 保留最近20条消息
.build();
}
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
return builder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
}
控制器示例
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder
.defaultSystem("你是一个有帮助的助手。")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
@PostMapping
public String chat(@RequestBody ChatRequest request) {
return chatClient.prompt()
.user(request.message())
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, request.conversationId())
)
.call()
.content();
}
@DeleteMapping("/{conversationId}")
public void clearHistory(@PathVariable String conversationId) {
// 清除对话历史
chatMemory.clear(conversationId);
}
}
record ChatRequest(String conversationId, String message) {}
存储实现
1. 内存存储
适合开发和测试:
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20)
.build();
}
2. JDBC 存储
适合生产环境,支持多种数据库:
添加依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
配置:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/chat_memory
username: postgres
password: postgres
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always
使用:
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(20)
.build();
}
支持的数据库:
| 数据库 | 方言类 |
|---|---|
| PostgreSQL | PostgresChatMemoryRepositoryDialect |
| MySQL / MariaDB | MySqlChatMemoryRepositoryDialect |
| SQL Server | SqlServerChatMemoryRepositoryDialect |
| Oracle | OracleChatMemoryRepositoryDialect |
| HSQLDB | HsqldbChatMemoryRepositoryDialect |
3. MongoDB 存储
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-mongodb</artifactId>
</dependency>
spring:
data:
mongodb:
uri: mongodb://localhost:27017/chat_memory
ai:
chat:
memory:
repository:
mongodb:
initialize-schema: true
ttl: 604800 # 7天过期(秒)
4. Redis 存储
@Bean
public ChatMemory chatMemory(RedisTemplate<String, Object> redisTemplate) {
// 使用 Redis 存储
RedisChatMemoryRepository repository = RedisChatMemoryRepository.builder()
.redisTemplate(redisTemplate)
.ttl(Duration.ofHours(24))
.build();
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(20)
.build();
}
Advisor 类型
Spring AI 提供了多种记忆 Advisor:
1. MessageChatMemoryAdvisor
将对话历史作为消息列表添加到提示词中:
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
2. PromptChatMemoryAdvisor
将对话历史以文本形式追加到系统提示词中:
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
PromptChatMemoryAdvisor.builder(chatMemory)
.promptTemplate(customTemplate) // 可选自定义模板
.build()
)
.build();
3. VectorStoreChatMemoryAdvisor
使用向量存储来检索相关的历史对话:
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
VectorStoreChatMemoryAdvisor.builder(vectorStore)
.topK(5) // 检索最相关的5条历史
.build()
)
.build();
高级配置
自定义消息窗口大小
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(30) // 保留最近30条消息
.build();
}
自定义模板
String customTemplate = """
以下是对话的历史记录:
{memory}
请根据以上历史继续对话。
""";
PromptTemplate template = new PromptTemplate(customTemplate);
PromptChatMemoryAdvisor advisor = PromptChatMemoryAdvisor.builder(chatMemory)
.promptTemplate(template)
.build();
会话隔离
@Service
public class MultiUserService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
public String chat(String userId, String sessionId, String message) {
// 使用复合键隔离不同用户的会话
String conversationId = userId + ":" + sessionId;
return chatClient.prompt()
.user(message)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, conversationId)
)
.call()
.content();
}
public void clearUserHistory(String userId) {
// 清除用户所有会话
// 需要根据存储实现来实现清除逻辑
}
}
实际应用示例
1. 多轮对话服务
@Service
public class ConversationService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
public ConversationService(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatMemory = chatMemory;
this.chatClient = builder
.defaultSystem("""
你是一个专业的客服助手。
请记住用户之前提供的信息,保持对话连贯。
""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory)
.build()
)
.build();
}
public ConversationResponse chat(String sessionId, String message) {
String response = chatClient.prompt()
.user(message)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, sessionId)
)
.call()
.content();
return new ConversationResponse(
response,
getMessageCount(sessionId)
);
}
public void resetSession(String sessionId) {
chatMemory.clear(sessionId);
}
public int getMessageCount(String sessionId) {
return chatMemory.get(sessionId).size();
}
record ConversationResponse(String response, int messageCount) {}
}
2. 用户偏好记忆
@Service
public class UserPreferenceService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
public UserPreferenceService(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatMemory = chatMemory;
this.chatClient = builder
.defaultSystem("""
你是一个个性化助手。
记住用户的偏好设置,如语言、风格等。
""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
public String chatWithPreference(String userId, String message) {
return chatClient.prompt()
.user(message)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, "user:" + userId)
)
.call()
.content();
}
public void setPreference(String userId, String key, String value) {
// 将偏好设置存储为系统消息
String preferenceMessage = String.format("用户偏好设置:%s = %s", key, value);
chatMemory.add("user:" + userId, List.of(new SystemMessage(preferenceMessage)));
}
}
3. 会话管理 API
@RestController
@RequestMapping("/api/conversations")
public class ConversationController {
private final ConversationService conversationService;
private final ChatMemory chatMemory;
@PostMapping("/{sessionId}/message")
public ConversationResponse sendMessage(
@PathVariable String sessionId,
@RequestBody MessageRequest request) {
return conversationService.chat(sessionId, request.message());
}
@GetMapping("/{sessionId}/history")
public List<MessageInfo> getHistory(@PathVariable String sessionId) {
return chatMemory.get(sessionId).stream()
.map(msg -> new MessageInfo(
msg.getMessageType().name(),
msg.getContent(),
msg.getMetadata()
))
.toList();
}
@DeleteMapping("/{sessionId}")
public void clearSession(@PathVariable String sessionId) {
chatMemory.clear(sessionId);
}
@GetMapping("/{sessionId}/stats")
public SessionStats getStats(@PathVariable String sessionId) {
List<Message> messages = chatMemory.get(sessionId);
return new SessionStats(
messages.size(),
messages.stream()
.filter(m -> m.getMessageType() == MessageType.USER)
.count(),
messages.stream()
.filter(m -> m.getMessageType() == MessageType.ASSISTANT)
.count()
);
}
}
record MessageRequest(String message) {}
record MessageInfo(String type, String content, Map<String, Object> metadata) {}
record SessionStats(long totalMessages, long userMessages, long assistantMessages) {}
4. 流式对话
@GetMapping(value = "/{sessionId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(
@PathVariable String sessionId,
@RequestParam String message) {
return chatClient.prompt()
.user(message)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, sessionId)
)
.stream()
.content();
}
记忆策略
1. 滑动窗口
保留最近的 N 条消息:
MessageWindowChatMemory.builder()
.maxMessages(20) // 保留最近20条
.build();
2. 基于 Token 限制
根据 Token 数量限制:
// 可以通过计算消息的 Token 数来决定保留多少
// 目前 Spring AI 主要使用消息数量限制
3. 重要性过滤
保留重要的消息:
// 可以自定义实现,根据消息重要性过滤
public class ImportanceFilteringChatMemory implements ChatMemory {
// 实现基于重要性的过滤逻辑
}
最佳实践
1. 合理设置窗口大小
// 简单对话:10-20 条消息
MessageWindowChatMemory.builder().maxMessages(15).build();
// 复杂任务:20-50 条消息
MessageWindowChatMemory.builder().maxMessages(30).build();
// 长对话场景:考虑使用 VectorStoreChatMemoryAdvisor
2. 会话 ID 设计
// 用户级别
String userSession = "user:" + userId;
// 会话级别
String conversationSession = "user:" + userId + ":conv:" + conversationId;
// 租户级别
String tenantSession = "tenant:" + tenantId + ":user:" + userId;
3. 定期清理
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点
public void cleanupOldSessions() {
// 清理过期的会话
// 具体实现取决于使用的存储
}
小结
本章我们学习了:
- 对话记忆概念:解决 LLM 无状态问题
- ChatMemory 接口:统一的记忆抽象
- 存储实现:内存、JDBC、MongoDB 等
- Advisor 类型:消息、提示词、向量存储
- 实际应用:多轮对话、用户偏好、会话管理
- 最佳实践:窗口大小、会话设计、定期清理
练习
- 实现一个支持多用户的聊天服务
- 使用 JDBC 存储创建持久化对话历史
- 实现会话导出和恢复功能