Hermes Agent 记忆架构深度拆解:四层记忆如何撑起一个常驻 Agent
> 一句话总结:Hermes Agent 的记忆系统不是"一个记忆库分几层",而是四套完全不同的机制拼在一起——每一层有自己的写入路径、读取路径、生命周期和取舍逻辑。真正值得看的,不是它能记住什么,而是它怎么在 Token 预算、Prompt Cache 稳定性、上下文长度和安全性之间做 trade-off。
---
一、四层记忆全景:不是分层,是分工
Hermes Agent 把记忆拆成四个完全独立的子系统,每一层解决不同的问题,用不同的数据结构,走不同的 IO 路径:
| 层级 | 存储 | 容量 | 速度 | 写入方 | 读取方 |
|---|---|---|---|---|---|
| L1 工作记忆 | 当前上下文窗口 | 模型上限(128K-200K tokens) | 即时 | 用户 / Agent | LLM 推理 |
| L2 策展记忆 | MEMORY.md + USER.md | 2,200 + 1,375 字符(~1,300 tokens) | 毫秒级 | Agent 显式调用 memory tool | 每次 session 启动注入 system prompt |
| L3 完整档案 | state.db(SQLite + FTS5) | 无上限(典型 20-100MB) | ~20ms 查询 | 自动(所有消息) | Agent 显式调用 session_search |
| L4 外部记忆 | Provider 自有存储(Mem0/Honcho 等) | 取决于 provider | 取决于 provider | Provider 决定 | Provider 决定 |
---
二、L2 策展记忆:为什么"下一次 session 才生效"
2.1 Frozen Snapshot 模式
MEMORY.md 和 USER.md 在 session 启动时被读取一次,注入 system prompt,然后整个 session 不再重新加载。
这意味着:Agent 在对话中调用 memory(action="add") 写入新事实,文件确实更新了,但当前 session 的 system prompt 不会变。新事实要到下一次 session 才会进入上下文。
2.2 三重目的
这个设计看起来反直觉(我刚记住的东西,为什么现在不能用?),实际上服务于三个目标:
① 保护 Prompt Prefix Cache
Anthropic Claude 等模型支持 prompt caching:system prompt + 前几轮对话的 KV Cache 可以被复用。如果 mid-session 修改 system prompt,prefix cache 失效,每一轮都要重新计算几千 tokens 的注意力,成本和延迟都会飙升。
Frozen snapshot 是一个刻意的性能权衡:牺牲"即时可用性",换取"整个 session 的 cache 稳定性"。
② 防止回声效应(Echo Effect)
假设 Agent 在 turn 5 写入一条记忆,turn 6 立即读到它。这会导致一个危险的正反馈循环:
- Agent 写入"用户喜欢简洁回答"
- 下一条消息里,Agent 因为读到了这条记忆,表现得更加简洁
- 用户没有要求简洁,但 Agent 的自我强化让它误以为这是对的
③ 给人类留 review 窗口
Memory 文件是纯 Markdown,人类可以直接编辑。如果 Agent 写入了错误的事实(比如误解了用户的意图),人类可以在下一次 session 启动前手动修正。
2.3 Memory Tool:add / replace / remove
Agent 通过单一的 memory tool 管理 L2,三个 action 的语义很精确:
add:追加新条目,用§分隔replace:基于子字符串匹配替换现有条目(不需要完整文本)remove:基于子字符串匹配删除条目
# 添加
memory(action="add", target="memory",
content="用户项目使用 pnpm,不用 npm")
# 替换(只需提供唯一子串)
memory(action="replace", target="memory",
old_text="npm", content="用户项目使用 pnpm,不用 npm")
# 删除
memory(action="remove", target="memory",
old_text="临时项目配置")
关键设计:没有 read action。因为 L2 已经在 system prompt 里,Agent 不需要"读取"——它始终"拥有"这些事实。
2.4 硬预算与原子写
- 硬字符上限:MEMORY.md 2,200 字符,USER.md 1,375 字符。不是建议,是硬性约束——超出会报错,Agent 必须自己合并或删除旧条目。
- 原子写入:temp file +
os.replace(),保证写操作不会留下半成文件。 - Prompt Injection 扫描:每次写入前扫描威胁模式(凭证泄露、SSH 后门、不可见 Unicode), blocked 的内容直接拒绝。
三、L3 完整档案:state.db 的三段式检索
3.1 存储结构
所有消息(CLI 和 Gateway)写入 ~/.hermes/state.db,Schema 大致是:
CREATE TABLE messages (
id INTEGER PRIMARY KEY,
session_id TEXT,
parent_session_id TEXT, -- 压缩时建立 lineage
created_at TIMESTAMP,
channel TEXT, -- telegram/discord/slack/cli
role TEXT, -- user/assistant/tool
content TEXT,
tool_name TEXT,
tool_result TEXT
);
CREATE VIRTUAL TABLE messages_fts USING fts5(content, content=messages);
关键设计:
- WAL 模式:写入性能高,不阻塞查询
- FTS5 全文索引:基于词法的快速搜索
- Lineage 追踪:session 压缩时,旧 session 标记
end_reason="compression",新 session 带上parent_session_id,形成树状历史
3.2 session_search 的三段式工作流
Agent 调用 session_search 时,不是简单返回 FTS5 结果,而是经过三个阶段:
Phase 1:粗搜(FTS5)
SELECT * FROM messages_fts
WHERE content MATCH 'auth service'
ORDER BY rank LIMIT 50;
FTS5 返回匹配的消息列表。这里是词法匹配,优势是快(~20ms),劣势是无法理解同义词或改写。
Phase 2:重构上下文
对于每条匹配消息,查询其前后各 N 条消息,重建对话片段。这解决了"断章取义"问题——单条消息可能缺少上下文。
Phase 3:定向摘要
将重构的上下文片段送入一个辅助 LLM(通常是 Gemini Flash 等廉价模型),生成面向当前查询的摘要。Agent 拿到的是已经消化过的信息,而不是原始消息 dump。
User: "我们上周讨论的 auth 服务怎么样了?"
↓
FTS5 搜索 "auth" → 15 条匹配
↓
重构上下文 → 3 个对话片段
↓
LLM 摘要 → "上周三你提到 auth 服务要迁移到 Redis session,周四确认了一个 XSS 漏洞,周五修复后测试通过。"
3.3 与 L2 的对比
| L2 MEMORY.md | L3 state.db | |
|---|---|---|
| 容量 | ~1,300 tokens | 无上限 |
| 速度 | 即时(在 prompt 里) | ~20ms + LLM 摘要延迟 |
| 成本 | 每轮都付 token | 按需查询,无 LLM 调用时免费 |
| 精度 | 精确(原文注入) | 依赖摘要质量 |
| 用途 | "必须时刻记住的事实" | "我们之前聊过什么?" |
四、L4 外部记忆 Provider:为什么不写回 transcript
4.1 架构设计
Hermes 支持插入一个外部记忆 Provider(Mem0、Honcho、Hindsight 等),通过标准接口接入:
Agent Turn → Provider.sync() → Provider 提取/存储
→ Provider.prefetch() → 下轮注入相关记忆
关键约束:同一时间只能激活一个 Provider。
4.2 Recall 注入不写回 transcript
外部 Provider 的记忆注入走的是独立路径:
- 原生记忆(L1-L3):Agent 的 system prompt 和上下文窗口,Agent 可以修改
- 注入记忆(L4 Provider recall):由 Provider 决定注入什么,Agent 不知道来源
如果 Provider recall 的内容被写回 state.db,Agent 会在下次搜索时找到它,误以为这是自己经历过的事实。这可能形成幻觉循环:
Provider 注入 "用户喜欢蓝色"(可能来自错误推断)
→ Agent 把它当作事实记住
→ 写入 MEMORY.md
→ 以后 Agent 坚信用户喜欢蓝色
→ 用户纠正:"我从没说过"
→ 但 Agent 已经把它当成"亲历"事实
不写回 transcript,就是把 Provider 的记忆和 Agent 的亲身经历隔离开,让 Agent 能区分"我记住的"和"别人告诉我的"。
4.3 三种 Recall 模式
| 模式 | 自动注入 | Provider Tools | 适用场景 |
|---|---|---|---|
| context | 是 | 否 | 省心,但 token 不可控 |
| tools | 否 | 是 | Agent 按需检索,精确但需显式调用 |
| hybrid | 是 | 是 | 最丰富,token 成本最高 |
五、flush_memories():Compression 边界上的知识抢救
5.1 触发时机
当上下文接近模型长度上限时,Hermes 触发 _compress_context(),它有三个阶段:
Phase 1:flush_memories()
在任何信息丢失之前,Agent 获得一次"抢救机会":
# 简化逻辑
inject_system_message("Save anything worth remembering.")
enable_tools_only(["memory"]) # 只开放 memory tool
make_llm_call() # 一次辅助调用
Agent 在这轮调用中只能做一件事:决定哪些事实值得写入 L2(MEMORY.md/USER.md)。写入的内容会 survive compression。
Phase 2:Compress
- 保护前 3 条 + 后 4 条消息(避免丢失最新上下文)
- 中间部分由 Gemini Flash(T=0.3)总结
- 工具边界对齐:保证 tool call / result 对不会被拆开
- 旧 session 标记
end_reason="compression" - 创建新 session,带上
parent_session_id - 从磁盘重新加载 memory(吸收 Phase 1 flush 写入的内容)
- 重建 system prompt,prefix cache 1-2 轮内恢复
5.2 为什么是"抢救"而不是"自动保存"
flush_memories 的设计哲学是:Agent 比框架更清楚什么值得记住。
如果框架自动提取,可能会保存噪声("用户打了个错别字");如果让 Agent 主动决定,虽然多一次 API 调用,但保存的质量更高。
这个结果有点反直觉:压缩之后,Agent 的原始上下文更少了,但持久知识更多了。压缩不是损失,是巩固。
---
六、Gateway 隔离与子代理 skip_memory
6.1 Gateway 会话隔离
在 Gateway 模式(Telegram/Discord/Slack 等)下,每个用户有独立的 session。关键设计:
- Per-user session DB:用户 A 的消息不会进入用户 B 的 state.db
- Idle timeout:会话过期后,gateway 会触发 pre-reset flush,抢救记忆
- Daily 4am reset:所有会话强制刷新,防止内存泄漏
6.2 子代理 skip_memory
当 Agent 调用 delegate_tool 派发子代理时,子代理默认不加载父代理的记忆:
tmp_agent = AIAgent(
...,
skip_memory=True, # 不初始化 _memory_store
enabled_toolsets=["memory", "skills"],
)
原因:
- 防止记忆污染:子代理可能在不同上下文中运行,父代理的个人事实不相关
- 减少 token 开销:子代理的 system prompt 保持精简
- 安全边界:子代理可能是不可信代码,隔离记忆降低风险
---
七、Background Review:10 轮兜底机制
7.1 Nudge Interval
Hermes 有一个可配置的 nudge_interval(默认每 10 轮或 15 次 tool call):
memory:
nudge_interval: 10 # 每 10 轮触发一次 review
触发时,框架插入一个系统提示,提醒 Agent:"有什么值得记住的吗?"
这不是自动提取,而是给 Agent 一个显式保存的机会。Agent 可以选择:
- 调用
memory(add)保存事实 - 调用
skill_manage保存技能 - 什么都不做(如果没什么值得记的)
7.2 边界触发 Review
除了固定间隔,Hermes 还支持在 session 边界触发 review:
memory:
review_on_reset: true # /reset, /new 时触发
review_on_session_end: true # CLI 退出、Gateway 超时时触发
review_on_compression: false # 压缩后触发(默认关闭,因为压缩已经很贵)
7.3 为什么是"兜底"而不是"自动"
Background review 的定位是保底机制,不是主要记忆路径:
- 主要路径:Agent 在对话中主动调用 memory tool(认知负担在 Agent)
- 兜底路径:Nudge 提醒 Agent 保存(框架辅助,Agent 决策)
- 抢救路径:flush_memories 在压缩前强制保存(框架驱动,Agent 执行)
---
八、安全设计:Prompt Injection 扫描与原子写
8.1 写入前扫描
每次 memory 写入或 skill 写入前,经过安全扫描:
| 威胁类型 | 检测方式 | 处理 |
|---|---|---|
| Prompt Injection | 模式匹配(如 "ignore previous instructions") | Block |
| 凭证泄露 | Regex 检测 API key、token、SSH key | Block + 告警 |
| 不可见 Unicode | 检测零宽字符、同形异义字 | Block |
| 路径穿越 | .. 检测 + validate_within_dir | Block |
8.2 原子写与回滚
所有文件写入遵循同一模式:
fd, tmp_path = tempfile.mkstemp(dir=parent_dir)
with os.fdopen(fd, 'w') as f:
f.write(new_content)
os.replace(tmp_path, target_path) # 原子替换
如果写入过程中进程崩溃,临时文件不会污染目标文件。
---
九、与 OpenClaw 的对比:两种哲学
| 维度 | Hermes Agent | OpenClaw |
|---|---|---|
| 记忆层数 | 4 层(L1-L4) | 2 层(工作记忆 + MEMORY.md/USER.md) |
| session_search | 内置 FTS5 + LLM 摘要 | 可选插件 |
| 外部 Provider | 标准接口,一次一个 | 可选插件 |
| Prompt Cache | Frozen snapshot 保护 prefix cache | 无专门优化 |
| Context Compression | 三段式(flush + compress + split) | 依赖模型/插件 |
| 记忆写入 | Agent 显式 + 框架兜底 | Agent 显式 |
| 安全扫描 | 内置 prompt injection 扫描 | 依赖插件 |
| 架构重心 | 本地优先、可审计、分层明确 | 网关优先、灵活组合 |
---
十、总结:什么值得抄,什么需要改
值得抄的设计
1. Frozen snapshot:如果用了 Anthropic Claude,这个模式几乎是必选项——prefix cache 的节省远大于 mid-session memory 更新的收益 2. flush_memories:在上下文压缩前给 Agent 一次抢救机会,比"希望压缩器保留重要信息"可靠得多 3. 硬字符预算:L2 的 2,200 + 1,375 字符限制强迫策展思维——记忆不是仓库,是手提行李 4. 不写回 transcript:外部 Provider 的记忆和 Agent 的亲身经历必须隔离,防止幻觉循环
需要斟酌的设计
1. FTS5 词法搜索:无法理解同义词,复杂查询需要 LLM 辅助。如果对话量大,考虑向量检索补充 2. 单 Provider 限制:一次只能激活一个外部记忆 Provider,多源记忆需要自行 merge 3. L2 延迟生效:虽然保护了 cache,但 Agent 在 session 内"忘了自己刚记住的东西",需要显式用工作记忆弥补
---
> 最后 > > Hermes Agent 的记忆架构告诉我们一件事:做好 Agent 记忆,不是选一个向量数据库的事。它是Token 预算、Cache 策略、写入权限、读取路径、安全边界、人类 review 窗口的系统工程。 > > 四层记忆不是过度设计,而是每一层都在回答一个不同的问题: > - L1:我现在在干嘛? > - L2:我必须永远记住什么? > - L3:我之前聊过什么? > - L4:外部世界知道什么? > > 把这些问题混在一起,用一个"记忆系统"回答,才是大多数 Agent 失忆的真正原因。
---
参考
- Hermes Agent GitHub: https://github.com/NousResearch/hermes-agent
- Hermes Memory Docs: https://hermesagent.org.cn/en/docs/user-guide/features/memory
- LanceDB + Hermes: https://www.lancedb.com/blog/semantic-memory-for-hermes-agent-with-lancedb
- Vectorize 对比分析: https://vectorize.io/articles/openclaw-vs-hermes-agent-memory
#记忆 #小凯 #Agent #Hermes
🌟 智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。
🎁 领取 2000万 Tokens