首页 / Hermes Agent 教程 / 会话管理与上下文压缩

Hermes Agent 教程

会话管理与上下文压缩

本教程共 25 篇 · 第 8 篇 · 更新于 2026-07-26 · 约 19 分钟阅读

Hermes AgentHermes Agent 教程会话管理上下文压缩SessionContext Referencessession_search

8. 会话管理与上下文压缩

本节目标:搞懂 Hermes Agent 的会话是怎么存储、恢复、压缩和检索的。从 SQLite + WAL 的持久化设计,到会话生命周期与谱系分裂,到 Context References 的 @ 语法注入,再到上下文压缩的三层递进算法和 session_search 跨会话检索。学完你能解释「为什么退出后对话还在」「长对话怎么不撞上下文上限」「怎么从历史会话里找东西」。

会话是什么:从一条消息到一段历史

session(会话)是一次完整的对话。它有开始时间、结束时间、唯一 ID。对话里的所有 messages 都属于这个 session。

  • CLI 模式下,从你启动 hermes 到退出是一个 session
  • Gateway 模式下,同一个聊天窗口里的连续对话是一个 session

Hermes Agent 自动把每个会话存下来,不管是 CLI、Telegram、Discord、Slack 还是其他 30+ 平台的消息,都进同一个存储。这让你能恢复对话、跨会话搜索、完整管理历史。

SQLite + WAL:为什么选这个组合

会话存储的底层是 SQLite 数据库,位置在 ~/.hermes/state.db。选 SQLite 不是因为「比文件高级」,而是因为它同时解决了三个问题:

  1. 读到之前的对话历史:进程退出后还能读回来
  2. 并发写入不冲突:Gateway 模式下多个平台消息同时到达,写同一个数据库文件不锁死
  3. 全文搜索:事后能搜索历史会话

WAL 模式解决读写阻塞

SQLite 默认模式下,有人在写数据库时,其他人连读都不行:

Telegram 适配器正在写消息 -> 整个数据库被锁住

Discord 适配器想读历史消息 -> 等着,读都不行

CLI 单人用无所谓,但 Gateway 模式下多个平台同时收发消息,这就卡住了。

开了 WAL(Write-Ahead Logging)模式后,写操作先写到一个临时日志文件,不动主数据库。读操作继续读主数据库文件,不受影响:

Telegram 适配器正在写消息 -> 写到 WAL 日志文件(不动主数据库)

Discord 适配器想读历史消息 -> 照读,读的是主数据库

日志会在合适时机自动合并回主数据库。

默认模式:
  写 ──阻塞──> 读
  写 ──阻塞──> 写

WAL 模式:
  写 ──不阻塞──> 读    ← 关键改进
  写 ──仍阻塞──> 写    ← 这个没变
Note

WAL 模式下写和写之间还是要排队的。Hermes 的做法是把 SQLite 超时设短(1 秒),然后在应用层做随机退避重试。随机间隔自然打散了竞争的写入者,避免高并发时的队列效应。

FTS5 全文搜索

FTS5 是 SQLite 的全文搜索扩展(Full-Text Search 5)。它让你能在大量历史消息中快速搜索关键词,不需要遍历所有行做 LIKE '%keyword%'。每次插入 message 时,SQLite trigger 自动更新 FTS 索引。

数据库存什么

state.db 存这些:

  • Session ID、来源平台、用户 ID
  • 会话标题(唯一、人类可读的名字)
  • 模型名和配置
  • System prompt 快照
  • 完整消息历史(role、content、tool_calls、tool 结果)
  • Token 计数(输入/输出)
  • 时间戳(started_at、ended_at)
  • 父会话 ID(压缩触发的会话分裂用)

会话生命周期:创建、续接、分裂、归档

agent 启动
  |
  v
新 session? ──是──> 建一条 session 记录
  |
  否(继续旧 session)
  |
  v
从 SQLite 读出历史 messages
  |
  v
传给 AIAgent.run_conversation()
  |
  v
每一轮对话结束后,新 messages 写入 SQLite
  |
  v
