文章目录
- [1. 实现方案](#1. 实现方案)
- [2. 记忆存储](#2. 记忆存储)
-
- [2.1 存储内容](#2.1 存储内容)
- [2.2 StoreItem](#2.2 StoreItem)
- [2.3 用户画像摘要](#2.3 用户画像摘要)
- [2.4 结构化事实](#2.4 结构化事实)
- [2.5 长期记忆数据](#2.5 长期记忆数据)
- [3. 记忆更新](#3. 记忆更新)
-
- [3.1 消息过滤器](#3.1 消息过滤器)
- [3.2 信号检测器](#3.2 信号检测器)
- [3.3 LLM 响应解析结果](#3.3 LLM 响应解析结果)
- [3.4 Prompt 构建器](#3.4 Prompt 构建器)
- [3.5 防抖实现](#3.5 防抖实现)
- [3.6 记忆更新器](#3.6 记忆更新器)
- [4. 记忆处理节点](#4. 记忆处理节点)
-
- [4.1 记忆准备节点](#4.1 记忆准备节点)
- [4.2 长期记忆更新入队节点](#4.2 长期记忆更新入队节点)
- [5. 项目集成](#5. 项目集成)
-
- [5.1 长期记忆配置类](#5.1 长期记忆配置类)
- [5.2 构建状态图](#5.2 构建状态图)
- [5.3 测试](#5.3 测试)
1. 实现方案
长期记忆不是简单的聊天记录,而是结构化的事实 与上下文摘要 存储,能够在不同会话间持久化,并影响智能体在未来对话中的行为。
极简落地链路(工程视角):
- 用户发消息 → 存入短期记忆
- 轮次结束 →
LLM抽取信息 →Record写入长期记忆 - 下一轮新请求到来 → 检索长期记忆 →
Retrieve注入短期上下文 - 短期记忆 (当前对话) + 召回记忆送入模型 → 生成回复,循环往复
这里,我们参考 DeerFlow 相关实现,记忆由记忆处理节点自行管理,在智能体每一轮执行时运行:
- 注入 :每次对话开始时,将当前记忆按
Token预算注入系统提示词 - 学习:对话结束后,后台提取新事实并更新对应记忆分类
- 防抖合并:短时间内的多次更新会合并处理,避免频繁写入
演示案例在之前 Graph 短期记忆实现的基础上进行改造:

2. 记忆存储
最终的 JOSN 内容如下:
json
{
"namespace": [
"longmemory",
"user_123456"
],
"key": "memory_123456",
"value": {
"user_id": "user_123456",
"updated_at": "2026-05-29T09:23:23.207253Z",
"summary": {
"identity": "姓名为张三"
},
"facts": [
{
"content": "用户姓名是张三",
"confidence": 0.95,
"source": "stated",
"category": "identity",
"recorded_at": "2026-05-29T09:17:35.406329700Z"
}
]
},
"createdAt": 1780046603208,
"updatedAt": 1780046603208
}
2.1 存储内容
记忆库保存以下几类信息,这些内容会随着对话持续更新:
- 工作上下文:用户正在进行的项目、目标和常用主题的摘要
- 个人上下文:用户偏好、沟通风格等个性化信息
- 当前重点:最近关注的领域和活跃任务
- 历史记录:数月内的上下文、长期背景信息
- 事实信息:从对话中提取的明确事实(如常用工具、团队名称、项目约束)
2.2 StoreItem
StoreItem 是 Spring AI Alibaba 框架中用于长期记忆存储的核心数据模型类,作用是定义存储在持久化 / 内存存储中的结构化记忆数据项,支持分层命名空间、唯一键、值存储和时间戳管理。
每个字段的真实含义:
| 字段 | 数据类型 | 长期记忆中的作用 |
|---|---|---|
| namespace | List<String> |
记忆的分类 / 目录 例:"user", "zhangsan", "preference"(用户张三的偏好记忆) |
| key | String |
记忆的唯一名称 例:"eating_habits"(饮食习惯) |
| value | Map<String, Object> |
记忆的具体内容,结构化 Map,存任意记忆数据 |
| createdAt | long |
记忆第一次形成的时间 |
| updatedAt | long |
记忆被修改 / 强化的最新时间 |
一条【用户长期偏好】记忆:
java
StoreItem userPreference = StoreItem.of(
List.of("user", "zhangsan", "preference"), // 记忆目录
"eating_habits", // 记忆唯一键
Map.of(
"like_food", "spicy", // 喜欢的水果
"allergic", "peanut",
"frequency", "eat_at_home"
)
);
2.3 用户画像摘要
用户画像摘要,包含 6 个维度,每个维度都是自由文本,由 LLM 在对话中逐轮提取和合并:
java
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MemorySummary {
/** 用户身份背景:姓名、角色、经验等 */
@JsonProperty("identity")
private String identity;
/** 当前工作内容:正在做的项目、任务等 */
@JsonProperty("current_work")
private String currentWork;
/** 技术栈:编程语言、框架、工具等 */
@JsonProperty("tech_stack")
private String techStack;
/** 编码偏好:代码风格、命名规范、架构偏好等 */
@JsonProperty("code_preferences")
private String codePreferences;
/** 沟通风格:语言、语气、回复详细程度等 */
@JsonProperty("communication_style")
private String communicationStyle;
/** 项目上下文:当前项目的背景、目标、约束等 */
@JsonProperty("project_context")
private String projectContext;
public String getIdentity() { return identity; }
public void setIdentity(String identity) { this.identity = identity; }
public String getCurrentWork() { return currentWork; }
public void setCurrentWork(String currentWork) { this.currentWork = currentWork; }
public String getTechStack() { return techStack; }
public void setTechStack(String techStack) { this.techStack = techStack; }
public String getCodePreferences() { return codePreferences; }
public void setCodePreferences(String codePreferences) { this.codePreferences = codePreferences; }
public String getCommunicationStyle() { return communicationStyle; }
public void setCommunicationStyle(String communicationStyle) { this.communicationStyle = communicationStyle; }
public String getProjectContext() { return projectContext; }
public void setProjectContext(String projectContext) { this.projectContext = projectContext; }
public Map<String, String> toMap() {
Map<String, String> map = new LinkedHashMap<>();
if (identity != null && !identity.isEmpty()) map.put("identity", identity);
if (currentWork != null && !currentWork.isEmpty()) map.put("current_work", currentWork);
if (techStack != null && !techStack.isEmpty()) map.put("tech_stack", techStack);
if (codePreferences != null && !codePreferences.isEmpty()) map.put("code_preferences", codePreferences);
if (communicationStyle != null && !communicationStyle.isEmpty()) map.put("communication_style", communicationStyle);
if (projectContext != null && !projectContext.isEmpty()) map.put("project_context", projectContext);
return map;
}
@SuppressWarnings("unchecked")
public static MemorySummary fromMap(Map<String, Object> map) {
if (map == null) return new MemorySummary();
MemorySummary s = new MemorySummary();
s.identity = (String) map.get("identity");
s.currentWork = (String) map.get("current_work");
s.techStack = (String) map.get("tech_stack");
s.codePreferences = (String) map.get("code_preferences");
s.communicationStyle = (String) map.get("communication_style");
s.projectContext = (String) map.get("project_context");
return s;
}
/** 合并单个分区,仅当新值非空时才覆盖 */
public void mergeSection(String section, String value) {
if (value == null || value.isBlank()) return;
switch (section) {
case "identity": identity = value; break;
case "current_work": currentWork = value; break;
case "tech_stack": techStack = value; break;
case "code_preferences": codePreferences = value; break;
case "communication_style": communicationStyle = value; break;
case "project_context": projectContext = value; break;
}
}
public void forEachNonEmpty(BiConsumer<String, String> consumer) {
if (identity != null && !identity.isBlank()) consumer.accept("identity", identity);
if (currentWork != null && !currentWork.isBlank()) consumer.accept("current_work", currentWork);
if (techStack != null && !techStack.isBlank()) consumer.accept("tech_stack", techStack);
if (codePreferences != null && !codePreferences.isBlank()) consumer.accept("code_preferences", codePreferences);
if (communicationStyle != null && !communicationStyle.isBlank()) consumer.accept("communication_style", communicationStyle);
if (projectContext != null && !projectContext.isBlank()) consumer.accept("project_context", projectContext);
}
public boolean isEmpty() {
return (identity == null || identity.isBlank())
&& (currentWork == null || currentWork.isBlank())
&& (techStack == null || techStack.isBlank())
&& (codePreferences == null || codePreferences.isBlank())
&& (communicationStyle == null || communicationStyle.isBlank())
&& (projectContext == null || projectContext.isBlank());
}
}
2.4 结构化事实
单条结构化事实。按置信度排序,按 content casefold 去重:
java
public class MemoryFact implements Comparable<MemoryFact> {
/** 事实内容:关于用户的明确陈述 */
@JsonProperty("content")
private String content;
/** 置信度 0-1:stated(用户明确说出)=0.9+,inferred(推断)=0.6-0.8 */
@JsonProperty("confidence")
private double confidence;
/** 来源类型:stated / inferred / corrected */
@JsonProperty("source")
private String source;
/** 所属分区:对应 MemorySummary 的 6 个维度之一 */
@JsonProperty("category")
private String category;
/** 记录时间 */
@JsonProperty("recorded_at")
private Instant recordedAt;
public MemoryFact() {
this.recordedAt = Instant.now();
}
public MemoryFact(String content, double confidence, String source, String category) {
this.content = content;
this.confidence = confidence;
this.source = source;
this.category = category;
this.recordedAt = Instant.now();
}
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public double getConfidence() { return confidence; }
public void setConfidence(double confidence) { this.confidence = confidence; }
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public Instant getRecordedAt() { return recordedAt; }
public void setRecordedAt(Instant recordedAt) { this.recordedAt = recordedAt; }
/** 去重键 = content 忽略大小写,用于判断两条事实是否重复 */
@JsonIgnore
public String dedupKey() {
return content != null ? content.toLowerCase(Locale.ROOT).trim() : "";
}
@Override
public int compareTo(MemoryFact other) {
return Double.compare(other.confidence, this.confidence);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MemoryFact that)) return false;
return dedupKey().equals(that.dedupKey());
}
@Override
public int hashCode() {
return dedupKey().hashCode();
}
public Map<String, Object> toMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("content", content);
map.put("confidence", confidence);
map.put("source", source);
map.put("category", category);
map.put("recorded_at", recordedAt != null ? recordedAt.toString() : Instant.now().toString());
return map;
}
@SuppressWarnings("unchecked")
public static MemoryFact fromMap(Map<String, Object> map) {
MemoryFact f = new MemoryFact();
f.content = (String) map.get("content");
Object conf = map.get("confidence");
f.confidence = conf instanceof Number ? ((Number) conf).doubleValue() : 0.5;
f.source = (String) map.getOrDefault("source", "inferred");
f.category = (String) map.getOrDefault("category", "tech_stack");
String recorded = (String) map.get("recorded_at");
f.recordedAt = recorded != null ? Instant.parse(recorded) : Instant.now();
return f;
}
}
2.5 长期记忆数据
长期记忆核心数据容器。包含用户画像摘要(6 个分区)和结构化事实列表,支持与 Store 接口互转(toMap / fromMap):
java
@JsonIgnoreProperties(ignoreUnknown = true)
public class LongMemory {
/** 用户标识,对应 Store namespace 第二层 */
@JsonProperty("user_id")
private String userId;
/** 最后更新时间 */
@JsonProperty("updated_at")
private Instant updatedAt;
/** 用户画像摘要(6 个维度) */
@JsonProperty("summary")
private MemorySummary summary;
/** 结构化事实列表,按置信度降序排列 */
@JsonProperty("facts")
private List<MemoryFact> facts;
public LongMemory() {
this.summary = new MemorySummary();
this.facts = new ArrayList<>();
this.updatedAt = Instant.now();
}
public static LongMemory createDefault(String userId) {
LongMemory memory = new LongMemory();
memory.userId = userId;
return memory;
}
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
public MemorySummary getSummary() { return summary; }
public void setSummary(MemorySummary summary) { this.summary = summary; }
public List<MemoryFact> getFacts() { return facts; }
public void setFacts(List<MemoryFact> facts) { this.facts = facts; }
/** 将 LLM 提取结果合并到当前记忆 */
@SuppressWarnings("unchecked")
public void applyUpdate(MemoryUpdateResult update, int maxFacts) {
if (update.getSummaryUpdates() != null) {
for (Map.Entry<String, String> entry : update.getSummaryUpdates().entrySet()) {
summary.mergeSection(entry.getKey(), entry.getValue());
}
}
if (update.getFactsToRemove() != null && !update.getFactsToRemove().isEmpty()) {
Set<String> toRemove = new HashSet<>(update.getFactsToRemove());
facts.removeIf(f -> toRemove.contains(f.dedupKey()));
}
if (update.getFactsToReplace() != null && !update.getFactsToReplace().isEmpty()) {
Set<String> toReplace = new HashSet<>(update.getFactsToReplace());
facts.removeIf(f -> toReplace.contains(f.dedupKey()));
}
if (update.getFactsToAdd() != null && !update.getFactsToAdd().isEmpty()) {
Set<String> existing = facts.stream().map(MemoryFact::dedupKey).collect(Collectors.toSet());
for (MemoryFact fact : update.getFactsToAdd()) {
if (!existing.contains(fact.dedupKey())) {
facts.add(fact);
existing.add(fact.dedupKey());
}
}
}
facts.sort(MemoryFact::compareTo);
if (facts.size() > maxFacts) {
facts = new ArrayList<>(facts.subList(0, maxFacts));
}
stripUploadMentions();
updatedAt = Instant.now();
}
/** 清除文件上传等会话级临时信息的痕迹 */
public void stripUploadMentions() {
if (facts != null) {
facts.removeIf(f -> f.getContent() != null &&
(f.getContent().contains("uploaded") || f.getContent().contains("上传了文件")));
}
if (summary != null) {
String[] patterns = {"uploaded", "上传了文件", "file upload"};
summary.forEachNonEmpty((key, value) -> {
for (String p : patterns) {
if (value.toLowerCase().contains(p)) {
summary.mergeSection(key, "");
break;
}
}
});
}
}
public boolean isEmpty() {
return summary.isEmpty() && (facts == null || facts.isEmpty());
}
/** 转为 Map 用于写入 Store */
public Map<String, Object> toMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("user_id", userId);
map.put("updated_at", updatedAt != null ? updatedAt.toString() : Instant.now().toString());
map.put("summary", summary.toMap());
map.put("facts", facts != null ? facts.stream().map(MemoryFact::toMap).collect(Collectors.toList()) : List.of());
return map;
}
/** 从 Store 读取的 Map 还原 */
@SuppressWarnings("unchecked")
public static LongMemory fromMap(Map<String, Object> map) {
if (map == null) return new LongMemory();
LongMemory m = new LongMemory();
m.userId = (String) map.get("user_id");
String updated = (String) map.get("updated_at");
m.updatedAt = updated != null ? Instant.parse(updated) : Instant.now();
m.summary = MemorySummary.fromMap((Map<String, Object>) map.get("summary"));
List<Map<String, Object>> factMaps = (List<Map<String, Object>>) map.get("facts");
if (factMaps != null) {
m.facts = factMaps.stream().map(MemoryFact::fromMap).collect(Collectors.toList());
}
return m;
}
}
3. 记忆更新
3.1 消息过滤器
只保留 UserMessage + 最后一条 AssistantMessage,超长截断,减少送入 LLM 提取器的上下文大小,降低 token 消耗。
java
public class MessageFilter {
private final int maxMessageChars;
private final boolean filterSystemMessages;
public MessageFilter(int maxMessageChars, boolean filterSystemMessages) {
this.maxMessageChars = maxMessageChars;
this.filterSystemMessages = filterSystemMessages;
}
public List<Message> filterForMemory(List<Message> messages) {
List<Message> filtered = new ArrayList<>();
AssistantMessage lastAssistant = null;
for (Message msg : messages) {
if (filterSystemMessages &&
!(msg instanceof UserMessage) &&
!(msg instanceof AssistantMessage)) {
continue;
}
if (msg instanceof UserMessage) {
filtered.add(truncate(msg));
} else if (msg instanceof AssistantMessage) {
lastAssistant = (AssistantMessage) msg;
}
}
if (lastAssistant != null) {
filtered.add(truncate(lastAssistant));
}
return filtered;
}
private Message truncate(Message msg) {
String text = msg.getText();
if (text != null && text.length() > maxMessageChars) {
String truncated = text.substring(0, maxMessageChars) + "[...truncated]";
if (msg instanceof UserMessage) {
return new UserMessage(truncated);
} else if (msg instanceof AssistantMessage) {
return new AssistantMessage(truncated);
}
}
return msg;
}
}
3.2 信号检测器
基于正则表达式的信号检测器(零 LLM 成本),检测用户纠正信号和肯定信号,引导 LLM 提取器提高记忆更新精度。
java
public class SignalDetector {
private static final Pattern[] CORRECTION_PATTERNS = {
Pattern.compile("不对[,,。.]?我?(其实|实际|原本)"),
Pattern.compile("纠正[一下]?[::]"),
Pattern.compile("更正[一下]?[::]"),
Pattern.compile("(不是|不对|错了)[,,].*应该"),
Pattern.compile("(?i)(no[,.\\s]?.*(actually|i mean)|correction[\\s:])"),
Pattern.compile("(?i)(that'?s not (right|correct|what i))"),
Pattern.compile("(?i)(i actually|let me correct)"),
Pattern.compile("说错了[,,]"),
Pattern.compile("搞错了[,,]"),
Pattern.compile("不是这样的[,,]")
};
private static final Pattern[] REINFORCEMENT_PATTERNS = {
Pattern.compile("(?i)(yes[,.\\s]?(that'?s? (right|correct|exactly|true)|exactly|indeed))"),
Pattern.compile("没错[,,。.!!]"),
Pattern.compile("(?i)(exactly[!]?|precisely[!]?)"),
Pattern.compile("正是[这样如此]"),
Pattern.compile("对的?[,,。.]"),
Pattern.compile("(?i)(good[,.]?\\s*(but|and|additionally|also))"),
Pattern.compile("(?i)(i agree[,.]?\\s*(but|and|additionally|also))"),
Pattern.compile("(?i)(right[,.]?\\s*(but|and|also|additionally))"),
Pattern.compile("说(得|的)对[,,。.!!]"),
Pattern.compile("正确[,,。.!!]"),
};
public List<String> detectCorrection(String userMessage) {
if (userMessage == null) return List.of();
List<String> signals = new ArrayList<>();
for (Pattern p : CORRECTION_PATTERNS) {
if (p.matcher(userMessage).find()) {
signals.add(userMessage);
break;
}
}
return signals;
}
public List<String> detectReinforcement(String userMessage) {
if (userMessage == null) return List.of();
List<String> signals = new ArrayList<>();
for (Pattern p : REINFORCEMENT_PATTERNS) {
if (p.matcher(userMessage).find()) {
signals.add(userMessage);
break;
}
}
return signals;
}
}
3.3 LLM 响应解析结果
LLM 响应解析结果,包含摘要更新和事实的增删改。
java
public class MemoryUpdateResult {
private Map<String, String> summaryUpdates;
private List<MemoryFact> factsToAdd;
private List<String> factsToReplace;
private List<String> factsToRemove;
public Map<String, String> getSummaryUpdates() { return summaryUpdates; }
public void setSummaryUpdates(Map<String, String> summaryUpdates) { this.summaryUpdates = summaryUpdates; }
public List<MemoryFact> getFactsToAdd() { return factsToAdd; }
public void setFactsToAdd(List<MemoryFact> factsToAdd) { this.factsToAdd = factsToAdd; }
public List<String> getFactsToReplace() { return factsToReplace; }
public void setFactsToReplace(List<String> factsToReplace) { this.factsToReplace = factsToReplace; }
public List<String> getFactsToRemove() { return factsToRemove; }
public void setFactsToRemove(List<String> factsToRemove) { this.factsToRemove = factsToRemove; }
}
3.4 Prompt 构建器
将当前记忆 JSON + 过滤后对话 + 纠正/肯定信号提示拼装成提取 Prompt 。
java
public class UpdatePromptBuilder {
private static final Logger logger = LoggerFactory.getLogger(UpdatePromptBuilder.class);
private final ObjectMapper objectMapper;
private final boolean signalsEnabled;
private static final String DEFAULT_PROMPT = """
你是一个长期记忆管理器。请根据以下对话更新用户画像。
## 当前记忆
```json
{current_memory}
```
## 最近的对话
{conversation}
## 指令
1. 更新以下 6 个摘要分区(identity 身份背景、current_work 当前工作、tech_stack 技术栈、code_preferences 编码偏好、communication_style 沟通风格、project_context 项目背景)。只有当对话中明确提到或强烈暗示了新信息时才更新,未涉及的分区保持为 null。
2. 提取事实:
- content:关于用户的明确事实陈述
- confidence:置信度 0.0-1.0(用户明确说出的用 0.9+,推论得出的用 0.6-0.8)
- source:"stated"(用户明确说出)、"inferred"(推断得出)、"corrected"(用户纠正了之前的信息)
- category:该事实所属的摘要分区
3. 只添加新事实或替换过时的事实,不要重复已有事实。
4. 所有内容使用中文。
{signal_hints}
## 响应格式
只返回合法的 JSON(不要 markdown 代码块,不要多余文字):
{"summary_updates": {"identity": "新内容或null", ...}, "facts_to_add": [{"content": "事实内容", "confidence": 0.9, "source": "stated", "category": "tech_stack"}], "facts_to_replace": ["要替换的事实的去重key"], "facts_to_remove": ["要删除的事实的去重key"]}
""";
public UpdatePromptBuilder(boolean signalsEnabled) {
this.objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
this.signalsEnabled = signalsEnabled;
}
public String buildPrompt(LongMemory currentMemory, List<Message> messages,
List<String> correctionSignals, List<String> reinforcementSignals) {
String currentMemoryJson;
try {
currentMemoryJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(currentMemory);
} catch (Exception e) {
currentMemoryJson = "{}";
}
StringBuilder conversation = new StringBuilder();
for (Message msg : messages) {
String role = msg.getMessageType().getValue();
String text = msg.getText();
if (text != null && !text.isBlank()) {
conversation.append("[").append(role).append("]: ").append(text).append("\n\n");
}
}
String signalHints = "";
if (signalsEnabled) {
if (correctionSignals != null && !correctionSignals.isEmpty()) {
signalHints = """
**重要**:用户在本次对话中纠正了之前的信息。
请更新记忆以反映纠正后的信息,并删除或更新与纠正内容冲突的事实。
""";
} else if (reinforcementSignals != null && !reinforcementSignals.isEmpty()) {
signalHints = """
**注意**:用户在本次对话中肯定或强化了之前的信息。
可以适当提高相关事实的置信度,但不要修改已有的准确信息。
""";
}
}
return DEFAULT_PROMPT
.replace("{current_memory}", currentMemoryJson)
.replace("{conversation}", conversation.toString())
.replace("{signal_hints}", signalHints);
}
}
3.5 防抖实现
同一 Thread 内,用户可能在 30 秒内连续发多轮消息,如果不防抖,每一轮都立即调用 LLM 提取记忆,会产生三个问题:
- 重复调用浪费 :
3轮对话调用3次记忆LLM,但合并成一次就够了 - 信息不完整 :第
1轮只看到1条对话,第2轮只看到2条,不如把3轮攒一起提取 - LLM 频率限制 :短时间内多次调用可能触发
API限流
设计一个队列条目,包含过滤后的对话消息和检测到的纠正/肯定信号,同一 key = userId 的旧条目会被覆盖以实现防抖批处理。
java
public record MemoryQueueEntry(
String userId,
String threadId,
List<Message> messages,
List<String> correctionSignals,
List<String> reinforcementSignals,
Instant queuedAt
) {
public String key() { return userId; }
}
设计一个异步防抖队列,同一 userId 的对话会覆盖旧条目并重置倒计时,计时器到期后调用 MemoryUpdater 批量提取记忆。
java
public class MemoryUpdateQueue {
private static final Logger logger = LoggerFactory.getLogger(MemoryUpdateQueue.class);
private final ConcurrentHashMap<String, MemoryQueueEntry> entries;
private final ConcurrentHashMap<String, ScheduledFuture<?>> timers;
private final ScheduledExecutorService scheduler;
private final MemoryUpdater memoryUpdater;
private final Duration debounceDuration;
public MemoryUpdateQueue(MemoryUpdater memoryUpdater, Duration debounceDuration, int threadPoolSize) {
this.memoryUpdater = memoryUpdater;
this.debounceDuration = debounceDuration;
this.entries = new ConcurrentHashMap<>();
this.timers = new ConcurrentHashMap<>();
this.scheduler = Executors.newScheduledThreadPool(threadPoolSize, r -> {
Thread t = new Thread(r, "longmemory-updater");
t.setDaemon(true);
return t;
});
}
/**
* 入队。同一 key 覆盖旧条目,取消旧计时器并重新开始防抖倒计时。
*/
public CompletableFuture<Void> enqueue(MemoryQueueEntry entry) {
entries.put(entry.key(), entry);
ScheduledFuture<?> existing = timers.get(entry.key());
if (existing != null && !existing.isDone()) {
existing.cancel(false); // 取消旧计时器,实现防抖
}
ScheduledFuture<?> timer = scheduler.schedule(
() -> processQueue(entry.key()),
debounceDuration.toMillis(),
TimeUnit.MILLISECONDS
);
timers.put(entry.key(), timer);
return CompletableFuture.completedFuture(null);
}
/** 计时器到期回调:取出条目,调用 LLM 提取记忆,异常不影响主流程 */
private void processQueue(String key) {
MemoryQueueEntry entry = entries.remove(key);
timers.remove(key);
if (entry == null) return;
try {
memoryUpdater.update(entry);
} catch (Exception e) {
logger.warn("记忆更新失败 key={}: {}", key, e.getMessage());
}
}
@PreDestroy
public void shutdown() {
logger.info("正在关闭 MemoryUpdateQueue...");
timers.values().forEach(f -> f.cancel(false));
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
3.6 记忆更新器
LLM 驱动的记忆提取的核心编排器,用 Store 接口持久化,运行在队列线程池中。所有异常被静默捕获,绝不传播到主对话流程。
java
public class MemoryUpdater {
private static final Logger logger = LoggerFactory.getLogger(MemoryUpdater.class);
/** Store 命名空间前缀,key 固定为 "memory" */
private static final List<String> NAMESPACE_PREFIX = List.of("longmemory");
private static final String STORE_KEY = "memory";
private final ChatModel chatModel;
private final Store store;
private final UpdatePromptBuilder promptBuilder;
private final ObjectMapper objectMapper;
private final int maxFacts;
private final MessageFilter messageFilter;
private final SignalDetector signalDetector;
private final boolean signalsEnabled;
public MemoryUpdater(ChatModel chatModel, Store store,
UpdatePromptBuilder promptBuilder,
MessageFilter messageFilter, SignalDetector signalDetector,
int maxFacts, boolean signalsEnabled) {
this.chatModel = chatModel;
this.store = store;
this.promptBuilder = promptBuilder;
this.messageFilter = messageFilter;
this.signalDetector = signalDetector;
this.maxFacts = maxFacts;
this.signalsEnabled = signalsEnabled;
this.objectMapper = new ObjectMapper();
}
/**
* 对单个队列条目执行完整更新流程(同步,运行在队列线程中)。
*/
public void update(MemoryQueueEntry entry) {
try {
// 1. 从 Store 加载当前记忆
LongMemory current = loadFromStore(entry.userId());
// 2. 过滤消息
List<Message> filtered = messageFilter.filterForMemory(entry.messages());
// 3. 检测纠正/肯定信号
List<String> correctionSignals = new ArrayList<>(entry.correctionSignals());
List<String> reinforcementSignals = new ArrayList<>(entry.reinforcementSignals());
if (signalsEnabled && correctionSignals.isEmpty() && reinforcementSignals.isEmpty()) {
for (Message msg : filtered) {
if (msg instanceof UserMessage) {
correctionSignals.addAll(signalDetector.detectCorrection(msg.getText()));
reinforcementSignals.addAll(signalDetector.detectReinforcement(msg.getText()));
}
}
}
// 4. 构建 Prompt
String prompt = promptBuilder.buildPrompt(current, filtered, correctionSignals, reinforcementSignals);
// 5. 调用 LLM
String response = chatModel.call(new Prompt(new UserMessage(prompt)))
.getResult().getOutput().getText();
// 6. 解析 JSON 响应
MemoryUpdateResult updateResult = parseResponse(response);
// 7. 应用更新
if (updateResult != null) {
current.applyUpdate(updateResult, maxFacts);
}
// 8. 写入 Store
saveToStore(current);
logger.debug("Memory updated for {}", entry.userId());
} catch (Exception e) {
logger.warn("Memory update failed for {}: {}", entry.userId(), e.getMessage());
}
}
/** 从 Store 加载 LongMemory */
public LongMemory loadFromStore(String userId) {
List<String> namespace = new ArrayList<>(NAMESPACE_PREFIX);
namespace.add(userId);
Optional<StoreItem> item = store.getItem(namespace, STORE_KEY);
if (item.isPresent()) {
Map<String, Object> value = item.get().getValue();
if (value != null) {
return LongMemory.fromMap(value);
}
}
return LongMemory.createDefault(userId);
}
/** 写入 Store */
public void saveToStore(LongMemory memory) {
List<String> namespace = new ArrayList<>(NAMESPACE_PREFIX);
namespace.add(memory.getUserId());
StoreItem item = StoreItem.of(namespace, STORE_KEY, memory.toMap());
store.putItem(item);
}
/** 从 LLM 原始响应中解析 JSON */
private MemoryUpdateResult parseResponse(String response) {
try {
String json = extractJson(response);
if (json == null) return null;
Map<String, Object> raw = objectMapper.readValue(json,
new TypeReference<Map<String, Object>>() {});
MemoryUpdateResult result = new MemoryUpdateResult();
// 解析摘要更新
@SuppressWarnings("unchecked")
Map<String, String> summaryUpdates = (Map<String, String>) raw.get("summary_updates");
if (summaryUpdates != null) {
Map<String, String> cleaned = new HashMap<>();
for (Map.Entry<String, String> e : summaryUpdates.entrySet()) {
if (e.getValue() != null && !e.getValue().isBlank() && !"null".equalsIgnoreCase(e.getValue())) {
cleaned.put(e.getKey(), e.getValue());
}
}
result.setSummaryUpdates(cleaned);
}
// 解析新增事实
@SuppressWarnings("unchecked")
List<Map<String, Object>> factsRaw = (List<Map<String, Object>>) raw.get("facts_to_add");
if (factsRaw != null) {
List<MemoryFact> facts = new ArrayList<>();
for (Map<String, Object> f : factsRaw) {
MemoryFact fact = new MemoryFact();
fact.setContent((String) f.get("content"));
Object conf = f.get("confidence");
fact.setConfidence(conf instanceof Number ? ((Number) conf).doubleValue() : 0.5);
fact.setSource((String) f.getOrDefault("source", "inferred"));
fact.setCategory((String) f.getOrDefault("category", "tech_stack"));
facts.add(fact);
}
result.setFactsToAdd(facts);
}
// 解析替换/删除
@SuppressWarnings("unchecked")
List<String> factsToReplace = (List<String>) raw.get("facts_to_replace");
@SuppressWarnings("unchecked")
List<String> factsToRemove = (List<String>) raw.get("facts_to_remove");
result.setFactsToReplace(factsToReplace != null ? factsToReplace : List.of());
result.setFactsToRemove(factsToRemove != null ? factsToRemove : List.of());
return result;
} catch (Exception e) {
logger.warn("Failed to parse LLM response: {}", e.getMessage());
return null;
}
}
/** 从响应文本中提取 JSON 对象 */
private String extractJson(String response) {
if (response == null) return null;
String trimmed = response.trim();
int start = trimmed.indexOf('{');
int end = trimmed.lastIndexOf('}');
if (start >= 0 && end > start) {
return trimmed.substring(start, end + 1);
}
return null;
}
}
4. 记忆处理节点
4.1 记忆准备节点
记忆准备节点放在 START 之后第一个,负责加载长期记忆写入 long_memory_context + 将 query 写入短期记忆。
java
public class MemoryPrepareNode implements NodeAction {
private static final Logger logger = LoggerFactory.getLogger(MemoryPrepareNode.class);
private final MemoryUpdater memoryUpdater;
private final MessageWindowChatMemory chatMemory;
public MemoryPrepareNode(MemoryUpdater memoryUpdater, MessageWindowChatMemory chatMemory) {
this.memoryUpdater = memoryUpdater;
this.chatMemory = chatMemory;
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
String threadId = state.value(THREAD_ID, "default").toString();
String userId = state.value("user_id", "default").toString();
String query = state.value(QUERY, "").toString();
// 1. 写入短期记忆
if (query != null && !query.isBlank()) {
chatMemory.add(threadId, List.of(new UserMessage(query)));
}
// 2. 加载长期记忆
LongMemory memory = memoryUpdater.loadFromStore(userId);
String context = "";
if (!memory.isEmpty()) {
context = buildMemoryContext(memory);
}
logger.debug("MemoryPrepare - threadId={}, query={}, longMemoryLen={}",
threadId, query, context.length());
return Map.of("long_memory_context", context);
}
private String buildMemoryContext(LongMemory memory) {
StringBuilder sb = new StringBuilder();
sb.append("<system-reminder>\n");
sb.append("以下是关于用户的长期记忆信息,请在回答时参考:\n\n");
sb.append("## 用户画像\n");
memory.getSummary().forEachNonEmpty((key, value) -> {
String label = switch (key) {
case "identity" -> "身份背景";
case "current_work" -> "当前工作";
case "tech_stack" -> "技术栈";
case "code_preferences" -> "编码偏好";
case "communication_style" -> "沟通风格";
case "project_context" -> "项目背景";
default -> key;
};
sb.append("- **").append(label).append("**: ").append(value).append("\n");
});
if (memory.getFacts() != null && !memory.getFacts().isEmpty()) {
sb.append("\n## 已知事实\n");
int count = 0;
for (MemoryFact fact : memory.getFacts()) {
if (count >= 10) break;
sb.append("- ").append(fact.getContent())
.append(" (置信度: ").append(String.format("%.0f%%", fact.getConfidence() * 100))
.append(")\n");
count++;
}
}
sb.append("\n</system-reminder>");
return sb.toString();
}
}
4.2 长期记忆更新入队节点
放在 StateGraph 的 END 之前,负责从 MessageWindowChatMemory 读取对话 → 过滤 → 检测信号 → 异步入队,不阻塞主流程。
java
public class MemoryUpdateQueueNode implements NodeAction {
private static final Logger logger = LoggerFactory.getLogger(MemoryUpdateQueueNode.class);
private final MemoryUpdateQueue queue;
private final MessageFilter messageFilter;
private final SignalDetector signalDetector;
private final boolean signalsEnabled;
private final MessageWindowChatMemory chatMemory;
public MemoryUpdateQueueNode(MemoryUpdateQueue queue, MessageFilter messageFilter,
SignalDetector signalDetector, boolean signalsEnabled,
MessageWindowChatMemory chatMemory) {
this.queue = queue;
this.messageFilter = messageFilter;
this.signalDetector = signalDetector;
this.signalsEnabled = signalsEnabled;
this.chatMemory = chatMemory;
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
String userId = state.value("user_id", "default").toString();
String threadId = state.value("thread_id", "default").toString();
// 从 MessageWindowChatMemory 读取对话历史
List<Message> allMessages = chatMemory.get(threadId);
if (allMessages == null || allMessages.isEmpty()) {
logger.debug("No messages in chatMemory for thread {}", threadId);
return Map.of();
}
// 过滤
List<Message> filtered = messageFilter.filterForMemory(allMessages);
// 检测信号
List<String> correctionSignals = new ArrayList<>();
List<String> reinforcementSignals = new ArrayList<>();
if (signalsEnabled) {
for (Message msg : filtered) {
if (msg instanceof UserMessage) {
correctionSignals.addAll(signalDetector.detectCorrection(msg.getText()));
reinforcementSignals.addAll(signalDetector.detectReinforcement(msg.getText()));
}
}
}
// 异步入队
MemoryQueueEntry entry = new MemoryQueueEntry(
userId, threadId,
filtered, correctionSignals, reinforcementSignals, Instant.now()
);
queue.enqueue(entry);
logger.debug("Queued memory update for user={}, {} messages",
userId, filtered.size());
return Map.of();
}
}
5. 项目集成
5.1 长期记忆配置类
基于 Store 接口的 FileSystemStore 实现磁盘持久化(目录:.longmemory)。
java
@Configuration
public class LongMemoryConfig {
/** 防抖时间,同一用户的多轮对话在此时间内只触发一次 LLM 提取 */
private static final Duration DEBOUNCE = Duration.ofSeconds(30);
/** 最大事实数量 */
private static final int MAX_FACTS = 20;
/** 单条消息最大字符数 */
private static final int MAX_MESSAGE_CHARS = 1000;
/** 队列线程数 */
private static final int QUEUE_THREADS = 2;
/** FileSystemStore 存储根目录 */
@Bean
public Store longMemoryStore() {
String path = System.getProperty("user.dir") + "/longmemory-store";
System.out.println("[LongMemory] Store 路径: " + path);
return new FileSystemStore(path);
}
@Bean
public MessageFilter messageFilter() {
return new MessageFilter(MAX_MESSAGE_CHARS, true);
}
@Bean
public SignalDetector signalDetector() {
return new SignalDetector();
}
@Bean
public UpdatePromptBuilder updatePromptBuilder() {
return new UpdatePromptBuilder(true);
}
@Bean
public MemoryUpdater memoryUpdater(
@Qualifier("dashScopeChatModel") ChatModel chatModel,
Store longMemoryStore,
UpdatePromptBuilder promptBuilder,
MessageFilter messageFilter,
SignalDetector signalDetector) {
return new MemoryUpdater(chatModel, longMemoryStore, promptBuilder,
messageFilter, signalDetector, MAX_FACTS, true);
}
@Bean
public MemoryUpdateQueue memoryUpdateQueue(MemoryUpdater updater) {
return new MemoryUpdateQueue(updater, DEBOUNCE, QUEUE_THREADS);
}
}
5.2 构建状态图
java
@Configuration
public class OrderGraphConfig2 {
private static final Logger log = LoggerFactory.getLogger(OrderGraphConfig2.class);
@Bean("order2CompiledGraph")
public CompiledGraph orderCompiledGraph(
@Qualifier("dashScopeChatModel") ChatModel chatModel,
MessageWindowChatMemory chatMemory,
MemoryUpdater memoryUpdater,
MemoryUpdateQueue memoryUpdateQueue,
MessageFilter messageFilter,
SignalDetector signalDetector) throws GraphStateException {
KeyStrategyFactory keyStrategyFactory = () -> {
HashMap<String, KeyStrategy> strategies = new HashMap<>();
strategies.put(QUERY, new ReplaceStrategy());
strategies.put(THREAD_ID, new ReplaceStrategy());
strategies.put(RESPONSE, new ReplaceStrategy());
strategies.put(USER_INTENT, new ReplaceStrategy());
strategies.put(ORDER_ID, new ReplaceStrategy());
strategies.put(LOGISTICS_INFO, new ReplaceStrategy());
strategies.put(URGE_RESULT, new ReplaceStrategy());
strategies.put(PROCESS_STATUS, new ReplaceStrategy());
strategies.put(LONG_MEMORY_CONTEXT, new ReplaceStrategy());
return strategies;
};
ChatClient chatClient = ChatClient.builder(chatModel).build();
StateGraph graph = new StateGraph(keyStrategyFactory);
// 记忆准备(长期记忆注入 + 短期记忆写入,合并为一个节点)
graph.addNode("memory_prepare", node_async(
new MemoryPrepareNode(memoryUpdater, chatMemory)));
graph.addNode("memory_update", node_async(new MemoryUpdateQueueNode(
memoryUpdateQueue, messageFilter, signalDetector, true, chatMemory)));
graph.addNode("intent_recognition", node_async(new IntentRecognitionNode(chatClient, chatMemory)));
graph.addNode("logistics_query", node_async(new LogisticsQueryNode()));
graph.addNode("urge_order", node_async(new UrgeOrderNode()));
graph.addNode("llm_response", node_async(new OrderLlmNode(chatClient, chatMemory)));
// START → 记忆准备 → 意图识别
graph.addEdge(START, "memory_prepare");
graph.addEdge("memory_prepare", "intent_recognition");
graph.addConditionalEdges("intent_recognition",
edge_async(state -> {
String intent = state.value(USER_INTENT).map(Object::toString).orElse("");
if ("物流查询".equals(intent)) return "物流查询";
if ("催单".equals(intent)) return "催单";
return "llm_response";
}),
Map.of("物流查询", "logistics_query",
"催单", "urge_order",
"llm_response", "llm_response"));
graph.addEdge("logistics_query", "llm_response");
graph.addEdge("urge_order", "llm_response");
// LLM 响应 → 记忆更新 → END
graph.addEdge("llm_response", "memory_update");
graph.addEdge("memory_update", END);
GraphRepresentation mermaid = graph.getGraph(
GraphRepresentation.Type.MERMAID, "电商订单咨询工作流", true);
log.info("Order2 StateGraph Mermaid:\n{}", mermaid.content());
SaverConfig saverConfig = SaverConfig.builder().register(new MemorySaver()).build();
CompiledGraph compiledGraph = graph.compile(
CompileConfig.builder().saverConfig(saverConfig).build());
log.info("Order2 CompiledGraph ready");
return compiledGraph;
}
}
5.3 测试
发起对话:

自动生成了一个记忆文件:

文件内容包含了个人喜好等内容:

重启项目,自动加载了长期记忆:
