Skip to content

3.5 对比学习:三个框架怎么处理记忆

前两节讲了我自己的记忆系统是怎么演进的。这一节换一个视角:把微软 MAF、阿里 AgentScope、Hermes 三个框架的记忆机制拆开来看,对比它们的设计选择,以及我从中学到了什么。

微软 Agent Framework (MAF):压缩系统

MAF 的记忆设计围绕一个核心概念:CompactionSystem(压缩系统)

它不关心"记忆是什么",它关心的是"上下文快满了怎么办"。

压缩策略矩阵

MAF 提供了 7 种压缩策略,可以组合使用:

策略原理适用场景
TruncationCompactionStrategy简单截断,删除最早的消息最简单场景,不需要保留历史
SlidingWindowCompactionStrategy滑动窗口,保留最近 N 条通用场景
SummarizationCompactionStrategy用 LLM 生成摘要替换旧消息需要保留关键信息的长对话
ToolResultCompactionStrategy压缩工具调用的结果(通常很大)工具密集型 Agent
ContextWindowCompactionStrategy根据上下文窗口大小自动触发Token 预算敏感场景
PipelineCompactionStrategy多种策略串联执行复杂场景
ChatReducerCompactionStrategy用 LLM 减少消息数量需要精细控制

核心设计:CompactionProvider

MAF 的压缩系统本身就是一个 AIContextProvider——它挂在上下文提供者链上,每次调用 LLM 前自动检查是否需要压缩:

用户发消息

CompactionProvider 检查触发条件(Token 超阈值?消息数超限?)

如果需要压缩:
  ├── 把消息按"原子组"分组(tool_call + tool_result 不能拆开)
  ├── 应用压缩策略(截断 / 摘要 / 工具结果压缩)
  └── 返回压缩后的消息列表

其他 AIContextProvider 执行

调用 LLM

SummarizationCompactionStrategy 是最值得关注的。它的压缩 Prompt 是这样的:

You are a conversation summarizer. Produce a concise summary that preserves:
- Key facts, decisions, and user preferences
- Important context needed for future turns
- Tool call outcomes and their significance

Omit pleasantries and redundant exchanges. Be factual and brief.

注意它保护了最近的 8 组消息(MinimumPreservedGroups = 8)不被压缩——这是一个硬性底线,即使 Token 还没超,也不会动最近的对话。

MAF 的向量记忆

除了压缩,MAF 还有一个 ChatHistoryMemoryProvider——把对话历史存到向量数据库里,下次需要时通过语义搜索召回相关的历史对话。

这个设计思路跟我的完全不同。MAF 走的是"用计算换检索":Embedding → 向量存储 → 语义搜索。我走的是"用结构换检索":L0/L1/L2 分层 → 文本匹配。

我从 MAF 学到了什么

  1. 原子组保护:tool_call 和 tool_result 是一对,压缩时不能拆开。我的 CompactionEngine 没考虑这个问题,MAF 的 CompactionMessageIndex 专门处理了这个。

  2. 策略可组合:7 种策略可以串联成 Pipeline。比如先压缩工具结果,再做摘要,最后截断。这种组合性比我"一个 CompactionEngine 打天下"灵活得多。

  3. LLM 失败时的回滚:SummarizationCompactionStrategy 在 LLM 摘要失败时,会把被标记为"已排除"的消息组恢复原状。这个容错设计很优雅——我的 CompactionEngine 失败时直接降级为简单截断,没有回滚。


阿里 AgentScope:短期 + 长期 + RAG 三件套

AgentScope 的记忆架构最"完整"——它把记忆分成了明确的短期和长期,还外加了 RAG 知识库。

短期记忆

短期记忆就是 InMemoryMemory——一个内存消息列表,没有压缩能力,消息会无限增长。

java
ReActAgent agent = ReActAgent.builder()
    .name("Assistant")
    .model(model)
    .memory(new InMemoryMemory())
    .build();

简单粗暴。如果需要持久化,配合 SessionManager 序列化到文件。

长期记忆

长期记忆是独立组件,有两个实现:

  • Mem0LongTermMemory:对接 Mem0 服务(云端或自建),自动提取和召回记忆
  • ReMeLongTermMemory:对接 ReMe 服务,阿里自研的记忆引擎

长期记忆的工作方式很清晰:

用户发消息

长期记忆.retrieve(msg) → 召回相关记忆 → 注入短期记忆

LLM 推理

长期记忆.record(msgs) → 异步存入新记忆

回复用户

长期记忆有两种控制模式:

  • STATIC_CONTROL:框架自动在推理前召回、回复后记录
  • AGENT_CONTROL:通过工具让 Agent 自主决定何时记录和召回

RAG 知识库

AgentScope 的 RAG 支持是最丰富的——4 种知识库实现:

类型实现适用场景
本地知识库SimpleKnowledge开发测试,完全控制数据
百炼知识库BailianKnowledge企业级,多轮对话
Dify 知识库DifyKnowledge多种检索模式
RAGFlow 知识库RAGFlowKnowledgeOCR、知识图谱

RAG 还有两种集成模式:

  • Generic 模式:每次推理前自动检索注入(被动)
  • Agentic 模式:Agent 通过工具决定何时检索(主动)

我从 AgentScope 学到了什么

  1. 短期/长期分离是正确的设计。短期记忆管"这次对话说了什么",长期记忆管"这个用户是谁"。我的 Messages + Facts 其实也是这个思路,但 AgentScope 把它做成了显式的接口分离。

  2. 记忆的控制权可以交给 AgentAGENT_CONTROL 模式让 Agent 自己决定什么时候记、什么时候查。这比"框架自动管理"更灵活——Agent 知道什么时候信息重要,什么时候不需要记。

  3. RAG 的 Generic vs Agentic 模式是个好思路。Generic 是"每次都查",Agentic 是"需要时才查"。对于编码场景,Agentic 模式更合适——不是每句话都需要查知识库。