agent 退出
  |
  v
下次启动时,从 SQLite 读回来,对话还在

增量写入

每轮对话后,Hermes 只写新增的 messages,不是全量重写。对话越长,这个差别越明显—全量重写会越来越慢。

System Prompt 缓存到 session 表

Gateway 每条消息可能创建新的 AIAgent 实例。如果每次都重新组装 system prompt,中间可能因为 MEMORY.md 被改了导致 prompt 变化,Anthropic 的 prompt cache 就失效了。

所以 Hermes 把第一次组装好的 system prompt 存到 session 表里。后续实例直接读缓存,保证 prompt 不变。只有上下文压缩事件才会清除缓存并重建。

会话来源标记

每个 session 有一个 source 字段,标记消息来自哪个入口:

来源说明
cli交互式 CLI
telegram / discord / slack各消息平台
whatsapp / signal / matrix即时通讯
feishu / dingtalk / wecom / weixin国内平台
email / sms邮件短信
cron / batch定时任务和批处理
webhook / api-server / acpAPI 接入

这让你能按平台过滤会话:「只看 Telegram 的对话」。

会话恢复:—continue 与 —resume

继续上个会话

# 恢复最近的 CLI 会话
hermes --continue
hermes -c

# 或用 chat 子命令
hermes chat --continue
hermes chat -c

这会从 SQLite 查最近的 cli 会话,加载完整对话历史。

按名字恢复

给会话起了标题后,可以按名字恢复:

# 按名字恢复
hermes -c "my project"

# 如果有谱系变体(my project、my project #2、my project #3),
# 自动恢复最近的一个
hermes -c "my project"   # -> 恢复 "my project #3"

按 ID 恢复

# 按 session ID 恢复
hermes --resume 20250305_091523_a1b2c3d4
hermes -r 20250305_091523_a1b2c3d4

# 按标题恢复
hermes --resume "refactoring auth"
Tip

Session ID 格式是 YYYYMMDD_HHMMSS_<hex>—CLI/TUI 会话用 6 位 hex 后缀(如 20250305_091523_a1b2c3),网关会话用 8 位后缀(如 20250305_091523_a1b2c3d4)。按 ID(完整或唯一前缀)或按标题恢复都行,-c-r 都支持。

恢复时的对话回顾

