Spring AI的Memory机制:让AI记住用户的偏好和历史
2026/10/30大约 7 分钟Spring AIMemory对话记忆个性化Java
Spring AI的Memory机制:让AI记住用户的偏好和历史
一、用户抱怨:"它每次都像第一次见我"
李华是某在线教育平台的产品经理。
2026年初,他们上线了AI学习助手。产品逻辑很好:学生提问,AI解答,根据学习历史推荐内容。
但三个月后,收到大量用户反馈:
"为什么每次打开都要重新介绍自己?" "我昨天说过我是初学者,今天它还在给我讲高中数学。" "上周问过这个问题,它给了我A方案。今天再问,给我B方案。前后矛盾。"
产品团队复盘:AI没有记忆。每次对话都是全新的开始,用户花在"告诉AI我是谁"上的时间,比真正学习的时间还多。
Java技术负责人小郑接到任务:给AI加上持久化记忆能力。
这篇文章记录了小郑的完整实现过程。
二、AI Memory的三种类型
三种记忆的比较:
| 类型 | 存储位置 | 生命周期 | 检索方式 | 适合存储 |
|---|---|---|---|---|
| 短期记忆 | Redis/内存 | 单次对话 | 完整传入Context | 当前对话消息 |
| 长期记忆 | 数据库 | 永久 | 精确查询 | 用户资料、偏好设置 |
| 语义记忆 | 向量数据库 | 永久 | 语义相似度 | 历史对话摘要、行为模式 |
三、Spring AI的ConversationHistory实现
3.1 短期记忆:MessageWindowChatMemory
package com.laozhang.ai.memory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
/**
* ChatMemory配置
* Spring AI 1.0提供了开箱即用的对话历史管理
*/
@Configuration
public class ChatMemoryConfig {
/**
* 使用Redis持久化的对话历史(生产推荐)
* 不同用户的会话相互隔离
*/
@Bean
public ChatMemory redisChatMemory(RedisTemplate<String, Object> redisTemplate) {
return new RedisChatMemory(redisTemplate, 24 * 60); // 24小时过期
}
/**
* 配置带记忆的ChatClient
* MessageChatMemoryAdvisor会自动处理历史消息的注入和保存
*/
@Bean
public ChatClient memoryEnabledChatClient(
ChatClient.Builder builder,
ChatMemory chatMemory) {
return builder
.defaultAdvisors(
// 窗口大小:保留最近20条消息
MessageChatMemoryAdvisor.builder(chatMemory)
.build()
)
.build();
}
}3.2 Redis持久化ChatMemory实现
package com.laozhang.ai.memory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.data.redis.core.RedisTemplate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 基于Redis的对话历史持久化实现
* 支持多用户、多会话的隔离存储
*/
@Slf4j
@RequiredArgsConstructor
public class RedisChatMemory implements ChatMemory {
private static final String KEY_PREFIX = "ai:chat:memory:";
private static final int MAX_MESSAGES_PER_CONVERSATION = 50;
private final RedisTemplate<String, Object> redisTemplate;
private final long ttlMinutes;
@Override
public void add(String conversationId, List<Message> messages) {
String key = buildKey(conversationId);
messages.forEach(msg -> {
// 将消息序列化存入Redis List
redisTemplate.opsForList().rightPush(key,
serializeMessage(msg));
});
// 限制消息数量(保留最近N条)
Long listSize = redisTemplate.opsForList().size(key);
if (listSize != null && listSize > MAX_MESSAGES_PER_CONVERSATION) {
redisTemplate.opsForList().trim(key,
listSize - MAX_MESSAGES_PER_CONVERSATION, -1);
}
// 刷新TTL
redisTemplate.expire(key, Duration.ofMinutes(ttlMinutes));
log.debug("对话历史已保存: conversationId={}, newMessages={}", conversationId, messages.size());
}
@Override
public List<Message> get(String conversationId, int lastN) {
String key = buildKey(conversationId);
List<Object> rawMessages = redisTemplate.opsForList().range(key, -lastN, -1);
if (rawMessages == null || rawMessages.isEmpty()) {
return new ArrayList<>();
}
return rawMessages.stream()
.map(raw -> deserializeMessage((String) raw))
.toList();
}
@Override
public void clear(String conversationId) {
redisTemplate.delete(buildKey(conversationId));
log.info("对话历史已清除: conversationId={}", conversationId);
}
private String buildKey(String conversationId) {
return KEY_PREFIX + conversationId;
}
private String serializeMessage(Message message) {
// 简化实现,实际使用Jackson序列化
return message.getMessageType().name() + "|||" + message.getContent();
}
private Message deserializeMessage(String raw) {
String[] parts = raw.split("\\|\\|\\|", 2);
String type = parts[0];
String content = parts.length > 1 ? parts[1] : "";
return switch (type) {
case "USER" -> new org.springframework.ai.chat.messages.UserMessage(content);
case "ASSISTANT" -> new org.springframework.ai.chat.messages.AssistantMessage(content);
default -> new org.springframework.ai.chat.messages.SystemMessage(content);
};
}
}四、长期记忆:跨对话的用户画像
package com.laozhang.ai.memory.longterm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
/**
* 用户长期画像服务
* 从对话中自动提取用户信息,构建持久化的用户画像
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserProfileMemoryService {
private final ChatClient chatClient;
private final UserProfileRepository profileRepository;
/**
* 在每次对话结束后,异步提取并更新用户画像
*/
@Async
public void updateUserProfile(String userId, String conversationHistory) {
log.info("开始提取用户画像: userId={}", userId);
// 使用LLM从对话中提取结构化信息
String extractPrompt = """
从以下对话中提取用户信息,以JSON格式返回:
{
"learningLevel": "初学者/中级/高级",
"interests": ["主题1", "主题2"],
"preferredStyle": "详细解释/简洁回答/代码示例为主",
"struggles": ["困难点1"],
"goals": "学习目标描述"
}
只返回JSON,不要其他文字。
对话记录:
%s
""".formatted(conversationHistory);
try {
String jsonResult = chatClient.prompt().user(extractPrompt).call().content();
profileRepository.updateProfile(userId, parseProfile(jsonResult));
log.info("用户画像更新完成: userId={}", userId);
} catch (Exception e) {
log.error("用户画像提取失败: userId={}, error={}", userId, e.getMessage());
}
}
/**
* 构建个性化系统提示词
* 将用户画像注入到每次对话的System Prompt中
*/
public String buildPersonalizedSystemPrompt(String userId, String basePrompt) {
UserProfile profile = profileRepository.findByUserId(userId).orElse(null);
if (profile == null) {
return basePrompt; // 新用户使用默认提示词
}
return basePrompt + """
=== 用户个性化信息 ===
学习水平:%s
兴趣领域:%s
偏好回答风格:%s
当前学习目标:%s
已知薄弱点:%s
请根据以上信息调整你的回答方式和深度。
""".formatted(
profile.getLearningLevel(),
String.join("、", profile.getInterests()),
profile.getPreferredStyle(),
profile.getGoals(),
String.join("、", profile.getStruggles())
);
}
private UserProfile parseProfile(String json) {
// 实际使用Jackson解析
return new UserProfile(); // 简化
}
}4.1 在对话中使用个性化记忆
package com.laozhang.ai.memory.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class PersonalizedAiTutorService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
private final UserProfileMemoryService profileService;
/**
* 个性化AI辅导对话
* 整合短期记忆(当前对话历史)+ 长期记忆(用户画像)
*/
public String chat(String userId, String sessionId, String question) {
// 构建个性化System Prompt(注入长期用户画像)
String systemPrompt = profileService.buildPersonalizedSystemPrompt(userId, """
你是一个专业的编程辅导老师,帮助学生学习Java和AI开发。
根据学生的水平和偏好调整你的解释方式。
""");
// 调用AI(自动处理短期对话历史)
String answer = chatClient.prompt()
.system(systemPrompt)
.user(question)
.advisors(advisorSpec -> advisorSpec
.param(MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId)
.param(MessageChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 20)
)
.call()
.content();
// 异步更新用户画像
String recentHistory = formatHistory(chatMemory.get(sessionId, 10));
profileService.updateUserProfile(userId, recentHistory);
return answer;
}
private String formatHistory(java.util.List<org.springframework.ai.chat.messages.Message> messages) {
return messages.stream()
.map(m -> m.getMessageType().name() + ": " + m.getContent())
.collect(java.util.stream.Collectors.joining("\n"));
}
}五、语义记忆:历史对话的向量化存储
package com.laozhang.ai.memory.semantic;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* 语义记忆服务
* 将对话摘要向量化存储,支持按语义相似度检索历史记忆
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SemanticMemoryService {
private final VectorStore vectorStore;
private final ChatClient summarizationClient;
/**
* 检索与当前问题最相关的历史记忆
*/
public List<String> retrieveRelevantMemories(String userId, String currentQuestion, int topK) {
List<Document> memories = vectorStore.similaritySearch(
SearchRequest.query(currentQuestion)
.withTopK(topK)
.withSimilarityThreshold(0.65f)
.withFilterExpression("userId == '" + userId + "'")
);
return memories.stream()
.map(Document::getContent)
.toList();
}
/**
* 对话结束后,异步生成并存储语义摘要
*/
@Async
public void storeConversationSummary(String userId, String sessionId,
List<org.springframework.ai.chat.messages.Message> messages) {
if (messages.size() < 4) return; // 太短的对话不存储
String conversationText = messages.stream()
.map(m -> m.getMessageType().name() + ": " + m.getContent())
.collect(java.util.stream.Collectors.joining("\n"));
// 用LLM生成摘要
String summary = summarizationClient.prompt()
.user("用3-5句话总结以下对话的关键信息,重点记录用户的学习情况和收获:\n\n" + conversationText)
.call()
.content();
// 向量化存储
Document memoryDoc = new Document(summary, Map.of(
"userId", userId,
"sessionId", sessionId,
"type", "conversation_summary",
"timestamp", String.valueOf(System.currentTimeMillis())
));
vectorStore.add(List.of(memoryDoc));
log.info("语义记忆已存储: userId={}, sessionId={}", userId, sessionId);
}
}六、改造成果
小郑完成Memory系统改造后的数据(三个月后复盘):
| 指标 | 改造前 | 改造后 | 变化 |
|---|---|---|---|
| 用户每次对话中自我介绍比例 | 78% | 12% | -85% |
| 平均对话轮数(达到同等效果) | 8.3轮 | 5.1轮 | -39% |
| 用户留存率(月) | 34% | 51% | +50% |
| 用户对AI满意度(NPS) | 38 | 67 | +76% |
| "回答前后矛盾"投诉 | 每周23条 | 每周2条 | -91% |
用户说得最多的一句话从"它不记得我"变成了"它越来越了解我"。
AI记忆不是炫技,是产品体验的基础。没有记忆的AI助手,就像每天都失忆的朋友——聪明,但让人疲惫。