Hermes:记忆提供者生态

Hermes 的记忆设计跟另外三个完全不同。它不做"记忆系统",它做"记忆生态"。

MemoryProvider 抽象

Hermes 的核心抽象是 MemoryProvider——一个有完整生命周期的插件:

initialize()          → 启动时连接后端
system_prompt_block() → 静态文本注入系统提示词
prefetch(query)       → 每轮对话前,主动召回相关记忆
sync_turn(user, asst) → 每轮对话后,异步写入新记忆
on_session_end()      → 会话结束时,提取关键事实
on_pre_compress()     → 压缩前,抢救即将被丢弃的信息
on_session_switch()   → 会话切换时,刷新状态
on_delegation()       → 子 Agent 完成时,观察结果
shutdown()            → 退出时清理

这个生命周期覆盖了记忆的完整链路:从初始化到召回、写入、抢救、清理。

内置 + 插件

Hermes 的记忆提供者分两层:

  • 内置提供者(builtin):始终存在,处理基础记忆功能
  • 外部提供者(插件):最多一个,可以是 Honcho、Mem0、Hindsight 等

为什么限制只允许一个外部提供者?Hermes 的设计者认为:多个记忆后端会导致工具 Schema 膨胀和记忆冲突。一个内置 + 一个外部,够了。

记忆的主动召回

Hermes记忆提供者生命周期:initialize→prefetch→sync_turn→pre_compress→session_end→shutdown

Hermes 最核心的设计是主动召回

用户发消息:"帮我看看数据库连接池的配置"

MemoryManager.prefetch_all("帮我看看数据库连接池的配置")

内置提供者:根据关键词召回 → "项目用 PostgreSQL,连接池最大 100"
外部提供者:根据语义召回 → "上次连接池溢出是因为 max_connections 设太大"

两段记忆合并,包裹在 <memory-context> 标签里注入上下文

调用 LLM

注意 <memory-context> 标签的设计——它告诉 LLM:"这是你的记忆,不是用户输入。" 这防止了 LLM 把记忆内容当成用户的新指令。

压缩前的抢救

Hermes 有一个独特的钩子:on_pre_compress()

在上下文压缩之前,记忆提供者有机会"抢救"即将被丢弃的信息。这就像人在整理桌子时,先把重要的东西拿出来放好,再扔剩下的。

我从 Hermes 学到了什么

  1. 记忆是管道,不是仓库。Hermes 的 MemoryProvider 不是一个存储后端,而是一个有生命周期的管道——initialize → prefetch → sync → shutdown。信息在这个管道里流动,而不是堆在仓库里。

  2. 主动召回是关键。不是把所有记忆都塞给 LLM,而是在每轮对话前,根据用户当前说的话,精准召回最相关的记忆。这是"越用越懂你"的核心机制。

  3. 压缩前抢救on_pre_compress() 这个钩子非常巧妙。我的 OpenViking 的 CompactionEngine 也做了类似的事(压缩前生成摘要),但没有把它做成一个显式的钩子让外部扩展。

  4. 标签隔离<memory-context> 标签告诉 LLM "这是记忆,不是用户输入"——这个设计防止了记忆注入被当成指令执行。安全细节很重要。


三个框架的对比

三个框架的记忆架构对比:MAF vs AgentScope vs Hermes

维度MAFAgentScopeHermes
记忆分类压缩系统 + 向量记忆短期 + 长期(显式分离)内置 + 外部(插件式)
压缩方式7 种策略可组合无内置压缩压缩前抢救钩子
检索方式向量语义搜索Mem0/ReMe 外部服务关键词 + 语义混合召回
RAG 支持无内置4 种知识库实现无内置(靠插件)
扩展性策略模式组合接口 + 外部服务插件生态
核心洞察上下文快满了怎么办短期管对话,长期管用户记忆是管道不是仓库

我的选择

对比完三个框架,我做了几个决策:

  1. 压缩策略:借鉴 MAF 的 SummarizationCompactionStrategy,但简化为"有 LLM 就智能压缩,没有就简单截断"。不需要 7 种策略矩阵,我的场景没那么复杂。

  2. 记忆结构:借鉴 OpenViking 的 L0/L1/L2 三层结构,而不是 AgentScope 的短期/长期显式分离。原因:三层结构更灵活——一条短信息可以 L0=L1=L2,一条长信息可以三层分开。

  3. 召回机制:借鉴 Hermes 的 prefetch + sync_turn 模式。每轮对话前主动召回,每轮对话后异步写入。但实现上简化了——我用加权文本匹配代替了 Hermes 的混合召回。

  4. RAG:不用。用 RepoWiki + AGENTS.md 的"结构化知识摘要"方案代替。原因在第 3.3 节讲过了。


关键认知

设计权衡:没有最好的系统,只有最适合的系统

对比学习的最大收获不是“抄作业”,是理解每个设计选择背后的权衡

  • MAF 选择了"策略可组合",代价是复杂度高——7 种策略要理解、要配置
  • AgentScope 选择了"短期/长期显式分离",代价是需要外部服务——Mem0、ReMe 都要独立部署
  • Hermes 选择了"插件生态",代价是限制了扩展数量——只允许一个外部提供者

没有"最好"的记忆系统,只有"最适合当前产品阶段"的记忆系统。

我的产品是桌面端软件,用户量不大,场景明确。所以我选择了:内存存储 + L0/L1/L2 三层 + 文本匹配召回 + 降级策略。够用,不过度设计。

等产品到了需要跨会话持久记忆的阶段,再升级到向量检索 + 外部记忆服务。一步一步来。