恢复会话时,Hermes 会在输入提示符前显示一个紧凑的上轮对话回顾面板:

  • 显示用户消息(金色 )和助手回复(绿色
  • 截断长消息(用户消息 300 字符,助手回复 200 字符/3 行)
  • 折叠工具调用为计数+工具名(如 [3 tool calls: terminal, web_search]
  • 隐藏系统消息、工具结果、内部推理
  • 上限最近 10 轮交流,带「… N earlier messages …」提示
  • 暗色样式跟活跃对话区分

要禁用回顾、保留极简的一行行为:

display:
  resume_display: minimal   # 默认: full

会话命名与谱系

自动标题

Hermes 在首次交流后自动生成简短描述性标题(3-7 个词)。这跑在后台线程,用快速辅助模型,不加延迟。自动标题只在每个会话触发一次,手动设过标题就跳过。

手动设标题

在任何会话里用 /title 斜杠命令:

/title my research project

标题立即生效。如果会话还没在数据库创建(比如你第一条消息前就敲 /title),会排队等会话开始后应用。

命令行也能改名:

hermes sessions rename 20250305_091523_a1b2c3d4 "refactoring auth module"

标题规则

  • 唯一:不能两个会话同名
  • 最多 100 字符:保持列表输出整洁
  • 净化:控制字符、零宽字符、RTL 覆盖自动剥离
  • Unicode 正常:emoji、CJK、带重音字符都行

压缩触发的谱系

当会话上下文被压缩(手动 /compress 或自动),Hermes 创建一个新的续接会话。如果原会话有标题,新会话自动获得编号标题:

"my project" -> "my project #2" -> "my project #3"

按名字恢复时(hermes -c "my project"),自动选谱系里最近的会话。

Note

压缩后的新会话通过 parent_session_id 指向旧会话。旧历史不删除,只是归档。这形成一条链:session_001 -> session_002 -> session_003,可追溯。

Context References:@ 语法注入

Context References(上下文引用)让你在消息里用 @ 加引用,把文件、目录、git diff、URL 的内容直接注入消息。Hermes 内联展开引用,把内容追加在 --- Attached Context --- 段下面。

支持的引用

语法说明
@file:path/to/file.py注入文件内容
@file:path/to/file.py:10-25注入指定行范围(1 索引,闭区间)
@folder:path/to/dir注入目录树列表+文件元数据
@diff注入 git diff(未暂存的工作树改动)
@staged注入 git diff --staged(已暂存改动)
@git:5注入最近 N 次提交带补丁(最多 10)
@url:https://example.com抓取并注入网页内容

用法示例

Review @file:src/main.py and suggest improvements

What changed? @diff

Compare @file:old_config.yaml and @file:new_config.yaml

What's in @folder:src/components?

Summarize this article @url:https://arxiv.org/abs/2301.00001

一条消息里可以多个引用:

Check @file:main.py, and also @file:test.py.

行范围

@file: 支持精确行范围:

@file:src/main.py:42        # 单行 42
@file:src/main.py:10-25     # 10 到 25 行(闭区间)

行号 1 索引。无效范围静默忽略(返回整文件)。

Tip

大文件用行范围只注入相关部分,避免撑爆上下文。比如 @file:main.py:100-200 只注入 100-200 行。

大小限制

引用有边界,防止压垮模型上下文窗口:

阈值行为
软限制上下文长度的 25%追加警告,展开继续
硬限制上下文长度的 50%拒绝展开,原消息不变返回
目录条目最多 200 文件超出替换成 - ...
Git 提交最多 10@git:N 钳制到 [1, 10]

敏感路径拦截

这些路径永远从 @file: 拦截,防止凭据泄露:

  • SSH 密钥和配置:~/.ssh/id_rsa~/.ssh/config
  • Shell 配置:~/.bashrc~/.zshrc~/.profile
  • 凭据文件:~/.netrc~/.pgpass~/.npmrc~/.pypirc
  • Hermes 环境:$HERMES_HOME/.env

这些目录整体拦截(里面任何文件):~/.ssh/~/.aws/~/.gnupg/~/.kube/$HERMES_HOME/skills/.hub/

Warning

Context References 主要是 CLI 特性。在消息平台(Telegram、Discord 等)上,@ 语法不被网关展开—消息原样传递。智能体自己仍可通过 read_filesearch_filesweb_extract 工具引用文件。

与上下文压缩的交互

对话上下文被压缩时,展开的引用内容会被纳入压缩摘要。这意味着:

  • @file: 注入的大文件内容会占用上下文
  • 对话后续被压缩时,文件内容会被摘要(不是逐字保留)
  • 大文件考虑用行范围(@file:main.py:100-200)只注入相关部分

上下文压缩:三层递进

上下文不是越多越好,而是要把「仍然有用的部分」留在活跃工作面里。

智能体会读大文件、跑长命令、多轮工具调用—上下文很快膨胀。没有压缩机制会出三个问题:1) 模型注意力被旧结果淹没;2) API 请求越来越重越来越贵;3) 撞上上下文上限任务中断。

压缩要解决的是:怎样在不丢掉工作连续性的前提下,把活跃上下文重新腾出空间。

三层递进算法

第 1 层:旧工具输出先裁剪
  -> 不需要 LLM,纯字符串替换
  -> 把很久以前的工具结果换成占位提示

第 2 层:保护头尾,只压中间
  -> 头部(任务定义)不动
  -> 尾部(最近工作)不动
  -> 只压中间那些已经「用过了」的轮次

第 3 层:用 LLM 把中间部分摘要化
  -> 调一个便宜的辅助模型
  -> 生成结构化摘要替代原文

画成流程:

