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

11 · 可以搬到自己项目里的实现范式

这一章把前面所有分析压缩成能直接抄进你自己项目的东西。骨架代码用 Python 写(服务端岗位更常见),但结构是语言无关的。

代码怎么读:每段代码都有中文注释。带 标记的行是最容易写错、也最关键的地方。如果你只看一处,看那些行。

11.1 骨架一:带恢复状态机的主循环

from dataclasses import dataclass, replace
from typing import Literal, Optional

# 所有可能的"继续原因",穷举出来。对应第 4.2 节的 transition 字段
Reason = Literal["next_turn", "compact_retry", "output_truncated",
                 "hook_blocking", "budget_continue"]

@dataclass(frozen=True)          # frozen=True 表示这个结构体不可修改
class LoopState:
    messages: list               # 当前的完整消息历史
    turn: int = 1                # 已经进行了几轮

    # —— 每条恢复路径一个独立的幂等锁或计数器 ——
    compact_attempted: bool = False        # 本轮是否已经压缩过(真假开关)
    truncation_retries: int = 0            # 输出截断已重试几次(计数器)
    max_tokens_override: Optional[int] = None   # 是否已升过输出上限档位
    hook_active: bool = False
    transition: Optional[Reason] = None    # ★ 只为可观测和可测试而存在


MAX_TRUNCATION_RETRIES = 3

def run(state: LoopState, ctx) -> str:
    while state.turn <= ctx.max_turns and ctx.cost < ctx.max_cost:

        # ① 上下文治理流水线(见骨架二)
        msgs = govern_context(state.messages, ctx)

        # ② 调用模型。可恢复错误在这里被"扣住",不向外抛(见第 4.3 节)
        resp, recoverable = call_model(msgs, ctx,
                                       max_tokens=state.max_tokens_override)

        # ③ 恢复路径 —— 每条都必须先检查自己的锁
        if recoverable == "context_overflow" and not state.compact_attempted:
            state = replace(state,
                messages=compact(state.messages, ctx),
                compact_attempted=True,               # ★ 上锁
                transition="compact_retry")
            continue

        if recoverable == "output_truncated":
            if state.max_tokens_override is None:     # 还没升过档 → 升一次
                state = replace(state, max_tokens_override=ctx.escalated_max,
                                transition="output_truncated")
                continue
            if state.truncation_retries < MAX_TRUNCATION_RETRIES:
                state = replace(state,
                    messages=state.messages + [resp, RESUME_NUDGE],
                    truncation_retries=state.truncation_retries + 1,
                    max_tokens_override=None,
                    transition="output_truncated")
                continue
            # 重试次数耗尽 —— 现在才把错误抛出去
            raise AgentError(recoverable)

        if recoverable:                # 是可恢复错误,但上面没有一条路能走
            raise AgentError(recoverable)

        # ④ 没有工具调用 = 任务完成(见第 1.5 节)
        if not resp.tool_calls:
            return resp.text

        # ⑤ 执行工具(见骨架三)
        results = execute_tools(resp.tool_calls, ctx)

        # ⑥ 正常推进:★ 只有这里才重置所有恢复计数器
        state = replace(state,
            messages=state.messages + [resp] + results,
            turn=state.turn + 1,
            compact_attempted=False,      # ★ 只在正常推进时重置
            truncation_retries=0,
            max_tokens_override=None,
            transition="next_turn")

    return "预算已耗尽"


RESUME_NUDGE = {
  "role": "user",
  "content": ("Output token limit hit. Resume directly — no apology, "
              "no recap of what you were doing. Pick up mid-thought if that "
              "is where the cut happened. Break remaining work into "
              "smaller pieces."),
}
三个最容易写错的地方
  1. 恢复推进时绝不能重置锁。只有第 ⑥ 步的 next_turn 分支才把 compact_attempted 设回 False。在恢复分支里重置 = 无限循环。Claude Code 有过这个真实事故,烧掉了几千次 API 调用(见第 4.2 节那段注释)。
  2. 可恢复错误不能立即抛给调用方。先在循环里试恢复,全部失败才抛。否则下游看到错误字段就断开连接,你的恢复逻辑白跑(见第 4.3 节)。
  3. frozen=True 配合 replace()状态不可变,每条转移边都显式写出所有字段的新值。可变状态加部分赋值,是这类循环最常见的 bug 温床 —— 你以为你只改了一个字段,实际上漏掉了另一个该重置的。

11.2 骨架二:上下文治理阶梯

