AgentScope Java Harness:4. 双层记忆系统 让 Agent 拥有真正的“长期大脑“

目录:

1. 如何优雅地驾驭长期运行的 AI Agent

2. 上下文压缩:让长期 Agent 永不"失忆"

3. 工作区(Workspace)文件即真理,目录即架构

4. 双层记忆系统 让 Agent 拥有真正的"长期大脑"

5. 文件系统一套代码,三种部署,零改动切换

6. 沙箱(Sandbox)让 Agent 在安全笼子里自由奔跑

7. 子 Agent 编排 文件驱动的多智能体协作架构

8. Skill技能让 Agent 从"会说话"进化为"会做事"

9. Plan Mode 让 Agent 先想清楚再动手

10. Channel Agent 通信的"神经系统"设计

对话会结束,但记忆不应该。AgentScope Harness 用"日流水账 + 精炼笔记"的双层架构,让 Agent 在跨会话、跨进程、跨天的场景下依然"记得你"。

一、引言:金鱼记忆的 Agent 走不远

想象这样一个场景:

用户:"帮我订明天去上海的航班,靠窗。"

Agent:"好的,已为您查询到 3 个航班..."

------第二天------

用户:"还是老规矩。"

Agent:"抱歉,请问您指的是什么?"

这就是大多数 Agent 的真实困境:**没有跨会话记忆。**每次对话都是一张白纸,用户不得不反复交代偏好、背景和历史决策。

更棘手的是,即使有了某种"记忆"机制,还面临两个工程难题:

  1. **信息爆炸:**对话越积越多,全部塞进 prompt 会撑爆上下文窗口;
  2. 信息噪声:大量对话是寒暄和确认,真正有价值的事实可能只占 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 为什么是"只追加"?

这是一个深思熟虑的工程决策:

  1. 并发安全:多个会话可能同时写入同一天的文件,追加操作天然无冲突;
  2. 审计友好:任何时间点都能回溯"Agent 当时知道了什么";
  3. 幂等性:即使 Flush 重复执行,最坏情况是多写一条,不会覆盖已有信息;
  4. 解耦:清洗和合并的职责交给 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 达到阈值 待压缩的消息前缀 摘要文本

为什么要分三个角色?

  1. 关注点分离:提取事实 ≠ 合并记忆 ≠ 压缩对话,三者的 prompt 和评判标准完全不同;
  2. 独立调优:可以为每个角色指定不同的模型(如 Flush 用轻量模型,Consolidation 用旗舰模型);
  3. 独立调度: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 系统,这套架构设计值得深入研究和借鉴。

相关推荐
伍一511 小时前
03-文件管理模块-一个看似简单的模块藏着多少细节
java·ruoyi·若依
gugucoding1 小时前
46. 【Java】JUC并发工具:让并发更简单
java·开发语言
Nebula_g2 小时前
JavaSE基础语法:面向对象高级(代码中的成分)
java·开发语言·编程·javase·技术栈·高级语法
for_ever_love__2 小时前
python基础语法学习: 异常的传递性
开发语言·python·学习
AC赳赳老秦2 小时前
公开图片 OCR 数据提取:OpenClaw 合规采集公开信息图,识别文字与表格并转化为结构化数据
java·大数据·前端·数据库·python·php·openclaw
依然鸣3 小时前
PTA团体程序设计天梯赛L1真题讲解L1-077-080
开发语言·c++·算法·深度优先·pat考试·图论
小强库计算机毕业设计4 小时前
SpringBoot+Vue3 学生宿舍管理系统实战
java·spring boot·后端·vue·学生宿舍管理系统
李冰然4 小时前
深入Java集合框架:HashMap源码解析(JDK 8)
java
库玛西4 小时前
现代 C++ 智能指针全景指南:从 RAII 思想到工业级实践
c语言·开发语言·c++·笔记·面试