Hermes 与 Claude CodeHermes 与 Claude Code第 9 章 · 14 章Chapter 9 of 14
全文目录Contents
  1. 这个网页怎么用
  2. 1 · 零基础前置知识
    1. 1.1 大语言模型是什么
    2. 1.2 token 是什么
    3. 1.3 上下文与上下文窗口是什么
    4. 1.4 智能体与聊天机器人的区别
    5. 1.5 工具调用是什么,它到底怎么工作
    6. 1.6 什么是流式输出
    7. 1.7 什么是 API
    8. 1.8 提示词缓存 —— 全文最重要的技术概念
    9. 1.9 还会遇到的几个词
  3. 2 · 两个系统的宏观定位与架构总图
    1. 2.1 用一句话说清各自的定位
    2. 2.2 架构总图
    3. 2.3 关键数字对照
    4. 2.4 第一个值得记住的洞察
  4. 3 · 垂直分层与水平分区
    1. 3.1 垂直分层:六层模型
    2. 3.2 水平分区:同一层内部怎么切
    3. 3.3 一个反直觉的观察:巨型文件
    4. 3.4 分层之外:穿透所有层的四类逻辑
  5. 4 · 智能体主循环与错误恢复状态机
    1. 4.1 教科书版本的循环,以及它会死在哪
    2. 4.2 Claude Code 的做法:写成显式状态机
    3. 4.3 错误扣留:可恢复的错误不能立刻往外发
    4. 4.4 中断的正确处理姿势
    5. 4.5 切换备用模型:一个想不到的坑
    6. 4.6 Hermes 的做法:预算驱动的循环
    7. 4.7 Hermes 独有的能力:轮次中途插话
    8. 4.8 两家对照与取舍
  6. 5 · 工具抽象层与工具执行编排
    1. 5.1 Claude Code 的工具接口:一个教科书级抽象
    2. 5.2 渐进式工具加载:工具搜索机制
    3. 5.3 工具清单的装配:一个只有深度用过缓存才知道的坑
    4. 5.4 执行编排:并发分区
    5. 5.5 流式工具执行器:边流边执行
    6. 5.6 Hermes 的工具层:中心分发 + 参数强制矫正
    7. 5.7 两家对照与取舍
  7. 6 · 上下文治理阶梯 ★ 全文核心
    1. 6.1 Claude Code 的做法:五级流水线
    2. 6.2 第 ① 级:工具结果预算与落盘
    3. 6.3 第 ③ 级:缓存编辑 —— 全文最精彩的一处
    4. 6.4 第 ⑤ 级:自动摘要压缩的工程细节
    5. 6.5 上下文真的超了之后:三级恢复瀑布
    6. 6.6 Hermes 的做法:把整条阶梯抽象成一个插座
    7. 6.7 两家横向对照
  8. 7 · 权限模型与安全边界
    1. 7.1 Claude Code 的做法:十级决策级联
    2. 7.2 自动模式:用模型判断安全性,加三级快速通道
    3. 7.3 Hermes 的做法:正则红线 + 对抗性解析
    4. 7.4 Hermes 的第二道防线:执行环境隔离
    5. 7.5 一个常被忽略的攻击面:错误消息回灌
    6. 7.6 两家对照与取舍
  9. 8 · 多智能体协作编排
    1. 8.1 子智能体到底解决什么问题
    2. 8.2 Claude Code 的三种子智能体形态
    3. 8.3 分叉子智能体:把提示词缓存用到极致
    4. 8.4 子智能体的工具限制
    5. 8.5 Hermes 的做法:任务委派 + 看板协作
    6. 8.6 一个两家共同的硬约束:中断的级联
  10. 9 · 记忆系统与扩展体系
    1. 9.1 记忆:两种截然不同的答案
    2. 9.2 Hermes 的全息记忆值得单独看
    3. 9.3 Claude Code 的记忆预取:藏在流水线里的优化
    4. 9.4 扩展体系:四种扩展点
    5. 9.5 Hermes 的网关层:Claude Code 完全没有的一层
  11. 10 · 两个系统的横向对照总表
    1. 10.1 机制对照
    2. 10.2 两条架构路线各自的账本
    3. 10.3 两家一致的地方 = 事实上的行业共识
  12. 11 · 可以搬到自己项目里的实现范式
    1. 11.1 骨架一:带恢复状态机的主循环
    2. 11.2 骨架二:上下文治理阶梯
    3. 11.3 骨架三:工具抽象 + 并发分区
    4. 11.4 骨架四:按信任边界配置工具面
    5. 11.5 骨架五:不可绕过的安全底座
    6. 11.6 自建智能体的决策清单
    7. 11.7 一页纸检查清单
  13. 12 · 面试话术卡
    1. Q1 · 说说你理解的智能体架构
    2. Q2 · 上下文满了怎么办
    3. Q3 · 怎么防止智能体执行危险命令
    4. Q4 · 多工具并行怎么保证不出竞态
    5. Q5 · 智能体循环怎么防止无限循环
    6. Q6 · 什么时候该用子智能体
    7. Q7 · 智能体的长期记忆怎么做
    8. Q8 · 你怎么优化智能体的成本和延迟
    9. Q9 · 反问环节可以问的问题
    10. 最后:这份文档的正确用法
  14. 13 · 术语表
    1. 13.1 模型与调用
    2. 13.2 提示词缓存(全文最重要的概念组)
    3. 13.3 智能体与工具
    4. 13.4 上下文治理
    5. 13.5 编程与架构概念
    6. 13.6 两个系统的关键模块名