def govern_context(messages: list, ctx) -> list:
    """成本递增的四级流水线。越靠前越便宜,能在前面解决就不往后走。
       对应第 6 章。"""

    # ① 落盘:单条工具结果超限 → 存文件,只回预览 + 路径。成本 0
    messages = spill_oversized_results(messages, ctx)

    # ② 精确删除:旧的可压缩工具结果,保留最近 N 条。成本 0
    #    ★ 关键:缓存状态是决策输入(见第 6.3 节)
    if ctx.cache_is_warm:
        # 缓存是热的 —— 本地一个字都不改,让服务端删(如果接口支持)
        ctx.queue_cache_edits(pick_stale_tool_ids(messages, keep=ctx.keep_recent))
    else:
        # 缓存已经凉了 —— 前缀反正要全部重写,直接就地清空内容
        messages = clear_stale_tool_results(messages, keep=max(1, ctx.keep_recent))
        #                                              ★ 至少留 1 条,别删光

    # ③ 结构化折叠:把整段交互折叠成可展开摘要,保留粒度。成本低
    if estimate_tokens(messages) > ctx.collapse_threshold:
        messages = apply_collapses(messages, ctx)

    # ④ 整段摘要:一次完整模型调用,有损不可逆。最后手段
    if estimate_tokens(messages) > ctx.compact_threshold:
        messages = summarize_and_replace(messages, ctx)

    return messages
如果你的模型供应商不支持缓存编辑(绝大多数情况)

不要试图模拟它。退回到「按缓存冷热分流」这个更朴素的版本就够了:

  • 缓存热(距上次请求 < 5 分钟)→ 什么都别删。宁可多花点输入 token,也别把整段缓存打掉。
  • 缓存凉(距上次请求 > 5 分钟)→ 大刀阔斧清理。前缀反正要重写,不清白浪费。

判断缓存冷热只需要一个时间戳,成本为零。这条规则能拿到缓存编辑大部分的收益,而且完全没有「本地状态和服务端状态双写」的复杂度。

真正值得抄的是「把缓存冷热建模成策略输入」这个思路本身,不是缓存编辑那个具体实现。

压缩用的提示词模板

COMPACT_PROMPT = """CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- You already have all the context you need in the conversation above.
- Tool calls will be REJECTED and will waste your only turn — you will fail.
- Your entire response must be plain text: an <analysis> block followed
  by a <summary> block.

Before the summary, wrap your analysis in <analysis> tags. Chronologically
go through each section of the conversation and identify:
  - the user's explicit requests and intents
  - your approach to addressing them
  - key decisions, technical concepts, and code patterns
  - specific details: file names, full code snippets, function signatures

Then produce <summary>.
"""

def summarize_and_replace(messages, ctx):
    raw = call_model_no_tools(COMPACT_PROMPT, messages, max_turns=1)
    summary = strip_analysis_block(raw)     # ★ analysis 是草稿纸,用完就扔
    return [protected_head(messages),        # 保留开头
            summary_message(summary),        # 中间换成摘要
            *protected_tail(messages, n=ctx.protect_last_n)]   # 保留结尾
为什么禁用工具的指令要写这么凶

因为压缩这次调用通常继承了父会话的完整工具集(为了缓存键匹配,见第 6.4 节)。工具还在清单里,模型就有概率去调用它。

Claude Code 的实测数据:措辞温和时,Sonnet 4.6 有 2.79% 的概率仍然尝试调用工具(4.5 只有 0.01%)。而由于最大轮次被设为 1,一次被拒绝的工具调用就等于整次压缩失败。

三个有效手法:放在最前面、穷举点名具体工具、明确说明违规后果。

11.3 骨架三:工具抽象 + 并发分区

from typing import Protocol, Any

class Tool(Protocol):            # Protocol 是 Python 里定义"接口"的方式
    name: str
    input_schema: dict
    max_result_chars: int        # 结果超过这个长度就落盘

    def call(self, args: dict, ctx) -> Any: ...

    # —— 给调度器看的安全谓词,全部 fail-closed(失败倒向保守)——
    def is_concurrency_safe(self, args: dict) -> bool: return False
    def is_read_only(self, args: dict)        -> bool: return False
    def is_destructive(self, args: dict)      -> bool: return False
    def check_permissions(self, args, ctx)    -> "Decision": ...
    #   ★ 注意这些方法都接收 args ——
    #     同一个 Bash 工具,跑 ls 安全,跑 rm 不安全


