在 Agent 领域,有一句名言:"没有记忆的 Agent 只是一个高级的函数,拥有记忆的 Agent 才有灵魂。"
然而,在 Java 生态中实现一个真正具备"自我进化"能力的记忆系统并不容易。本文将深度解析如何基于 Solon AI 框架(及 Solon AI Skill 接口),参考 MemSkill 论文核心思想,实现一个具备提取、整合、修剪与检索能力的长期记忆组件。
一、 Agent 的记忆长效难题
目前大多数 AI 应用的记忆实现非常简单:直接将最近的对话记录塞进 Context 窗口。这种做法面临三大难题:
- Context 溢出 :LLM 的上下文窗口是昂贵且有限的,流水账式的记忆很快就会触顶。
- 认知噪声: 历史记录中充当"废话"的信息会干扰 AI 对当前任务的判断。
- 认知冲突: 当用户状态发生改变(例如:"我以前喜欢 Java,现在更喜欢 Go"),简单的检索(RAG)往往会搜出过时的信息,导致 AI 逻辑混乱。
MemSkill 的核心理念 : Agent 不应只是被动地存取数据,而应像人一样,能够主动地 归纳 事实、修正 错误,并将碎片化信息 升维 为心智模型(Mental Model)。
二、 思考与设计:心智模型自演进
我们要设计的 MemSkill 遵循四个核心动作:
- Extract(提取):从对话中提取关键事实,并赋予重要度。
- Consolidate(整合):将多个低级事实压缩为一个高层级的用户偏好摘要。
- Prune(修剪):识别并删除过时或错误的记忆。
- Search(检索):通过语义搜索找回最相关的上下文。
架构设计
为了保证工程落地,我们采用了 二级存储架构:
- Redis (KV 存储):利用 TTL 机制,根据重要度(Importance)实现记忆的物理淘汰。
- Vector/Lucene (语义索引):提供多租户隔离的语义检索能力。
三、 代码实现:构建 MemSkill 核心
1. 记忆搜索供应商(MemSearchProvider)
首先,我们定义一个抽象层,既可以对接分布式的矢量数据库,也可以对接本地的 Lucene。
java
public interface MemSearchProvider {
/** 语义/模糊搜索 */
List<MemSearchResult> search(String userId, String query, int limit);
/** 获取高价值热记忆(用于画像注入) */
List<MemSearchResult> getHotMemories(String userId, int limit);
/** 同步索引 */
void updateIndex(String userId, String key, String fact, int importance, String time);
/** 移除索引 */
void removeIndex(String userId, String key);
}
2. 核心技能类:MemSkill
这是 Agent 的"心智处理器"。它不仅提供工具给 AI 调用,还通过 getInstruction 在对话开启前动态注入用户画像。
java
public class MemSkill extends AbsSkill {
private final RedisClient redis;
private final String userId;
private final MemSearchProvider searchProvider;
@Override
public String getInstruction(Prompt prompt) {
// 动态加载前 5 条核心认知碎片
List<MemSearchResult> hot = searchProvider.getHotMemories(userId, 5);
String mentalModel = formatModel(hot);
return "### 长期记忆自演进指南\n" +
"1. **核心心智模型**:这是你对用户的既有认知:\n" + mentalModel +
"\n2. **认知演进准则**:\n" +
" - **时序优先**:冲突时以最近时间戳为准。\n" +
" - **主动修正**:现状与模型冲突时,须通过 mem_extract 或 mem_prune 更新。";
}
@ToolMapping(name = "mem_extract", description = "提取事实或偏好存入心智模型")
public String extract(@Param("key") String key, @Param("fact") String fact, @Param("importance") int importance) {
// 1. 获取旧记忆进行反思
String oldJson = redis.getBucket().get(getFinalKey(key));
// 2. 动态计算 TTL:核心总结(>=10)永久存储,普通事实 7-30 天
int ttl = calculateTTL(importance);
// 3. 同步到 Redis 和 搜索供应商
saveMemory(key, fact, importance, ttl);
return "【操作成功】心智模型已更新。若发现认知差异,请体现出你的认知进化。";
}
}
3. 本地化落地:Lucene 适配
对于许多 Java 开发者,部署一套向量库成本太高。我们实现了一个基于 Lucene 的 MemSearchProvider,支持在本地磁盘或内存中实现"多租户隔离"的语义匹配。
java
public class MemSearchProviderLuceneImpl implements MemSearchProvider, AutoCloseable {
private final IndexWriter writer;
@Override
public List<MemSearchResult> search(String userId, String query, int limit) {
// 通过 BooleanQuery 实现 (user_id = X AND content LIKE query)
BooleanQuery.Builder mainQuery = new BooleanQuery.Builder();
mainQuery.add(new TermQuery(new Term("user_id", userId)), BooleanClause.Occur.MUST);
// ... 添加 QueryParser 解析的 query
// 按相关度(或重要度)排序返回
TopDocs topDocs = searcher.search(mainQuery.build(), limit);
// ... 转换为 MemSearchResult
}
}
四、 这种实现的精妙之处
- 认知对比(Reflexion) : 在
mem_extract时,系统会把旧的记忆片段反馈给 AI。这会触发 Agent 的"惊觉"机制,使其能够说出:"我记得你之前提到过 A,但现在你似乎更倾向于 B,我已经为你更新了认知。" - 重要度驱动的生命周期 : 不是所有记忆都是平等的。通过
importance控制 TTL,实现了从"瞬时记忆"到"长期经验"的自然沉淀。 - Designer 角色注入 : 通过
getInstruction,我们将心智模型变成了 Agent 的"前置意识",这比单纯的 Tool Call 检索更高效、更拟人。
五、 结语
在 Solon AI 生态中,solon-ai-skill-memory 的实现证明了:长期记忆不只是数据的堆砌,而是认知的管理。 通过这套基于 Lucene/Redis 的自演进方案,Java 开发者可以非常轻松地为自己的 Agent 构建出一套过目不忘、且能自我反思的心智模型。
附:完整的实现参考
java
import org.noear.redisx.RedisClient;
import org.noear.snack4.ONode;
import org.noear.solon.Utils;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.chat.skill.AbsSkill;
import org.noear.solon.ai.chat.prompt.Prompt;
import org.noear.solon.annotation.Param;
import org.noear.solon.core.util.Assert;
import org.noear.solon.lang.Preview;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* MemSkill:基于自演进心智模型的长期记忆技能
*
* 遵从 MemSkill 论文核心:Extract (提取), Consolidate (整合), Prune (修剪), Search (检索)
* 通过 Designer 指令引导 Agent 实现认知的自我更新。
*/
public class MemSkill extends AbsSkill {
private static final Logger LOG = LoggerFactory.getLogger(MemSkill.class);
private static final String BASE_PREFIX = "ai:memskill:";
private final RedisClient redis;
private final String userId;
private final MemSearchProvider searchProvider;
public MemSkill(RedisClient redis, String userId, MemSearchProvider searchProvider) {
this.redis = redis;
this.userId = userId;
this.searchProvider = searchProvider;
}
@Override
public String name() { return "mem_skill"; }
@Override
public String description() {
return "长期记忆专家:负责用户心智模型的提取、演进、冲突消解与深度检索。";
}
/**
* 充当 Designer 引导逻辑,动态加载心智模型
*/
@Override
public String getInstruction(Prompt prompt) {
String mentalModel = "";
if (searchProvider != null) {
// 获取核心认知碎片(通常是重要度高或最近更新的)
List<MemSearchResult> hot = searchProvider.getHotMemories(userId, 5);
if (!hot.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (MemSearchResult r : hot) {
sb.append(String.format("- %s: %s (Time: %s)\n", r.key, r.content, r.time));
}
mentalModel = sb.toString();
}
}
return "### 长期记忆自演进指南 (当前系统时间: " + getNow() + ")\n" +
"你具备自主管理和演进用户心智模型的能力,请遵循以下原则:\n" +
"1. **核心心智模型**:这是你对当前用户的既有认知,请基于此进行对话:\n" +
(Assert.isEmpty(mentalModel) ? "- (心智模型构建中,请多提问以了解用户)" : mentalModel) +
"\n\n2. **认知演进准则**:\n" +
" - **时序优先**:若认知记录存在冲突,以时间戳最近的为准。\n" +
" - **主动修正**:若用户现状与上述模型冲突,必须通过 `mem_extract` 更新或 `mem_prune` 修剪。\n" +
" - **归纳升维**:当认知库出现冗余时,主动使用 `mem_consolidate` 将低层事实归纳为高层偏好。";
}
private String getFinalKey(String key) { return BASE_PREFIX + userId + ":" + key; }
private String getNow() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); }
/**
* EXTRACT & UPDATE: 提取与覆盖
* 解决了记忆冲突与反思逻辑
*/
@ToolMapping(name = "mem_extract",
description = "将事实、偏好或进度存入用户心智模型。若存在同名 Key,系统将返回旧记录以供你对比反思。")
public String extract(@Param("key") String key,
@Param("fact") String fact,
@Param("importance") int importance) {
try {
String finalKey = getFinalKey(key);
String oldJson = redis.getBucket().get(finalKey);
String now = getNow();
StringBuilder feedback = new StringBuilder("【操作成功】心智模型已更新。");
if (Utils.isNotEmpty(oldJson)) {
ONode old = ONode.ofJson(oldJson);
feedback.append("\n[认知对比] 发现历史记录:")
.append("\n- 旧内容: ").append(old.get("content").getString())
.append("\n- 旧时间: ").append(old.get("time").getString())
.append("\n请对比新旧信息差异。若发生改变,请在后续对话中体现出你的认知进化。");
}
Map<String, Object> data = new HashMap<>();
data.put("content", fact);
data.put("time", now);
data.put("importance", importance);
// 动态 TTL:重要信息(>=5)保留 30 天,核心总结(>=10)永久或极长,普通信息 7 天
int ttl;
if (importance >= 10) ttl = -1; // 核心经验永不过期
else if (importance >= 5) ttl = 2592000;
else ttl = 604800;
redis.getBucket().store(finalKey, ONode.serialize(data), ttl);
if (searchProvider != null) {
searchProvider.updateIndex(userId, key, fact, importance, now);
}
return feedback.toString();
} catch (Exception e) {
LOG.error("MemSkill extract error", e);
return "存储异常。";
}
}
/**
* SEARCH: 语义搜索
*/
@ToolMapping(name = "mem_search",
description = "语义检索:通过自然语言描述在心智模型中寻找相关的记忆碎片,辅助找回背景信息。")
public String search(@Param("query") String query) {
if (searchProvider == null) return "搜索适配器未配置。";
List<MemSearchResult> results = searchProvider.search(userId, query, 3);
if (results.isEmpty()) return "未发现相关认知片段。";
StringBuilder sb = new StringBuilder("匹配到以下认知参考(建议优先参考时间戳较近的记录):\n");
for (MemSearchResult res : results) {
sb.append(String.format("- [%s] (Key: %s): %s\n",
Utils.isNotEmpty(res.time) ? res.time : "未知时间", res.key, res.content));
}
return sb.toString();
}
/**
* RECALL: 精确召回
*/
@ToolMapping(name = "mem_recall", description = "精确召回:通过 Key 获取该认知条目的完整细节。")
public String recall(@Param("key") String key) {
try {
String val = redis.getBucket().get(getFinalKey(key));
if (Utils.isEmpty(val)) return "未找到认知条目 [" + key + "]。";
ONode node = ONode.ofJson(val);
return String.format("【认知详情】内容:%s | 记录时间:%s | 重要度:%s",
node.get("content").getString(), node.get("time").getString(), node.get("importance").getString());
} catch (Exception e) { return "读取异常。"; }
}
/**
* CONSOLIDATE: 知识整合
* 对齐 MemSkill 的"压缩"思想,将事实进化为经验
*/
@ToolMapping(name = "mem_consolidate",
description = "认知整合:将多个碎片的 Key 合并为一个高层级的总结记录。这能显著提升心智模型的质量。")
public String consolidate(@Param("old_keys") List<String> oldKeys,
@Param("new_key") String newKey,
@Param("summary") String summary) {
// 核心总结赋予最高重要度 10,确保持久存储
extract(newKey, "[认知总结] " + summary, 10);
for (String k : oldKeys) {
redis.getBucket().remove(getFinalKey(k));
if (searchProvider != null) searchProvider.removeIndex(userId, k);
}
LOG.info("MemSkill: {} 进行了心智模型演进 -> {}", userId, newKey);
return "整合完成。碎片信息已归纳,心智模型已升维。";
}
/**
* PRUNE: 记忆修剪
*/
@ToolMapping(name = "mem_prune", description = "认知修正:删除错误、重复或过时的认知。")
public String prune(@Param("key") String key) {
redis.getBucket().remove(getFinalKey(key));
if (searchProvider != null) searchProvider.removeIndex(userId, key);
return "已从模型中清理 Key: " + key;
}
}