9 · 记忆系统与扩展体系

9.1 记忆:两种截然不同的答案

先说清楚问题:回顾第 1.1 节,大语言模型完全没有记忆。一场会话结束,一切归零。那么「让智能体记住上次的事」这件事,必须由外部程序实现。

Claude Code —— 文件即记忆
  • CLAUDE.md —— 项目级的指令文件,按目录层级嵌套加载(子目录的会追加到父目录的后面)
  • memdir/ —— 一条记忆一个文件,外加一个 MEMORY.md 作为索引目录
  • 对话记录存成 JSONL 文件(每行一条 JSON),用 --resume 参数可以回放恢复
  • 召回机制:提前预取 + 让模型判断相关性(函数名 startRelevantMemoryPrefetch
  • 没有向量、没有数据库。记忆就是人类可读、可以用 git 管理的纯文本。
Hermes —— 结构化记忆栈
  • hermes_state.py(698 KB)—— 用 SQLite 数据库存储,配合 FTS5 全文索引
  • 全息记忆插件 —— HRR 相位向量 + 事实与实体关系图 + 信任分数
  • MemoryProvider 抽象基类 → 8 种可插拔的外部记忆服务
  • 四个身份文件:SOUL.md(人格设定)、MEMORY.mdUSER.mdAGENTS.md
  • 召回机制:FTS5 词法检索 + 向量相似度 + 模型摘要

什么是 FTS5?Full-Text Search 版本 5 的缩写,是 SQLite 数据库内置的全文搜索引擎。它做的是「词法检索」—— 按关键词精确匹配,就像用 Ctrl+F 在文档里搜词。它不理解语义,搜「容器」不会命中「Docker」。

9.2 Hermes 的全息记忆值得单独看

这是整个代码库里最「学术」的一块。它用的是一种叫 HRR(Holographic Reduced Representations,全息缩减表示)的技术 —— 属于「向量符号架构」这一类方法。

先说清楚它想解决什么问题

常规的记忆检索有两条路:

方法怎么工作缺点
词法检索
FTS5
按关键词精确匹配 不理解同义词。搜「容器」搜不到「Docker」
向量检索
embedding
用一个神经网络把文字转成一串数字(向量),意思相近的文字向量也相近 换了那个神经网络,全库的向量就得重算。这是所有向量记忆方案最大的运维噩梦

HRR 走的是第三条路:用确定性的数学运算把符号组合成向量,不需要神经网络。

三个核心运算

def bind(a, b):        # 绑定 = 循环卷积 = 逐元素相位相加
    return (a + b) % _TWO_PI
    # 把两个概念绑定成一个复合向量。
    # 结果与两个输入都不相似(数学上叫"准正交")

def unbind(memory, key):   # 解绑 = 循环相关 = 相位相减
    return (memory - key) % _TWO_PI
    # 从一个记忆向量里取回和某个键关联的值。
    # unbind(bind(a, b), a) ≈ b   (差一点叠加带来的噪声)

def bundle(*vectors):  # 打包 = 叠加 = 复指数的圆均值
    complex_sum = np.sum([np.exp(1j * v) for v in vectors], axis=0)
    return np.angle(complex_sum) % _TWO_PI
    # 把多个向量合并成一个,结果与每一个输入都相似。
    # 能容纳大约 √维度 个项,超过就开始退化

def similarity(a, b):  # 相似度 = 相位余弦,范围 [-1, 1]
    return float(np.mean(np.cos(a - b)))

hermes-agent/plugins/memory/holographic/holographic.py

不需要懂数学也能理解这三个运算的用途:

  • 绑定把「键」和「值」粘成一个向量,比如把「用户的编辑器」和「Vim」绑成一个
  • 解绑是绑定的逆运算,给一个键能取回对应的值
  • 打包把很多条记忆压成一个向量,用一个向量代表整个类别

最值得注意的工程决策:用 SHA-256 而不是随机数

def encode_atom(word: str, dim: int = 1024):
    """Deterministic phase vector via SHA-256 counter blocks.
    Uses hashlib (not numpy RNG) for cross-platform reproducibility.
    """
    for i in range(blocks_needed):
        digest = hashlib.sha256(f"{word}:{i}".encode()).digest()
        uint16_values.extend(struct.unpack("<16H", digest))
    phases = np.array(uint16_values[:dim]) * (_TWO_PI / 65536.0)

译:通过 SHA-256 计数器分块,生成确定性的相位向量。用 hashlib 而不是 numpy 的随机数生成器,是为了跨平台可复现。

(SHA-256 是一种哈希算法:同样的输入永远得到同样的输出,而且输出看起来像随机数。dim: int = 1024 表示向量有 1024 个维度。)

这个选择的工程含义

同一个词,比如 "docker",在任何机器、任何 Python 版本、任何进程里,编码出来的 1024 维相位向量完全一致

这意味着:
· 记忆向量可以直接存进 SQLite 的二进制字段
· 可以跨机器同步
· 不存在「换了 embedding 模型,全库要重算」这个运维噩梦

代价是:它是「词袋级」的符号组合,完全没有语义理解能力。"docker""container" 的相似度接近 0,因为它们是两个不同的字符串,哈希结果毫无关系。

所以它必须和 FTS5 配合使用,而不是替代它。这是一个很清醒的定位:用零成本的确定性方法解决「组合结构」问题,把「语义理解」问题留给别的手段。

存储层的设计也值得看

CREATE TABLE facts (...)            -- 事实表,带信任分数和分类
CREATE TABLE entities (...)         -- 实体表(人、项目、技术名词)
CREATE TABLE fact_entities (...)    -- 事实与实体的多对多关联
CREATE INDEX idx_facts_trust ON facts(trust_score DESC);   -- 按信任分排序的索引
CREATE VIRTUAL TABLE facts_fts ...  -- FTS5 全文索引
CREATE TABLE memory_banks (...)     -- 按分类聚合的 HRR 打包向量

并且有一个反馈闭环

def record_feedback(self, fact_id: int, helpful: bool) -> dict:
    # 根据这条记忆是否有帮助,调整它的 trust_score(信任分数)
为什么信任分数是刚需,不是锦上添花

设想一个真实场景:智能体在第一次会话里误以为「这个项目用的是 npm」,把这条记忆存了下来。实际上项目用的是 pnpm。

如果没有信任分数衰减机制,这条错误记忆会永久污染后续所有会话 —— 每次智能体都会先读到「这个项目用 npm」,然后执行 npm 命令,然后失败,然后困惑。

有了 record_feedback:这条记忆被证明误导之后,信任分下降,排序沉底,最终不再被召回。记忆系统必须有自我纠错的能力,否则它是负资产。

还有一个体现工程成熟度的函数:snr_estimate(dim, n_items) —— 估算「在给定维度下塞进 N 条记忆后的信噪比」。因为 bundle() 打包运算只能容纳大约 √维度 个项,1024 维大约在 32 项之后就开始退化。

把自己方案的容量上限写成一个可调用的函数暴露出来,这是很成熟的做法。它承认了「这个方法有边界」,并且让使用者能测出这个边界在哪。

9.3 Claude Code 的记忆预取:藏在流水线里的优化

// query.ts,主循环入口处
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
    state.messages, state.toolUseContext)