def partition(calls: list, tools: dict) -> list[tuple[bool, list]]:
    """贪心分区:相邻的安全工具合并并行,遇到不安全的就切断。
       ★ 完整保留模型隐含的顺序语义。见第 5.4 节。"""
    batches = []
    for c in calls:
        tool = tools.get(c.name)
        try:
            args = validate(tool.input_schema, c.args)
            safe = bool(tool.is_concurrency_safe(args))
        except Exception:
            safe = False              # ★ fail-closed:解析或判定失败都当不安全
        if safe and batches and batches[-1][0]:
            batches[-1][1].append(c)  # 上一批也安全 → 并进去
        else:
            batches.append((safe, [c]))   # 否则 → 开新批
    return batches


def execute_tools(calls, ctx):
    results = []
    for is_safe, batch in partition(calls, ctx.tools):
        if is_safe:
            # ★ 两级中止作用域:批内失败杀兄弟进程,但不杀整个轮次
            #   见第 5.5 节
            sibling = ChildCancelScope(parent=ctx.cancel)
            with ThreadPoolExecutor(max_workers=10) as pool:
                results += list(pool.map(
                    lambda c: run_one(c, ctx, cancel=sibling), batch))
        else:
            for c in batch:
                results.append(run_one(c, ctx, cancel=ctx.cancel))

        if ctx.cancel.is_set():
            # ★ 中断兜底:为每个还没有结果的工具调用补一条合成结果,
            #   否则下一轮 API 会因为"便条与回复不配对"直接报错
            #   见第 4.4 节
            results += synth_results_for_missing(calls, results,
                                                 "Interrupted by user")
            break
    return results

11.4 骨架四:按信任边界配置工具面

# ★ 工具的"实现"和"投放"必须解耦。见第 3.2 节
CORE_TOOLS = ["web_search", "read_file", "write_file", "terminal",
              "patch", "search_files", "delegate", "memory"]

TOOLSETS = {
    # 交互式会话:有人在场,能弹确认框 → 全量工具
    "interactive": {"tools": CORE_TOOLS},

    # 后台任务:弹不出确认框 → 收窄到基本只读
    "background":  {"tools": ["web_search", "read_file", "search_files"]},

    # 公开 webhook:内容来自不可信第三方(PR 标题、issue 评论)
    # → 只给只读工具。
    #   ★ 防提示词注入靠"危险工具根本不在清单里",
    #     不靠在提示词里写"请不要执行危险命令"
    "webhook":     {"tools": ["web_search", "read_file"]},
}

def tools_for(context) -> list[Tool]:
    preset = ("webhook"    if context.source == "webhook"
         else "background" if not context.can_prompt_user
         else "interactive")
    names = TOOLSETS[preset]["tools"]
    return [t for t in ALL_TOOLS
            if t.name in names and not denied_by_rule(t, context)]

11.5 骨架五:不可绕过的安全底座

import re

# 命令位置锚点:行首 / 分隔符后 / 子 shell 开启符后 / sudo|env|exec 包装后
# ★ 这个锚点是全部的关键。见第 7.3 节
_CMDPOS = r'(?:^|[\n;&|]|\$\(|`|\b(?:sudo|env|exec)\s+)\s*'

HARDLINE = [
    (_CMDPOS + r'rm\s+(-[^\s]*\s+)*(/(?:(?:\.\.?)?/)*(?:\.\.?)?\**|/ \*)(?=[\s;&|]|$)',
                                              "递归删除根目录"),
    (_CMDPOS + r'mkfs(\.[a-z0-9]+)?\b',       "格式化文件系统"),
    (_CMDPOS + r'dd\b[^\n]*\bof=/dev/(sd|nvme|vd)[a-z0-9]*', "写入裸磁盘设备"),
    (_CMDPOS + r'(shutdown|reboot|halt|poweroff)\b',         "关机或重启"),
]
# ★ 模块加载时就预编译。见第 7.3 节末尾(避免正则缓存被挤掉后反复重编)
HARDLINE_C = [(re.compile(p, re.IGNORECASE | re.DOTALL), d) for p, d in HARDLINE]

# 这两条在命令行任意位置都有效,没有命令名可以锚定 → 用引号遮蔽
POSITIONLESS = [
    (re.compile(r'>\s*/dev/(sd|nvme|vd)[a-z0-9]*\b'), "重定向到裸磁盘设备"),
    (re.compile(r':\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:'), "分叉炸弹"),
]

# 会把引号内容交给另一个 shell 执行的命令
SHELL_CARRIERS = {"eval", "sh", "bash", "zsh", "ksh", "dash", "source", "."}

