系列第四篇:Agent 长期记忆与用户画像——让智能体真正"认识"你的用户
前言
前三篇文章中,我们构建了单体 Agent、Multi-Agent 协作体系,并建立了完整的评估框架。但有一个问题始终困扰着落地团队:
“Agent 每次对话都像第一次见面,它不记得张工喜欢简洁的回复、不知道李经理只看数据不看分析、忘了上次那个区域的能耗问题是怎么解决的。”
短期记忆(消息窗口)只能维持一次会话的上下文。长期记忆 才是让 Agent 从"通用助手"进化为"专属顾问"的关键。本文将系统性地构建一套 Agent 长期记忆 + 用户画像系统。
一、记忆系统的完整分层架构
1.1 人类记忆的启发
在设计 Agent 记忆系统之前,我们先看看人类记忆是怎么工作的。认知心理学将人类记忆分为三个层次:
- 工作记忆(Working Memory):容量小(约 7±2 项)、持续时间短(约 20 秒)。你正在读这句话时,脑中暂时保持的那几个词就是工作记忆。
- 情景记忆(Episodic Memory):对具体事件的记忆。“上周三 C 栋数据中心能耗飙升,最后发现是空调系统故障”——这是一个情景记忆。
- 语义记忆(Semantic Memory):抽象化的知识和规律。“数据中心的能耗通常在夏季会偏高”——这是从多次经验中总结出的规律性知识。
Agent 的记忆系统也应该遵循类似的分层设计:
┌────────────────────────────────────────────────────────────┐
│ Agent 记忆分层架构 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Layer 1: 工作记忆 (Working Memory) │ │
│ │ · 当前会话的消息窗口 │ │
│ │ · 容量:最近 20~50 条消息 │ │
│ │ · 生命周期:会话级别 │ │
│ │ · 用途:多轮对话的上下文连贯性 │ │
│ └────────────────────────────────────────────────────┘ │
│ │ 会话结束时摘要沉淀 │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Layer 2: 情景记忆 (Episodic Memory) │ │
│ │ · 历史对话的结构化摘要 │ │
│ │ · 容量:无限制(向量数据库存储) │ │
│ │ · 生命周期:永久(可设定衰减权重) │ │
│ │ · 用途:回忆"上次那个问题是怎么解决的" │ │
│ └────────────────────────────────────────────────────┘ │
│ │ 多次经验抽象提炼 │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Layer 3: 语义记忆 (Semantic Memory / User Profile) │ │
│ │ · 用户偏好、行为习惯、领域知识 │ │
│ │ · 容量:精炼的键值对 + 向量知识 │ │
│ │ · 生命周期:长期(定期更新) │ │
│ │ · 用途:"张工喜欢简洁回答"、"李经理关注成本指标" │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
1.2 三层记忆的协作机制
当用户发起一次对话时,三层记忆的协作过程如下:
用户提问:"上次C栋的问题解决了吗?"
Step 1: 工作记忆 — 检查当前会话是否有 C 栋相关上下文
→ 没有(这是新会话的第一轮)
Step 2: 情景记忆 — 在历史对话摘要中检索"C栋"相关内容
→ 命中: "2026-08-20 C栋能耗异常,排查发现空调系统故障,
已提交维修工单 #20260820-003"
Step 3: 语义记忆 — 检索当前用户的偏好画像
→ 用户: 张工,偏好: 简洁回复,关注: 维修进度
Step 4: Agent 生成回答(融合三层记忆的信息)
→ "C栋上次是空调系统故障,工单 #20260820-003。
需要我查一下当前维修进度吗?"
关键点:三层记忆不是简单叠加,而是 按需检索、按需注入。不是每次对话都需要三层全查——工作记忆始终参与,情景记忆和语义记忆只在相关时才检索注入,避免无关信息干扰模型。
二、情景记忆:让 Agent 能"回忆往事"
2.1 对话摘要的自动生成
情景记忆的核心是 对话摘要——每次会话结束后,用一个专门的"摘要 Agent"将对话内容压缩为结构化记录。
为什么不直接存原始对话?因为:
- 一次对话可能有 30~50 轮消息,Token 量很大
- 大部分消息是"过程性"的(工具调用、中间推理),对未来的记忆价值很低
- 摘要可以提取关键信息,压缩比通常能达到 10:1 以上
/**
* 对话摘要服务
*
* 触发时机:每次会话结束(用户关闭页面 / 超时不活跃 / 手动结束)
*
* 设计原则:
* 1. 摘要要保留"决策性信息",丢弃"过程性信息"
* 2. 必须提取实体(人名、地点、设备、数值),便于后续检索
* 3. 保留"结论"和"行动",弱化"推理过程"
*/
@Service
public class ConversationSummaryService {
private final ChatLanguageModel summaryModel;
public ConversationSummaryService() {
// 摘要任务用小模型即可(如 gpt-4o-mini),节省成本
this.summaryModel = OpenAiChatModel.builder()
.apiKey(System.getenv("LLM_API_KEY"))
.modelName("gpt-4o-mini")
.temperature(0.1) // 摘要需要高保真,低温度
.build();
}
/**
* 将一次完整对话压缩为结构化摘要
*
* @param sessionId 会话 ID
* @param userId 用户 ID
* @param messages 完整对话消息列表
* @return 结构化摘要对象
*/
public ConversationSummary summarize(String sessionId,
String userId,
List<ChatMessage> messages) {
String conversationText = messages.stream()
.map(msg -> String.format("[%s] %s",
msg.type() == ChatMessageType.USER ? "用户" : "Agent",
msg.text()))
.collect(Collectors.joining("\n"));
String prompt = String.format("""
你是一个对话摘要专家。请将以下对话压缩为结构化摘要。
## 摘要要求
1. topic: 一句话概括对话主题
2. keyFacts: 提取关键事实(数据、结论、决策),保留数值和单位
3. actions: 列出执行的行动和结果
4. entities: 提取涉及的实体(人名、区域、设备、日期)
5. unresolved: 未解决的问题或待跟进事项(如果有)
6. outcome: 最终结果(成功/待跟进/失败)
严格按 JSON 格式返回:
{
"topic": "...",
"keyFacts": ["...", "..."],
"actions": [{"action": "...", "result": "..."}],
"entities": {
"people": ["..."],
"areas": ["..."],
"devices": ["..."],
"dates": ["..."]
},
"unresolved": ["..."],
"outcome": "success|pending|failed"
}
## 对话内容
%s
""", conversationText);
String result = summaryModel.generate(
UserMessage.from(prompt)).content().text().trim();
// 解析 JSON 并构建摘要对象
JsonNode node = JsonUtils.parse(result);
return ConversationSummary.builder()
.sessionId(sessionId)
.userId(userId)
.timestamp(Instant.now())
.topic(node.get("topic").asText())
.keyFacts(parseStringList(node.get("keyFacts")))
.actions(parseActions(node.get("actions")))
.entities(parseEntities(node.get("entities")))
.unresolved(parseStringList(node.get("unresolved")))
.outcome(node.get("outcome").asText())
.originalMessageCount(messages.size())
.build();
}
}
2.2 摘要的结构化定义
/**
* 对话摘要
*
* 设计考量:
* - 保留 sessionId 用于溯源(可以追溯到原始对话)
* - timestamp 用于时间衰减(越老的记忆权重越低)
* - entities 用于精确检索("所有和C栋相关的历史")
* - outcome 用于快速过滤(只看未解决的问题)
*/
public class ConversationSummary {
private String sessionId;
private String userId;
private Instant timestamp;
// 核心内容
private String topic; // "C栋能耗异常排查"
private List<String> keyFacts; // ["C栋日耗8734kWh,超出均值84%"]
private List<Action> actions; // [{通知张工, 成功}, {提交工单, #003}]
private Entities entities; // {people:[张工], areas:[C栋], ...}
private List<String> unresolved; // ["等待维修结果"]
private String outcome; // "pending"
// 元数据
private int originalMessageCount; // 原始消息数(用于衡量信息压缩比)
/**
* 生成用于向量化的文本表示
*
* 为什么不用原始对话做 Embedding?
* 因为摘要去除了噪声信息,Embedding 质量更高,
* 检索时语义匹配更精准。
*/
public String toEmbeddingText() {
return String.format("""
主题: %s
关键事实: %s
执行行动: %s
涉及实体: %s
未解决: %s
""",
topic,
String.join("; ", keyFacts),
actions.stream()
.map(a -> a.action() + "→" + a.result())
.collect(Collectors.joining("; ")),
entities.toBriefString(),
String.join("; ", unresolved)
);
}
}
2.3 情景记忆的存储与检索
/**
* 情景记忆存储服务
*
* 存储策略:双写
* 1. 摘要的结构化字段存入关系数据库(支持精确查询)
* 2. 摘要的文本表示向量化后存入向量数据库(支持语义检索)
*
* 两种检索方式互补:
* - 精确查询:"查找张工相关的所有对话" → SQL WHERE entities LIKE '%张工%'
* - 语义检索:"上次那个设备故障是怎么处理的" → 向量相似度搜索
*/
@Service
public class EpisodicMemoryService {
private final SummaryRepository summaryRepo; // JPA / MyBatis
private final EmbeddingStore<TextSegment> vectorStore;
private final EmbeddingModel embeddingModel;
public EpisodicMemoryService(SummaryRepository summaryRepo,
EmbeddingModel embeddingModel) {
this.summaryRepo = summaryRepo;
this.embeddingModel = embeddingModel;
// 情景记忆使用独立的向量集合,与 RAG 知识库隔离
this.vectorStore = new InMemoryEmbeddingStore<>();
}
/**
* 保存对话摘要(双写)
*/
@Transactional
public void save(ConversationSummary summary) {
// 1. 结构化存储
summaryRepo.save(summary);
// 2. 向量化存储
String embeddingText = summary.toEmbeddingText();
Embedding embedding = embeddingModel.embed(embeddingText).content();
TextSegment segment = TextSegment.from(embeddingText,
Metadata.from("sessionId", summary.getSessionId())
.add("userId", summary.getUserId())
.add("timestamp", summary.getTimestamp().toString())
.add("outcome", summary.getOutcome())
);
vectorStore.add(embedding, segment);
log.info("情景记忆已保存: session={}, topic={}",
summary.getSessionId(), summary.getTopic());
}
/**
* 语义检索:根据当前问题检索历史对话
*
* @param query 当前问题
* @param userId 用户 ID(只检索该用户的历史)
* @param maxResults 最大返回数
* @return 相关的历史摘要列表(按相关性排序)
*/
public List<ConversationSummary> search(String query,
String userId,
int maxResults) {
// 1. 向量检索(语义匹配)
Embedding queryVec = embeddingModel.embed(query).content();
List<EmbeddingSearchResult<TextSegment>> results =
vectorStore.findRelevant(queryVec, maxResults * 2, 0.6);
// 2. 按用户过滤 + 时间衰减加权
return results.stream()
.filter(r -> userId.equals(r.embeddedObject()
.metadata().getString("userId")))
.map(r -> {
ConversationSummary summary = summaryRepo
.findBySessionId(r.embeddedObject()
.metadata().getString("sessionId"))
.orElse(null);
return summary;
})
.filter(Objects::nonNull)
.sorted((a, b) -> {
// 时间衰减:近期记忆优先
double decayA = timeDecay(a.getTimestamp());
double decayB = timeDecay(b.getTimestamp());
return Double.compare(decayB, decayA);
})
.limit(maxResults)
.toList();
}
/**
* 精确查询:按实体检索(如"所有和C栋相关的历史")
*/
public List<ConversationSummary> searchByEntity(String userId,
String entityType,
String entityValue) {
return summaryRepo.findByUserIdAndEntity(
userId, entityType, entityValue);
}
/**
* 时间衰减函数
*
* 原理:借鉴 Ebbinghaus 遗忘曲线
* - 1 天前的记忆:权重 0.95(几乎不衰减)
* - 7 天前:权重 0.80
* - 30 天前:权重 0.50
* - 90 天前:权重 0.20(大幅衰减但仍保留)
*
* 衰减不是删除——老记忆仍然可以被高相关性检索召回,
* 只是在综合排序中权重降低。
*/
private double timeDecay(Instant timestamp) {
long daysAgo = Duration.between(timestamp, Instant.now()).toDays();
return Math.exp(-0.01 * daysAgo);
}
}
三、语义记忆:用户画像系统
3.1 用户画像的本质
如果说情景记忆是"Agent 经历了什么",那语义记忆(用户画像)就是"Agent 对用户了解多少"。
用户画像不是简单的标签集合,而是从多次交互中 自动提炼 出的、关于用户的结构化认知。它包括:
| 维度 | 示例 | 用途 |
|---|---|---|
| 回复偏好 | “喜欢简洁回复,不要过多解释” | 控制回答的长度和风格 |
| 关注指标 | “主要关注成本(电费),而非技术细节” | 调整回答的侧重点 |
| 职责范围 | “负责 A/B 栋的运维管理” | 限定默认查询范围 |
| 技术水平 | “对能耗分析有专业背景” | 决定是否使用专业术语 |
| 历史关切 | “持续关注C栋数据中心能耗” | 主动关联相关信息 |
| 交互习惯 | “通常在早上9~10点查询日报” | 预测性推荐 |
3.2 用户画像的自动提取
用户画像不应该让用户手动填写——那样用户体验极差,而且信息会很快过时。正确的做法是:从对话历史中自动提取和更新。
/**
* 用户画像自动提取服务
*
* 工作原理:
* 1. 每次会话结束后,用 LLM 分析对话中透露的用户偏好信号
* 2. 与现有画像合并(增量更新,而非覆盖)
* 3. 对冲突信息进行仲裁(以最新对话为准)
*
* 提取信号举例:
* - 用户说"说重点"→ 偏好简洁回复
* - 用户问"这得花多少钱"→ 关注成本指标
* - 用户说"我是负责B栋的"→ 职责范围: B栋
* - 用户连续三次查询C栋→ 历史关切: C栋能耗
*/
@Service
public class UserProfileExtractor {
private final ChatLanguageModel extractorModel;
public UserProfileExtractor() {
this.extractorModel = OpenAiChatModel.builder()
.apiKey(System.getenv("LLM_API_KEY"))
.modelName("gpt-4o-mini") // 提取任务用小模型
.temperature(0.0)
.build();
}
/**
* 从对话中提取用户画像信号
*
* @param messages 完整对话消息
* @return 提取到的画像信号(可能为空)
*/
public UserProfileSignals extractSignals(List<ChatMessage> messages) {
String conversationText = messages.stream()
.map(msg -> String.format("[%s] %s",
msg.type() == ChatMessageType.USER ? "用户" : "Agent",
msg.text()))
.collect(Collectors.joining("\n"));
String prompt = String.format("""
你是一个用户画像分析专家。请从以下对话中提取用户偏好信号。
## 提取维度
1. replyPreference: 用户偏好的回复风格
信号示例:"说重点"="简洁"、"能详细说说吗"="详细"、无信号=null
2. focusMetrics: 用户关注的指标类型
可选值: cost(成本), efficiency(效率), safety(安全),
environment(环保), 无信号=null
3. responsibilityScope: 用户的管理/职责范围
如"我负责A栋"→ "A栋"
4. expertiseLevel: 用户的专业水平
可选值: beginner, intermediate, expert
5. interests: 用户表现出持续关注的主题
6. implicitNeeds: 对话中暗示但未明确说出的需求
重要:只提取有明确证据支撑的信号,不要猜测。
如果某个维度没有信号,填 null。
严格按 JSON 返回:
{
"replyPreference": null 或 "简洁/详细/专业/通俗",
"focusMetrics": null 或 ["cost", "safety"],
"responsibilityScope": null 或 "区域描述",
"expertiseLevel": null 或 "beginner/intermediate/expert",
"interests": null 或 ["主题1", "主题2"],
"implicitNeeds": null 或 ["需求1"]
}
## 对话内容
%s
""", conversationText);
String result = extractorModel.generate(
UserMessage.from(prompt)).content().text().trim();
return parseSignals(result);
}
}
3.3 用户画像的增量更新
画像更新不是简单的"新覆盖旧",而是一个 增量合并 + 冲突仲裁 的过程:
/**
* 用户画像管理服务
*
* 更新策略:
* - 新维度直接追加
* - 同维度新值:与旧值对比,冲突时以最新为准
* - 兴趣/关注点:采用计数器模式,多次出现权重增加
* - 长期未验证的画像:随时间降低置信度
*/
@Service
public class UserProfileService {
private final UserProfileRepository profileRepo;
private final UserProfileExtractor extractor;
/**
* 会话结束后更新用户画像
*/
@Transactional
public void updateFromConversation(String userId,
List<ChatMessage> messages) {
// 1. 从对话中提取信号
UserProfileSignals signals = extractor.extractSignals(messages);
// 2. 如果没有任何信号,跳过
if (signals.isEmpty()) {
log.debug("本次对话未提取到用户画像信号: userId={}", userId);
return;
}
// 3. 加载现有画像
UserProfile profile = profileRepo.findByUserId(userId)
.orElse(UserProfile.newProfile(userId));
// 4. 增量合并
mergeSignals(profile, signals);
// 5. 持久化
profileRepo.save(profile);
log.info("用户画像已更新: userId={}, 更新维度数={}",
userId, signals.nonNullCount());
}
/**
* 增量合并逻辑
*
* 核心原则:
* - 直接属性(偏好、专业水平):新值覆盖旧值
* - 集合属性(兴趣、关注指标):累加权重,不直接删除
* - 置信度:每次验证 +0.1,长期未验证每月 -0.05
*/
private void mergeSignals(UserProfile profile,
UserProfileSignals signals) {
Instant now = Instant.now();
// 回复偏好:直接覆盖(用户偏好可能随场景变化)
if (signals.replyPreference() != null) {
profile.setReplyPreference(signals.replyPreference());
profile.setPreferenceConfidence(
Math.min(1.0, profile.getPreferenceConfidence() + 0.1));
profile.setPreferenceUpdatedAt(now);
}
// 关注指标:累加计数(出现越多次,权重越高)
if (signals.focusMetrics() != null) {
for (String metric : signals.focusMetrics()) {
profile.incrementMetricCount(metric);
}
}
// 职责范围:直接覆盖
if (signals.responsibilityScope() != null) {
profile.setResponsibilityScope(signals.responsibilityScope());
}
// 专业水平:只升不降(用户展示更高水平时更新)
if (signals.expertiseLevel() != null) {
int newLevel = expertiseRank(signals.expertiseLevel());
int oldLevel = expertiseRank(profile.getExpertiseLevel());
if (newLevel > oldLevel) {
profile.setExpertiseLevel(signals.expertiseLevel());
}
}
// 兴趣主题:计数器累加
if (signals.interests() != null) {
for (String interest : signals.interests()) {
profile.incrementInterestWeight(interest);
}
}
}
private int expertiseRank(String level) {
return switch (level) {
case "beginner" -> 1;
case "intermediate" -> 2;
case "expert" -> 3;
default -> 0;
};
}
}
3.4 画像数据的定义
/**
* 用户画像实体
*
* 存储设计:
* - 结构化字段存主表(高频读取)
* - 兴趣计数、指标计数存 JSON 字段(灵活扩展)
* - 置信度字段用于时间衰减
*/
@Table(name = "agent_user_profile")
public class UserProfile {
@Id
private String userId;
// ===== 核心画像 =====
/** 回复偏好: "简洁" / "详细" / "专业" / "通俗" */
private String replyPreference;
private double preferenceConfidence; // 0.0~1.0 置信度
private Instant preferenceUpdatedAt;
/** 专业水平: beginner / intermediate / expert */
private String expertiseLevel;
/** 职责范围: "A栋办公楼运维" */
private String responsibilityScope;
/** 关注指标及计数: {"cost": 12, "safety": 5} */
@Column(columnDefinition = "json")
private Map<String, Integer> focusMetricCounts;
/** 兴趣主题及权重: {"数据中心能耗": 8, "空调系统": 3} */
@Column(columnDefinition = "json")
private Map<String, Integer> interestWeights;
/** 画像最后活跃时间 */
private Instant lastActiveAt;
/**
* 生成注入 Prompt 的画像描述文本
*
* 这个方法决定了画像信息如何"喂给"大模型。
* 描述要精炼,控制在 200 字以内,避免占用过多上下文窗口。
*/
public String toPromptDescription() {
StringBuilder sb = new StringBuilder("当前用户画像:\n");
if (replyPreference != null) {
sb.append(String.format("- 偏好%s风格的回复\n",
replyPreference));
}
if (expertiseLevel != null) {
sb.append(String.format("- 专业水平: %s\n",
switch (expertiseLevel) {
case "beginner" -> "入门,请用通俗语言";
case "intermediate" -> "中等,可使用专业术语";
case "expert" -> "专家,可使用深度技术内容";
default -> "未知";
}));
}
if (responsibilityScope != null) {
sb.append(String.format("- 负责范围: %s\n",
responsibilityScope));
}
// 只取 Top 3 关注指标
if (focusMetricCounts != null && !focusMetricCounts.isEmpty()) {
String topMetrics = focusMetricCounts.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue()
.reversed())
.limit(3)
.map(e -> metricLabel(e.getKey()))
.collect(Collectors.joining("、"));
sb.append(String.format("- 重点关注: %s\n", topMetrics));
}
// 只取 Top 3 兴趣主题
if (interestWeights != null && !interestWeights.isEmpty()) {
String topInterests = interestWeights.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue()
.reversed())
.limit(3)
.map(Map.Entry::getKey)
.collect(Collectors.joining("、"));
sb.append(String.format("- 持续关注: %s\n", topInterests));
}
return sb.toString();
}
}
四、三层记忆的统一注入
4.1 记忆编排器:决定"什么时候注入什么"
/**
* 统一记忆编排器
*
* 职责:在每次对话前,协调三层记忆的检索与注入
*
* 注入策略:
* 1. 工作记忆:始终注入(由 ChatMemory 自动管理)
* 2. 用户画像:始终注入(200字以内,开销极低)
* 3. 情景记忆:按需检索,相关性 > 0.65 时才注入
*
* Token 预算控制:
* 记忆注入总量不超过 800 tokens,避免挤压对话空间
*/
@Service
public class MemoryOrchestrator {
private final UserProfileService profileService;
private final EpisodicMemoryService episodicService;
private static final int MAX_EPISODIC_RESULTS = 3;
private static final double MIN_RELEVANCE = 0.65;
/**
* 构建增强型 System Prompt
*
* 将用户画像 + 相关历史记忆注入到系统提示词中
*/
public String buildEnhancedSystemPrompt(String userId,
String userQuery,
String basePrompt) {
StringBuilder enhanced = new StringBuilder(basePrompt);
// ===== 注入用户画像(语义记忆)=====
UserProfile profile = profileService.getProfile(userId);
if (profile != null) {
enhanced.append("\n\n");
enhanced.append(profile.toPromptDescription());
}
// ===== 注入相关历史(情景记忆)=====
List<ConversationSummary> memories = episodicService.search(
userQuery, userId, MAX_EPISODIC_RESULTS);
if (!memories.isEmpty()) {
enhanced.append("\n\n相关历史对话记忆:\n");
for (ConversationSummary memory : memories) {
enhanced.append(String.format(
"- [%s] %s(结果: %s)\n",
memory.getTimestamp().toLocalDate(),
memory.getTopic(),
memory.getOutcome()
));
// 只注入关键事实,不注入完整摘要
if (!memory.getKeyFacts().isEmpty()) {
enhanced.append(String.format(" 关键信息: %s\n",
String.join("; ", memory.getKeyFacts())));
}
}
}
return enhanced.toString();
}
/**
* 构建动态 System Prompt 的 Agent 接口
*
* 通过 @V 注解注入动态变量,实现每次对话的个性化提示词
*/
public interface PersonalizedAgent {
@SystemMessage("{{systemPrompt}}")
String chat(@UserMessage String userMessage,
@V("systemPrompt") String systemPrompt);
}
}
4.2 会话生命周期钩子
/**
* 会话生命周期管理器
*
* 在关键节点触发记忆操作:
* - 会话开始:加载用户画像
* - 每轮对话:更新工作记忆
* - 会话结束:生成情景记忆摘要 + 更新用户画像
*/
@Component
public class SessionLifecycleManager {
private final ConversationSummaryService summaryService;
private final UserProfileService profileService;
private final ChatMemoryStore memoryStore;
/**
* 会话结束回调
*
* 这是一个异步操作,不阻塞用户响应
*/
@Async
@EventListener
public void onSessionEnd(SessionEndEvent event) {
String sessionId = event.getSessionId();
String userId = event.getUserId();
log.info("会话结束,开始记忆沉淀: session={}", sessionId);
// 1. 获取完整对话历史
List<ChatMessage> messages = memoryStore.getMessages(sessionId);
if (messages.size() < 4) {
// 对话太短(少于2轮),没有记忆价值
log.debug("对话过短,跳过记忆沉淀: {} 条消息", messages.size());
return;
}
// 2. 生成情景记忆(对话摘要)
ConversationSummary summary =
summaryService.summarize(sessionId, userId, messages);
episodicMemoryService.save(summary);
// 3. 更新用户画像(语义记忆)
profileService.updateFromConversation(userId, messages);
log.info("记忆沉淀完成: session={}, topic={}",
sessionId, summary.getTopic());
}
}
五、数据库设计
-- 用户画像主表
CREATE TABLE agent_user_profile (
user_id VARCHAR(64) PRIMARY KEY,
reply_preference VARCHAR(32),
preference_confidence DOUBLE DEFAULT 0.0,
preference_updated_at TIMESTAMP,
expertise_level VARCHAR(32),
responsibility_scope VARCHAR(256),
focus_metric_counts JSON, -- {"cost": 12, "safety": 5}
interest_weights JSON, -- {"数据中心能耗": 8}
last_active_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 对话摘要表(情景记忆)
CREATE TABLE agent_conversation_summary (
session_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
topic VARCHAR(512) NOT NULL,
key_facts JSON, -- ["事实1", "事实2"]
actions JSON, -- [{"action":"...", "result":"..."}]
entities JSON, -- {"people":[], "areas":[], ...}
unresolved JSON, -- ["未解决问题"]
outcome VARCHAR(32), -- success/pending/failed
original_msg_count INT,
summary_text TEXT, -- 摘要全文(用于向量化)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
);
3826



被折叠的 条评论
为什么被折叠?



