Hermes 与 Claude CodeHermes 与 Claude Code第 8 章 · 14 章Chapter 8 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 两个系统的关键模块名

8 · 多智能体协作编排

8.1 子智能体到底解决什么问题

先说清楚什么是「子智能体」:主智能体在执行任务时,可以创建一个新的、独立的智能体实例,把某个子任务派给它,等它做完拿回结果。被派出去的那个就叫子智能体(subagent)。

大多数人的第一反应是「这是为了并行加速」。但两套系统的源代码显示,真正的首要动机是上下文隔离

设想一个场景:主智能体要找到某个函数定义在哪个文件里,可能需要读 20 个文件才能确定。

如果它自己读:这 20 个文件的完整内容全部进入主上下文,而且此后每一轮都要重发一遍(回顾第 1.1 节)。假设每个文件 2,000 token,那就是 40,000 token 永久占用,一直付费到会话结束。

如果派一个子智能体去读:子智能体的上下文用完即弃 —— 它读完 20 个文件、得出结论、返回一句「在 foo.ts 第 42 行」,然后它的整个上下文被丢弃。主智能体只收到那一句话,大约 15 个 token。

并行只是副产品。子智能体的第一性原理是「用一次性的上下文,换一个结论」。

8.2 Claude Code 的三种子智能体形态

