SpringAI2.0 对话记忆管理:ChatMemory、Advisor 链与长期记忆架构
前言:多轮对话的核心挑战
在构建 AI 应用时,实现自然的对话体验至关重要。用户期望 AI 能够记住之前的对话上下文,理解上下文,而不是每次对话都从零开始。Spring AI 2.0 提供了一套完整的对话记忆管理机制,让开发者能够轻松实现多轮对话和长期记忆。
作为架构师,我见过很多 AI 应用因为缺乏有效的记忆管理而导致用户体验差。Spring AI 2.0 通过 ChatMemory、Advisor 链等机制,让记忆管理变得简单而强大。
一、MessageChatMemoryAdvisor 与 VectorStoreChatMemoryAdvisor 差异对比
1.1 两种记忆类型
Spring AI 2.0 提供两种主要的 ChatMemory 实现:
- MessageChatMemoryAdvisor:基于内存或 Redis 的简单记忆存储
- VectorStoreChatMemoryAdvisor:基于向量存储的语义记忆检索
1.2 MessageChatMemoryAdvisor
java
@Service
public class SimpleMemoryService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
// 使用 MessageChatMemoryAdvisor
public String chat(String conversationId, String message) {
return chatClient.prompt()
.user(message)
.advisors(
new MessageChatMemoryAdvisor(chatMemory),
new PromptChatMemoryAdvisor(chatMemory)
)
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
)
.call()
.content();
}
}
1.3 VectorStoreChatMemoryAdvisor
java
@Service
public class SemanticMemoryService {
private final ChatClient chatClient;
private final VectorStore memoryStore;
// 使用 VectorStoreChatMemoryAdvisor
public String chatWithSemanticMemory(
String conversationId,
String message
) {
return chatClient.prompt()
.user(message)
.advisors(new VectorStoreChatMemoryAdvisor(
memoryStore,
SearchRequest.builder()
.withTopK(10)
.withSimilarityThreshold(0.7)
.build()
))
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
)
.call()
.content();
}
}
1.4 对比表格
| 特性 | MessageChatMemoryAdvisor | VectorStoreChatMemoryAdvisor |
|---|---|---|
| 存储方式 | 线性存储 | 向量检索 |
| 检索策略 | 最近 N 条 | 语义相似度 |
| 内存占用 | 较大 | 较小 |
| 适用场景 | 短期对话 | 长期记忆 |
| 性能 | 高 | 中等 |
二、记忆存储后端选型
2.1 RedisChatMemory
java
@Configuration
public class RedisChatMemoryConfig {
@Bean
public ChatMemory redisChatMemory(
RedisConnectionFactory connectionFactory
) {
RedisChatMemoryConfig config = RedisChatMemoryConfig.builder()
.withConversationKeyPrefix("chat:")
.build();
return new RedisChatMemory(connectionFactory, config);
}
}
2.2 Neo4jChatMemory
java
@Configuration
public class Neo4jChatMemoryConfig {
@Bean
public ChatMemory neo4jChatMemory(Driver neo4jDriver) {
Neo4jChatMemoryConfig config = Neo4jChatMemoryConfig.builder()
.withMemoryNodeLabel("Memory")
.build();
return new Neo4jChatMemory(neo4jDriver, config);
}
}
2.3 JDBC 持久化
java
@Configuration
public class JdbcChatMemoryConfig {
@Bean
public ChatMemory jdbcChatMemory(JdbcTemplate jdbcTemplate) {
return new JdbcChatMemory(jdbcTemplate);
}
}
三、上下文修剪与记忆压缩
3.1 上下文修剪
java
@Service
public class PruningMemoryService {
private final ChatMemory chatMemory;
private final int maxMessages = 50;
public void addMessage(
String conversationId,
Message message
) {
List<Message> messages = chatMemory.get(
conversationId,
maxMessages + 10
);
if (messages.size() >= maxMessages) {
// 保留最近的消息
List<Message> recent = messages.subList(
messages.size() - maxMessages,
messages.size()
);
// 更新记忆
chatMemory.clear(conversationId);
chatMemory.add(conversationId, recent);
}
chatMemory.add(conversationId, List.of(message));
}
}
3.2 记忆压缩
java
@Service
public class CompressedMemoryService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
// 定期压缩记忆
@Scheduled(cron = "0 0 */4 * * ?") // 每 4 小时
public void compressMemories() {
List<String> conversationIds = getAllConversationIds();
for (String conversationId : conversationIds) {
try {
compressConversation(conversationId);
} catch (Exception e) {
log.error("压缩记忆失败:{}", conversationId, e);
}
}
}
private void compressConversation(String conversationId) {
List<Message> messages = chatMemory.get(conversationId, null);
if (messages == null || messages.size() < 20) {
return; // 消息太少,不需要压缩
}
// 提取关键信息
String summary = chatClient.prompt()
.user("""
请总结以下对话的要点,保持简洁:
%s
""".formatted(formatMessages(messages)))
.call()
.content();
// 创建压缩消息
Message summaryMessage = new SystemMessage(
"之前的对话摘要:" + summary
);
// 清空旧记忆,只保留摘要和最近的消息
chatMemory.clear(conversationId);
chatMemory.add(conversationId, List.of(summaryMessage));
List<Message> recent = messages.subList(
messages.size() - 10,
messages.size()
);
chatMemory.add(conversationId, recent);
}
private String formatMessages(List<Message> messages) {
return messages.stream()
.map(msg -> "[%s] %s".formatted(
msg.getMessageType(),
msg.getContent()
))
.collect(Collectors.joining("\n"));
}
}
四、Advisor 执行顺序控制
4.1 执行顺序
用户请求
↓
[Advisor 1] 提示词增强
↓
[Advisor 2] 记忆检索(历史)
↓
[Advisor 3] RAG 检索
↓
[Advisor 4] 工具调用
↓
ChatModel 处理
↓
[Advisor 5] 响应后处理
↓
用户响应
4.2 先历史后检索
java
@Service
public class HistoryFirstService {
public String chatHistoryFirst(
String conversationId,
String message
) {
return chatClient.prompt()
.user(message)
.advisors(
// 先检索历史对话
new PromptChatMemoryAdvisor(chatMemory),
// 再检索知识库
new QuestionAnswerAdvisor(vectorStore)
)
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 50)
)
.call()
.content();
}
}
4.3 先检索后历史
java
@Service
public class KnowledgeFirstService {
public String chatKnowledgeFirst(
String conversationId,
String message
) {
return chatClient.prompt()
.user(message)
.advisors(
// 先检索知识库
new QuestionAnswerAdvisor(vectorStore),
// 再检索历史对话
new PromptChatMemoryAdvisor(chatMemory)
)
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 20)
)
.call()
.content();
}
}
五、长期记忆架构
5.1 三层记忆架构
┌─────────────────────────────────────┐
│ 工作记忆(10 条最近消息) │
│ - Redis ChatMemory │
│ - 快速访问 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 短期记忆(最近 24 小时) │
│ - VectorStoreChatMemory │
│ - 语义检索 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 长期记忆(用户偏好、重要事件) │
│ - 专用数据库 │
│ - 定期更新 │
└─────────────────────────────────────┘
5.2 长期记忆服务
java
@Service
public class LongTermMemoryService {
private final VectorStore memoryStore;
private final JdbcTemplate jdbcTemplate;
private final ChatClient chatClient;
// 保存重要信息到长期记忆
public void saveImportantInfo(
String userId,
String info,
double importance
) {
// 存储到向量数据库
Document memory = new Document(info, Map.of(
"user_id", userId,
"importance", importance,
"created_at", Instant.now(),
"type", "long_term"
));
memoryStore.add(List.of(memory));
// 更新用户画像
updateUserProfile(userId, info);
}
// 从长期记忆检索
public List<Document> retrieveLongTermMemory(
String userId,
String query
) {
return memoryStore.similaritySearch(
SearchRequest.query(query)
.withTopK(5)
.withFilterExpression("user_id == '%s'".formatted(userId))
);
}
// 更新用户画像
private void updateUserProfile(String userId, String info) {
String sql = """
INSERT INTO user_profiles (user_id, preferences, updated_at)
VALUES (?, ?, ?)
ON CONFLICT (user_id)
DO UPDATE SET
preferences = user_profiles.preferences || ?,
updated_at = ?
""";
jdbcTemplate.update(
sql,
userId,
info,
Instant.now(),
info,
Instant.now()
);
}
}
六、实战案例:智能客服记忆系统
java
@Service
public class CustomerServiceMemorySystem {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
private final LongTermMemoryService longTermMemory;
public String handleCustomerInquiry(
String customerId,
String inquiry
) {
// 1. 检索长期记忆
List<Document> userProfile = longTermMemory
.retrieveLongTermMemory(customerId, "用户偏好");
// 2. 构建系统提示词
String systemPrompt = buildSystemPrompt(userProfile);
// 3. 调用 ChatClient
String response = chatClient.prompt()
.system(systemPrompt)
.user(inquiry)
.advisors(
new PromptChatMemoryAdvisor(chatMemory),
new QuestionAnswerAdvisor(knowledgeBase)
)
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, customerId)
)
.call()
.content();
// 4. 提取并保存重要信息
extractAndSaveImportantInfo(customerId, inquiry, response);
return response;
}
private String buildSystemPrompt(List<Document> userProfile) {
if (userProfile.isEmpty()) {
return "你是一个专业的客服代表";
}
String preferences = userProfile.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n"));
return """
你是一个专业的客服代表。
用户偏好:
%s
请根据用户偏好提供个性化的服务。
""".formatted(preferences);
}
private void extractAndSaveImportantInfo(
String customerId,
String inquiry,
String response
) {
String prompt = """
从以下对话中提取重要的用户信息:
用户提问:%s
客服回答:%s
重要信息包括:姓名、联系方式、偏好、投诉等
""".formatted(inquiry, response);
String extractedInfo = chatClient.prompt()
.user(prompt)
.call()
.content();
if (!extractedInfo.isBlank()) {
longTermMemory.saveImportantInfo(
customerId,
extractedInfo,
0.8 // 重要性评分
);
}
}
}
总结
Spring AI 2.0 的对话记忆管理机制让构建自然的 AI 对话应用变得简单。通过合理选择 ChatMemory 实现、优化 Advisor 执行顺序、实现记忆压缩,我们可以构建出高效、智能的对话系统。
作为架构师,建议:
- 混合使用:结合短期和长期记忆
- 定期压缩:避免记忆无限增长
- 语义检索:使用向量存储提高检索精度
- 个性化:基于用户偏好定制对话
- 性能优化:合理设置记忆大小和检索数量
参考资料:
- Spring AI ChatMemory 文档:https://docs.spring.io/spring-ai/reference/api/chatclient-memory.html
- Redis Memory 文档:https://docs.spring.io/spring-ai/reference/api/vectordbs/redis-vector-store.html