第十七章 长期记忆系统:从 LongTermMemory 到 workspace/MEMORY.md 中间件驱动
谈到 AgentScope 的长期记忆机制,2.0 版本堪称一次彻底重构。1.x 中的 LongTermMemory 接口虽然功能完整,但随着架构快速迭代,新型文件存储配合中间件设计让记忆管理变得更加轻量和透明。我们先回顾旧版 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
简单来说:MEMORY.md 存放跨会话、跨天都能复用的稳定信息,例如用户偏好、长期目标;而 memory/YYYY-MM-DD.md 则按天追加事件流,记录当天发生的内容。
17.2.1 MEMORY.md
举个例子,MEMORY.md 的内容大概像这样:
# MEMORY.md
## 用户偏好
- 常驻城市:杭州
- 时区:UTC 8
- 语言:中文
## 长期目标
- 2026 年内学完 Rust
这些信息不会随单次对话结束而消失,agent 每次启动都会自动读取。
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
这个中间件由 HarnessAgent 在 build() 时根据 MemoryConfig 自动挂载,业务方只需通过 .memory() 配置是否开启即可:
HarnessAgent agent = HarnessAgent.builder()
.name("assistant")
.sysPrompt("...")
.model(model())
.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()
.name("assistant")
.sysPrompt("...")
.model(model())
.memory(
MemoryConfig.builder()
.build() // 默认开启每轮结束后冲刷记忆
)
.build();
同样由 HarnessAgent 根据 MemoryConfig 自动挂载,业务方无需手动构造。它的工作流程是:
- 监听每轮 call 结束
- 让 LLM 判断“本轮是否有值得长期记忆的内容”(事实、用户偏好变化等)
- 如果有 → 追加到当日
memory/2026-06-07.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 顶部几行,作为“已知背景”。如果你想让它主动更新 MEMORY.md,只需提供一个 @Tool:
@Tool(name = "update_long_term_memory", description = "更新长期记忆")
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";
String updated = existing + "\n## " + section + "\n- " + content + "\n";
Files.writeString(memFile, updated);
return "memory updated";
}
通过 Toolkit 将这个工具注册进 agent,当 LLM 看到用户说“以后请默认中文回答”时,就会自行调用该工具写入 MEMORY.md。下次对话立即生效,无需额外配置。
17.5 完整可运行示例
下面的例子演示了三层记忆机制如何协同工作:
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())
.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 下看到:
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。
可以说,这套全新的记忆系统让开发者能够更精细地控制哪些信息应该长久保留,哪些可以当天消化。迁移虽然需要一定工作量,但换来的是更透明、更灵活的管理方式——值得一试。