messages(100 条,150K tokens)
   |
   +-- 第 1 层:旧 tool 结果 -> "[Old tool output cleared]"
   |   (不需要 LLM,先减掉一批 token)
   |
   +-- 还是太长?
   |
   +-- 第 2 层:找边界
   |   头部:前 N 条(不动)
   |   尾部:最近 ~20K tokens(不动)
   |   中间:要被压缩的部分
   |
   +-- 第 3 层:中间部分 -> 辅助 LLM -> 结构化摘要
   |
   v
新 messages = [头部] + [摘要] + [尾部]

三层是递进的:第 1 层最便宜(不花钱),第 2 层是边界计算,第 3 层才真正调 LLM。

压缩后要保住什么

压缩不是「把历史缩短」这么简单。真正重要的是:让模型还能继续接着干活。

一份合格的摘要至少要保住:

  1. 当前任务的目标是什么
  2. 已经完成了哪些关键动作
  3. 做过哪些重要决定
  4. 改过或重点查看过哪些文件
  5. 下一步应该做什么

Hermes 用结构化摘要模板确保这些不丢:

## Goal
...
## Progress
...
## Key Decisions
...
## Files Modified
...
## Next Steps
...

不是自由文本,而是有格式。这让模型更容易从摘要中提取关键信息。

Note

摘要消息的 roleassistant 而不是 useruser role 会被模型理解为「用户的新一轮发言」,触发重新规划,反而让智能体重复已做过的工作。assistant role 配合明确的 [CONTEXT COMPACTION - system-generated] 前缀,是更稳的方案。

关键设计:边界对齐避免孤儿 tool_call

assistant.tool_calls 和它的 tool 响应必须配对—只留下前者、把后者压进摘要,下一次 API 请求就会被拒。

find_boundaries 在两端都会把候选切点「吸附」到下一个非 tool 消息上:

def _align_to_assistant_boundary(messages, index):
    while index < len(messages) and messages[index].get("role") == "tool":
        index += 1
    return index

碰到 tool 就往后走,直到找到 assistant 边界。这样切下来的中段始终是若干完整的 assistant <-> tool 配对,怎么压都不会出孤儿。比「压完再扫一遍清孤儿」更彻底:从源头就保证消息流合法。

压缩失败早退

极端情况下头部本身就吃掉大部分预算(比如用户首条消息塞了一整篇文档),compress 找不到可压的中段,再多压几轮也只是空转。compress 比较前后 token 估算,如果没降到原值的 90% 以下,抛 CompressionStuckError

class CompressionStuckError(RuntimeError):
    """Compress couldn't meaningfully shrink the message list."""

# in compress():
after = estimate_tokens(new_messages)
if after >= int(before * COMPRESSION_MIN_SHRINK):
    raise CompressionStuckError(before, after)

主循环捕获后立即退出当轮,告诉用户「会话已无法压缩,请新开 session」,而不是一直循环到 MAX_ITERATIONS

Preflight 压缩

run_conversation() 进入主循环之前就检查 token 数。如果已经超了(比如用户从大窗口模型切到小窗口模型),在第一次 API 调用之前就压缩。不等到 API 报错再处理—主动防御比被动恢复好。

压缩配置详解

所有压缩设置在 config.yaml(没有环境变量):

compression:
  enabled: true                       # 开关
  progress_notices: false             # 是否向聊天平台推送压缩进度通知
  threshold: 0.50                     # 上下文占比到这个比例时压缩
  threshold_tokens: null              # 绝对 token 上限(可选),取比例和绝对值较低者
  target_ratio: 0.20                  # 保留为最近尾部的比例
  protect_last_n: 20                  # 最少保留的最近消息数(不压缩)
  protect_first_n: 3                  # 跨压缩固定的非系统头部消息数(0 = 不固定)
  idle_compact_after_seconds: 0       # 空闲压缩(0 = 禁用)
  hygiene_hard_message_limit: 5000    # 网关安全阀
  hygiene_timeout_seconds: 30         # 摘要模型无输出的最大秒数
  hygiene_total_ceiling_seconds: 600  # 即使 token 还在流的绝对上限
  hygiene_failure_cooldown_seconds: 300  # 跳过重复失败的 hygiene 尝试
  proactive_prune_tokens: 0           # 无 LLM 工具结果裁剪的 token 触发器(0 = 关)
  proactive_prune_min_result_chars: 8000  # 裁剪只触及大于此值的工具结果

