Hermes 的长期记忆不是「全量保存一切对话」,而是 分层存储 + 主动策展 + 容量硬限制 。会遗忘、会遗漏细节,而且 很多细节是刻意不记的。
一、记忆体系总览(四层)
text
┌─────────────────────────────────────────────────────────────────┐
│ Layer 1: 内置策展记忆 (MEMORY.md + USER.md) │
│ ~2200 + ~1375 字符,每 session 冻结注入 system prompt │
├─────────────────────────────────────────────────────────────────┤
│ Layer 2: 会话全量存储 (SQLite + FTS5, state.db) │
│ 所有对话原文保留,按需 session_search 检索(不占固定 prompt) │
├─────────────────────────────────────────────────────────────────┤
│ Layer 3: 程序性记忆 (Skills, SKILL.md) │
│ 工作流/方法论,体量大,按需 skill_view 加载 │
├─────────────────────────────────────────────────────────────────┤
│ Layer 4: 外部记忆 Provider (Honcho/Mem0/Hindsight 等,最多 1 个) │
│ 语义检索、用户建模、自动抽取,与内置记忆并行 │
└─────────────────────────────────────────────────────────────────┘
| 层级 | 存储位置 | 容量 | 注入方式 | 典型内容 |
|---|---|---|---|---|
| MEMORY.md | ~/.hermes/memories/ |
~2200 字符 | 每 session 开头 frozen snapshot | 环境、项目惯例、工具坑 |
| USER.md | 同上 | ~1375 字符 | 同上 | 用户偏好、沟通风格 |
| Session DB | ~/.hermes/state.db |
无上限 | session_search 按需 |
完整历史对话 |
| Skills | ~/.hermes/skills/ |
无硬限 | 索引在 prompt,正文按需加载 | 复杂工作流 |
| External Provider | 云端/本地 DB | 依 provider | prefetch → user message | 语义相关回忆 |
二、长期记忆如何实现?
1. 写入路径(四条)
路径 A:Agent 主动写(foreground)
模型在对话中调用 memory 工具:
text
memory(action="add", target="user", content="用户偏好 TypeScript")
memory(action="replace", target="memory", old_text="...", content="...")
memory(action="remove", target="memory", old_text="...")
写入后立即落盘,但 不会更新当前 session 的 system prompt(见下文 frozen snapshot)。
路径 B:后台自改进 review(background)
每 10 个 user turn (默认 memory.nudge_interval: 10)触发一次后台 review:
160:168:hermes-agent/agent/background_review.py
_MEMORY_REVIEW_PROMPT = (
"Review the conversation above and consider saving to memory if appropriate.\n\n"
"Focus on:\n"
"1. Has the user revealed things about themselves --- their persona, desires, "
"preferences, or personal details worth remembering?\n"
"2. Has the user expressed expectations about how you should behave, their work "
"style, or ways they want you to operate?\n\n"
"If something stands out, save it using the memory tool. "
"If nothing is worth saving, just say 'Nothing to save.' and stop."
)
Fork 一个子 Agent,白名单只有 memory + skill_manage,主对话结束后异步跑,不抢用户任务的注意力。
路径 C:外部 Provider 自动同步
每 turn 结束后(非 interrupted):
text
memory_manager.sync_all(user_msg, assistant_response) # 写入 provider
memory_manager.queue_prefetch_all(user_msg) # 为下轮预热检索
Session 结束或压缩前还会 commit_memory_session() → on_session_end() 做抽取。
路径 D:上下文压缩前的抢救
压缩丢弃中间消息前,调用 on_pre_compress(messages),让 provider 把即将丢失的内容提炼进外部记忆或压缩摘要。
2. 读取路径(三条)
读取 A:Session 启动时 frozen snapshot
text
load_from_disk() → 捕获 _system_prompt_snapshot → 注入 volatile tier
整个 session 内 字节级不变,保证 LLM prefix cache 稳定。
读取 B:每 turn 的 prefetch
text
memory_manager.prefetch_all(user_query) → 追加到当前 user message(非 system prompt)
Honcho 等 provider 做语义召回,只注入与当前 query 相关的片段。
读取 C:按需 session_search
Agent 主动搜历史:
text
session_search(query="auth refactor", limit=3)
session_search(session_id="...", around_message_id=12345)
FTS5 全文检索,不经过 LLM 摘要,返回原始消息。
3. 生命周期示意
#mermaid-svg-Cb1VJjddD4FDVVHe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Cb1VJjddD4FDVVHe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Cb1VJjddD4FDVVHe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Cb1VJjddD4FDVVHe .error-icon{fill:#552222;}#mermaid-svg-Cb1VJjddD4FDVVHe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Cb1VJjddD4FDVVHe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Cb1VJjddD4FDVVHe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Cb1VJjddD4FDVVHe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Cb1VJjddD4FDVVHe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Cb1VJjddD4FDVVHe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Cb1VJjddD4FDVVHe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Cb1VJjddD4FDVVHe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Cb1VJjddD4FDVVHe .marker.cross{stroke:#333333;}#mermaid-svg-Cb1VJjddD4FDVVHe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Cb1VJjddD4FDVVHe p{margin:0;}#mermaid-svg-Cb1VJjddD4FDVVHe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Cb1VJjddD4FDVVHe .cluster-label text{fill:#333;}#mermaid-svg-Cb1VJjddD4FDVVHe .cluster-label span{color:#333;}#mermaid-svg-Cb1VJjddD4FDVVHe .cluster-label span p{background-color:transparent;}#mermaid-svg-Cb1VJjddD4FDVVHe .label text,#mermaid-svg-Cb1VJjddD4FDVVHe span{fill:#333;color:#333;}#mermaid-svg-Cb1VJjddD4FDVVHe .node rect,#mermaid-svg-Cb1VJjddD4FDVVHe .node circle,#mermaid-svg-Cb1VJjddD4FDVVHe .node ellipse,#mermaid-svg-Cb1VJjddD4FDVVHe .node polygon,#mermaid-svg-Cb1VJjddD4FDVVHe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Cb1VJjddD4FDVVHe .rough-node .label text,#mermaid-svg-Cb1VJjddD4FDVVHe .node .label text,#mermaid-svg-Cb1VJjddD4FDVVHe .image-shape .label,#mermaid-svg-Cb1VJjddD4FDVVHe .icon-shape .label{text-anchor:middle;}#mermaid-svg-Cb1VJjddD4FDVVHe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Cb1VJjddD4FDVVHe .rough-node .label,#mermaid-svg-Cb1VJjddD4FDVVHe .node .label,#mermaid-svg-Cb1VJjddD4FDVVHe .image-shape .label,#mermaid-svg-Cb1VJjddD4FDVVHe .icon-shape .label{text-align:center;}#mermaid-svg-Cb1VJjddD4FDVVHe .node.clickable{cursor:pointer;}#mermaid-svg-Cb1VJjddD4FDVVHe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Cb1VJjddD4FDVVHe .arrowheadPath{fill:#333333;}#mermaid-svg-Cb1VJjddD4FDVVHe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Cb1VJjddD4FDVVHe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Cb1VJjddD4FDVVHe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Cb1VJjddD4FDVVHe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Cb1VJjddD4FDVVHe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Cb1VJjddD4FDVVHe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Cb1VJjddD4FDVVHe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Cb1VJjddD4FDVVHe .cluster text{fill:#333;}#mermaid-svg-Cb1VJjddD4FDVVHe .cluster span{color:#333;}#mermaid-svg-Cb1VJjddD4FDVVHe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Cb1VJjddD4FDVVHe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Cb1VJjddD4FDVVHe rect.text{fill:none;stroke-width:0;}#mermaid-svg-Cb1VJjddD4FDVVHe .icon-shape,#mermaid-svg-Cb1VJjddD4FDVVHe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Cb1VJjddD4FDVVHe .icon-shape p,#mermaid-svg-Cb1VJjddD4FDVVHe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Cb1VJjddD4FDVVHe .icon-shape .label rect,#mermaid-svg-Cb1VJjddD4FDVVHe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Cb1VJjddD4FDVVHe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Cb1VJjddD4FDVVHe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Cb1VJjddD4FDVVHe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
用户对话
Agent 是否调用 memory 工具?
写入 MEMORY.md / USER.md
可能不记
sync_all → 外部 Provider
每 10 turn?
Background Review 抽取
磁盘持久化
下个 Session 才进入 system prompt
SQLite 全量存储
session_search 按需召回
三、会遗忘吗?会遗漏细节吗?
会,而且多数是设计使然。
1. 容量硬限制 → 被迫遗忘/合并
| Store | 默认上限 | 典型条目数 |
|---|---|---|
| memory | 2200 字符 | 8--15 条 |
| user | 1375 字符 | 5--10 条 |
写满时 不会静默截断 ,而是返回错误,要求 Agent 用 replace/remove 合并或删除旧条目后再写。
旧信息被挤掉 = 主动遗忘。
2. 系统级「不该记什么」------刻意不记细节
MEMORY_GUIDANCE 和 memory 工具 schema 都明确写了 skip 规则:
151:171:hermes-agent/agent/prompt_builder.py
MEMORY_GUIDANCE = (
...
"Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO "
"state to memory; use session_search to recall those from past transcripts. "
"Specifically: do not record PR numbers, issue numbers, commit SHAs, 'fixed bug X', "
"'submitted PR Y', 'Phase N done', file counts, or any artifact that will be stale "
"in 7 days. If a fact will be stale in a week, it does not belong in memory. "
...
"Procedures and workflows belong in skills, not memory."
)
刻意不记的内容包括:
- 任务进度、完成日志、临时 TODO
- PR/Issue 号、commit SHA
- 7 天内会过期的信息
- 琐碎/可 Google 的事实
- 原始数据 dump
- 可复现的工作流(应进 Skill,不进 Memory)
3. Agent 可能「该记却没记」
记忆依赖 模型是否调用 memory 工具,不是自动全量抽取:
- 用户没明确说「记住这个」时,可能漏记
- Background review 可能返回
"Nothing to save." - 写满且 consolidate 失败(每 turn 最多 3 次)→ 直接 skip
memory.write_approval: true→ 写入需人工批准,未批准则不落盘
4. Frozen Snapshot → 同 session 内「看不见刚写的」
text
Session 开始: snapshot = load_from_disk()
Session 中: memory(action="add") → 磁盘更新,但 snapshot 不变
Session 内 LLM: 仍看 session 开始时的旧 memory
下个 Session: 重新 load_from_disk() → 新 memory 才进 prompt
同 session 内 Agent 写 memory 后,system prompt 里的 memory 块不会变;只有 tool 返回能看到最新状态。跨 turn 的「即时记忆更新」在 prompt 层是滞后的。
5. 上下文压缩 → 对话细节丢失
长对话 token 超限时,中间轮次被 LLM 摘要 替换:
- 保留首尾 N 轮
- 中间变成一段 summary(有损)
- 压缩前
on_pre_compress可抢救一部分到外部 memory - 压缩后
commit_memory_session()触发 session-end 抽取
未写入 MEMORY.md 也未被 provider 抽取的细节,压缩后就找不回来了 (除非还在 SQLite 原文里,需 session_search)。
6. 外部 Provider 也是选择性记忆
Honcho/Mem0 等会做语义抽取和去重,不是 verbatim 存储;sync_all 在 interrupted turn 时跳过,避免污染:
3389:3391:hermes-agent/run_agent.py
if interrupted:
return
四、三层记忆的职责分工
text
MEMORY.md / USER.md → 「永远该知道的关键事实」(极小、高信号)
session_search (SQLite) → 「某次对话里说过什么」(全量、按需、免费)
Skills → 「怎么做某类事」(流程、步骤、reference)
External Provider → 「跨 session 语义关联 + 用户建模」(智能但有损)
官方对比(memory.md):
| 持久 Memory | Session Search | |
|---|---|---|
| 容量 | ~1300 tokens 固定 | 无限 |
| 速度 | 每 turn 都在 prompt 里 | ~20ms FTS5 |
| 成本 | 占 prompt token | 按需、无 LLM 成本 |
| 适用 | 关键事实常驻 | 「上周讨论过 X 吗?」 |
五、记忆闭环如何运转
text
用户纠正/表达偏好
↓
MEMORY_GUIDANCE 引导 Agent 主动 memory(action="add")
↓
或每 10 turn → Background Review 抽取
↓
写入 MEMORY.md / USER.md(受字符上限约束,需合并旧条目)
↓
下个 Session → frozen snapshot 注入 system prompt
↓
用户不必重复纠正(若 Agent 记对了)
复杂工作流
↓
SKILLS_GUIDANCE → skill_manage 创建/修补 Skill
↓
Skills Index 在 prompt 里,正文按需 skill_view 加载
历史细节
↓
不写入 Memory(按设计)
↓
留在 SQLite → session_search 按需找回
六、结论
| 问题 | 答案 |
|---|---|
| 如何实现长期记忆? | 文件策展 (MEMORY/USER) + SQLite 全量 + Skills + 可选外部 Provider + 后台 review |
| 会遗忘吗? | 会:容量挤占、explicit remove、压缩摘要、7 天过期规则 |
| 会遗漏细节吗? | 会 :只记「 durable facts」,任务细节/进度/PR 号等 刻意不记 |
| 细节去哪了? | 完整对话在 SQLite,用 session_search 按需查;流程进 Skills |
| 是 bug 还是设计? | 主要是设计:Memory 是「高信号摘要」,不是 transcript 备份 |
如果关心的是「Hermes 会不会像人一样记住每次对话的所有细节」------不会 。它更像:小笔记本(MEMORY)+ 无限档案室(SQLite)+ 操作手册(Skills)+ 可选的长期语义记忆(Provider),并且 notebook 有严格页数限制,Agent 被明确 instruct 不要往 notebook 里塞细节。