这一行代码有三个细节值得注意:

细节为什么这样设计
每个用户轮次只触发一次,不是每次循环迭代都触发 源码注释:「the prompt is invariant across loop iterations, so per-iteration firing would ask sideQuery the same question N times」
译:用户的提问在整个轮次的多次循环迭代中是不变的,所以每次迭代都触发会向侧查询问同一个问题 N 次。
消费点从不阻塞 它只检查 settledAt 字段(是否已完成)—— 没完成就跳过,下次迭代再看。一个轮次里有几次迭代,它就有几次机会。绝不等待。
using 声明 这是 JavaScript 的一个较新语法(显式资源管理),保证无论函数从哪条路径退出,这个预取任务都会被正确清理。生成器函数有很多退出路径(正常返回、抛异常、被外部关闭),漏掉任何一条就会资源泄漏。

消费时还要用 readFileState 做过滤 —— 模型自己已经读过、写过、改过的记忆文件,不再重复注入一遍。而这个 readFileState跨迭代累积的,所以能过滤掉早期迭代里读过的文件。

可以搬走的做法:把慢操作藏进模型流式输出的时间窗

回顾第 1.6 节:模型流式返回一个完整回复要 5 到 30 秒。这段时间你的 CPU 基本闲着。

Claude Code 在这个时间窗里塞了至少三件事:

  • 记忆预取 —— 判断哪些历史记忆和当前问题相关
  • 技能发现预取 —— 判断哪些技能文件可能有用
  • 上一批工具的摘要生成 —— 用一个更便宜的小模型(Haiku)给工具执行结果生成摘要