# 摘要模型/Provider 配在 auxiliary 下:
auxiliary:
  compression:
    model: ""                         # 空 = 用主聊天模型。覆盖可用更便宜的,如 "google/gemini-3-flash-preview"
    provider: "auto"                  # "auto" / "openrouter" / "nous" / "codex" / "main" 等
    base_url: null                    # 自定义 OpenAI 兼容端点(覆盖 provider)
Note

旧配置里的 compression.summary_modelcompression.summary_providercompression.summary_base_url 会在首次加载时自动迁移到 auxiliary.compression.*(config 版本 17)。无需手动操作。

触发时机

  • Preflight(API 调用前):对话超过模型上下文窗口的 50%
  • 网关自动压缩:对话超过 85%(更激进,回合间运行)

网关热重载

Tip

编辑 config.yaml 里的 model.context_length 或任何 compression.* 键,运行中的网关在下一条消息就生效—不用重启网关、不用 /reset、不用轮换会话。缓存智能体签名包含这些键,网关看到变化会透明重建智能体。API Key 和工具/技能配置仍走通常的重载路径。

没有可用摘要模型时

如果没有 Provider 可用于压缩,Hermes 会丢弃中间对话轮次而不生成摘要,而不是让会话失败。这是最后的保底—宁可丢上下文也不让对话挂死。

session_search:跨会话检索

session_search 是一个独立工具,让智能体能搜索本地会话库里过去的会话,或在某个会话内滚动浏览。FTS5 支撑的检索,返回数据库里的实际消息(不调 LLM)。

三种用法

形态参数用途
发现query跨会话搜索关键词
滚动session_id + around_message_id在某会话内定位消息
浏览不传参数列出会话

智能体在干活时可以自己调 session_search 找历史上下文。比如你问「上周我们讨论的那个 bug 修复方案是什么」,智能体可以搜历史会话找答案。

命令行会话管理

Hermes 提供完整的 hermes sessions 命令集:

# 列出最近会话(默认最近 20 个)
hermes sessions list

# 按平台过滤
hermes sessions list --source telegram

# 显示更多
hermes sessions list --limit 50

有标题时输出显示标题、预览、相对时间戳:

Title                  Preview                                  Last Active   ID
──────────────────────────────────────────────────────────────────────────────
refactoring auth       Help me refactor the auth module please   2h ago        20250305_091523_a
my project #3          Can you check the test failures?          yesterday     20250304_143022_e

会话导出

hermes sessions export 是一个入口支持所有导出格式,用 --format 选:

格式输出用途
jsonl(默认)每会话一个 JSON 对象备份、机器回环
md / qmd每会话一个 Markdown/Quarto 文件+清单可读归档、笔记
html单个自包含页面(多会话有侧边栏)分享、浏览
traceClaude Code JSONLHF Agent Trace Viewer、--upload
# 全部会话导出 JSONL
hermes sessions export backup.jsonl

# 按平台导出
hermes sessions export telegram-history.jsonl --source telegram

# 单个会话导出
hermes sessions export session.jsonl --session-id 20250305_091523_a1b2c3d4

# 脱敏(API Key、token、凭据)后导出
hermes sessions export backup.jsonl --redact

# 导出为 HTML(自包含,可分享)
hermes sessions export --format html --newer-than 1w --source telegram --redact archive.html

# 只导出用户提示词(建提示词库)
hermes sessions export prompts.jsonl --session-id 20250305_091523_a1b2c3d4 --only user-prompts
Tip

--redact 会擦除导出内容里的 API Key、token、凭据。任何打算分享的导出都建议加上。trace 格式默认就脱敏(因为设计上要离开本机),--no-redact 可手动关闭。