形态子智能体的上下文用途
命名子智能体
调用时指定 subagent_type
全新的,只带一段任务描述 探索代码库、通用任务,或者用户自定义的智能体类型(写在 .claude/agents/*.md 文件里)
分叉子智能体
调用时省略 subagent_type
完整继承父智能体的对话历史和系统提示词 并行探索同一个问题的多个方向。比如「用三种不同思路各写一版实现,然后比较」
协调者模式的工人 受限的工具集,上下文独立 在「协调者」模式下负责实际干活的执行单元

8.3 分叉子智能体:把提示词缓存用到极致

这是 Claude Code 里最精巧的机制之一,而且它是第 1.8 节那个缓存概念的终极应用

先看清楚这里的机会

分叉的典型用法是「同时派 5 个子智能体,从不同角度探索同一个问题」。这 5 个子智能体的上下文几乎完全一样 —— 都继承了父智能体的全部历史,唯一的区别是最后那一句「你负责探索方向 A / B / C / D / E」。

而提示词缓存是前缀匹配的。所以:

如果能让这 5 个子智能体发出的请求前缀达到「字节级完全一致」,那么第 1 个建立缓存,后面 4 个全部命中缓存。

这意味着输入 token 的成本从 5 份降到大约 1.4 份(1 份全价 + 4 份 10% 折扣价)。省下 70% 以上。

为了做到字节级一致,Claude Code 做了四件事

① 系统提示词传递「已渲染好的字节」,而不是重新生成
/**
 * The getSystemPrompt here is unused: the fork path passes
 * `override.systemPrompt` with the parent's already-rendered system prompt
 * bytes, threaded via `toolUseContext.renderedSystemPrompt`. Reconstructing
 * by re-calling getSystemPrompt() can diverge (GrowthBook cold→warm)
 * and bust the prompt cache; threading the rendered bytes is byte-exact.
 */

claude-code/src/tools/AgentTool/forkSubagent.ts

译:这里的 getSystemPrompt 是没用到的:分叉路径传递的是父智能体已经渲染好的系统提示词字节,通过 renderedSystemPrompt 这个字段串下来。重新调用 getSystemPrompt() 来构造可能产生分歧(因为特性开关配置可能从冷缓存变成热缓存),从而毁掉提示词缓存;而传递已渲染的字节是字节级精确的。

展开解释这个坑:系统提示词的内容并不是完全固定的,它可能包含 A/B 实验的变体。而实验配置本身是有缓存的 —— 父智能体生成系统提示词的那一刻,某个实验配置可能还是「冷缓存」状态(用默认值);几秒钟后子智能体重新生成时,配置已经变成「热缓存」(用真实值)。结果是两次生成的字节不同,缓存全废。

解法:父智能体在轮次开始时就把渲染好的字节冻结下来,分叉时原样传递。

② 工具清单原样继承
export const FORK_AGENT = {
  tools: ['*'],              // 配合 useExactTools:继承父的"精确"工具集
  permissionMode: 'bubble',  // 权限确认冒泡到父智能体所在的终端
  model: 'inherit',          // 继承父的模型(保证上下文窗口大小一致)
  ...
}

子智能体其实用不到父的全部工具。但工具定义是请求前缀的一部分(回顾第 5.3 节),改了就没缓存了。所以宁可给它一堆用不到的工具。

③ 消息构造:只让最后一个文本块不同
/**
 * For prompt cache sharing, all fork children must produce byte-identical
 * API request prefixes. This function:
 * 1. Keeps the full parent assistant message (all tool_use blocks, thinking, text)
 * 2. Builds a single user message with tool_results for every tool_use block
 *    using an identical placeholder, then appends a per-child directive text block
 *
 * Result: [...history, assistant(all_tool_uses), user(placeholder_results..., directive)]
 * Only the final text block differs per child, maximizing cache hits.
 */
export function buildForkedMessages(...)

译:为了共享提示词缓存,所有分叉出的子智能体必须产生字节级相同的请求前缀。这个函数做的事是:
1. 完整保留父智能体那条消息(包含所有工具调用块、思考块、文字)
2. 构造一条用户消息,为每一个工具调用块都配一个完全相同的占位工具结果,然后在末尾追加各自不同的一小段指令文字

结果形状是:[...历史, 模型消息(所有工具调用), 用户消息(占位结果×N, 指令)]
每个子智能体只有最后那个文字块不同,从而最大化缓存命中。

分叉子 #1: [...历史, 模型消息(工具调用×5), 用户消息(占位×5, "探索方向A")] 分叉子 #2: [...历史, 模型消息(工具调用×5), 用户消息(占位×5, "探索方向B")] 分叉子 #3: [...历史, 模型消息(工具调用×5), 用户消息(占位×5, "探索方向C")] └──────────── 完全相同的前缀,全部命中缓存 ───────────┘ └─唯一差异─┘

注意那个「占位工具结果」的巧妙之处:父智能体那条消息里有 5 个工具调用(每个对应一个分叉)。按 API 规则,每个工具调用必须有配对的结果(回顾第 4.4 节)。但这 5 个分叉还没跑完,真实结果不存在。于是给每一个都填一个完全相同的占位内容 —— 既满足了配对要求,又保证了 5 个子智能体看到的这段内容一模一样。

④ 递归分叉的守卫方式很特别
/**
 * Guard against recursive forking. Fork children keep the Agent tool in their
 * tool pool for cache-identical tool definitions, so we reject fork attempts
 * at call time by detecting the fork boilerplate tag in conversation history.
 */
export function isInForkChild(messages: MessageType[]): boolean { ... }

译:防止递归分叉。分叉出的子智能体为了保持工具定义的缓存一致性,工具池里仍然保留着 Agent 工具,所以我们改为在调用时拒绝 —— 方法是检测对话历史里有没有分叉的样板标记。

常规做法是「把 Agent 工具从子智能体的工具池里去掉」。但那样就改变了工具定义,破坏缓存。所以 Claude Code 选择:工具留着,但在真正调用的那一刻检查对话历史里有没有分叉标记,有就拒绝:

throw new Error('Fork is not available inside a forked worker. '
              + 'Complete your task directly using your tools.')
// 译:分叉功能在分叉出的工人内部不可用。请直接用你手上的工具完成任务。
这四件事的共同点:为了缓存一致性,牺牲代码整洁度

逐条看这四个决定,每一个都在常规评审里会被挑刺

  • 给用不到的工具 → 「为什么不做最小权限?」
  • 传字节而不是重新生成 → 「为什么不复用现成的生成函数?」
  • 用无意义的占位符填充工具结果 → 「这不是在造假数据吗?」
  • 把守卫从「接口层」下移到「调用层」 → 「为什么不在类型系统里禁掉?」

但如果你的智能体要做扇出(一次派多个子智能体),这些代价值得付:5 个子智能体里 4 个走缓存,输入 token 成本降到 1/3 以下。

这是一个非常具体、可量化、能在面试里讲清楚的架构决策。它体现的能力是「知道什么时候该为性能牺牲整洁度」—— 比单纯背诵设计原则有价值得多。

8.4 子智能体的工具限制

export const ALL_AGENT_DISALLOWED_TOOLS = new Set([
  TASK_OUTPUT_TOOL_NAME,        // 不能查看其他任务的输出
  EXIT_PLAN_MODE_V2_TOOL_NAME,  // 不能退出计划模式(那是会话级的全局状态)
  ENTER_PLAN_MODE_TOOL_NAME,    // 不能进入计划模式
  ...(USER_TYPE === 'ant' ? [] : [AGENT_TOOL_NAME]),  // 默认禁止嵌套创建子智能体
  ASK_USER_QUESTION_TOOL_NAME,  // ★ 不能向用户提问
  TASK_STOP_TOOL_NAME,          // 不能停止其他任务
  ...(feature('WORKFLOW_SCRIPTS') ? [WORKFLOW_TOOL_NAME] : []),  // 不能递归执行工作流
])

claude-code/src/constants/tools.ts

三条原则清晰可见:

原则为什么
不能修改全局状态 计划模式是整场会话级别的开关。子智能体改了它,会影响父智能体和所有兄弟智能体,而它们完全不知道发生了什么。
不能直接和用户对话 子智能体没有界面通道 —— 它跑在后台,弹不出确认框。它想传递信息只能通过「返回结果」这一条路。
不能操作兄弟任务 没有横向权限。子智能体之间互相不可见,避免它们互相干扰或形成意料之外的协作。

另外还有一个更严格的白名单 ASYNC_AGENT_ALLOWED_TOOLS(异步智能体允许的工具),只包含 Read、WebSearch、TodoWrite、Grep、WebFetch、Glob 等基本只读工具。因为后台异步运行的智能体完全无法弹出权限确认框,所以它的工具面必须进一步收窄到只读。

这一点和第 3.2 节的 Hermes webhook 工具集完全呼应

能力面必须随着「交互能力」和「信任级别」同步收缩。

· 有人在场、能弹确认框 → 给全量工具
· 后台跑、弹不出确认框 → 只给只读工具
· 输入来自不可信的外部 → 进一步收窄

两个独立团队从不同角度得出了同一条规则。这说明它不是巧合,是必然。

8.5 Hermes 的做法:任务委派 + 看板协作

Hermes 的子智能体通过一个叫 delegate_task(委派任务)的工具创建,实现在 tools/delegate_tool.py,共 5,071 行。

MAX_DEPTH = 1   # 扁平结构:父(第0层) -> 子(第1层);
                # 孙子级会被拒绝,除非显式调高 max_spawn_depth 配置
_MIN_SPAWN_DEPTH = 1
_DEFAULT_MAX_CONCURRENT_CHILDREN = 10    # 最多同时跑 10 个子智能体
_RECENT_SUBAGENTS_CAP = 200              # 最近子智能体记录保留 200 条

DELEGATE_BLOCKED_TOOLS = frozenset(...)  # 子智能体禁用的工具清单

def _subagent_auto_deny(command, description, **kwargs) -> str: ...
def _subagent_auto_approve(command, description, **kwargs) -> str: ...
def _get_subagent_approval_callback(): ...

hermes-agent/tools/delegate_tool.py

那两个 _subagent_auto_deny(子智能体自动拒绝)和 _subagent_auto_approve(子智能体自动批准)函数很关键:

子智能体没有终端,弹不出确认框。所以它的审批回调函数必须被替换成一个「自动决策」函数 —— 要么自动拒绝,要么自动批准,不能等人。

这和 Claude Code 的 shouldAvoidPermissionPrompts(应当避免权限提示)标记是同一个问题的两种解法。两家都不得不面对「后台任务无法交互」这个现实。

Hermes 独有:子智能体跑起来之后还能被实时控制

def interrupt_subagent(subagent_id: str) -> bool: ...    # 中止某个子智能体
def steer_subagent(subagent_id, ...) -> ...:  ...        # ★ 中途插话纠偏
def list_active_subagents() -> List[Dict]: ...           # 列出当前活跃的子智能体
def set_spawn_paused(paused: bool) -> bool: ...          # ★ 全局暂停派生新的
def _is_descendant_of(child_agent, parent_agent, max_hops=8) -> bool: ...
                                                          # 检查谱系关系,防环
_CONTROL_ACTIONS = frozenset({"list", "steer", "stop"})

子智能体启动之后,父智能体(或用户)还可以:列出它们、给某个插话纠偏、中止某个、甚至全局暂停派生新的。

Claude Code 的子智能体一旦派出去,只能等它完成或者全部中止,没有中间地带。

这个差别源于定位不同:

  • Claude Code 的子智能体通常是秒级的探索任务(「帮我找找这个函数在哪」),不值得设计干预机制
  • Hermes 的子智能体可能跑几十分钟(「帮我重构整个模块」),必须可干预 —— 否则用户发现方向错了只能全部推倒重来

那个 _is_descendant_of(..., max_hops=8) 也值得注意:它说明 Hermes 在架构上允许更深的谱系(虽然默认配置是 1 层),并且要防止「A 是 B 的子、B 又是 A 的子」这种循环 —— 所以设了 8 跳的遍历上限。

看板:持久化的多智能体协作面

Hermes 还有一套 Claude Code 完全没有的东西 —— 看板(Kanban)作为多个智能体的共享协作界面:

kanban_show      kanban_list        kanban_create     kanban_link
kanban_complete  kanban_block       kanban_unblock    kanban_comment
kanban_request_review               kanban_request_changes
kanban_heartbeat                    ★ 心跳:证明自己还活着
kanban_attach    kanban_attach_url  kanban_attachments

hermes-agent/toolsets.py · 核心工具清单

这些工具只在两种情况下才会出现在模型的工具清单里:智能体是作为「看板工人」被派生的(通过环境变量 HERMES_KANBAN_TASK 判断),或者当前身份配置显式启用了看板工具集。

注意那个 kanban_heartbeat(心跳)—— 它的存在说明这套机制是为长时间运行设计的:工人需要定期报告「我还活着,还在干这个任务」,否则协调者无法区分「它在慢慢干」和「它已经崩了」。

两种多智能体模型的本质差别
Claude Code:调用栈模型
  • 父调用子,子返回结果,栈帧弹出
  • 生命周期 = 一次工具调用的时长
  • 通信方式 = 返回值(单向
  • 状态在内存里,进程结束即消失
  • 优化目标:延迟与缓存命中率
Hermes:工作流模型
  • 任务写进看板,工人主动认领
  • 生命周期 = 跨会话、跨进程
  • 通信方式 = 看板评论 + 插话 + 心跳(双向
  • 状态在数据库里,程序重启后继续
  • 优化目标:持久性与可干预性

8.6 一个两家共同的硬约束:中断的级联

两套系统都花了不少代码处理「中断如何向下传播、结果如何向上收敛」:

  • Claude Code:中止控制器构成一棵树。父的信号被拉 → 所有子智能体的信号一起被拉 → 每一个未完成的工具调用都必须收到合成的工具结果,否则 API 直接报错(回顾第 4.4 节)。另外还有第 5.5 节讲的「兄弟中止控制器」做批内隔离。
  • Hermesinterrupt_subagent(id) 显式向下级联,加上 _close_subagent_steering() 清理插话通道、_unregister_subagent() 从注册表注销。

这是自建智能体最容易漏的地方:派生容易,回收难。

具体的暴雷场景:派出 5 个子智能体之后用户按了 Ctrl+C。
· 如果没有级联中止 → 那 5 个进程继续跑完,继续烧钱,而且没人在看它们的结果
· 如果没有补齐合成的工具结果 → 下一轮 API 调用直接报格式错误,这场会话再也恢复不了

两个问题都不会在开发阶段暴露(开发时你不会去按 Ctrl+C),但在生产环境每天都会发生。

面试追问:什么时候该用子智能体?

先纠正一个常见误解:子智能体的首要价值不是并行,是上下文隔离。

要读 20 个文件才能得出一个结论时,自己读会让那 20 个文件的内容永久占用主上下文、每一轮都重发一遍;派子智能体去读,主上下文只收到那一句结论。在长任务里,这个交易是决定性的。

然后给判据:

  • 该用:探索型任务(结论远小于探索过程)、需要不同工具面或不同权限的任务、可以并行且互不依赖的任务。
  • 不该用:需要主智能体完整上下文才能做对的任务(那还不如自己干 —— 除了分叉模式,因为分叉本来就继承全部上下文)、需要和用户交互的任务(子智能体通常被禁用「向用户提问」工具)、单步就能完成的任务(派生的开销大于收益)。

如果要展示深度,讲扇出时的缓存策略:N 个子智能体如果能让请求前缀达到字节级一致,后 N−1 个全是缓存命中,输入成本降到 1/3 以下。为此值得付出「给用不到的工具」「传已渲染好的系统提示词字节而不是重新生成」「用完全相同的占位符填充工具结果」「把递归守卫从接口层下移到调用层」这些看起来不优雅的代价 —— Claude Code 的 buildForkedMessages 函数就是这么做的,只让最后一个文本块携带各自的指令