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.md 和 USER.md 是全局的,所有 session 共享 |
| 切换 session 后旧对话怎么办? | 保留 — 旧 transcript 留在 SQLite 和磁盘上,可随时 /resume |
| 飞书每个聊天是独立 session 吗? | DM 是,群聊看配置(默认每个用户独立) |
| Session 会过期吗? | 会,默认 idle 24h 或每天凌晨 4 点自动重置 |
目录¶
- Session 的本质:一个 session_key 对应一个 session_id
- Session Key 的构建规则
- 存储架构:SQLite + JSONL 双写
- Session 生命周期
- 记忆(Memory)是否共享?
- 对话内容(Transcript)的管理
- Session 切换与恢复
- 飞书集成详解
- 跨 Session 搜索
- 常用命令速查
- 架构图
- 源码索引
1. Session 的本质¶
Hermes 的 "session" 本质上是一个键值映射:
session_key: 由消息来源自动计算的稳定标识符,格式如agent:main:feishu:dm:oc_xxxsession_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:583 — build_session_key()
DM(私聊)规则¶
| 场景 | Session Key 示例 |
|---|---|
| 飞书私聊 | agent:main:feishu:dm:ou_xxx |
| 飞书私聊 + 话题 | agent:main:feishu:dm:ou_xxx:thread_123 |
| Telegram 私聊 | agent:main:telegram:dm:123456789 |
关键点:每个私聊对话天然隔离,不同用户的私聊互不影响。
群聊规则¶
| 场景 | 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_atmessages表: 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() 被调用:
- 根据
SessionSource计算session_key - 如果该 key 已存在且未过期 → 返回现有 session
- 如果不存在或已过期 → 创建新
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 文件仍在磁盘上
- 可以通过 /resume 或 hermes sessions list 找回
5. 记忆是否共享?¶
是的,记忆是全局共享的。
┌─────────────────────────────────────────────────────┐
│ 记忆(Memory)共享机制 │
├─────────────────────────────────────────────────────┤
│ │
│ ~/.hermes/MEMORY.md ← agent 的个人笔记 │
│ ~/.hermes/USER.md ← 用户画像 │
│ │
│ 这两个文件是全局的,不区分 session │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Session A │ │Session B │ │Session C │ │
│ │(飞书DM) │ │(飞书群) │ │(Telegram)│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ ▼ │
│ 同一份 MEMORY.md │
│ 同一份 USER.md │
│ │
└─────────────────────────────────────────────────────┘
记忆的工作方式¶
- 每个 session 开始时:从磁盘加载
MEMORY.md和USER.md,冻结快照注入系统提示 - session 过程中:agent 可以通过
memory工具修改这些文件 - 下次新 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 会自动压缩:
- 触发条件:
last_prompt_tokens接近模型 context 长度 - 压缩方式: 创建新的
session_id("子 session"),将旧对话摘要作为系统上下文注入 - 标题继承: 压缩后的 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 你当前的配置¶
飞书 DM 中:
- 你的每条消息 → 同一个 session(agent:main:feishu:dm:{your_open_id})
- /reset 或 /new → 创建新 session,旧对话保留在 SQLite 中
- /resume → 可以找回所有历史 session
9. 跨 Session 搜索¶
9.1 session_search 工具¶
Agent 可以通过 session_search 工具搜索所有历史 session 的内容:
- 使用 SQLite 的 FTS5 全文索引 搜索 messages 表
- 按相关度排序,取 top N 个 session
- 使用辅助模型(如 Gemini Flash)生成摘要
- 返回给主模型
源码: 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 文件中。可以通过 /resume 或 hermes 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 中设置:
Q: session_search 能搜到所有历史 session 吗?¶
A: 是的。它搜索 SQLite 中所有 session 的 messages 表,使用 FTS5 全文索引,包括已结束的 session。