乐于分享
好东西不私藏

做着做着发现OpenClaw 在压缩机制上痛改前非,完全向 Hermes 殊途同归了

做着做着发现OpenClaw 在压缩机制上痛改前非,完全向 Hermes 殊途同归了

两个独立项目,用不同语言、不同架构,却在上下文压缩这件事上走向了几乎相同的设计。是巧合?还是好方案总会殊途同归?


最近我在配置 Hermes 的上下文压缩机制,做着做着发现了一件有意思的事——OpenClaw 也在做类似的事。

不是"差不多"的那种类似,而是核心架构高度趋同。现存差异主要在功能丰富度上——OpenClaw 配置更细,Hermes 更简洁。但核心思路完全一致。

这大概是技术圈最让人欣慰的一种"撞衫"了 🤝


一、Agent 的"上下文焦虑"

每个 Agent 对话的上下文都会持续增长。消息越多,token 消耗越大,最终触及模型上下文窗口的上限。

Hermes 的做法是:当上下文达到 token 限制的 50% 时触发压缩——把最旧的消息发给 LLM 生成摘要,用摘要替换旧消息,保持上下文在预算内。

这里有个关键事实:无论哪种模式,原始消息都不会从数据库中删除。 只是模型在下一轮对话中看不到了,需要通过 session_search 工具去检索。换句话说:数据还在,只是换了个"可见"的方式存在 🧠

但是 OpenClaw(龙虾)在7月以前不是这样的。3月我还在用的时候就记得,Compaction(龙虾的叫法)是有损的,而且龙虾的 session 都是记在 json 里,可以直接看的。那些 Compaction 的补丁,真是到处都是。

换句话说:聊天记录会自动消失的,为了节省上下文,提高注意力 😅


二、Hermes 的压缩机制:两种模式,一个真相

模式 A:Rotation(旧 session 结束,开新子 session)

压缩时:

  • 旧 session 结束(closed),不再写入
  • 创建新子 session,包含摘要 + 最近消息
  • 旧 session 所有消息 active=1,磁盘记录完整不变

但这种方式有 6 个已知 bug 🐛:压缩后 /goal 丢失、响应丢失、孤立 session、搜索断层、cwd 为空、甚至无限压缩循环。

模式 B:in_place(原地压缩,当前使用)

这才是我现在用的模式。压缩时:

  • session_id 不变
  • 旧消息标记 active=0, compacted=1(软归档)
  • 摘要作为新消息插入同一 session

以上 6 个 bug 全部消失 ✅

三阶段压缩流程

Phase
做什么
是否调用 LLM
1
将旧 tool result 替换为 1 行摘要,去重相同结果,截断大参数
2
确定压缩边界:保护头尾消息,定位可压缩区间
3
LLM 摘要生成:将可压缩区间发给专用压缩模型

Phase 1 的 1-line summary 长这样:

[terminal] ran `npm test` -> exit 0, 47 lines output[read_file] read config.py from line 1 (3,400 chars)

很简洁,但信息量其实不小 👀

消息状态模型

activecompacted
含义
默认搜索
1
0/1
正常消息
✅ 可见
0
1
in_place 压缩归档
✅ 可见(源码)
0
0
/undo 回退
❌ 需 include_inactive=True

三、OpenClaw 的压缩:从 JSONL 到 SQLite 的进化之路

OpenClaw 和 Hermes 的压缩机制原本差异很大,但经过 OpenClaw 的 SQLite 迁移后,两者在核心设计上已经高度趋同。

最大的差异点:存储格式

旧版 OpenClaw(v2026.6.11 之前):JSONL 文件存储

  • Transcript 存储在 .jsonl 文件中(每行一个 JSON)
  • Compaction 时:会改写文件 — 旧消息被替换为 compactionSummary 条目
  • 原始消息在 JSONL 文件中不可见,只能看到 summary 😱

新版 OpenClaw(v2026.7.1 起):SQLite 存储

  • Transcript 迁移到 SQLite 数据库
  • append-only,不再改写旧记录
  • Compaction 时:旧消息保留在数据库中,新增 compaction 类型条目
  • 原始消息可搜索,可恢复 🎉

