第十七章 长期记忆系统:从 LongTermMemory 到 workspace/MEMORY.md + 中间件驱动
1.x 的
io.agentscope.core.memory.LongTermMemory及其子类(LongTermMemoryBase/Mem0LongTermMemory等)在 2.0.0-RC2 中被标记为@Deprecated(forRemoval = true)。原因:硬编码"知识库"接口很难适配不同业务——什么是"长期"?是按主题、按时间、还是按人?2.0 把这件事下沉到 workspace 文件 + 中间件,业务方自己决定写什么、何时写、写到哪。
本章先给出 1.x 旧 API 的最小示例(仅供维护老项目参考),再讲 2.0 推荐的新做法。
17.1 1.x LongTermMemory 旧 API 回顾
import io.agentscope.core.memory.LongTermMemory;
import io.agentscope.core.memory.LongTermMemoryBase;
import io.agentscope.core.memory.Mem0LongTermMemory;
import io.agentscope.core.ReActAgent;
public class Chapter17_LegacyLongTerm {
public static void main(String[] args) {
LongTermMemory memory = new Mem0LongTermMemory(
"https://api.mem0.ai",
System.getenv("MEM0_API_KEY"));
ReActAgent agent = ReActAgent.builder()
.name("assistant")
.sysPrompt("你是一个长期记忆助理。")
.model(model())
.longTermMemory(memory) // 1.x
.build();
}
}
2.0 仍可编译:ReActAgent.longTermMemory(...) 在 RC2 中标记为 @Deprecated(forRemoval = true),编译通过但会告警。
17.2 2.0 推荐的两层记忆结构
workspace 下两份文件,按不同"时间粒度"组织:
workspace/
├── MEMORY.md # 长期稳定的事实笔记
└── memory/
├── 2026-06-05.md # 当天事件流
├── 2026-06-06.md
└── 2026-06-07.md
17.2.1 MEMORY.md
跨会话、跨天都会用到的"稳定信息"——
# MEMORY.md
## 用户偏好
- 常驻城市:杭州
- 时区:UTC+8
- 语言:中文
## 长期目标
- 2026 年内学完 Rust
17.2.2 memory/YYYY-MM-DD.md
按天追加的"事件流"——
# 2026-06-07
## 14:32
- 用户问"今天杭州天气",回答:22~28℃,局部多云,建议带伞
## 15:01
- 用户问"我常驻哪里",agent 从 MEMORY.md 知道"杭州",回答一致
17.3 两个核心中间件
io.agentscope.core.middleware 提供两个跟记忆直接相关的中间件:
| 中间件 | 何时触发 | 副作用 |
|---|---|---|
CompactionMiddleware |
上下文 token 超过阈值 | 早期消息压成摘要写进 MEMORY.md |
MemoryFlushMiddleware |
每轮 call 结束 | 把"该记的"刷到当日 memory/YYYY-MM-DD.md |
17.3.1 CompactionMiddleware
CompactionMiddleware 由 HarnessAgent 在 build() 时根据 MemoryConfig 自动挂载,业务方通过 .memory() 配置即可:
HarnessAgent agent = HarnessAgent.builder()
...
.memory(MemoryConfig.builder()
.compactionEnabled(true) // 超过 token 阈值自动压缩
.build())
.build();
CompactionMiddleware 工作流程:
- 监听每轮 call 结束
- 检查当前上下文 token 数
- 超过阈值 → 把"较旧的消息"让 LLM 总结成几行
- 摘要写进
MEMORY.md的"近期摘要"小节 - 旧消息从
AgentState移除
17.3.2 MemoryFlushMiddleware
import io.agentscope.harness.agent.middleware.MemoryFlushMiddleware;
HarnessAgent agent = HarnessAgent.builder()
...
.memory(MemoryConfig.builder().build()) // 默认开启每轮结束后冲刷记忆
.build();
MemoryFlushMiddleware 由 HarnessAgent 根据 MemoryConfig 自动挂载,业务方无需手动构造。工作流程:
- 监听每轮 call 结束
- 让 LLM 决定"本轮是否有值得长期记的事"(事实、用户偏好变化等)
- 如果有 → 追加到当日
memory/2026-06-07.md - 没有 → 跳过,不写
注意:每日事件文件不保证一定生成。LLM 可能判断当前对话太短、没有新信息,从而跳过写入。另外 MemoryFlush 是异步回调,进程如果在 flush 完成前退出,文件也来不及创建。实际使用中建议关注
MEMORY.md(CompactionMiddleware 写入,更稳定),memory/*.md作为辅助查看。
17.3.3 完整配置
HarnessAgent agent = HarnessAgent.builder()
.name("assistant")
.sysPrompt("...")
.model(model())
.workspace(Path.of("./workspace"))
.memory(MemoryConfig.builder().build()) // 默认开启 MemoryFlushMiddleware
.build(); // CompactionMiddleware 按 CompactionConfig 挂载
17.4 主 agent 自动读 / 写 MEMORY.md
主 agent 在每轮推理时会自动读 MEMORY.md 顶部几行作为"已知背景"。如果你想"agent 决定更新 MEMORY.md"——给它 @Tool:
@Tool(name = "update_long_term_memory", description = "更新长期记忆") // 注册为 agent 可调工具
public String updateMemory(
@ToolParam(name = "section") String section, // 比如 "用户偏好"
@ToolParam(name = "content") String content) {
// 比如 "默认中文回答"
Path memFile = Path.of("./workspace/MEMORY.md"); // 记忆文件固定路径
String existing = memFile.toFile().exists()
? Files.readString(memFile) // 已有记忆 → 追加
: "# MEMORY.md\n\n"; // 新文件 → 创建
String updated = existing + "\n## " + section + "\n- " + content + "\n";
Files.writeString(memFile, updated); // 写回文件
return "memory updated";
}
把工具通过 Toolkit 注册进 agent,LLM 看到用户说"以后请默认中文回答"会自己调这个工具写进 MEMORY.md,下次对话时立刻生效。
17.5 完整可运行示例
这个例子在演示什么?
一个 agent 同时运行三种记忆机制,各管各的时间粒度:
CompactionMiddleware(.compaction()配置):上下文太长时自动压缩旧消息为摘要,写进MEMORY.mdMemoryFlushMiddleware(.memory().flushEnabled(true)配置):每轮对话结束自动提取重要信息,写进memory/YYYY-MM-DD.mdUpdateMemoryTool(@Tool工具):agent 在对话中主动决定"这件事值得长期记住",自己调工具写MEMORY.md三者互补:Compaction 管上下文压缩(被动)、MemoryFlush 管日常记录(自动)、UpdateMemoryTool 管即时决策(主动)。
public class Chapter17_MemoryStack {
public static void main(String[] args) {
DashScopeChatModel model = DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.build();
// 1. 手工更新记忆的工具(agent 主动决策"记住这个")
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new UpdateMemoryTool());
// 2. agent:三层记忆机制一起跑
HarnessAgent agent = HarnessAgent.builder()
.name("assistant")
.sysPrompt("你是一个有长期记忆的助理。用户告诉你的事如果需要长期记住,调用 update_long_term_memory 工具写下来。")
.model(model)
.workspace(Path.of("./workspace"))
.toolkit(toolkit)
.compaction(CompactionConfig.builder().build()) // 上下文太长时自动压缩
.memory(MemoryConfig.builder().build()) // 默认 flush=每轮结束自动冲刷
.build();
// 3. 多轮对话(同一个 session,演示跨轮记忆)
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("user-9527-2026-06-07")
.userId("9527")
.build();
// Round 1:agent 记住用户基本信息
agent.call(List.of(new UserMessage("user", "我叫小李,住在杭州。")), ctx).block();
// Round 2:agent 应该能从记忆里调出信息
agent.call(List.of(new UserMessage("user", "我叫什么?住在哪?")), ctx).block();
// Round 3:agent 主动调 update_long_term_memory 写偏好
agent.call(List.of(new UserMessage("user", "以后请用中文回答。")), ctx).block();
}
}
跑完你会看到:
workspace/MEMORY.md— CompactionMiddleware 写的摘要 + UpdateMemoryTool 写的用户偏好(@ToolParam缺了这里会是 null)workspace/memory/— 不一定生成。MemoryFlushMiddleware 的写入取决于 LLM 判断"是否值得记"以及异步回调是否执行完毕- Round 2 时 agent 能从上下文记忆里知道"小李 / 杭州"
- Round 3 时 agent 主动调工具把"中文回答"偏好写进 MEMORY.md
17.6 最小迁移清单(1.x LongTermMemory → 2.0 文件记忆)
| 1.x 用法 | 2.0 等价 |
|---|---|
LongTermMemory.retrieve(query) |
业务方手写 @Tool 调自家向量库;或 subagent 用 grep_files |
LongTermMemory.record(messages) |
MemoryFlushMiddleware + update_long_term_memory 工具 |
LongTermMemoryBase 子类 |
业务方自己实现 @Tool |
agent.longTermMemory(memory) |
workspace/MEMORY.md + memory/*.md + 中间件 |
17.7 本章小结
- 1.x
LongTermMemory在 2.0 中被标记为弃用,未来会移除。 - 2.0 用
MEMORY.md+memory/YYYY-MM-DD.md两层文件做长期记忆。 CompactionMiddleware自动压缩 +MemoryFlushMiddleware自动写入。- 业务方可以手写
@Tool让 agent 主动更新 MEMORY.md。