两个独立项目,用不同语言、不同架构,却在上下文压缩这件事上走向了几乎相同的设计。是巧合?还是好方案总会殊途同归?
最近我在配置 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 1 的 1-line summary 长这样:
[terminal] ran `npm test` -> exit 0, 47 lines output[read_file] read config.py from line 1 (3,400 chars)很简洁,但信息量其实不小 👀
消息状态模型
active | compacted | ||
|---|---|---|---|
| in_place 压缩归档 | |||
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类型条目原始消息可搜索,可恢复 🎉
迁移时间线:
趋同后: 两者都是 SQLite append-only,原始消息不丢失,可搜索可恢复 🤝
已趋同的 9 项设计
| 存储格式 | |||
| 压缩后改记录? | |||
| 原始消息可搜索? | |||
| 数据可恢复? | |||
| 双模式 | |||
| 专用压缩模型 | |||
| 预压缩 tool result 修剪 | |||
| 防抖保护 | |||
| 可插拔引擎 |
9 项趋同,3 项有差异 📊
四、Hermes 的关键配置项总览
compression.enabled | true | |
compression.threshold | ||
compression.target_ratio | ||
compression.protect_last_n | ||
compression.protect_first_n | ||
compression.abort_on_summary_failure | true | |
compression.in_place | true | |
auxiliary.compression.provider | custom:sensenova-ds4f | |
auxiliary.compression.model | deepseek-v4-flash | |
auxiliary.compression.timeout |
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 的上下文管理机制,欢迎评论区聊聊你的体验。
夜雨聆风