def check_hardline(cmd: str) -> str | None:
    for rx, desc in HARDLINE_C:
        if rx.search(cmd):
            return desc
    # ★ 遇到 sh -c / eval 时,引号里是代码不是散文 → 必须扫原始字符串
    scan = cmd if has_shell_carrier(cmd, SHELL_CARRIERS) else mask_quoted(cmd)
    for rx, desc in POSITIONLESS:
        if rx.search(scan):
            return desc
    return None


def mask_quoted(cmd: str) -> str:
    """把引号内容清空(保留引号字符本身)。
    ★ 但双引号里的 $(...) 和反引号必须保持原样 —— shell 真的会执行它们。
    见第 7.3 节那张三行对照表。"""
    ...
这里的关键判断

check_hardline() 的返回值必须排在所有用户配置之前生效。用户可以关掉确认弹窗,但关不掉这一层。

同时 _CMDPOS 锚点是必需的,不是可选优化 —— 没有它,git commit -m "fix: block rm -rf / spellings" 会被误拦,而用户会因此很快把整个安全机制关掉。

误报率过高的安全措施,等于没有安全措施。

11.6 自建智能体的决策清单

建议的决策顺序排列。每一条都对应前面某一章的分析。

决策选 A选 B
1. 供应商绑定
第 6、10 章
绑定单一供应商 → 能用提示词缓存的高级能力、缓存编辑、原生思考块。成本可能差一个量级。 做多供应商抽象 → 必须加参数矫正层、按供应商重新标记缓存分界点、放弃所有私有优化
2. 上下文策略
第 6 章
硬编码阶梯 → 你自己最了解你的负载特征,能做到最优 定义成抽象基类交出去 → 只在你真的要开放给第三方时才值得
3. 工具建模
第 5 章
富接口(安全谓词 + 权限 + 预算 + 渲染) → 调度策略能从工具实现里剥离出来 普通函数注册 → 起步快,但并发和权限逻辑迟早散落到各个调用处
4. 执行隔离
第 7 章
没有 A/B 选项。生产环境必须有硬隔离(容器 / 独立用户账号 / 沙箱)。正则表达式和权限规则是纵深防御的最外层,不是唯一层。Hermes 那次审计报告的头号严重问题,就是「默认无沙箱」。
5. 子智能体
第 8 章
调用栈模型 → 秒级探索任务,追求延迟和缓存命中 工作流模型(看板 + 心跳 + 插话)→ 长时任务,必须可干预、可恢复
6. 记忆
第 9 章
纯文本 + 全量加载 → 用于指令性记忆(偏好、规范)。人类可读可审阅是刚需 检索 → 用于事实性记忆。词法 + 向量混合,而且必须有信任分数衰减
7. 权限
第 7 章
确定性规则 → 零成本、可审计、可测试 模型分类器 → 能理解意图,但必须有快速通道挡掉 80% 的调用
8. 多入口
第 9 章
直接在业务代码里写 if platform == ... → 2 个平台以内可以接受 独立网关层 + 能力声明式适配器 → 3 个平台以上必须这样做

11.7 一页纸检查清单

上线前逐条自查
  • ☐ 每条错误恢复路径都有独立的幂等锁或计数器,而且只在正常推进时重置
  • ☐ 可恢复错误在循环内被扣住,所有恢复手段都失败后才向外抛
  • ☐ 中断时为每一个未完成的工具调用补齐合成结果(便条与回复必须配对)
  • ☐ 并发执行有独立的批内中止作用域,一个失败不拖垮整个轮次
  • ☐ 并发安全性由工具自己声明,接收参数,而且解析失败时倒向「不安全」
  • ☐ 上下文治理是一条成本递增的阶梯,摘要是最后一级而不是第一级
  • ☐ 缓存冷热是压缩策略的输入,而不是两种场景共用同一套策略
  • ☐ 单条工具结果有大小上限,超限落盘只回预览和文件路径
  • ☐ 存在用户无法通过任何配置关掉的安全底座
  • ☐ 危险模式匹配只在命令位置生效,不误伤参数里的字面量
  • ☐ 工具面随入口的信任级别和交互能力同步收缩
  • ☐ 工具的错误消息在回灌进上下文前做过净化
  • ☐ 子智能体不能向用户提问、不能改全局状态、不能操作兄弟任务
  • ☐ 生产环境有容器或沙箱级别的硬隔离
  • ☐ 扩展点区分「可叠加的能力」与「互斥的策略」
  • ☐ 慢的旁路计算藏在模型流式输出的时间窗里,而且消费点不阻塞