第2129篇:AI Agent的记忆管理——让AI"记住"用户,而不是每次都从零开始
2026/4/30大约 8 分钟
第2129篇:AI Agent的记忆管理——让AI"记住"用户,而不是每次都从零开始
适读人群:构建个性化AI助手和长期对话系统的工程师 | 阅读时长:约20分钟 | 核心价值:理解长期记忆与短期记忆的区别,建立工程化的记忆管理体系,让AI随时间积累对用户的理解
"我都跟AI说了三次我是Java工程师,但它每次回答还是给Python示例。"
这是一个真实的用户反馈。对话结束后,那次对话里用户透露的信息就全丢了。下次对话,AI又是一块白板。
这对闲聊AI来说还好,但对于定位成"私人助手"的AI产品,这是硬伤。一个好的秘书应该记住你的习惯、偏好和背景;一个专业顾问应该记住你之前咨询的内容和决策。
AI的记忆管理是一个工程问题,不是"把所有历史塞给LLM"就能解决的。
记忆的类型与层次
/**
* AI记忆的四个层次
*
* ===== 层次一:工作记忆(Working Memory)=====
*
* 范围:当前对话的上下文(同一次对话内)
* 存储:对话消息历史(UserMessage/AiMessage列表)
* 生命周期:对话结束即消失
* 容量:受LLM上下文窗口限制(4K-128K tokens)
*
* ===== 层次二:情节记忆(Episodic Memory)=====
*
* 范围:跨对话的具体事件("上周用户问了一个关于部署的问题")
* 存储:结构化的对话摘要或关键事件记录
* 生命周期:保留一段时间(周/月级别)
* 用途:让AI能引用之前的对话("上次你说的那个问题解决了吗?")
*
* ===== 层次三:语义记忆(Semantic Memory)=====
*
* 范围:关于用户的事实性知识(角色、偏好、技术栈)
* 存储:结构化的用户画像或知识库
* 生命周期:长期(月/年级别)
* 用途:个性化响应("给你Java示例,因为你是Java工程师")
*
* ===== 层次四:程序记忆(Procedural Memory)=====
*
* 范围:用户的操作习惯(总是先问背景再问方案)
* 存储:用户行为模式
* 生命周期:长期
* 用途:主动适应用户的沟通风格
*
* 实践中最重要的是:语义记忆(用户画像)+ 情节记忆(关键事件)
*/用户画像记忆(语义记忆)
/**
* 用户知识图谱——语义记忆的核心
*
* 把用户透露的信息结构化存储
* 每次对话开始时注入相关信息
*/
@Entity
@Table(name = "user_memory_profiles")
@Data
@Builder
public class UserMemoryProfile {
@Id
private String userId;
// 职业背景
private String jobRole; // "后端工程师"
private String company; // "字节跳动"
private String industry; // "互联网"
private int yearsOfExperience; // 5
// 技术栈
@Column(columnDefinition = "TEXT")
private String primaryLanguage; // "Java"
@Column(columnDefinition = "TEXT")
private String techStackJson; // ["Spring Boot", "MySQL", "Kafka"]
// 当前项目/关注点
@Column(columnDefinition = "TEXT")
private String currentProjectContext; // 用户最近在做什么项目
// 偏好设置
private String preferredCodeStyle; // "详细注释" / "简洁代码"
private String preferredAnswerStyle; // "直接给答案" / "先讲原理"
private String languagePreference; // "中文"
// 已知的问题/目标
@Column(columnDefinition = "TEXT")
private String knownChallengesJson; // 用户面临的主要挑战
// 记忆质量评分
private double profileConfidence; // 0-1,信息的可信度
private int interactionCount; // 已交互次数
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public List<String> getTechStack() {
if (techStackJson == null) return List.of();
try {
return new ObjectMapper().readValue(techStackJson, new TypeReference<List<String>>() {});
} catch (Exception e) {
return List.of();
}
}
/**
* 生成个性化上下文(注入到System Prompt)
*/
public String toContextString() {
if (interactionCount < 2) return ""; // 信息不足,不注入
StringBuilder context = new StringBuilder("关于用户的已知信息:\n");
if (jobRole != null) context.append("- 职业:").append(jobRole).append("\n");
if (primaryLanguage != null) context.append("- 主要编程语言:").append(primaryLanguage).append("\n");
if (!getTechStack().isEmpty()) {
context.append("- 技术栈:").append(String.join(", ", getTechStack())).append("\n");
}
if (yearsOfExperience > 0) {
context.append("- 经验:").append(yearsOfExperience).append("年\n");
}
if (preferredAnswerStyle != null) {
context.append("- 偏好:").append(preferredAnswerStyle).append("\n");
}
if (currentProjectContext != null) {
context.append("- 当前项目背景:").append(currentProjectContext).append("\n");
}
return context.toString();
}
}记忆提取与更新
/**
* 记忆管理服务
*
* 从对话中提取用户信息,更新用户画像
* 这是让AI"越来越了解用户"的核心机制
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class MemoryManagementService {
private final ChatLanguageModel llm;
private final UserMemoryProfileRepository profileRepo;
/**
* 从对话中提取用户信息,更新记忆
*
* 在每次对话结束后异步调用
*/
@Async
public void extractAndUpdateMemory(String userId, List<ChatMessage> conversation) {
// 只分析用户消息
String userMessages = conversation.stream()
.filter(m -> m instanceof UserMessage)
.map(m -> ((UserMessage) m).singleText())
.collect(Collectors.joining("\n"));
if (userMessages.isBlank()) return;
// 用LLM提取信息
String prompt = """
请从以下对话中提取关于用户的结构化信息。
只提取明确说明的信息,不要推测。
对话内容:
%s
返回JSON(没有提到的字段用null):
{
"jobRole": "用户的职业/岗位",
"primaryLanguage": "主要使用的编程语言",
"techStack": ["技术1", "技术2"],
"yearsOfExperience": 工作年限数字或null,
"currentProjectContext": "正在做的项目简述",
"preferredAnswerStyle": "用户偏好的回答风格",
"knownChallenges": ["挑战1", "挑战2"]
}
只返回JSON。
""".formatted(userMessages.substring(0, Math.min(2000, userMessages.length())));
try {
String response = llm.generate(prompt);
String json = extractJson(response);
ObjectMapper mapper = new ObjectMapper();
JsonNode extracted = mapper.readTree(json);
// 加载或创建用户画像
UserMemoryProfile profile = profileRepo.findById(userId)
.orElseGet(() -> UserMemoryProfile.builder()
.userId(userId)
.createdAt(LocalDateTime.now())
.profileConfidence(0)
.interactionCount(0)
.build());
// 合并新信息(非null字段覆盖,null字段保留旧值)
mergeProfile(profile, extracted);
// 更新统计
profile.setInteractionCount(profile.getInteractionCount() + 1);
profile.setProfileConfidence(Math.min(1.0, profile.getInteractionCount() / 5.0));
profile.setUpdatedAt(LocalDateTime.now());
profileRepo.save(profile);
log.debug("用户记忆已更新: userId={}, interactions={}",
userId, profile.getInteractionCount());
} catch (Exception e) {
log.warn("记忆提取失败: userId={}, error={}", userId, e.getMessage());
}
}
private void mergeProfile(UserMemoryProfile profile, JsonNode extracted) {
if (!extracted.path("jobRole").isNull()) {
profile.setJobRole(extracted.path("jobRole").asText());
}
if (!extracted.path("primaryLanguage").isNull()) {
profile.setPrimaryLanguage(extracted.path("primaryLanguage").asText());
}
if (!extracted.path("yearsOfExperience").isNull()) {
profile.setYearsOfExperience(extracted.path("yearsOfExperience").asInt());
}
if (!extracted.path("currentProjectContext").isNull()) {
profile.setCurrentProjectContext(extracted.path("currentProjectContext").asText());
}
if (!extracted.path("preferredAnswerStyle").isNull()) {
profile.setPreferredAnswerStyle(extracted.path("preferredAnswerStyle").asText());
}
// 技术栈:合并而不是覆盖
JsonNode techStackNode = extracted.path("techStack");
if (techStackNode.isArray() && techStackNode.size() > 0) {
List<String> existing = profile.getTechStack();
List<String> newItems = new ArrayList<>();
techStackNode.forEach(item -> newItems.add(item.asText()));
// 合并去重
Set<String> merged = new LinkedHashSet<>(existing);
merged.addAll(newItems);
try {
profile.setTechStackJson(new ObjectMapper().writeValueAsString(
new ArrayList<>(merged)));
} catch (Exception ignored) {}
}
}
/**
* 获取个性化的System Prompt
*/
public String getPersonalizedSystemPrompt(String userId, String basePrompt) {
UserMemoryProfile profile = profileRepo.findById(userId).orElse(null);
if (profile == null || profile.getInteractionCount() < 2) {
return basePrompt; // 信息不足,使用基础Prompt
}
String memoryContext = profile.toContextString();
return basePrompt + "\n\n" + memoryContext +
"\n请根据用户的背景信息,个性化你的回答风格和示例。";
}
private String extractJson(String s) {
int start = s.indexOf('{'); int end = s.lastIndexOf('}');
return (start >= 0 && end > start) ? s.substring(start, end + 1) : s;
}
}情节记忆管理
/**
* 情节记忆服务
*
* 记录跨对话的关键事件和决策
* 让AI能在后续对话中引用
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class EpisodicMemoryService {
private final ChatLanguageModel llm;
private final EmbeddingModel embeddingModel;
private final VectorStore memoryVectorStore; // 专用于记忆的向量库
private final EpisodicMemoryRepository memoryRepo;
/**
* 保存对话摘要作为情节记忆
*/
@Async
public void saveConversationMemory(String userId, List<ChatMessage> conversation) {
if (conversation.size() < 4) return; // 对话太短,不值得记录
// 用LLM生成对话摘要
String conversationText = conversation.stream()
.map(m -> {
if (m instanceof UserMessage um) return "用户:" + um.singleText();
if (m instanceof AiMessage am) return "AI:" + am.text();
return "";
})
.filter(s -> !s.isEmpty())
.collect(Collectors.joining("\n"));
String summaryPrompt = """
请简洁地总结以下对话中的关键信息(不超过150字):
1. 用户问了什么主要问题
2. 达成了什么结论或决策
3. 遗留了什么未解决的问题
对话:
%s
总结:
""".formatted(conversationText.substring(0, Math.min(3000, conversationText.length())));
try {
String summary = llm.generate(summaryPrompt);
// 保存到数据库
EpisodicMemory memory = EpisodicMemory.builder()
.memoryId(UUID.randomUUID().toString())
.userId(userId)
.summary(summary)
.timestamp(LocalDateTime.now())
.build();
memoryRepo.save(memory);
// 向量化摘要(用于语义检索相关记忆)
float[] vector = embeddingModel.embed(summary).content().vector();
memoryVectorStore.add(VectorStore.Document.builder()
.id(memory.getMemoryId())
.content(summary)
.vector(vector)
.metadata(Map.of("userId", userId, "type", "episodic"))
.build());
} catch (Exception e) {
log.warn("情节记忆保存失败: {}", e.getMessage());
}
}
/**
* 检索与当前问题相关的历史记忆
*/
public List<String> retrieveRelevantMemories(
String userId, String currentQuery, int maxMemories) {
float[] queryVector = embeddingModel.embed(currentQuery).content().vector();
// 从用户的记忆空间检索相关记忆
List<VectorStore.SearchResult> hits = memoryVectorStore.search(
queryVector, maxMemories,
VectorStore.SearchFilter.builder()
.conditions(List.of(
VectorStore.SearchFilter.FilterCondition.builder()
.field("userId").value(userId)
.operator(VectorStore.SearchFilter.FilterOperator.EQ)
.build()
))
.build()
);
return hits.stream()
.filter(h -> h.getScore() > 0.7) // 只返回相关度高的记忆
.map(VectorStore.SearchResult::getContent)
.toList();
}
/**
* 构建带记忆的对话上下文
*
* 把相关的历史记忆注入到对话开始
*/
public String buildContextWithMemory(
String userId, String currentQuery, String baseContext) {
List<String> relevantMemories = retrieveRelevantMemories(userId, currentQuery, 3);
if (relevantMemories.isEmpty()) return baseContext;
String memorySection = "以下是用户相关的历史对话摘要,供参考:\n" +
relevantMemories.stream()
.map(m -> "- " + m)
.collect(Collectors.joining("\n"));
return baseContext + "\n\n" + memorySection;
}
@Data
@Builder
@Entity
@Table(name = "episodic_memories")
public static class EpisodicMemory {
@Id private String memoryId;
private String userId;
@Column(columnDefinition = "TEXT") private String summary;
private LocalDateTime timestamp;
}
}实践建议
记忆管理要有"遗忘"机制
人类记忆会遗忘,AI的记忆系统也需要。如果不加控制,记忆会无限增长,而且旧信息可能干扰新信息(用户换了工作,但AI还记着旧的职业信息)。建议:情节记忆设置保留期(比如6个月);用户画像中的时效性信息(如"当前项目")要标注更新时间,超过一定时间自动降低权重或清除;让用户可以主动"重置"AI对自己的记忆。
记忆提取要异步,不要阻塞用户响应
从对话中提取信息需要额外的LLM调用,这个操作应该在对话结束后异步执行,不能让用户等待。用@Async或消息队列处理记忆更新。对用户来说,记忆是在下次对话才生效的,所以这次对话结束后慢慢处理没有问题。
第一次交互别假设用户信息存在
新用户没有任何记忆,第一次对话要能优雅降级:没有记忆时用默认的通用Prompt;不要因为记忆为空就报错或给出奇怪的回答。随着对话次数增加,逐渐积累信息,体验越来越好。这种"越用越好"的感觉,是长期记忆带给用户的核心价值。
