📖 深入解析 Hermes — Context Memory Layer for AI Coding Agents
🔬 基于 5 篇顶会研究 · 在 LoCoEval 上验证 · Topic Awareness +93%
| 轮次 | 发生了什么 | Agent 还记得吗? |
|---|---|---|
| #3 | 用户选择了 Postgres 作为数据库 | ✅ |
| #8 | 定义了 Schema 结构 | ✅ |
| #12 | 选择了 JWT Auth 方案 | ✅ |
| #25 | 上下文接近极限,旧消息被静默丢弃 | ⚠️ |
| #40 | Agent 开始自相矛盾,建议用 MongoDB | ❌ |
核心矛盾:用户在第 3 轮做的关键决策,到第 40 轮时已从上下文中完全消失
| 🎯 定位 | 轻量级中间件,不是 Agent 运行时 |
| 🧠 核心能力 | 从对话中永久提取架构决策 + 智能压缩旧消息 |
| ⚡ 技术栈 | TypeScript + Bun + Google Gemini |
| 🏷️ 许可证 | MIT Open Source |
"Conversation length becomes irrelevant."
— Hermes README
📤 Extraction — 提取
📦 Compaction — 压缩
💡 最终效果:Agent 始终知道"什么被决定了"和"什么被讨论过"
┌─────────────────────────────────────┐
│ Python Bridge │
│ (25 lines, HTTP ↔ LoCoEval) │
└──────────┬──────────────────────────┘
│ /init /query /state
┌──────────▼──────────────────────────┐
│ Hermes TypeScript Server │
│ │
│ Extractor ──▶ Dedup ──▶ Store │
│ Compactor ──▶ Assembler ──▶ API │
└─────────────────────────────────────┘
| 端点 | 方法 | 说明 |
|---|---|---|
/init |
POST | 重置状态,开始新对话 |
/query |
POST | 发送查询,返回带上下文的回答 |
/state |
GET | 检查当前内存状态(调试用) |
/costs |
GET | Token 消耗统计 |
快速启动:
bun install
export GEMINI_API_KEY="sk-..."
bun run src/index.ts
# hermes listening on :3000
# 查看记忆状态
curl localhost:3000/state | jq
Step 1 🔍 检查 & 压缩
判断主题是否偏移 或 Token 是否超限(35K 阈值)→ 触发 Compaction
Step 2 🧩 组装 LitM 上下文
提取的决策放在顶部,摘要放在 Query 前,最新消息在中间
Step 3 🤖 调用 Gemini
生成回答
Step 4 📤 异步提取决策
从回答中提取架构决策(非阻塞,每 4 轮批量执行)
Step 5 🔗 去重存入
CREATE / MERGE / SKIP 三路判定,存入 Memory Store
Step 6 ✅ 返回回答
// server.ts — handleQuery 核心逻辑(精简)
async function handleQuery(body: QueryRequest) {
const { query, task, relevantCodes } = body;
turnCount++;
// 1. Compaction Check
if (shouldCompact(messages, systemTokens) || shiftWithPressure) {
await flushBuffer(turnCount);
const result = await compact(messages, compactedSummary);
messages = result.remaining;
compactedSummary = result.summary;
}
// 2. Assemble LitM Context
const { assembled, breakdown } = assemble(
repoName, relevantCodes, messages, query, compactedSummary
);
// 3. Generate Answer
const { text } = await generate(forLlm);
// 4. Buffer & Async Extract
exchangeBuffer.push({ query, response: text });
if (exchangeBuffer.length >= BATCH_SIZE) { // 每 4 轮
pendingExtraction = (async () => {
const candidates = await extractBatch(batch);
for (const candidate of candidates) {
const action = await dedup(candidate);
// CREATE | MERGE | SKIP
}
})();
}
return { answer: text };
}
| 类别 | 含义 | 示例 |
|---|---|---|
| 🏗️ structure | 存在什么 | Signal 类在 playhouse/signals.py |
| ⚙️ behavior | 如何工作 | send() 遍历 receivers |
| 🔗 relationships | 如何连接 | signals 与 peewee core 的循环依赖 |
| 🎯 decisions | 为何选择 | 选择 dict 而非自定义类 → 避免导入循环 |
🎯 30+ 个决策仅占 < 4K Tokens — 无需分类器、无需 Embedding、无需路由逻辑
┌────────────────────────────────────────────────────┐
│ Decision Store │
│ │
│ 📂 structure: [ │
│ ├── { L0: "Signal class in playhouse/signals.py" │
│ │ L1: "- connect(receiver): adds callback\n │
│ │ - disconnect(receiver): removes\n │
│ │ - send(): iterates receivers" │
│ │ createdAt: 12 } │
│ └── { ... } │
│ ] │
│ 📂 behavior: [...] │
│ 📂 relationships: [...] │
│ 📂 decisions: [...] │
│ │
│ 🔒 7.5K Token Budget Cap │
│ 📌 保留最新决策,超出预算时裁剪 │
└────────────────────────────────────────────────────┘
| 层级 | 说明 | 长度 |
|---|---|---|
| L0 | 一句话摘要 | ~15 tokens |
| L1 | 结构化 Markdown 要点 | ~50-100 tokens |
文献依据:Lost in the Middle: How Language Models Use Long Contexts (Stanford / Berkeley)
📊 LLM 对长上下文的注意力分布呈 U 型曲线 —— 开头和结尾的注意力最高,中间最低
❌ Before (v1): ✅ After (v2):
🔝 <role> + <code_context>
🔝 system prompt + <project_decisions>
(含摘要) ... 历史消息 ...
... 历史消息 ...
... 14K tokens ... 📍 <conversation_summary>
(合成 assistant 消息)
💤 摘要 (埋在中间)
❓ user query
❓ user query
| 指标 | v1(摘要埋中间) | v2(LitM 优化) | Δ |
|---|---|---|---|
| Topic Awareness | 0.541 | 0.899 | +66% 🔥 |
总结:将摘要从系统提示末尾移到 Query 前的高注意力区域,TA 几乎翻倍!
Token Pressure (35K) ──┐
├──▶ OR ──▶ 🔄 Compact()
Topic Shift (>25K) ───┘
Summarize this coding conversation segment...
Priority:
1. 探索了哪些主题和代码区域
2. 关于运作方式得出的结论
3. 修正或更新的理解
4. 未解决的问题
5. 对话的总体进展
CRITICAL: 保留精确的函数名、文件路径、
类名、参数签名和返回值类型(verbatim)
📐 目标长度:800-1200 tokens — 足够具体,又高度浓缩
提取时机:每 4 轮 批量异步提取(不阻塞主流程)
// 4 种提取类别 + 示例
<structure> "Signal class in playhouse/signals.py"
<behavior> "Signal.send() iterates receiver callbacks"
<relationships> "signals <-> peewee core: circular dep via lazy imports"
<decisions> "Dict over custom class to avoid import cycles"
提取规则:
┌─────────────┐
│ 候选决策 │
│ category │────▶ 查现有同类别决策
│ L0 / L1 │
└─────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Gemini 3.1 Flash Lite │
│ │
│ 🔍 比较候选 vs 现有事实 │
├─────────────────────────────────────────────┤
│ │
│ 📝 CREATE → 全新事实,存入 │
│ 🔗 MERGE → 补充已有事实,合并扩展 │
│ ⏭️ SKIP → 完全冗余,丢弃 │
│ │
│ 默认倾向: SKIP → 保持 Store 精简 │
└─────────────────────────────────────────────┘
| 指标 | TruncateAgent | HermesAgent | Δ |
|---|---|---|---|
| 🧠 Topic Awareness F1 | 0.381 | 0.736 | +93% 🚀 |
| 📋 Information Extraction F1 | 0.763 | 0.652 | -14.5% |
| 代码库 | 指标 | TruncateAgent | HermesAgent | Δ |
|---|---|---|---|---|
| 🔷 Kinto | TA | 0.362 | 1.000 | +176% 🔥 |
| 🔷 Kinto | IE | 0.728 | 0.647 | -11% |
| 🟢 Falcon | TA | 0.400 | 0.471 | +18% |
| 🟢 Falcon | IE | 0.797 | 0.657 | -18% |
Kinto 上 Topic Awareness 达到完美的 1.000 —— 48 轮对话中的每一个主题都记住了!
条件完全公平:同一代码检索器 · 同一 Gemini Flash 骨干 · 同一模拟用户 · 同一评判标准
唯一变量:上下文管理策略
TA +93% ✅
提取的决策 + 压缩的摘要保存了被截断策略静默丢弃的主题信息
IE -14.5% ⚠️
Hermes 的召回率更高(找到更多 ground truth),但精度下降(产生更多假阳性)。决策块给模型提供了架构上下文,鼓励了过度阐述
🔧 这是生成侧的问题,不是上下文管理的失败 —— 明确的迭代目标
| 指标 | TruncateAgent | HermesAgent |
|---|---|---|
| Function Completion | 50% | 50% |
两个 Agent 完全一致,相同的两个函数失败。FC 测试的是底层模型的代码生成能力,而非上下文管理策略。就像测试一个更好的文件归档系统是否能让人成为更好的作家 —— 正交的问题 ✅
| 研究 | 来源 | 在 Hermes 中的角色 |
|---|---|---|
| 📄 Lost-in-the-Middle | Stanford / Berkeley | LitM 上下文编排 — 摘要放在 Query 前 |
| 📄 OpenViking L0/L1/L2 | ByteDance 2026 | 双层记忆架构(L0 摘要 + L1 详情) |
| 📄 Deep Agents Compaction | LangChain 2026 | 消息压缩策略 |
| 📄 ACE Pattern | Zhang et al. 2026 | 提取-压缩-评估 循环模式 |
| 📄 Factory.ai | Factory 2025 | 结构化编码 Agent 压缩(验证方向) |
| 工具 | 解决什么问题 | 与 Hermes 的关系 |
|---|---|---|
| 🧠 OMEGA / Mem0 / Zep | 跨会话记忆 | 互补 — Hermes 是会话内的 |
| 🤖 Letta (MemGPT) | 带记忆的完整 Agent 运行时 | Hermes 是中间件,无需重写 |
| 📉 Deep Agents / FlashCompact | 上下文压缩 | Hermes 在压缩之上增加了决策提取 |
| 🏭 Factory.ai | 结构化编码 Agent 压缩 | 验证了方向 — 相同洞察,不同实现 |
| 🖥️ Server | TypeScript, Bun |
| 🤖 LLM | Gemini 3 Flash(骨干/AI/提取/压缩), Gemini 3.1 Flash Lite(去重) |
| 💾 Memory | In-memory Map<Category, Decision[]>,4 类 |
| 📊 Benchmark | LoCoEval (Python, 仅修改 Gemini 适配器补丁) |
| 🌉 Bridge | 25 行 Python 包装器,转发 HTTP 到 TS Server |
| # | 要点 |
|---|---|
| 1️⃣ | 会话长度不再重要 — Extraction + Compaction 使上下文永不丢失 |
| 2️⃣ | LitM Placement 是关键 — 摘要位置从 0.541 → 0.899 TA |
| 3️⃣ | 默认 SKIP — 去重策略:保守比激进好,保持 Store 精简 |
| 4️⃣ | 压缩时机决定成败 — v2 的 Token Pressure Gate 阻止了过早压缩 |
| 5️⃣ | 生成的精度问题 — 需要在输入侧(提取质量)而非输出侧(限制模型)解决 |
v1 ──────────────────────► v2 ──────────────► v3 (reverted)
│ │ │
├─ IE: 0.607 ├─ IE: 0.631 ├─ IE: 0.524 ❌
├─ TA: 0.541 ├─ TA: 0.899 🚀 ├─ TA: 0.783
│ │ │
└─ 5 个根因问题 └─ 5 个修复 └─ 过度约束
• 过早压缩 • 35K 阈值 !模型变得太保守
• 摘要太浅 • 摘要移至 LitM !完成 Token 降 33%
• 提取太抽象 • 1200 token 目标 !Recall 暴跌
• 去重从不 SKIP • 具体事实优先
• 主题偏移检测 Bug • 默认 SKIP ✅ 回退至 v2 🚢
| 方向 | 说明 |
|---|---|
| 🎯 Extraction Prompt Tuning | 提高提取精度,减少假阳性 |
| 🔄 Decision Compaction | 当决策过多时,智能合并而非简单裁剪 |
| 🔀 Alternative Dedup | 探索 Embedding 相似度替代 LLM 去重 |
| 🌐 Cross-Session Memory | 与 Mem0 / Zep 集成,实现跨会话持久记忆 |
| ⚡ More LLM Backends | 扩展至 Claude、GPT 等 |
🔗 GitHub: github.com/RA1NCS/hermes
📄 License: MIT
🏆 Benchmark: LoCoEval — TA +93%, Kinto perfect 1.000
“The agent always knows what was decided and what was discussed. Conversation length becomes irrelevant.”
Created with ❤️ using Hexo + Reveal.js
| 术语 | 含义 |
|---|---|
| LitM | Lost-in-the-Middle — LLM 对长上下文中间段注意力下降现象 |
| LoCoEval | Long Context Coding Evaluation — 长上下文编码对话评测集 |
| TA | Topic Awareness — 主题意识,Agent 是否记得讨论过什么 |
| IE | Information Extraction — 信息提取,事实召回准确率 |
| FC | Function Completion — 函数补全,代码生成能力 |
| L0 / L1 | 双层决策表示:一句话摘要 / 结构化详情 |
| Compaction | 消息压缩 — 将旧消息智能总结而非丢弃 |