迁移时间线:

版本
时间
状态
v2026.5.28 及之前
2026年5月
JSONL 文件存储,compaction 会改写文件
v2026.6.1
2026年6月初
开始把插件状态、队列等迁入 SQLite,但 transcript 仍是 JSONL
v2026.6.11
2026年6月11日
迁移进行中,旧 JSONL 文件继续兼容
v2026.7.1
2026年7月1日
SQLite flip 完成,新 session 默认 SQLite-only

趋同后: 两者都是 SQLite append-only,原始消息不丢失,可搜索可恢复 🤝

已趋同的 9 项设计

维度
Hermes
OpenClaw
状态
存储格式
SQLite
SQLite
✅ 已趋同
压缩后改记录?
❌ 只翻 active 标志
❌ append-only
✅ 已趋同
原始消息可搜索?
✅ FTS5
✅ session tools
✅ 已趋同
数据可恢复?
✅ 改 active=1
✅ 可恢复
✅ 已趋同
双模式
rotation + in_place
rotation + in_place
✅ 已趋同
专用压缩模型
✅ 已趋同
预压缩 tool result 修剪
✅ 已趋同
防抖保护
✅ 已趋同
可插拔引擎
✅ 已趋同

9 项趋同,3 项有差异 📊


四、Hermes 的关键配置项总览

配置
当前值
说明
compression.enabledtrue
启用压缩
compression.threshold
0.5
上下文达到 50% token 限制时触发
compression.target_ratio
0.2
压缩到原来的 20%
compression.protect_last_n
20
保留最近 20 条消息不动
compression.protect_first_n
3
保留开头 3 条消息不动
compression.abort_on_summary_failuretrue
 ✅
摘要失败时中止压缩,保留原上下文
compression.in_placetrue
 ✅
原地压缩,不创建新 session
auxiliary.compression.providercustom:sensenova-ds4f
做摘要的模型
auxiliary.compression.modeldeepseek-v4-flash
auxiliary.compression.timeout
120s

abort_on_summary_failure 为什么重要

默认 false 时,摘要调用超时或失败,会生成一个 fallback dump:

  • 把所有消息的原始元数据(tool_call 名、参数、返回值、文件路径、错误信息)全部 dump 成 ~8K chars 的文本块
  • 注入新 session 的上下文
  • 导致模型在后续对话中表现急剧退化 😰

设为 true 后:摘要失败时直接中止压缩,保留原上下文不动。宁可上下文大一点,也不要注入垃圾数据 💪

会话中手动压缩

/compress              ← 手动触发压缩/compress --preview    ← 预览压缩效果/undo                  ← 回退上一条用户消息/undo N                ← 回退 N 条用户消息

数据恢复

in_place: true 下压缩归档的消息(active=0, compacted=1):

  • 默认搜索 ✅ 可搜到
  • 如需完全恢复,执行 SQL:
UPDATE messages SET active=1WHERE session_id='目标session_id';

/undo 回退的消息(active=0, compacted=0):

  • 默认搜索 ❌ 排除
  • 搜索时加 include_inactive=True 参数可搜到

五、虽然是7月初的事儿,是我后知后觉了 🤦

说实话,看到两个独立项目在核心架构上高度趋同,第一反应是"巧合"。但仔细想想,这其实是好方案总会殊途同归的典型例子。

上下文压缩这件事,本质上就是在"保留信息"和"控制成本"之间找平衡。SQLite append-only 保证信息不丢,LLM 摘要保证成本可控——这两者的组合几乎是唯一合理的答案。

OpenClaw 从 JSONL 到 SQLite 的迁移,本质上是在走一条已经被验证过的路。而 Hermes 从一开始就选了这条路的另一端,只是用了不同的实现方式。

都不想丢失任何对话记录 🤝


本文是个人学习笔记,数据有出入的地方以官方为准。如果你也在关注 Hermes 或 OpenClaw 的上下文管理机制,欢迎评论区聊聊你的体验。