Hermes Memory 源码深潜:受限自管理、快照隔离与事务化巩固
Agent Memory Engineering 专栏 · 3/6。 Hermes 的价值不在于“用 Markdown 保存记忆”,而在于它把 Agent 自主管理 Memory 变成一个受容量、并发、漂移、安全、审批和事务语义约束的 Tool Protocol。
专栏导航:总览与选型地图 · Codex · Claude Code · Hermes · Mem0 · Letta · Graphiti
0. 源码基线与阅读方法
本文资料于 2026-07-15 联网复核,源码固定到 NousResearch/hermes-agent Commit 77d5b2d573f82fc514fbef02f0b6303c44149805。[H1]
核心文件是 tools/memory_tool.py。本文按真实调用关系阅读:
MEMORY_SCHEMA↓memory_tool()↓Write Approval Gate↓MemoryStore.add / replace / remove / apply_batch↓File Lock + Re-read + Drift Check↓Atomic Temp Write + Replace↓Future Session Frozen Snapshot源码引用只保留关键签名;较长流程使用结构化还原。本文提出的分布式锁、审计日志、跨机器同步等内容会明确标为生产化补强,不冒充 Hermes 已实现能力。
0.1 十一个研究问题
- 为什么把 User Profile 与 Agent Experience 分开?
- Frozen Snapshot 和 Live State 为什么必须并存?
- 为什么预算使用字符而不是 Token?
- 容量满时为什么拒绝 Add,而不是后台静默摘要?
add为什么可以跳过一部分 Drift Guard?replace/remove为什么必须在锁内重读?- Atomic Rename 解决了哪一个并发窗口?
apply_batch为什么只检查最终状态预算?- Threat Scan 为什么既发生在写入时,也发生在启动加载时?
- Write Approval 如何把“Agent 想写”与“系统允许写”分开?
- 为什么 Memory 失败不能阻塞用户主任务?
1. Hermes 的核心路线:Bounded Agent-controlled Memory
Hermes 把 Memory Mutation 放进 Agent Action Space。Agent 可以调用同一个 Tool 执行:[H12]
addreplaceremoveapply_batch但“Agent-controlled”不等于“无限自治”。源码同时施加:
- 目标白名单;
- 容量上限;
- Exact Duplicate 幂等;
- Threat Pattern 扫描;
- File Lock;
- External Drift 检测;
- 原子文件替换;
- 可选写审批;
- 单 Turn 巩固失败上限。
因此更准确的定义是:
Agent 提议并执行 Memory Mutation,Runtime 负责约束、验证、持久化和失败语义。
1.1 两个 Target:User Model 与 Agent Experience
| Target | 文件 | 默认预算 | 语义 |
|---|---|---|---|
user | USER.md | 1,375 chars | 用户身份、角色、偏好、沟通风格 |
memory | MEMORY.md | 2,200 chars | 环境事实、约定、工具陷阱、任务经验 |
MEMORY_SCHEMA 的描述还给出一条重要分类规则:[H2]
user preference / correction > environment fact > procedure同时明确:可复用 Procedure 更适合进入 Skill,临时任务进度更适合 Session Search,而不是长期 Memory。
这说明 Hermes 并不是只有一个长期文本框,而是隐含了一个写入 Router:
Candidate├── Who the user is → USER.md├── Stable environment lesson → MEMORY.md├── Reusable procedure → Skill├── Temporary task progress → Session state / search└── Trivial rediscoverable fact → Drop1.2 为什么 Scope Separation 很重要
如果把用户偏好和 Agent 技术经验混在一个列表中,会出现:
- 用户隐私和工程经验共享同一保留策略;
- “回答风格”与“构建命令”竞争同一个预算;
- 删除用户 Profile 可能误删 Agent 经验;
- Prompt Builder 无法给两类信息不同标签。
两个文件并不是完美的数据模型,但它至少让 Ownership 和用途显式化。
2. 双状态模型:Frozen Snapshot 与 Live State
MemoryStore 的类注释直接定义两套平行状态:[H3]
_system_prompt_snapshot→ load_from_disk() 时生成→ 本 Session 内不再变化→ 用于系统提示词注入
memory_entries / user_entries→ Tool 调用实时修改→ 每次 Mutation 持久化到磁盘→ Tool Response 的 Usage / Entry Count 从 Live State 计算2.1 为什么写成功后当前 Agent 仍不应该“假装已经看到”
如果同一 Turn 修改系统提示词前缀:
Turn 1 Prefix = P1Turn 2 Prefix = P2Turn 3 Prefix = P3会破坏 Prefix Cache 稳定性,也会让同一 Session 的行为规则悄然变化。
Hermes 选择:
Session Start→ Capture Frozen Snapshot S0
Memory Tool writes S1, S2...→ Persist for future sessions→ Current system prompt remains S0这接近数据库 Snapshot Isolation:当前会话读取一个稳定快照,写入对未来会话可见。
2.2 Read-your-own-write 的语义被拆开
Hermes 不是完全没有 Read-your-own-write:成功响应会返回 Live State 计算出的 Usage、Entry Count 和完成标记;需要模型修复调用的失败响应才会按需返回 current_entries。Agent 因此知道修改是否落盘以及剩余预算,但系统提示词注入不会随写入重建。
因此应区分:
Tool-level visibility≠System-prompt snapshot visibility这是一个很成熟的 Context Engineering 取舍:一致的 Prompt Prefix 优先于“每次写完立刻改系统提示词”。
2.3 状态转换图
stateDiagram-v2 [*] --> LoadDisk LoadDisk --> Sanitize Sanitize --> FrozenSnapshot LoadDisk --> LiveState LiveState --> LockedReload: memory tool mutation LockedReload --> Reject: threat / drift / budget / match failure LockedReload --> Commit: validation success Commit --> LiveState: atomic replace FrozenSnapshot --> [*]: session ends LiveState --> LoadDisk: next session3. 启动 Read Path:去重、威胁扫描与可恢复隔离
load_from_disk() 依次完成:[H3]
- 创建 Memory Directory;
- 读取
MEMORY.md与USER.md; - 按首次出现顺序去重;
- 对每条 Entry 执行 Strict Threat Scan;
- 用安全文本构造 Frozen Snapshot;
- Live State 仍保留原始磁盘内容。
3.1 为什么 Threat Hit 不直接从磁盘删除
命中威胁模式的 Entry 在 Snapshot 中会被替换为类似:
[BLOCKED: MEMORY.md entry contained threat pattern(s): ...]但原始 Entry 保留在 Live State 和磁盘中,用户仍可以检查并删除。[H3]
这是一个值得学习的安全设计:
Prevent executionbut preserve evidence如果安全过滤器静默删除原文:
- 用户不知道发生过投毒;
- 无法复盘来源;
- 误报时无法恢复;
- 攻击证据被防线自己破坏。
3.2 Snapshot Sanitization 与 Write-time Scan 是两道不同防线
写入时扫描防止当前 Tool 把危险文本持久化;启动时扫描防止以下旁路:[H3][H4][H13]
- 用户手工编辑;
- Sister Session 写入;
- Patch / Shell 直接修改;
- Supply-chain 文件替换;
- 旧版本留下的危险内容。
只做 Write-time Scan 无法覆盖外部 Writer;只做 Load-time Scan 又会让危险内容长期留存。两者共同形成:
Mutation Gate+Consumption Gate4. Bounded Memory:容量是控制信号,不是存储异常
默认预算由 MemoryStore.__init__ 设置为:
MEMORY.md = 2,200 charactersUSER.md = 1,375 characters配置加载失败时回落到这两个默认值,避免 /memory 或无 Live Agent 的入口因配置不可用而完全失效。[H5]
4.1 为什么用 Character Budget
字符预算并不精确等于模型 Token,但它有四个工程优势:
- 与 Provider / Tokenizer 解耦;
- 文件长度可直接计算;
- 错误信息对人可读;
- 不需要在每次 Mutation 中加载 Tokenizer。
代价是不同语言的 Token Ratio 不同:2,200 个 ASCII 字符与 2,200 个中文字符的 Token 成本并不相同。
生产系统可以在保持字符 Hard Limit 的同时补充 Token Estimate:
hard_char_limit+ soft_token_warning+ measured_prompt_tokens4.2 容量满时故意 Fail Closed
add() 会先计算加入后的完整最终字符数。超过预算时不写入,而是返回当前 Entries、Usage 和巩固建议。[H6]
这比后台静默摘要更诚实:
Memory Full→ Agent must choose what to replace or remove→ Information loss becomes an explicit decision自动压缩如果没有可验证策略,可能把用户纠正、关键环境限制或少见但高风险经验一起删掉。
4.3 Capacity Pressure 是 Agent 的认知压力
当预算接近上限,Agent 应执行:
Deduplicate→ Merge overlapping lessons→ Remove stale entries→ Promote reusable procedure to Skill→ Add new high-value fact容量限制迫使 Agent 从“日志追加器”转向 Curator。
5. Single Mutation:Add、Replace、Remove 不是同一种写路径
5.1 add:Append Semantics
add() 在进入锁之前完成空内容与 Threat Scan;进入锁后重新读取磁盘,Exact Duplicate 直接返回成功,不重复写入。[H6]
关键点是:
_reload_target(target, skip_drift=True)为什么可以弱化 Drift Guard?
因为 Add 的逻辑是:
Re-read latest entries→ append one new entry→ preserve all existing parsed entries它不需要定位并重写某一条旧 Entry,因此外部内容被覆盖的风险低于 Replace/Remove。
但“低于”不等于“没有”。如果外部文件完全不符合 Entry Delimiter 结构,重新序列化仍可能改变格式。Hermes 当前策略是针对已知事故窗口做务实取舍,而不是通用无损文本编辑器。
5.2 replace:Targeted Rewrite
Replace 要求:
old_text非空;new_content非空;- 新内容通过 Threat Scan;
- 锁内 Drift Check 通过;
old_text命中至少一条;- 多个不同 Entry 命中时拒绝歧义;
- 替换后的最终状态不超预算。
如果 Structured-output Client 漏掉 old_text,Tool 不只返回“参数缺失”,还回传当前 Entries 和明确的重试说明,让模型能选择唯一子串再次调用。[H7]
这是 Tool Protocol 的一个重要原则:
错误响应应该携带完成修复所需的最小状态,而不是只给一个死胡同错误码。
5.3 remove:Deletion 也需要唯一定位
Remove 同样使用子串匹配。如果一个子串命中多个不同 Entry,则拒绝删除并返回 Preview,防止一个含糊的 old_text 批量破坏 Memory。[H6]
这相当于乐观的人机协议:Agent 必须先观察当前状态,再给出足够精确的 Mutation Selector。
6. 并发控制:File Lock、锁内重读与 Atomic Replace
Hermes 使用独立 .lock 文件获取排他锁,使目标 Memory 文件仍可以通过 os.replace() 原子切换。[H8]
这个保证有明确的平台前提:_file_lock() 在 Unix 使用 fcntl,在 Windows 使用 msvcrt;如果两者都无法导入,固定版本会直接执行无锁路径。因此部署到非标准 Runtime 或共享文件系统时,必须把“排他锁是否真实生效”作为启动检查,而不能只看到 .lock 文件就假定并发安全。
完整写路径是:
Acquire target.lock→ Re-read latest disk state→ Detect drift when required→ Apply mutation in memory→ Write temp file in same directory→ flush + fsync→ atomic_replace(temp, target)→ Release lock6.1 为什么锁必须覆盖 Re-read
错误方式:
Read v1→ wait for lock→ write mutation(v1)在等待期间其他 Session 可能已经写入 v2,当前 Writer 仍会覆盖它。
正确方式:
Acquire lock→ Read latest v2→ Mutate v2→ Commit v36.2 为什么不用 open("w") + flock
源码注释指出,旧式 open("w") 会在获取锁之前截断文件,读者可能看到短暂空文件。[H8]
Temp File + Atomic Rename 保证无锁 Reader 看到:
old complete fileornew complete file而不是半写入或空文件。
6.3 Atomic Rename 不等于跨进程完整事务
它保证单文件替换原子性,但不自动保证:
- 跨两个 Target 的事务;
- Network Filesystem 上完全一致的语义;
- 进程 Crash 后所有目录元数据已持久化;
- 多机器共享目录中的分布式锁;
- Mutation Audit 与文件提交原子一致。
这些属于生产部署时必须重新验证的 Filesystem Contract。
7. Drift Detection:保护无法 Round-trip 的外部内容
_detect_external_drift() 使用两个信号:[H9]
- 将文件按 Entry Delimiter 解析后重新序列化,
raw.strip()与重建文本不一致; - 某一条 Entry 长度超过整个 Target 的字符预算。
第二个信号很聪明:Tool 自己绝不可能写入比全文件预算还大的单条 Entry,因此一旦出现,强烈暗示外部 Writer 写入了不符合 Tool Shape 的自由文本。
7.1 Drift 时为什么先 Backup 再 Abort
检测到 Drift 后:
Read raw text→ write timestamped .bak→ return backup path→ reject replace/remove这体现:
Potential Data Loss 时 Fail Closed,并保留恢复副本。
如果 Backup 自身失败,返回值会明确标记 BACKUP FAILED,而不是假装已经安全保存。
7.2 Drift Guard 的边界
Round-trip 相等只能证明格式可往返,不证明内容语义正确;Entry 未超长也不能证明它来自可信 Writer。
因此还需要:
format drift detection+ source provenance+ content threat scan+ mutation audit8. apply_batch:把 Memory Consolidation 变成单文件事务
apply_batch(target, operations) 在同一 Target 上执行 Add / Replace / Remove 序列,并对操作校验、最终预算检查与文件提交提供 All-or-nothing 语义。[H10]
8.1 为什么只检查 Final Budget
假设当前已满:
Current = 2,200 / 2,200计划:
Remove stale 500 charsAdd valuable 300 charsFinal = 2,000逐操作提交会先失败或暴露中间状态。Batch 在 Working Copy 上执行全部 Operation,只对最终状态检查预算:
working = copy(current_entries)for op in operations: validate_and_apply(working, op)
if chars(working) > limit: abort()else: atomic_write(working)8.2 Transaction Boundary
Batch 保证的是:
One Target+ One Lock+ One Working Copy+ One Final Budget Check+ One Atomic File Replace它不保证 USER.md 与 MEMORY.md 跨文件原子,也不包含外部 Skill Store。
这里的事务边界还不包括进程内状态回滚:固定版本先用 _set_entries() 更新 Live State,再调用 save_to_disk()。如果文件写入抛出异常,正式文件仍受 Atomic Replace 保护而保持旧完整版本,但当前 MemoryStore 对象可能短暂领先于磁盘,直到下一次锁内 Reload。因而它是单文件提交协议,不是涵盖 RAM、磁盘与 Crash Recovery 的数据库事务。
8.3 幂等与歧义
- Batch Add 遇到 Exact Duplicate 会跳过;
- Replace/Remove 无命中时整个 Batch 失败;
- 一个 Selector 命中多个不同 Entry 时整个 Batch 失败;
- 任一新内容命中 Threat Pattern,
MemoryStore.apply_batch()会在触碰USER.md/MEMORY.md目标文件前拒绝整个 Batch。
这让 Memory Mutation 更接近事务命令,而不是一串 Best-effort Tool Calls。
9. Write Approval Gate:Agent 意图与持久化权限分离
Single Operation 在必填字段校验后、Batch 在列表形状校验后,都会在真正调用 Store Mutation 前经过可选 Write Gate。[H11]
决策有三种:
allow → 直接进入 Storeblocked → 返回错误,不写入stage → 生成 Pending Record,等待批准Staged Payload 会记录 Action、Target、Content / Old Text、摘要和 Origin。批准后 apply_memory_pending() 绕过 Gate,直接重放到 Store,但仍会走 Store 自身的锁、预算、漂移和 Threat Validation。[H11]
这里还有一个容易遗漏的信任边界:Gate 位于 Store Threat Scan 之前,stage_write() 会先把原始提案写入 <Hermes home>/pending/memory/*.json。Pending 内容不会进入 Memory Snapshot,批准重放时也会再次经过 Store 扫描,但 Pending Directory 仍可能暂存恶意文本、Secret 或个人信息,必须有独立的文件权限、Retention 与清理策略。[H11]
固定版本的 Pending 持久化还是 Best-effort:写盘失败时只记录日志,stage_write() 仍返回 Record,前台因而可能得到“已暂存”的 ID,实际却无法再次读取。这个取舍是安全 Fail-closed(目标 Memory 没有写入),但不是可靠队列;生产实现应在返回成功前验证 Pending Record 可读。
9.1 为什么审批必须位于 Store 之前
如果先写磁盘再审批,就只剩撤销,不是批准;持久化 Prompt Injection 已经可能影响其他 Session。
正确顺序:
Agent proposes mutation→ Policy evaluates→ Optional human approval→ Store validates current state→ Commit9.2 当前 Fail-open 取舍
固定版本源码中,如果 Write Approval Module 无法 Import,Gate 会 Fail Open,以避免所有 Memory 写入被基础设施故障阻塞。[H11]
这是可用性取舍,不是普适安全答案。高合规环境更可能要求:
approval infrastructure unavailable→ fail closed for sensitive targets→ allow only low-risk local target策略应由 Risk Tier 决定,而不是所有环境共享一个默认值。
10. Memory Side Effect 不能阻塞用户主回复
容量超限、Replace 无命中或 Batch 失败会鼓励模型修正后重试。但无限重试会耗尽 Turn Budget,导致用户本来要的答案被 Memory Maintenance 阻塞。
Hermes 用 _MAX_CONSOLIDATION_FAILURES_PER_TURN = 3 限制同一 Turn 内连续失败的自修复窗口。[H3]
前三次连续失败仍返回可重试响应;第四次失败才返回 Terminal 语义:
success = falsedone = truestop retrying memory callscontinue replying to the user10.1 Side-effect Priority
Primary effect: answer user / complete taskSecondary effect: improve future memory除非用户请求本身就是管理 Memory,否则 Secondary Effect 失败不应取消 Primary Effect。
10.2 重置边界
计数器在 Turn 开始时通过 reset_consolidation_failures() 清零,任意一次成功写入也会将其清零。[H3][H7] 因此它限制的是同一 Turn 中没有取得进展的连续失败,不会把间歇性错误或历史失败累积成永久不可写。
11. Prompt ABI:Memory Tool Response 也是控制协议
Memory Tool 不只返回 success: true/false。成功响应会携带:[H7]
- Target;
- Usage;
- Entry Count;
done=true;- “写入已完成、不要重复”的终止说明。
成功响应故意不回传完整 Entries,避免模型看到清单后继续寻找并重复执行不必要的巩固操作。完整状态只在模型确实需要修复调用时按需进入失败响应。
失败响应会尽量携带:
- 当前 Entries;
- 哪一步失败;
- 唯一 Selector 需要什么;
- 是否可在本 Turn 重试;
- 是否必须停止重试。
这是一种 Prompt ABI:它要让模型能从错误中构造下一次合法调用。
11.1 Schema Description 是 Policy Prompt
MEMORY_SCHEMA.description 包含:
- 何时保存;
- 优先级;
- 什么不保存;
- Target 如何选择;
- 满容量时使用 Batch;
- Procedure 应进入 Skill。
也就是说,Tool Schema 同时承载:
API Contract+ Memory Write Policy+ Recovery Playbook这提高模型自主性,但也意味着 Schema 改动属于行为变更,应该像 Prompt 与 API 一样做 Regression Eval。
12. 故障窗口与生产化补强
12.1 Process Crash
Temp File 写入后、Atomic Replace 前 Crash,会留下临时文件,但正式文件保持旧完整版本。需要周期清理 .mem_*.tmp。
12.2 Backup Accumulation
外部 Drift 每次都可能生成 Timestamped Backup。没有 Retention Policy 时,恢复文件会无限增长,也可能长期保留用户敏感内容。
12.3 Cross-target Transaction
同时修改 USER 与 MEMORY 时没有跨文件事务。可以通过:
Mutation Journal→ target A commit→ target B commit→ mark journal committed→ recover incomplete journal补强,但这不是固定版本已提供能力。
12.4 Audit Log
建议记录:
{ "mutation_id": "...", "session_id": "...", "target": "memory", "action": "batch", "before_hash": "...", "after_hash": "...", "origin": "agent|user|approval", "policy_decision": "allow|stage|block", "committed_at": "..."}文件 Diff 很透明,但没有 Immutable Audit 时无法证明谁在何时通过哪条路径修改。
12.5 Privacy
USER.md 可能包含身份、偏好和个人信息。需要独立考虑:
- 存储权限;
- Backup Retention;
- 日志脱敏;
- 用户删除请求;
- Snapshot、缓存、Pending Record 与临时文件同步删除;
- 多用户机器隔离。
13. 测试与可观测性
13.1 必测状态机
Empty → Add → Duplicate AddFull → Add Rejected → Batch ConsolidateExternal Drift → Replace Rejected + BackupThreat Write → Rejected Before LockPoisoned Disk → Snapshot Placeholder + Raw PreservedConcurrent Sessions → No Lost UpdateFour Consecutive Failures → Terminal done=trueStaged Write → Approve → Store Validation → Commit13.2 Crash Test
在以下位置注入进程终止:
- Temp File 创建后;
fsync前后;- Atomic Replace 前后;
- Backup 写入期间;
- Pending Approval 重放期间。
验证正式文件始终是旧完整版本或新完整版本,不能出现空文件与半 Entry。
13.3 指标
memory_usage_ratio{target}memory_mutation_total{action,result}memory_consolidation_failure_totalmemory_terminal_skip_totalmemory_drift_detected_totalmemory_threat_block_total{pattern}memory_pending_total{decision}memory_lock_wait_msmemory_write_latency_msmemory_backup_count13.4 Eval 不能只测“记住了吗”
至少拆成:
| Eval | 问题 |
|---|---|
| Write Precision | 写入的是否真值得未来复用? |
| Target Accuracy | USER / MEMORY / Skill 是否路由正确? |
| Consolidation Quality | 压缩后是否保留关键约束? |
| Snapshot Safety | 恶意 Entry 是否被隔离但保留证据? |
| Mutation Correctness | 并发与 Drift 下是否丢数据? |
| Future Utility | 后续任务是否少重复询问、少犯同类错误? |
14. Hermes 路线的优势与边界
14.1 优势
- Memory Mutation 进入 Agent Action Space;
- 强制容量使 Agent 主动 Curate;
- Frozen Snapshot 保持 Session Prefix 稳定;
- Live State 与未来会话持久化分离;
- 锁内重读、Atomic Replace 和 Drift Backup 降低数据损失;
- Batch 提供单 Target All-or-nothing Consolidation;
- Threat Scan 覆盖写入与消费两条路径;
- Terminal Failure 保护用户主回复。
14.2 边界
- Character Limit 对不同语言 Token 成本不一致;
- 两个文件无法表达复杂时间冲突与实体关系;
- 单机 File Lock 不等于分布式一致性;
- 缺少跨 Target Transaction;
- Snapshot 中的 Memory 默认 Always-on,会持续占用 Context;
- 自动写入质量仍受模型判断影响;
- Threat Pattern 是规则检测,无法证明语义无害;
- Backup、临时文件与审计需要额外治理。
14.3 最准确的架构定义
Hermes Memory 不是:
Agent can edit MEMORY.md而是:
Bounded Agent-controlled Store+ Frozen Prompt Snapshot+ Live Mutation State+ Lock + Reload + Drift Guard+ Atomic Single-file Commit+ Transactional Batch Consolidation+ Write / Load Threat Gates+ Optional Approval Control Plane+ Turn-level Failure Circuit Breaker它最值得学习的结论是:
当 Memory 写入交给 Agent 后,系统必须把容量、失败、并发、安全和权限变成 Tool 的显式协议,而不能只依赖模型“谨慎一点”。
参考资料与源码
[H1] Hermes Agent 固定版本源码
- Repository:NousResearch/hermes-agent @
77d5b2d - 核心文件:tools/memory_tool.py
[H2] Memory Tool Schema 与路由策略
MEMORY_SCHEMA- 核对内容:Target、Batch 建议、Write Priority、Skip 条件、Skill 与 Session State 边界。
[H3] MemoryStore、冻结快照与失败上限
MemoryStore- 核对内容:双状态、2,200 / 1,375 字符预算、前三次连续失败可重试、第四次后 Terminal、加载时去重与 Snapshot Sanitization。
[H4] Threat Pattern Library
tools/threat_patterns.py- 核对内容:Strict Scope 扫描与 Pattern ID。
[H5] 无 Live Agent 的 Store 构造
[H6] Single Mutation
[H7] Tool Dispatcher 与可恢复错误
[H8] File Lock 与 Atomic Write
[H9] External Drift Detection
[H10] Atomic Batch Consolidation
[H11] Write Approval Gate
[H12] 官方 Persistent Memory 文档
- Persistent Memory
- 核对内容:Bounded Curated Memory、
MEMORY.md/USER.md、容量与 Agent-managed Mutation。