🏛️ Hermes

AI Agent 上下文记忆层的设计与实现


📖 深入解析 Hermes — Context Memory Layer for AI Coding Agents

🔬 基于 5 篇顶会研究 · 在 LoCoEval 上验证 · Topic Awareness +93%

😱 问题的本质

FIFO 上下文缓冲区的致命缺陷


轮次 发生了什么 Agent 还记得吗?
#3 用户选择了 Postgres 作为数据库
#8 定义了 Schema 结构
#12 选择了 JWT Auth 方案
#25 上下文接近极限,旧消息被静默丢弃 ⚠️
#40 Agent 开始自相矛盾,建议用 MongoDB

核心矛盾:用户在第 3 轮做的关键决策,到第 40 轮时已从上下文中完全消失

🚀 Hermes 是什么?

Context Memory Layer for AI Coding Agents

🎯 定位 轻量级中间件,不是 Agent 运行时
🧠 核心能力 从对话中永久提取架构决策 + 智能压缩旧消息
技术栈 TypeScript + Bun + Google Gemini
🏷️ 许可证 MIT Open Source
"Conversation length becomes irrelevant."
— Hermes README

🔄 两大核心机制


📤 Extraction — 提取

  • 每次响应后提取架构决策(Schema、Auth、路由、依赖)
  • 永久存储,跨轮次不丢失

📦 Compaction — 压缩

  • 任务边界处智能总结旧消息
  • 主题意识在原始消息消失后依然存活

💡 最终效果:Agent 始终知道"什么被决定了"和"什么被讨论过"

🏗️ 系统架构

整体设计一览

┌─────────────────────────────────────┐
│          Python Bridge              │
│  (25 lines, HTTP ↔ LoCoEval)       │
└──────────┬──────────────────────────┘
           │ /init  /query  /state
┌──────────▼──────────────────────────┐
│      Hermes TypeScript Server       │
│                                     │
│  Extractor ──▶ Dedup ──▶ Store     │
│  Compactor ──▶ Assembler ──▶ API   │
└─────────────────────────────────────┘

🔌 三个核心 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

🔬 Pipeline 详解

每一轮对话的完整流程


Step 1 🔍 检查 & 压缩

判断主题是否偏移 或 Token 是否超限(35K 阈值)→ 触发 Compaction

Step 2 🧩 组装 LitM 上下文

提取的决策放在顶部,摘要放在 Query 前,最新消息在中间

Step 3 🤖 调用 Gemini

生成回答

Step 4 📤 异步提取决策

从回答中提取架构决策(非阻塞,每 4 轮批量执行)

Step 5 🔗 去重存入

CREATE / MERGE / SKIP 三路判定,存入 Memory Store

Step 6返回回答

🔄 完整 Pipeline 代码

// 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

💡 LitM 上下文编排

Lost-in-the-Middle 的实战应用


文献依据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 几乎翻倍!

📦 Compaction — 智能压缩

何时触发?

Token Pressure (35K) ──┐
                        ├──▶ OR ──▶ 🔄 Compact()
Topic Shift (>25K)  ───┘
  • 首次压缩:~第 23 轮(v2),而非第 9 轮(v1)
  • ✂️ 压缩比例:移除最旧的 60% 消息
  • 🔄 摘要可叠加:每次压缩都会继承前一次的摘要

📝 Compaction Prompt

Summarize this coding conversation segment...

Priority:
1. 探索了哪些主题和代码区域
2. 关于运作方式得出的结论  
3. 修正或更新的理解
4. 未解决的问题
5. 对话的总体进展

CRITICAL: 保留精确的函数名、文件路径、
类名、参数签名和返回值类型(verbatim)

📐 目标长度:800-1200 tokens — 足够具体,又高度浓缩

📤 Extraction — 决策提取

从对话中挖掘持久知识

提取时机:每 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"

提取规则

  • ✅ 保留精确标识符(函数名、参数、类型)
  • ✅ 优先提取:具体技术事实 > 修正 > 约束 > 设计决策
  • ❌ 不提取:模糊设计哲学、隐喻、抽象原则
  • ❌ 如果只是确认/感谢 → 返回空数组

🔗 三路去重 Dedup

┌─────────────┐
│  候选决策    │
│  category   │────▶ 查现有同类别决策
│  L0 / L1    │
└─────────────┘
       │
       ▼
┌─────────────────────────────────────────────┐
│           Gemini 3.1 Flash Lite              │
│                                              │
│  🔍 比较候选 vs 现有事实                      │
├─────────────────────────────────────────────┤
│                                              │
│  📝 CREATE  → 全新事实,存入                   │
│  🔗 MERGE   → 补充已有事实,合并扩展           │
│  ⏭️  SKIP   → 完全冗余,丢弃                  │
│                                              │
│  默认倾向: SKIP → 保持 Store 精简             │
└─────────────────────────────────────────────┘

📊 Benchmarks

在 LoCoEval 上的验证结果

指标 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 大涨而 IE 下降?


TA +93%

提取的决策 + 压缩的摘要保存了被截断策略静默丢弃的主题信息

IE -14.5% ⚠️

Hermes 的召回率更高(找到更多 ground truth),但精度下降(产生更多假阳性)。决策块给模型提供了架构上下文,鼓励了过度阐述

🔧 这是生成侧的问题,不是上下文管理的失败 —— 明确的迭代目标

📌 Function Completion 为何排除?

指标 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 与其他工具的定位

工具 解决什么问题 与 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

🎯 关键要点

Takeaways


# 要点
1️⃣ 会话长度不再重要 — Extraction + Compaction 使上下文永不丢失
2️⃣ LitM Placement 是关键 — 摘要位置从 0.541 → 0.899 TA
3️⃣ 默认 SKIP — 去重策略:保守比激进好,保持 Store 精简
4️⃣ 压缩时机决定成败 — v2 的 Token Pressure Gate 阻止了过早压缩
5️⃣ 生成的精度问题 — 需要在输入侧(提取质量)而非输出侧(限制模型)解决

⚡ Pipeline 演化历程

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 🚢

🔮 未来方向

What’s Next?


方向 说明
🎯 Extraction Prompt Tuning 提高提取精度,减少假阳性
🔄 Decision Compaction 当决策过多时,智能合并而非简单裁剪
🔀 Alternative Dedup 探索 Embedding 相似度替代 LLM 去重
🌐 Cross-Session Memory 与 Mem0 / Zep 集成,实现跨会话持久记忆
More LLM Backends 扩展至 Claude、GPT 等

🙏 感谢阅读

Hermes — Context Memory Layer for AI Agents


🔗 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 消息压缩 — 将旧消息智能总结而非丢弃