源码里留了一个数字:技能发现预取的完成率大于 98%(预取本身耗时 250 到 573 毫秒,而轮次时长是 2 到 30 秒)。

「藏在主流程延迟下的旁路计算」几乎是免费的,但只有一个前提:消费点必须设计成「好了就用,没好就算了」,绝不能阻塞主流程等它。一旦开始等,这个优化就变成了负优化。

9.4 扩展体系:四种扩展点

「扩展点」的意思是:让第三方(或用户自己)在不修改主程序源代码的前提下,往系统里添加能力。

类型Claude CodeHermes
技能
Skill
SKILL.md 文件 + 文件头部的元数据(frontmatter)
可以声明自己需要哪些钩子、允许用哪些工具
通过 SkillTool 被模型调用
skills/<分类>/<名字>/SKILL.md
15 个分类,全部是纯 Markdown 文本
三个工具:列出技能、查看技能、管理技能
插件
Plugin
pluginLoader.ts(107 KB)
有插件市场和引用版本追踪
3 个发现来源(下面详述)
插件可以注册工具、钩子、命令行子命令
MCP
模型上下文协议
支持标准输入输出和 HTTP 两种传输方式
工具名加前缀 mcp__服务名__工具名
支持延迟加载、支持向用户索取信息
同样两种传输方式
前缀 mcp_<服务名>_<工具名>
支持选择性加载、自动重载
自带 67 个可选的 MCP 服务
钩子
Hook
10 类事件:工具执行前 / 工具执行后 / 工具失败后 /
用户提交提问 / 会话开始 / 通知 /
结束前 / 压缩前 / 采样后 / 权限请求
gateway/hooks.py + 内置钩子目录
模型调用前 / 工具调用后 / 每步事件 / 审批钩子

技能的本质:把「知识」变成「可寻址的能力」

两家的技能都是 Markdown 文本文件,核心机制都是渐进式披露

程序启动时 └─ 只加载每个技能文件的"头部元数据"(名字 + 一句话描述) 每个只占几十个 token,几百个技能也就几千 token ↓ 模型判断某个技能可能相关时 └─ 主动调用 SkillTool(名字) / skill_view(名字) ↓ 运行时 └─ 完整的 SKILL.md 正文才被注入上下文(可能几千 token)

Claude Code 甚至有一个专门的函数 estimateSkillFrontmatterTokens(skill)(估算技能头部元数据的 token 数)—— 因为所有技能的头部元数据都是常驻上下文的,装 100 个技能的固定成本必须可测量。

而这和第 5.2 节的工具延迟加载是完全相同的模式

渐进式披露 = 智能体系统的通用扩展范式

一句话概括:目录常驻(便宜),内容按需展开(贵)。

场景常驻的部分按需加载的部分
工具名字 + 关键词完整的参数格式说明
技能名字 + 一句话描述完整的技能正文
MCP服务列表该服务下的具体工具
记忆MEMORY.md 索引具体的记忆文件内容

这四个场景是同一个设计模式的四次应用。面试里能把它们归纳成一条原则,比逐个描述强得多 —— 因为它证明你看到的是模式,不是细节。

Hermes 的插件发现三个来源

~/.hermes/plugins/     用户级(对这台机器上的所有项目生效)
./.hermes/plugins/     项目级(跟着代码仓库走,团队共享)
pip entry points       包级(用 pip install 安装某个包就自动生效)

而且 Hermes 区分了两类插件,这是一个容易被忽略但很重要的设计点:

「可叠加的能力」与「互斥的策略」必须区别对待

可叠加的:工具插件、平台适配器插件。装 10 个就有 10 份能力,互不冲突。

互斥的(Hermes 里叫「单选」插件):记忆提供者、上下文引擎。

为什么这两类必须区别对待?因为记忆提供者和上下文引擎是「策略」而不是「能力」。同时装两个上下文引擎没有任何意义 —— 一个说要压缩、一个说不压缩,系统该听谁的?

所以 Hermes 在插件系统层面就把它们标记成单选,装第二个时直接报错,而不是留到运行时产生诡异行为。

