Skip to content

Hermes-agent 源码笔记 —— memory #68

@zhuzhh

Description

@zhuzhh

memory机制至关重要,一起探究一下hermes的记忆是如何管理的

hermes的记忆类型?

分内部记忆和外部记忆

  • 内部记忆
    静态文件,memory.md、user.md两个文件,大小有限制,没有检索能力,初始化一次性加载,注入到prompt prefix cache,
  • 外部记忆
    第三方服务,honcho、mem0等,可以支持语义检索、向量检索、用户画像构建,每次用户输入 都会检查是否可以读入、可写,注入到 user message 尾部

什么时候读入记忆?

内部记忆:每次初始化时,只加载一次
外部记忆:

  • 每次用户输入 都会check一下是否需要读入(当然,这个检索频率是可以配置的);
  • 外部记忆支持很多种,honcho和mem0等,但是同时只能支持一次(不然多个记忆provider互相打架);
  • 优点是,可支持跨会话持久化(换设备也能访问,确保不会丢失)、语义检索、用户画像、团队多用户使用等(根据用户id存储对应记忆)
  • 初始阶段,provider的静态memory也会放进 system prompt

工作流程(以 Honcho 为例)

每轮对话开始
    ↓
memory_manager.on_turn_start()     # 通知 provider 新 turn 开始
    ↓
memory_manager.prefetch_all(query)  # 用当前用户消息做语义检索
    ↓
honcho.search(query) → 返回相关记忆片段
    ↓
结果注入到 user message 尾部(不破坏 prompt cache)
    ↓
模型生成回复
    ↓
memory_manager.sync_turn(user_msg, assistant_response)  # 异步写入对话

外部memory装载时,每次查询都是同步的吗?这耗时是怎样的呢?会不会影响响应速度呢?

不一定会,因为有预热以及缓存,存在阻塞的情况如下

完整的时序图

                    ┌─────────────────────────────────────────────────────────┐
                    │                    Session 初始化                         │
                    │  manager.prefetch_context() + 预热 dialectic (后台线程)   │
                    └─────────────────────────────────────────────────────────┘
                                              ↓
    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                              Turn 1                                          │
    │                                                                              │
    │  prefetch() 被调用                                                           │
    │      ↓                                                                       │
    │  Base Context: 首次同步获取 (缓存到 _base_context_cache)                      │
    │      ↓                                                                       │
    │  Dialectic: 检查预热结果是否已就绪                                            │
    │      - 就绪 → 直接使用                                                        │
    │      - 未就绪 → 启动后台线程 + join(timeout=8s)  ← 唯一可能阻塞点              │
    │      ↓                                                                       │
    │  返回结果 → 注入 user message                                                 │
    │                                                                              │
    │  Turn 结束 → queue_prefetch() → 后台线程预取 Turn 2 的 dialectic             │
    └─────────────────────────────────────────────────────────────────────────────┘
                                              ↓
    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                              Turn 2+                                         │
    │                                                                              │
    │  prefetch() 被调用                                                           │
    │      ↓                                                                       │
    │  Base Context: 从缓存读取 (_base_context_cache)                               │
    │      - 后台按 context_cadence 刷新缓存                                        │
    │      ↓                                                                       │
    │  Dialectic: 从缓存读取 (_prefetch_result)                                     │
    │      - 上一 turn 的 queue_prefetch 已预取好了                                 │
    │      - 不阻塞,直接返回                                                        │
    │      ↓                                                                       │
    │  返回结果 → 注入 user message                                                 │
    │                                                                              │
    │  Turn 结束 → queue_prefetch() → 后台线程预取下一 turn                         │
    └─────────────────────────────────────────────────────────────────────────────┘

即turn1可能会存在8s的阻塞,不过前置有预期,应该还好;
后续的turn都是从缓存里取,

外部记忆 检索频率控制机制

主流程是每个 turn 都会触发检索,但实际检索行为由 provider 控制

honcho 实现有两层

  • Base Context 用户画像 + peer card context_cadence 控制(默认每 turn)
  • Dialectic Supplement 语义检索 + 推理 dialectic_cadence 控制(默认每 turn)

什么时候写入记忆?

内部记忆

每隔一段时间(用户输入10次)提醒 llm 做一下检查,看看是否要写入记忆,模型调用 memory 工具,写入内容,等下次启动时生效
review 只是提醒模型检查,模型可以选择不写、写一点、写很多

外部记忆

每次用户输入结束后自动触发,后台任务执行

存了什么东西?

  • 内部记忆

  • 外部记忆
    记录的是用户的输入和llm的返回,不包含工具调用

如何评估是否对任务有用

解决了读取记忆、写入记忆,这些对任务完成是否有帮组呢?
如何验证呢?

  • 基准跑分
  • 红队攻击

出了问题如何优化呢?

如果基准跑分、红队攻击等手段发现有问题,我们有哪些手动解决对应的问题呢?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions