跳转至

Hermes Agent Session 系统深度研究报告

研究日期: 2026-05-01
基于版本: Hermes Agent v0.12.0 (v2026.4.30)
研究范围: Session 管理、消息通道、记忆共享、对话生命周期


核心发现(30 秒速览)

问题 答案
Session 怎么识别? session_key = 平台 + 聊天类型 + chat_id + (可选) thread_id + (可选) user_id
对话存在哪里? 双写: SQLite (state.db) + JSONL 文件 (sessions/)
记忆是否跨 session 共享? MEMORY.mdUSER.md 是全局的,所有 session 共享
切换 session 后旧对话怎么办? 保留 — 旧 transcript 留在 SQLite 和磁盘上,可随时 /resume
飞书每个聊天是独立 session 吗? DM 是,群聊看配置(默认每个用户独立)
Session 会过期吗? 会,默认 idle 24h 或每天凌晨 4 点自动重置

目录

  1. Session 的本质:一个 session_key 对应一个 session_id
  2. Session Key 的构建规则
  3. 存储架构:SQLite + JSONL 双写
  4. Session 生命周期
  5. 记忆(Memory)是否共享?
  6. 对话内容(Transcript)的管理
  7. Session 切换与恢复
  8. 飞书集成详解
  9. 跨 Session 搜索
  10. 常用命令速查
  11. 架构图
  12. 源码索引

1. Session 的本质

Hermes 的 "session" 本质上是一个键值映射

session_key  →  session_id  →  transcript (对话内容)
  • session_key: 由消息来源自动计算的稳定标识符,格式如 agent:main:feishu:dm:oc_xxx
  • session_id: 每次新建 session 时生成的唯一 ID,格式 YYYYMMDD_HHMMSS_<8位hex>

说人话session_key 决定"谁跟谁聊",session_id 决定"哪一次对话"。同一个 session_key 在过期或重置后会指向新的 session_id

关键数据结构

# gateway/session.py:71
@dataclass
class SessionSource:
    platform: Platform        # feishu, telegram, discord, ...
    chat_id: str              # 聊天室 ID(飞书的 oc_xxx)
    chat_type: str            # "dm" | "group" | "channel" | "thread"
    user_id: str              # 发消息的用户 ID
    thread_id: str            # 话题/帖子 ID(可选)
    ...

# gateway/session.py:425
@dataclass
class SessionEntry:
    session_key: str          # 稳定的 session 标识符
    session_id: str           # 当前对话 ID
    created_at: datetime
    updated_at: datetime
    origin: SessionSource     # 消息来源
    input_tokens: int         # token 统计
    output_tokens: int
    total_tokens: int
    ...

2. Session Key 的构建规则

Session key 是整个系统的核心——它决定了"哪些消息属于同一个对话"。

源码位置: gateway/session.py:583build_session_key()

DM(私聊)规则

agent:main:{platform}:dm:{chat_id}[:{thread_id}]
场景 Session Key 示例
飞书私聊 agent:main:feishu:dm:ou_xxx
飞书私聊 + 话题 agent:main:feishu:dm:ou_xxx:thread_123
Telegram 私聊 agent:main:telegram:dm:123456789

关键点:每个私聊对话天然隔离,不同用户的私聊互不影响。

群聊规则

agent:main:{platform}:{chat_type}:{chat_id}[:{user_id}][:{thread_id}]
场景 Session Key 示例 说明
飞书群聊(默认) agent:main:feishu:group:oc_xxx:ou_user1 每个用户独立 session
Discord 线程(默认) agent:main:discord:thread:ch_xxx 线程内所有人共享
Telegram 群 + 话题 agent:main:telegram:group:ch_xxx:topic_456 话题共享

关键配置: - group_sessions_per_user (默认 True): 群聊中每个用户有独立 session - thread_sessions_per_user (默认 False): 线程/话题中所有人共享 session


3. 存储架构

Hermes 使用双写策略存储会话数据:

┌─────────────────────────────────────────────────────┐
│               Session 存储架构                        │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌──────────────────────┐  ┌──────────────────────┐ │
│  │  SQLite (state.db)   │  │  JSONL 文件           │ │
│  │  ~~~~~~~~~~~~~~~~~~~~│  │  ~~~~~~~~~~~~~~~~~~~~│ │
│  │  • sessions 表       │  │  sessions/            │ │
│  │  • messages 表       │  │    {session_id}.jsonl  │ │
│  │  • FTS5 全文索引     │  │                       │ │
│  │                      │  │  每行一个 JSON 对象    │ │
│  │  ← 主要数据源        │  │  ← 兼容旧工具         │ │
│  └──────────────────────┘  └──────────────────────┘ │
│                                                     │
│  ┌──────────────────────┐                           │
│  │  sessions.json       │                           │
│  │  ~~~~~~~~~~~~~~~~~~~~│                           │
│  │  session_key → entry  │  ← 内存索引的持久化      │
│  └──────────────────────┘                           │
└─────────────────────────────────────────────────────┘

SQLite 结构 (~/.hermes/state.db)

  • sessions: session_id, source, user_id, title, model, started_at, ended_at
  • messages: session_id, role, content, tool_calls, reasoning, ...
  • FTS5 索引: 对 messages.content 建立全文搜索索引

JSONL 文件 (~/.hermes/sessions/)

每个 session 一个文件,如 20260501_024922_a1b2c3d4.jsonl,每行一个 JSON 消息对象。

sessions.json

SessionStore 的内存索引,记录 session_key → SessionEntry 的映射。Gateway 启动时从这里恢复状态。


4. Session 生命周期

4.1 创建

当用户发第一条消息时,get_or_create_session() 被调用:

  1. 根据 SessionSource 计算 session_key
  2. 如果该 key 已存在且未过期 → 返回现有 session
  3. 如果不存在或已过期 → 创建新 session_id,写入 SQLite 和 sessions.json
# gateway/session.py:901 — 新 session ID 生成
session_id = f"{now.strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"

4.2 自动过期(Reset Policy)

Session 会在满足以下条件时自动创建新 session(旧的不删除):

模式 行为 默认值
idle 闲置超过 N 分钟后重置 1440 分钟(24 小时)
daily 每天固定时间重置 凌晨 4 点
both 两个条件都触发
none 永不过期

源码: gateway/session.py:735_is_session_expired()

关键:有后台任务运行的 session(如 cron job、background process)不会被过期

4.3 手动重置

  • /reset/new: 立刻创建新 session,旧 session 完整保留
  • /stop: 标记 session 为 suspended,下次访问自动创建新 session

4.4 旧 session 的命运

旧 session 不会被删除或压缩。它只是不再被 session_key 指向,但: - SQLite 中的 messages 表完整保留 - JSONL 文件仍在磁盘上 - 可以通过 /resumehermes sessions list 找回


5. 记忆是否共享?

是的,记忆是全局共享的。

┌─────────────────────────────────────────────────────┐
│              记忆(Memory)共享机制                    │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ~/.hermes/MEMORY.md   ←  agent 的个人笔记           │
│  ~/.hermes/USER.md     ←  用户画像                   │
│                                                     │
│  这两个文件是全局的,不区分 session                    │
│                                                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │Session A │  │Session B │  │Session C │          │
│  │(飞书DM)  │  │(飞书群)  │  │(Telegram)│          │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘          │
│       │              │              │               │
│       └──────────────┼──────────────┘               │
│                      ▼                              │
│              同一份 MEMORY.md                        │
│              同一份 USER.md                          │
│                                                     │
└─────────────────────────────────────────────────────┘

记忆的工作方式

  1. 每个 session 开始时:从磁盘加载 MEMORY.mdUSER.md,冻结快照注入系统提示
  2. session 过程中:agent 可以通过 memory 工具修改这些文件
  3. 下次新 session:加载修改后的版本

源码: run_agent.py:1678-1683

if self._memory_enabled or self._user_profile_enabled:
    from tools.memory_tool import MemoryStore
    self._memory_store = MemoryStore(...)
    self._memory_store.load_from_disk()  # 从全局文件加载

设计意图

  • 记忆是 agent 的长期知识,不应该因为切换 session 而丢失
  • 用户画像描述用户偏好,跨所有平台共享
  • 这是"冻结快照"模式:session 开始时冻结,保持系统提示稳定(利用 prefix cache)

6. 对话内容的管理

6.1 对话内容是否跨 session 共享?

不共享。每个 session 有独立的 transcript。

  • Session A 的对话内容只存在于 Session A 的记录中
  • Session B 看不到 Session A 的消息
  • 除非使用 session_search 工具跨 session 搜索

6.2 上下文压缩(Context Compression)

当对话过长(接近模型 context window 限制)时,Hermes 会自动压缩:

  1. 触发条件: last_prompt_tokens 接近模型 context 长度
  2. 压缩方式: 创建新的 session_id("子 session"),将旧对话摘要作为系统上下文注入
  3. 标题继承: 压缩后的 session 自动继承标题("my project""my project #2"

源码: 压缩相关逻辑在 agent/context_compressor.py

6.3 /retry 和 /undo

  • /retry: 撤销最后一轮对话,重新生成回答
  • /undo: 撤销用户最后一条消息

两者都通过 rewrite_transcript() 重写 SQLite 和 JSONL。


7. Session 切换与恢复

7.1 切换命令

命令 效果
/new/reset 创建全新 session,旧 session 完整保留
/resume 浏览并恢复旧 session
/resume <title> 按标题恢复
/resume <session_id> 按 ID 恢复
/stop 挂起当前 session(下次访问自动新建)

7.2 switch_session() 的实现

# gateway/session.py:1159
def switch_session(self, session_key, target_session_id):
    """把 session_key 指向一个已有的 session_id"""
    # 1. 结束当前 session(SQLite 标记 ended)
    db.end_session(old_entry.session_id, "session_switch")
    # 2. 重新打开目标 session
    db.reopen_session(target_session_id)
    # 3. 更新映射
    self._entries[session_key] = new_entry

关键:切换 session 后,session_key 指向新的 session_id,但旧的 transcript 完整保留在 SQLite 中。

7.3 Gateway 的 Agent 缓存

Gateway 会为每个 session 缓存一个 AIAgent 实例(避免每次消息都重新初始化)。缓存策略:

  • LRU 淘汰: 超过最大缓存数时,最久未用的 agent 被淘汰
  • Idle TTL: 超过空闲时间的 agent 被淘汰
  • 淘汰时: agent 的 memory provider 被正确关闭

8. 飞书集成详解

8.1 飞书消息 → Session 的映射

飞书场景 Session Key 格式 说明
私聊 agent:main:feishu:dm:{open_id} 每个用户独立
群聊 @机器人 agent:main:feishu:group:{chat_id}:{open_id} 默认每个用户独立
群聊话题 agent:main:feishu:group:{chat_id}:{open_id}:{thread_id} 话题级别隔离

8.2 飞书的特殊处理

  • Chat lock: 每个飞书聊天有独立的 asyncio.Lock,保证同一聊天内的消息顺序处理
  • Group policy: 群聊可配置 open / allowlist / blacklist / admin_only / disabled
  • Markdown 转换: 飞书的 Markdown 语法会被转换为标准格式

8.3 你当前的配置

# 当前使用 steer busy 模式
display:
  busy_input_mode: steer

飞书 DM 中: - 你的每条消息 → 同一个 session(agent:main:feishu:dm:{your_open_id}) - /reset/new → 创建新 session,旧对话保留在 SQLite 中 - /resume → 可以找回所有历史 session


9. 跨 Session 搜索

Agent 可以通过 session_search 工具搜索所有历史 session 的内容:

  1. 使用 SQLite 的 FTS5 全文索引 搜索 messages 表
  2. 按相关度排序,取 top N 个 session
  3. 使用辅助模型(如 Gemini Flash)生成摘要
  4. 返回给主模型

源码: tools/session_search_tool.py

9.2 FTS5 的优势

  • 支持中英文混合搜索
  • 按相关度排序
  • 支持布尔查询(AND, OR, NOT)
  • 搜索速度极快(毫秒级)

10. 常用命令速查

飞书/消息平台中的命令

命令 说明
/new/reset 开始新对话(旧对话保留)
/resume 浏览历史对话
/resume <标题> 恢复指定标题的对话
/stop 挂起当前对话
/title <名称> 给当前对话命名
/compress 手动压缩上下文
/retry 重试最后一轮
/undo 撤销最后一条消息

CLI 命令

hermes sessions list                # 列出最近 session
hermes sessions list --source feishu  # 只看飞书
hermes sessions delete <id>         # 删除 session
hermes sessions rename <id> "name"  # 重命名
hermes sessions export backup.jsonl # 导出
hermes --continue                   # 继续最近 session
hermes --resume <id>                # 恢复指定 session

11. 架构图

Session 系统全局架构

用户消息 (飞书/Telegram/CLI)
┌─────────────────────────────┐
│      SessionSource          │  ← 平台、chat_id、user_id、thread_id
│   (消息来源描述)             │
└──────────┬──────────────────┘
┌─────────────────────────────┐
│    build_session_key()      │  ← 计算稳定的 session_key
│   agent:main:feishu:dm:...  │
└──────────┬──────────────────┘
┌─────────────────────────────┐
│   SessionStore              │
│   ┌─────────────────────┐   │
│   │ session_key → Entry  │   │  ← 内存 + sessions.json
│   └──────────┬──────────┘   │
│              │              │
│   ┌──────────▼──────────┐   │
│   │  SessionDB (SQLite)  │   │  ← sessions + messages 表
│   │  FTS5 全文索引        │   │
│   └──────────┬──────────┘   │
│              │              │
│   ┌──────────▼──────────┐   │
│   │  JSONL transcripts   │   │  ← 兼容旧工具
│   └─────────────────────┘   │
└──────────┬──────────────────┘
┌─────────────────────────────┐
│      AIAgent (缓存)          │  ← 每个 session 一个 agent 实例
│   ┌─────────────────────┐   │
│   │  conversation_history │   │  ← 当前对话的消息列表
│   │  MEMORY.md (全局)     │   │  ← 跨 session 共享
│   │  USER.md (全局)       │   │  ← 跨 session 共享
│   └─────────────────────┘   │
└─────────────────────────────┘

12. 源码索引

文件 核心内容
gateway/session.py SessionSource, SessionContext, SessionEntry, SessionStore, build_session_key()
gateway/run.py GatewayRunner — 消息路由、session 忙碌处理、agent 缓存
hermes_state.py SessionDB — SQLite 存储层(sessions + messages + FTS5)
tools/session_search_tool.py session_search — 跨 session 全文搜索
gateway/session_context.py Session 上下文构建
gateway/platforms/feishu.py 飞书适配器 — 消息接收、SessionSource 构建
agent/context_compressor.py 上下文压缩 — 对话过长时自动压缩
gateway/config.py SessionResetPolicy — session 过期策略配置
website/docs/user-guide/sessions.md 官方文档 — 用户视角的 session 说明

附录:常见问题

Q: 切换 session 后,旧对话会被删除吗?

A: 不会。旧 session 的 transcript 完整保留在 SQLite 和 JSONL 文件中。可以通过 /resumehermes sessions list 找回。

Q: 不同飞书群的 session 是隔离的吗?

A: 是的。每个群有独立的 chat_id,生成不同的 session_key

Q: 飞书群聊中,不同用户的对话是共享的吗?

A: 默认不共享。group_sessions_per_user=True(默认)意味着每个用户在群聊中有独立的 session。但线程/话题默认共享(thread_sessions_per_user=False)。

Q: Memory(MEMORY.md)在不同 session 间共享吗?

A: 是的。MEMORY.md 和 USER.md 是全局文件,所有 session 共享。这是设计如此——记忆是 agent 的长期知识,不应该因为切换 session 而丢失。

Q: Session 过期后数据还在吗?

A: 在。"过期"只是 session_key 指向新的 session_id,旧的 transcript 数据完整保留在数据库中。

Q: 如何让某个 session 永不过期?

A: 在 config.yaml 中设置:

gateway:
  default_reset_policy:
    mode: none

Q: session_search 能搜到所有历史 session 吗?

A: 是的。它搜索 SQLite 中所有 session 的 messages 表,使用 FTS5 全文索引,包括已结束的 session。