只导出用户提示词

--only user-prompts 只导出你写的提示词—没有助手回复、工具输出、系统上下文。适合建提示词库或回顾自己问过什么:

# 每条提示词一个 JSONL 记录(会话 ID、索引、时间戳、文本)
hermes sessions export prompts.jsonl --session-id 20250305_091523_a1b2c3d4 --only user-prompts

# Markdown,直接到 stdout
hermes sessions export - --session-id 20250305_091523_a1b2c3d4 --only user-prompts --format md

上传到 Hugging Face

--format trace --upload 把 trace 推到你自己的私有 hermes-traces 数据集(读 HF_TOKEN):

# 单个会话上传到 HF traces 数据集
hermes sessions export --format trace --session-id 20250305_091523_a1b2c3d4 --upload

--upload 默认私有,加 --public 才公开。

会话清理

# 删除特定会话(带确认)
hermes sessions delete 20250305_091523_a1b2c3d4

# 清理老会话
hermes sessions prune
Warning

压缩减少活跃上下文,但不是隐私删除。/compress 把旧消息摘要化,原始数据仍在 SQLite 里。要真正删除用 hermes sessions prunehermes sessions delete

导出后删除

--delete-after-verified 模式先导出验证再删除,限 --session-id 使用,需要 --yes

hermes sessions export --format md --session-id 20250305_091523_a1b2c3d4 --delete-after-verified --yes

因为删除父会话也会删除其委托/子智能体会话,这个模式会先单独导出并验证每个委托,再删任何东西。如果导出期间委托集合变了,拒绝删除。

什么算进上下文

Hermes 存会话历史是为了恢复对话,但不会反复重发它处理过的每个字节。每轮模型看到的是:选定的 system prompt、当前对话窗口、Hermes 为该轮显式注入的内容。

媒体附件按回合级输入处理:

  • 图片可能原生附加到下次模型调用,或在不支持原生视觉时预分析成文本描述
  • 音频在配置了语音转文字后转成文本
  • 文档可包含提取的文本;其他类型通常用本地路径+简短说明表示
  • 附件路径和提取/派生文本可能出现在转录里,但原始图片、音频、二进制字节不会重复复制到未来提示词
Note

上下文增长最常见的原因不是媒体文件本身,而是冗长文本:粘贴的转录、完整日志、大工具输出、长 diff、重复状态报告、详细证明转储。优先用摘要、文件路径、聚焦摘录、工具查询,而不是把大工件复制进聊天。

常见踩坑

1. 不开 WAL 模式:默认模式下一个写操作阻塞所有读。Gateway 场景下智能体写消息时,另一个平台的读取请求会挂住。

2. 存整个 messages 列表而不是增量:每轮对话后全量写入而不是只写新增。对话越长写入越慢。

3. 把 session_id 写死:每次启动用同一个 session_id,所有对话混在一起。session_id 应该每次新对话生成一个。

4. 以为压缩等于删除:压缩是把「不必常驻活跃上下文」的内容换一种表示。旧历史通过 session 链保留。

5. 只在撞上限后才处理:更好做法是三层递进—旧输出先裁剪、找边界、再摘要。不是一上来就调 LLM。

6. 摘要只写成一句空话:如果摘要没保住目标、决定、文件、下一步,它对继续工作没帮助。

7. 用主模型做摘要:压缩是系统操作,不是用户请求。用便宜的辅助模型就够了。

8. 不保护尾部:把最近的消息也压缩了,智能体立刻忘记刚才在做什么。

9. 在 Gateway 里不分 chat_id:不同平台的不同聊天窗口应该是不同的 session。所有消息混到一个 session 里,智能体把 A 用户的对话当成 B 用户的上下文。


这一章讲完了会话存储、恢复、压缩、检索的全链路。到这里,Hermes Agent 的核心机制—从安装配置、模型管理、Agent 循环到会话上下文—就串起来了。后面的章节会进入工具系统、技能、委托、MCP 这些更高层的能力扩展。