在你自己的插件系统里,这个区分要在设计阶段就做出来。否则用户装了两个策略插件,你的系统会以某种未定义的方式工作,而且极难排查。

9.5 Hermes 的网关层:Claude Code 完全没有的一层

gateway/run.py 是单个文件 1.55 MB,是 Hermes 最大的模块。它解决的是 Claude Code 根本不面对的问题:一个长期在线的智能体,如何被 22 个不同的聊天平台以统一的方式触达。

gateway/
├── run.py                  长驻主循环
├── platforms/base.py       BasePlatformAdapter 抽象基类,333 KB
├── platform_registry.py    平台注册表
├── profile_routing.py      多身份路由(一个进程可以承载多个不同人格的智能体)
├── delivery.py             消息投递
├── delivery_ledger.py      投递账本(防止重复发送)
├── restart.py              重启逻辑
├── restart_loop_guard.py   重启风暴防护(防止崩溃-重启-崩溃的无限循环)
├── memory_monitor.py       内存占用监控
├── agent_cache_pressure.py 智能体缓存压力管理
├── drain_control.py        优雅排空(关闭前把手头的消息处理完)
└── builtin_hooks/          内置钩子

那个抽象基类 BasePlatformAdapter 抽出的能力差异非常细致:

def max_message_length_for_chat(chat_id) -> int      # 这个平台单条消息最长多少字
def supports_draft_streaming(...) -> bool            # 支持"草稿式流式更新"吗
def prefers_fresh_final_streaming(...) -> bool       # 偏好重发最终版本吗
def streaming_overflow_limit() -> Optional[int]      # 流式更新的溢出上限
def enforces_own_access_policy() -> bool             # 平台自己管权限吗
def authorization_is_upstream() -> bool              # 授权在上游完成吗
def format_tool_event(event, *, mode='all') -> str   # 工具事件怎么渲染显示
class EphemeralReply(str): ...                       # 带过期时间的临时回复

hermes-agent/gateway/platforms/base.py

这层抽象的关键洞察

不同聊天平台的能力差异非常大:

  • Slack 支持编辑已发出的消息 → 可以流式更新同一条消息,用户看到文字逐渐生长
  • 短信 不支持编辑 → 只能发新消息,流式输出根本没法做
  • Discord 单条消息上限 2,000 字;Telegram 是 4,096 字
  • 企业微信 自己有一套完整的权限体系;IRC 完全没有权限概念

把这些差异抽象成「能力查询方法」,而不是写成 if platform == 'slack' 的分支 —— 这是这个适配层能撑住 22 个平台的根本原因。

新增一个平台 = 实现一组能力声明。而不是往主流程里再加一堆分支判断。

这个模式的通用价值:当你要适配 3 个以上的外部系统时,就该把「它们的差异」抽象成一组问题,让每个适配器自己回答,而不是在主流程里做分支。

面试追问:智能体的长期记忆你会怎么做?

先区分三种「记忆」,别一股脑塞进向量数据库:

  • 指令性记忆(用户偏好、团队约定、项目规范)—— 这类不该用检索。它应该是人类可读、可以审阅、可以用 git 管理的纯文本,每次全量加载。理由:用户改了要立刻生效,而且必须能看到自己写了什么。Claude Code 的 CLAUDE.md 就是这个定位。
  • 事实性记忆(谁是谁、什么时候做了什么决定)—— 这类需要检索。词法检索(FTS5)+ 向量检索混合,优于纯向量,因为事实里有大量专有名词,而向量检索恰好对专有名词不敏感。
  • 过程性记忆(上次这个问题是怎么解决的)—— 本质是会话历史检索,存对话记录 + 全文索引。

然后讲两个容易被忽略的工程点:

  • 记忆需要信任衰减。Hermes 用 trust_scorerecord_feedback(fact_id, helpful):被证明有用的记忆上浮,误导过人的沉底。没有这个机制,一条早期的错误记忆会永久污染后续所有会话。
  • 注入要去重。模型自己刚刚读过的文件,不要再作为「记忆」注入一遍。Claude Code 用跨迭代累积的已读文件状态来过滤 —— 注意是跨迭代累积,只看本次迭代会漏掉早期读过的。

如果对方问到向量方案的选型,可以提 Hermes 的 HRR 作为一个有意思的对照:用 SHA-256 确定性生成原子向量,同一个词在任何机器、任何版本上编码结果完全一致 —— 彻底避开了「换 embedding 模型要重算全库」这个运维噩梦。代价是它是词袋级的符号组合,没有语义泛化能力(「docker」和「container」相似度接近 0),所以必须配合词法检索使用,而不是替代它。这个权衡本身就很值得讨论。