Memory Provider 架构:可插拔的记忆后端
本章核心源码:
agent/memory_provider.py(231 行)、agent/memory_manager.py(367 行)、agent/builtin_memory_provider.py(114 行)、plugins/memory/
定位:本章将 Memory Provider 系统作为“如何设计可插拔记忆系统”的案例来写,涵盖 MemoryProvider ABC、MemoryManager 的职责边界、当前运行时里内置记忆与外部 provider 的接线关系,以及 8 个外部插件。 前置依赖:第 10 章(SessionDB)。适用场景:想理解 agent 如何实现"越用越懂你",或准备开发自定义 memory provider。
为什么记忆需要可插拔
SessionDB(第 10 章)存储的是会话状态——完整的消息历史。但"记忆"不等于"历史"。
- 历史是原始数据:用户在第 37 次对话中说了什么
- 记忆是提炼后的知识:用户偏好 Python 而非 JavaScript、用户的项目使用 PostgreSQL
不同的记忆策略各有优劣:有的擅长用户建模(Honcho),有的擅长向量检索(Mem0),有的擅长时序窗口(Hindsight)。Hermes 的解法不是选一种,而是提供一个可插拔的抽象层,让用户按需选择。
MemoryProvider ABC
MemoryProvider(agent/memory_provider.py:42)定义了 17 个方法的抽象接口:
graph TD
subgraph "生命周期方法"
INIT["initialize()"] --> PROMPT["system_prompt_block()"]
PROMPT --> PREFETCH["prefetch()"]
PREFETCH --> SYNC["sync_turn()"]
SYNC --> SHUTDOWN["shutdown()"]
end
subgraph "事件钩子"
ON_TURN["on_turn_start()"]
ON_SESSION["on_session_end()"]
ON_COMPRESS["on_pre_compress()"]
ON_DELEGATE["on_delegation()"]
ON_WRITE["on_memory_write()"]
end
subgraph "工具接口"
SCHEMA["get_tool_schemas()"]
HANDLE["handle_tool_call()"]
end
subgraph "配置"
AVAIL["is_available()"]
CONFIG["get_config_schema()"]
SAVE["save_config()"]
end
核心生命周期(4 个 @abstractmethod)
# agent/memory_provider.py:42-139
class MemoryProvider(ABC):
@property
@abstractmethod
def name(self) -> str: ... # "honcho", "mem0", ...
@abstractmethod
def is_available(self) -> bool: ... # 依赖检查
@abstractmethod
def initialize(self, session_id, **kwargs): ... # 连接/初始化
@abstractmethod
def system_prompt_block(self) -> str: ... # 注入 system prompt 的文本
数据流方法(默认空实现)
| 方法 | 调用时机 | 作用 |
|---|---|---|
prefetch(query) | 主循环前 | 根据用户消息预取相关记忆 |
queue_prefetch(query) | 主循环后 | 为下一轮预取排队(后台) |
sync_turn(user, assistant) | 主循环后 | 将本轮对话同步到记忆存储 |
shutdown() | session 结束 | 清理连接 |
事件钩子(可选)
| 钩子 | 触发时机 | 用途 |
|---|---|---|
on_turn_start(turn, message) | 每轮对话开始 | 每轮 tick(如更新用户模型) |
on_session_end(messages) | session 结束 | 最终记忆归档 |
on_pre_compress(messages) | 压缩前 | 在消息被压缩丢弃前提取信息 |
on_delegation(task, result) | 子代理完成 | 观察子代理工作 |
on_memory_write(action, target, content) | 内置记忆被修改 | 镜像内置记忆的写入 |
on_memory_write 值得特别说明:当 agent 通过内置 memory 工具修改 MEMORY.md 时,外部 provider 会收到通知。这让 Honcho 可以将内置记忆的变更纳入自己的用户模型——两个系统保持同步。
MemoryManager:抽象目标与当前接线
MemoryManager(agent/memory_manager.py:72)的抽象目标是统一编排内置 provider 和一个外部 provider;agent/builtin_memory_provider.py 也已经为此准备好了适配层:
# agent/memory_manager.py:72-86
class MemoryManager:
def __init__(self):
self._providers: List[MemoryProvider] = []
self._tool_provider_map: Dict[str, MemoryProvider] = {}
def add_provider(self, provider: MemoryProvider):
self._providers.append(provider)
for schema in provider.get_tool_schemas():
self._tool_provider_map[schema["name"]] = provider
但当前主运行路径还没有完全收敛到这条抽象线上。run_agent.py 里的接线是双轨的:
- 内置记忆
MEMORY.md / USER.md仍由MemoryStore直接加载,并直接注入 system prompt MemoryManager当前主路径承接的是外部 provider:初始化、system prompt block、prefetch、tool routing、sync、压缩前钩子
换句话说,BuiltinMemoryProvider 更像是代码库已经存在的统一抽象方向,而不是当前运行时唯一的内置记忆接线方式。
Fan-out 容错
所有 fan-out 方法都对每个 provider 独立 try/except:
# agent/memory_manager.py:204(简化)
def sync_all(self, user_content, assistant_content, *, session_id=""):
for provider in self._providers:
try:
provider.sync_turn(user_content, assistant_content, session_id=session_id)
except Exception as exc:
logger.warning("Memory provider %s sync failed: %s", provider.name, exc)
一个 provider 失败不影响其他 provider,也不影响主对话流程。这是 graceful degradation 原则的直接应用。当前主路径通常只挂一个外部 provider,但 manager 的接口已经按多 provider 容错来设计。
为什么只允许一个外部 provider
设计上限制最多一个外部 provider。原因很直接:多个外部 provider 的 prefetch 结果可能冲突,tool schema 可能重名,成本也会线性增长。Hermes 的现实取舍不是“堆多个后端”,而是“保留一套内置记忆,再允许用户额外接一个最想要的外部记忆后端”。
BuiltinMemoryProvider 与当前主路径
agent/builtin_memory_provider.py(114 行)实现了最简单的记忆方案:
MEMORY.md:agent 的持久记忆(用户偏好、环境细节、工具经验)USER.md:用户画像(角色、背景、习惯)
需要特别注意:当前主路径里,真正负责读写这两份 Markdown 的仍是 MemoryStore(tools/memory_tool.py),不是 BuiltinMemoryProvider。 也就是说,这个 provider 已经存在,但运行时还没有完全把内置记忆切到 provider 管理器里。现阶段已经落地的桥接点主要有两处:
- system prompt 仍由
run_agent.py直接从MemoryStore读取并拼装 - 当内置
memory工具写入 MEMORY.md / USER.md 时,run_agent.py会调用MemoryManager.on_memory_write()通知外部 provider
这种状态反映的是一次增量重构中的中间形态:抽象已经建立,统一接线尚未彻底完成。无论是旧路径还是抽象路径,字符数上限仍由 memory_char_limit=2200、user_char_limit=1375 控制。
8 个外部 Provider
| Provider | 方案 | 特色 |
|---|---|---|
| Honcho | Dialectic 用户建模 | Plastic Labs 的对话式用户模型,自动生成用户画像 |
| Hindsight | 时序滑动窗口 | 按时间衰减的记忆,近期对话权重高 |
| Mem0 | 向量数据库 | Qdrant 后端,语义相似度检索 |
| Holographic | 压缩全息存储 | 将对话压缩为高密度表示 |
| OpenViking | 语义嵌入 | 嵌入向量驱动的语义记忆 |
| RetainDB | 保留策略 | 可配置的记忆保留规则 |
| SuperMemory | 多源聚合 | 聚合多个来源的记忆 |
| ByteRover | 替代向量存储 | 轻量级向量存储方案 |
每个 provider 实现 MemoryProvider ABC 的对应方法。以 Honcho 为例:它的 prefetch() 返回用户的对话式画像,sync_turn() 将每轮对话提交给 Honcho API 更新用户模型,on_memory_write() 将内置记忆的变更同步到 Honcho 的知识图谱。
设计启示
Memory Provider 系统展示了可插拔架构的三个关键决策:
- ABC 定义生命周期而非行为:17 个方法中只有 4 个是 abstract——其余 13 个有默认空实现,让 provider 只需实现自己关心的部分
- 增量收敛而非一次性替换:Hermes 先引入统一的 provider 抽象,再逐步把原有内置记忆逻辑向它收拢,这降低了重构风险
- Mirror hook:
on_memory_write让内置记忆和外部 provider 可以保持同步,即使在“双轨接线”阶段也能减少两套记忆系统的分歧
第 12 章将分析上下文压缩——当对话太长时,如何在不丢失关键信息的情况下缩减消息历史。
设计赌注回扣:Memory Provider 是 Personal Long-Term 赌注的核心基础设施。Honcho 的用户建模让 agent "越用越懂你"从理念变为可测量的工程实现。
版本演化说明
本章核心分析基于 Hermes Agent v0.8.0(2026 年 4 月)。
MemoryProvider/MemoryManager这套可插拔抽象可以明确定位到 v0.7.0 发布窗口。它进入主线之后,provider hook 和外部记忆后端的生态在 v0.7.0-v0.8.0 之间快速成型。