目录:
6. 沙箱(Sandbox)让 Agent 在安全笼子里自由奔跑
8. Skill技能让 Agent 从"会说话"进化为"会做事"
对话会结束,但记忆不应该。AgentScope Harness 用"日流水账 + 精炼笔记"的双层架构,让 Agent 在跨会话、跨进程、跨天的场景下依然"记得你"。
一、引言:金鱼记忆的 Agent 走不远
想象这样一个场景:
用户:"帮我订明天去上海的航班,靠窗。"
Agent:"好的,已为您查询到 3 个航班..."
------第二天------
用户:"还是老规矩。"
Agent:"抱歉,请问您指的是什么?"
这就是大多数 Agent 的真实困境:**没有跨会话记忆。**每次对话都是一张白纸,用户不得不反复交代偏好、背景和历史决策。
更棘手的是,即使有了某种"记忆"机制,还面临两个工程难题:
- **信息爆炸:**对话越积越多,全部塞进 prompt 会撑爆上下文窗口;
- 信息噪声:大量对话是寒暄和确认,真正有价值的事实可能只占 5%。
AgentScope Java 2.0 的 Harness 模块用一套双层记忆架构优雅地解决了这些问题。
二、架构全景:双层存储 + 三个 LLM 角色
2.1 整体架构图
文本
┌─────────────────────────────────────────────────────────────────┐
│ 每轮推理 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ WorkspaceContextHook │ │
│ │ 读取 MEMORY.md → 注入 system prompt(全局背景知识) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ReAct 推理循环(对话 + 工具调用) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ MemoryFlushMiddleware │ │
│ │ 对话结束 → 提取有价值事实 → 追加到 memory/YYYY-MM-DD.md │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ MemoryConsolidator(后台定时任务) │ │
│ │ 读取多日流水账 → LLM 合并/去重/提炼 → 更新 MEMORY.md │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
2.2 双层存储设计
| 层级 | 存储位置 | 数据属性 | 写入方式 | 读取时机 |
|---|---|---|---|---|
| 第一层:日流水账 | memory/YYYY-MM-DD.md | 原始、完整、未清洗 | 只追加(append-only) | memory_search 工具按需检索 |
| 第二层:精炼记忆 | MEMORY.md | 合并、去重、提炼后 | 后台 LLM 周期性覆写 | 每轮推理自动注入 system prompt |
设计精妙之处:
- 第一层负责**"节流"**------控制每轮注入 prompt 的 token 量(只有精炼后的 MEMORY.md 被注入);
- 第二层负责**"开源"**------确保任何有价值的信息都不会丢失(原始流水账永久保留);
两层之间由 MemoryConsolidator 后台任务桥接,对业务完全透明。
三、第一层:日流水账(memory/YYYY-MM-DD.md)
3.1 存储规则
markdown
<!-- memory/2026-08-11.md -->
## 15:32 - 用户偏好更新
- 用户张三确认偏好靠窗座位
- 出差报销标准:经济舱,单程不超过 2000 元
## 16:45 - 项目决策
- 决定将数据迁移窗口定在周六凌晨 2:00-6:00
- 原因:工作日 QPS 峰值在下午 2-5 点,迁移风险大
关键规则:
| 规则 | 说明 |
|---|---|
| 按日期分文件 | 每天一个 YYYY-MM-DD.md,自然归档 |
| 只追加 | 不修改、不删除已有内容,保证审计可追溯 |
| 不做清洗 | 原始事实直接写入,不去重、不合并 |
| 增量写入 | 每次 Flush 只追加新提取的事实 |
3.2 为什么是"只追加"?
这是一个深思熟虑的工程决策:
- 并发安全:多个会话可能同时写入同一天的文件,追加操作天然无冲突;
- 审计友好:任何时间点都能回溯"Agent 当时知道了什么";
- 幂等性:即使 Flush 重复执行,最坏情况是多写一条,不会覆盖已有信息;
- 解耦:清洗和合并的职责交给 Consolidator,Flush 只管"记下来"。
四、第二层:精炼记忆(MEMORY.md)
4.1 生成逻辑
MemoryConsolidator 是一个后台定时调度器,周期性地调用 LLM 执行合并:
java
MemoryConsolidator consolidator = new MemoryConsolidator(
model, // 用于合并摘要的 LLM 模型
memoryConfig // 记忆配置
);
// 启动后台调度(默认每小时执行一次合并)
consolidator.start();
// 也可以手动触发一次合并
consolidator.consolidate();
4.2 合并流程
文本
memory/2026-08-09.md ─┐
memory/2026-08-10.md ─┼──▶ Consolidation LLM ──▶ MEMORY.md
memory/2026-08-11.md ─┘ │
├── 合并:同类信息归并
├── 去重:重复事实只保留一条
├── 提炼:去除寒暄、确认等噪声
└── 归纳:抽象为可复用的知识点
4.3 MEMORY.md 示例
markdown
# Long-term Memory
## 用户偏好
- 张三:靠窗座位、经济舱、报销上限 2000 元/程
- 李四:偏好早班机(8:00 前)、素食餐
## 项目约定
- 数据迁移窗口:周六 02:00-06:00(避开工作日下午峰值)
- 代码审查:所有 SQL 变更必须经 DBA 审批
## 技术决策
- 2026-08-05:选定 RocketMQ 替代 Kafka(团队更熟悉运维)
- 2026-08-10:前端框架升级为 React 19
4.4 注入时机
每轮推理启动时,WorkspaceContextHook 自动将 MEMORY.md 的内容拼入 system prompt:
文本
[System Prompt]
├── AGENTS.md(人格与行为约定)
├── MEMORY.md(精炼长期记忆) ← 这里
├── knowledge/(领域知识)
└── skills/(技能描述)
💡 关键设计:只有精炼后的 MEMORY.md 被注入,而非所有流水账。这确保了注入的 token 量可控,同时关键信息不丢失。
五、MemoryFlush:事实提取的三大触发时机
MemoryFlushMiddleware 负责从对话中提取有价值的事实并写入流水账。它有三个触发时机:
5.1 触发时机一:每轮对话正常结束
文本
用户提问 → Agent 推理 → 工具调用 → 返回结果
│
▼
MemoryFlushMiddleware
提取本轮新事实
追加到 memory/2026-08-11.md
这是最常见的触发路径。一轮问答/工具调用完整跑完后,中间件自动执行 Flush。
5.2 触发时机二:对话压缩前置预提取
文本
上下文达到压缩阈值
│
▼ flushBeforeCompact = true(默认)
│
├── MemoryFlush:先提取即将被压缩的消息中的事实
│
└── Compaction:再执行摘要压缩
为什么需要这一步? 压缩会把前缀消息替换为摘要,细节必然丢失。在压缩前先把有价值的事实"抢救"到流水账中,确保信息不因压缩而永久丢失。
5.3 触发时机三:上下文溢出兜底紧急 Flush
文本
模型返回 context_length_exceeded
│
▼ HarnessAgent.recoverFromOverflow()
│
├── 紧急 Flush:尽最大努力提取当前上下文中的事实
│
└── 极端压缩:triggerMessages=1,只保留最近 1 条
这是"最后防线"------即使真的溢出了,也要在丢弃消息前尽可能保存信息。
5.4 flushTrigger 策略配置
java
MemoryConfig.builder()
.flushTrigger(FlushTrigger.ALWAYS) // 默认:每轮都提取
// .flushTrigger(FlushTrigger.ON_COMPACT) // 仅在压缩时提取
// .flushTrigger(FlushTrigger.NEVER) // 从不自动提取
.build();
| 策略 | 行为 | 适用场景 |
|---|---|---|
| ALWAYS(默认) | 每轮对话结束都提取事实 | 需要精细记忆的场景 |
| ON_COMPACT | 仅在压缩触发时才提取 | 成本敏感,减少 LLM 调用 |
| NEVER | 从不自动提取 | 完全依赖手动 @Tool 写入 |
六、三个 LLM 角色分工
Harness 记忆系统涉及三个独立的 LLM 调用角色,各司其职:
| 角色 | 职责 | 触发时机 | 输入 | 输出 |
|---|---|---|---|---|
| Flush LLM | 从对话中提取有价值事实 | 每轮结束 / 压缩前 / 溢出时 | 本轮对话消息 | 结构化事实列表 |
| Consolidation LLM | 合并多日流水账为精炼记忆 | 后台定时(默认每小时) | 多日 memory/*.md + 现有 MEMORY.md | 更新后的 MEMORY.md |
| Compaction LLM | 压缩当前对话窗口 | 消息数/token 达到阈值 | 待压缩的消息前缀 | 摘要文本 |
为什么要分三个角色?
- 关注点分离:提取事实 ≠ 合并记忆 ≠ 压缩对话,三者的 prompt 和评判标准完全不同;
- 独立调优:可以为每个角色指定不同的模型(如 Flush 用轻量模型,Consolidation 用旗舰模型);
- 独立调度:Flush 是同步的(跟随对话),Consolidation 是异步的(后台定时),Compaction 是条件触发的。
七、记忆检索工具:Agent 主动回忆
除了 MEMORY.md 的自动注入,Harness 还注册了记忆检索工具,让 Agent 可以主动查阅历史:
7.1 内置记忆工具
| 工具 | 功能 | 数据来源 |
|---|---|---|
| memory_search | 关键词搜索记忆内容 | memory/*.md(流水账) |
| memory_get | 获取指定日期的完整记忆 | memory/YYYY-MM-DD.md |
| session_search | 搜索历史会话原文 | agents//sessions/*.log.jsonl |
7.2 使用场景
文本
用户:"上次我们讨论的数据库迁移方案,最终定的几点执行?"
Agent 内部推理:
→ 当前上下文中没有这个信息(可能已被压缩)
→ 调用 memory_search("数据库迁移 执行时间")
→ 命中 memory/2026-08-05.md 中的记录
→ 回复:"最终定在周六凌晨 2:00-6:00 执行..."
7.3 与 MEMORY.md 注入的互补关系
文本
┌─────────────────────────────────────────────────────────┐
│ MEMORY.md 自动注入 │
│ → 高频、精炼、全局背景知识 │
│ → 每轮都有,零成本(已在 prompt 中) │
├─────────────────────────────────────────────────────────┤
│ memory_search / memory_get 主动检索 │
│ → 低频、细节、特定历史事实 │
│ → 按需调用,有工具调用开销 │
└─────────────────────────────────────────────────────────┘
这种"热数据自动注入 + 冷数据按需检索"的分层策略,在 token 效率和信息完整性之间取得了最佳平衡。
八、完整工作流程
8.1 链路一:正常单轮对话(无压缩触发)
文本
用户发送消息
│
▼ WorkspaceContextHook
│ 读取 MEMORY.md → 注入 system prompt
│
▼ ReAct 推理循环
│ 思考 → 调用工具 → 生成回复
│
▼ MemoryFlushMiddleware
│ 提取本轮新事实
│ 追加到 memory/2026-08-11.md
│
▼ 返回响应给用户
...(后台)...
▼ MemoryConsolidator(定时触发)
读取近几日流水账
LLM 合并 → 更新 MEMORY.md
8.2 链路二:触发对话压缩
文本
用户发送消息
│
▼ WorkspaceContextHook
│ 读取 MEMORY.md → 注入 system prompt
│
▼ 压缩检测:消息数 ≥ 阈值
│
├── ① MemoryFlush(flushBeforeCompact)
│ 提取即将被压缩的消息中的事实
│ 追加到 memory/YYYY-MM-DD.md
│
├── ② OffloadBeforeCompact
│ 原始消息写入 sessions/*.log.jsonl(永不压缩)
│
├── ③ Compaction LLM
│ 前缀消息 → 结构化摘要
│
└── ④ 写回 AgentState
[summary] + [recent tail]
│
▼ ReAct 推理循环(使用压缩后的上下文)
│
▼ 返回响应给用户
8.3 完整生命周期视图
文本
Day 1: 对话 → Flush → memory/2026-08-09.md
Day 2: 对话 → Flush → memory/2026-08-10.md
Day 3: 对话 → Flush → memory/2026-08-11.md
│
▼ Consolidation(后台定时)
│
MEMORY.md 更新(合并 Day1-3 的事实)
│
▼ 下一轮推理
│
MEMORY.md 注入 system prompt
Agent "记得" Day 1-3 的关键信息
九、配置实战
9.1 最简配置(开启默认记忆)
java
HarnessAgent agent = HarnessAgent.builder()
.name("assistant")
.model(new OpenAIChatModel(apiKey, "gpt-4o"))
.workspace(Path.of("./workspace"))
.memory(MemoryConfig.defaults()) // 开启双层记忆
.build();
9.2 进阶配置(精细控制)
java
HarnessAgent agent = HarnessAgent.builder()
.name("travel-assistant")
.model(new OpenAIChatModel(apiKey, "gpt-4o"))
.workspace(Path.of("./workspace"))
// 记忆配置
.memory(MemoryConfig.builder()
.flushTrigger(FlushTrigger.ALWAYS) // 每轮都提取
.consolidationInterval(Duration.ofHours(1)) // 每小时合并一次
.maxMemoryTokens(4000) // MEMORY.md 注入上限
.build())
// 压缩配置(与记忆联动)
.compaction(CompactionConfig.builder()
.triggerMessages(30)
.keepMessages(10)
.flushBeforeCompact(true) // 压缩前先 Flush(默认 true)
.offloadBeforeCompact(true) // 压缩前先备份(默认 true)
.build())
.build();
9.3 成本优化配置(用轻量模型做 Flush)
java
HarnessAgent agent = HarnessAgent.builder()
.name("assistant")
.model(expensiveModel) // 主推理用旗舰模型
.workspace(Path.of("./workspace"))
.memory(MemoryConfig.builder()
.flushModel(cheapModel) // Flush 用轻量模型
.consolidationModel(cheapModel) // 合并也用轻量模型
.flushTrigger(FlushTrigger.ON_COMPACT) // 只在压缩时提取,减少调用
.build())
.build();
十、与 1.x LongTermMemory 的对比
AgentScope Java 1.x 使用 LongTermMemory 接口(及其实现如 Mem0LongTermMemory),2.0 中已被彻底替代:
| 维度 | 1.x LongTermMemory | 2.0 Harness 双层记忆 |
|---|---|---|
| 存储 | 向量数据库 / 外部服务 | 工作区文件(Markdown) |
| 观测性 | 需要查询接口 | 直接打开文件 |
| 版本管理 | 不支持 | 天然 Git 友好 |
| 与压缩联动 | 无 | flushBeforeCompact 原生集成 |
| Agent 自主写入 | 有限 | @Tool 主动写 + 自动 Flush |
| 多租户隔离 | 需要自行实现 | 工作区目录天然隔离 |
| 人机共编 | 不支持 | 人类可直接编辑 MEMORY.md |
2.0 的设计哲学:记忆不是外部服务的附属品,而是工作区的一等公民。
十一、与其他子系统的协作关系
记忆系统不是孤立存在的,它与 Harness 的其他子系统紧密协作:
文本
┌─────────────────────────────────────────────────────────────┐
│ Harness 记忆生态 │
│ │
│ ┌──────────┐ flushBeforeCompact ┌──────────────┐ │
│ │ 压缩链路 │ ──────────────────────▶ │ 记忆 Flush │ │
│ │Compaction│ │MemoryFlush │ │
│ └──────────┘ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ 定时合并 ┌──────────────┐ │
│ │Consolidator│ ◀──────────────────── │memory/*.md │ │
│ └─────┬────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ 每轮注入 ┌──────────────┐ │
│ │MEMORY.md │ ──────────────────────▶ │System Prompt │ │
│ └──────────┘ └──────────────┘ │
│ │
│ ┌──────────┐ 按需检索 ┌──────────────┐ │
│ │memory_search│ ◀──────────────────── │ Agent 推理 │ │
│ │memory_get │ └──────────────┘ │
│ └──────────┘ │
│ │
│ ┌──────────┐ 全量备份 ┌──────────────┐ │
│ │session日志│ ◀────────────────────── │offloadBefore │ │
│ │.log.jsonl│ │ Compact │ │
│ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
| 协作方 | 关系 |
|---|---|
| 压缩链路 | 压缩前触发 Flush,确保信息不丢失 |
| Session 存储 | 原始对话备份到 .log.jsonl,供 session_search 检索 |
| Workspace | 记忆文件是工作区的一部分,共享文件系统抽象 |
| 子 Agent | 子 Agent 的决策也可被 Flush 记录 |
| 沙箱 | 记忆文件通过 AbstractFilesystem 路由,沙箱内外一致 |
十二、设计哲学与工程启示
12.1 先记后炼,宽进严出
流水账"宽进"------不做过滤,宁可多记;MEMORY.md"严出"------经过 LLM 提炼,只保留高价值信息。这种"先全量记录、后选择性注入"的策略,在信息完整性和 token 效率之间取得了最佳平衡。
12.2 文件即记忆,人机共编
记忆不是锁在数据库里的黑盒,而是工作区中的 Markdown 文件。这意味着:
- 开发者可以直接编辑 MEMORY.md 修正错误记忆;
- 产品经理可以预置领域知识;
- Agent 自己可以在运行时更新记忆(自我进化)。
12.3 节流闸门,按需触发
Flush 和 Consolidation 都有节流机制,不会每轮都触发 LLM 调用。特别是 FlushTrigger.ON_COMPACT 策略,可以大幅降低 Flush 的调用频率,适合成本敏感场景。
12.4 与压缩深度联动
flushBeforeCompact 这个看似简单的布尔值,实际上是整个记忆系统最关键的安全阀。它确保了:无论压缩如何激进,有价值的事实永远有"逃生通道"。
12.5 三层记忆模型
从更宏观的视角看,Harness 实际上实现了一个三层记忆模型:
| 层级 | 对应人类记忆 | 实现 | 生命周期 |
|---|---|---|---|
| 工作记忆 | 当前正在想的事 | AgentState.contextMutable() | 单次调用 |
| 情景记忆 | 昨天发生了什么 | memory/.md + sessions/.log.jsonl | 永久 |
| 语义记忆 | 我知道的事实和规则 | MEMORY.md | 永久,持续更新 |
十三、最佳实践清单
| 场景 | 推荐配置 |
|---|---|
| 个人助手(对话轮次少) | MemoryConfig.defaults() 即可 |
| 企业客服(多用户、高频) | flushTrigger(ALWAYS) + 每小时 Consolidation |
| 编码 Agent(长对话) | flushTrigger(ALWAYS) + flushBeforeCompact(true) + 压缩 |
| 成本敏感 | flushTrigger(ON_COMPACT) + 轻量模型做 Flush |
| 需要人工干预记忆 | 直接编辑 MEMORY.md,下轮立即生效 |
| 多租户 SaaS | 每个 userId 独立工作区,记忆天然隔离 |
十四、结语
AgentScope Harness 的双层记忆系统,回答了一个根本问题:
如何让 Agent 在"有限的上下文窗口"中,拥有"无限的长期记忆"?
答案是:不要试图把所有东西塞进窗口,而是建立一套"记录 → 提炼 → 注入 → 检索"的完整生命周期。
- 流水账确保不丢失;
- Consolidation 确保不膨胀;
- 自动注入确保不遗忘;
- 检索工具确保可回忆。
这四个环节形成闭环,让 Agent 真正拥有了跨会话、跨天、跨进程的"长期大脑"。如果你正在构建需要"记住用户"的 Agent 系统,这套架构设计值得深入研究和借鉴。