Hermes Agent 怎样组织上下文、工具与 Token 成本

核验版本:本文按 2026 年 7 月 19 日 Hermes Agent main 分支的提交 e598cef 阅读。重点核对了 agent/system_prompt.pyagent/prompt_builder.pyagent/agent_init.pymodel_tools.py。项目会持续更新,路径、默认值和提示词内容都可能变化。文中把源码事实、一个旧样本的数字和我的工程判断分开写,不拿某次实测当作所有人的默认值。

很多人第一次看到 Agent 的 system prompt,会有一种不太舒服的感觉:我只发了句「帮我看下这个报错」,为什么模型账单里已经先躺着一大段输入?

原因不神秘。模型并不认识你的项目、偏好、工具权限和聊天平台。每次请求前,Agent 都要把其中一部分信息重新交给模型。Hermes Agent 也是这样做的,只是它把这些信息拆得比较细,而且新版本已经不适合再用「固定九层」来概括。

这篇文章只回答四个问题:

  • 一条用户消息发出去时,Hermes 到底准备了什么?
  • system prompt、工具 schema 和聊天历史是不是同一笔 Token?
  • 项目里的 AGENTS.md.hermes.md 为什么会影响回答和成本?
  • 如果觉得会话又贵又笨,应该从哪里查,而不是凭感觉删文件?

读完以后,你不需要记住所有函数名。能分清「提示词内容」「工具定义」「会话历史」这三件事,并能在自己的环境里定位最大的上下文来源,就够用了。

先建立一张正确的地图

先把一次模型调用想成一个快递包。用户的一句话只是包里的一个小纸条,Hermes 还会装进一份系统说明、已有对话和可调用工具的说明。不同 API 对这些东西的字段名不完全一样,但对模型来说,它们共同占用输入上下文窗口。

1
2
3
4
5
6
7
8
本轮请求
├── 系统提示词
│ ├── 稳定部分:身份、规则、Skills 索引、平台说明
│ ├── 项目部分:.hermes.md / AGENTS.md 等
│ └── 会话快照:记忆、用户画像、日期和模型信息
├── 对话历史:此前的用户消息、助手回复、工具结果
├── 工具 schema:每个可调用工具的名字、说明和参数 JSON Schema
└── 本轮用户消息

这里有个很容易混淆的点:工具 schema 通常不是拼在 system prompt 字符串里。Hermes 会将 system prompt 和 tools 作为 API 请求里的不同字段发送。计费和上下文窗口层面,两者都可能算作输入;排查时却要分开看,因为缩短 system prompt 并不会自动让工具 schema 变小。

另一个常见误会是「每轮都会从磁盘重新读取一切」。当前 Hermes 的实现会在一个 AIAgent 会话开始时构建并缓存完整 system prompt,通常只在上下文压缩等重建节点重新生成。这是为了提高 provider 的前缀缓存命中率。换句话说,MEMORY.md 在这个会话里更接近一次性快照,而不是你刚写进去就会在下一轮自动刷新的实时数据库。

这也解释了两个看似矛盾的说法:从逻辑上看,系统提示词是每次模型调用的输入;从实际账单看,如果 provider 支持前缀缓存,重复前缀可能按较低价格计费。缓存能降低重复读取的价格,却不会让上下文窗口凭空变大,也不会消除长提示词对注意力和首 token 延迟的影响。

Hermes 不是固定九层,而是三段拼装

旧版拆解常把 Hermes 描述成「九层 system prompt」。那个视角在某个导出样本里能帮助数清来源,但不是当前源码的结构。现在 agent/system_prompt.pybuild_system_prompt_parts() 明确把内容分成三段:stablecontextvolatile,最后按这个顺序连接。

放什么 在同一会话中是否通常不变 为什么这样放
stable 身份、工具行为规则、Skills 索引、环境与平台提示 尽量让请求前缀稳定,便于缓存
context 调用者传入的系统消息、项目上下文文件 与当前工作目录或入口有关
volatile 记忆快照、用户画像、外部记忆块、日期和会话信息 会话级通常不变 新会话或重建时可能不同

volatile 这个名字容易让人误解。它不是说每一轮一定变动,而是相对于长期稳定的身份与规则,它更容易因会话、日期、用户资料或记忆状态而不同。当前源码为了保住缓存,甚至只放日期而不是每分钟变化的时间。

如果把这三段继续拆开,就能看到原先「九层」的来源,但不能假定每个会话都会有所有层。许多段都受工具是否启用、平台、模型、配置和环境变量控制。

稳定段:先让模型知道自己是谁、该怎么做事

稳定段从身份开始。Hermes 会优先读取 ~/.hermes/SOUL.md;文件不存在或为空时,才回退到代码里的默认身份文本。SOUL.md 是用户可编辑的人格和长期行为边界,不等于项目开发说明。

身份后面还可能出现一串规则。例如:

  • 当会话已加载 memory 工具时,提示词会说明什么值得存成长期记忆,什么只是短期任务进度。
  • 当有 session_search 时,它会要求模型先搜索旧会话,再让用户重复上下文。
  • 当有 skill_manage 时,它会鼓励模型把复杂、可复用的流程沉淀为 Skill,并修订发现错误的 Skill。
  • 有工具时,可能加入「完成任务、避免编造结果、并行发起独立工具调用」等执行规则。
  • computer_use、看板工具或 Nous 订阅能力时,会追加对应的专门说明。

这些不是装饰文字。它们在改变模型的默认行为。例如「把独立读取放到同一轮工具调用」这条规则,目标不是让答案更漂亮,而是减少多次来回时反复携带上下文的成本。

模型也会影响稳定段。当前代码会给部分 GPT、Codex、Grok 模型加入更强的工具使用和验证纪律;Gemini/Gemma 则可能拿到另一组操作说明。平台同样如此:Telegram、WhatsApp 等入口需要不同的 Markdown 和媒体投递规则。于是两个用户问同一个问题,若运行在不同平台或模型上,看到的 system prompt 不一定相同。

Skills 索引:给模型目录,不把整本手册塞进去

Skills 是 Hermes 里最值得单独理解的一部分。一个 Skill 通常是一份 SKILL.md,记录某类任务的流程、约束和工具用法。若每个 Skill 全文都放进系统提示词,Skill 多了以后几乎必然失控。Hermes 的做法是先放一个索引:按分类列出名字和简短描述,需要时由模型调用 skill_view 再加载全文。

这是一种「目录 + 按需阅读」的策略。它能把常驻成本压低,但并不等于索引没有成本。每新增一个 Skill,至少会增加名称、分类和描述的一部分文字。当前实现还会检查:Skill 是否适配当前平台、环境条件和已启用工具集,是否被配置禁用,外部 Skill 目录是否与本地同名冲突。

新版本还提供了一个很实用的折中:在编码姿态下,可以把不相关分类降级为「只显示名字」。名字仍在,模型仍能用 skill_view 加载,只是描述被省掉。这比粗暴隐藏更稳妥。完全从索引里删掉的 Skill,模型往往根本想不起它存在。

所以 Skill 的维护目标不是「越少越好」,而是「索引中每一行都有继续存在的理由」。重复、过期、名字模糊且从未使用的 Skill 会让模型多一个错误分支,也让每次请求多一点固定输入。

项目段:一轮只选一种项目上下文

项目上下文是最容易被误读的一段。当前 build_context_files_prompt() 的优先级如下:

1
2
3
4
1. .hermes.md 或 HERMES.md
2. AGENTS.md 或 agents.md
3. CLAUDE.md 或 claude.md
4. .cursorrules,加上 .cursor/rules/*.mdc

这里的「优先级」是先命中者获胜,不是四类文件全部叠加。找到 .hermes.md 后,当前会话不会继续再注入同目录的 AGENTS.mdCLAUDE.md。因此放一份短小、明确的 .hermes.md,确实可以覆盖大型 AGENTS.md,但它也意味着你必须把仍然必要的规则迁过去。

搜索范围也不完全相同:

  • .hermes.md 会从当前目录向上找,找到 Git 仓库根目录为止。它适合放仓库级约定,也允许子目录放更具体的覆盖文件。
  • AGENTS.mdCLAUDE.md.cursorrules 只检查当前工作目录。
  • .cursor/rules/*.mdc 属于最后一种 Cursor 规则来源,会和 .cursorrules 一起组成这一类上下文。

系统还会先扫描这些文件中的潜在提示注入模式。扫描命中时,Hermes 会阻止该文件内容进入 system prompt,并以一个说明占位。这不是完整的安全边界,但很重要:克隆一个不可信仓库时,项目说明文件不是天然可信的纯文档。

当前版本还修复了一个很实际的坑。对于桌面端或网关这类「进程可能从 Hermes 安装目录启动」的场景,如果工作目录只是回退得到的安装目录,系统会跳过项目上下文发现,避免把 Hermes 源码仓库自身的贡献者 AGENTS.md 当成用户项目规则。CLI/TUI 从用户 shell 启动时仍可以显式在 Hermes 仓库里工作。

这会直接修正一个流传很广的说法:「默认工作目录会把 hermes-agent 仓库的 AGENTS.md 每轮塞进去。」它可以解释某个旧样本,但不能描述当前网关的默认行为。排查自己的会话时,先确认实际解析到的工作目录,再看加载日志或上下文体积,别根据目录名推断。

会话快照段:记忆不是聊天记录的另一个名字

volatile 段中,Hermes 可以注入内置记忆、USER.md 用户画像和外部记忆提供商返回的提示块,最后加上会话开始日期、可选的 session ID、模型和 provider 信息。

这几种信息各有用途:

内容 适合放什么 不适合放什么
长期记忆 稳定偏好、固定环境细节、长期约定 某次任务进度、临时待办、几天后就过期的状态
用户画像 与用户有关且会长期影响回答方式的信息 项目实现细节、密钥、一次性需求
会话搜索 已完成任务、旧对话中的细节 用来替代稳定偏好的存储
Skill 可重复执行的步骤、排错方法、工作流 一句事实或用户偏好

为什么要把这几件事分开?因为它们的生命周期不同。把「本周修了哪个 bug」写进长期记忆,会让以后的每轮请求都带着它,既浪费 Token,也可能让模型对过时状态过度自信。反过来,把「用户偏好简洁答案」只留在一次聊天记录里,新会话又会丢掉它。

原文把记忆称为「冻结」是有道理的,但需要补一句前提:冻结的是当前 agent 实例已构建好的提示词快照。内存中的记忆文件可能已经被工具写入;要不要马上重新加载,取决于当前实现是否重建 system prompt。不要把「文件已写成功」误读成「模型下一句必然已经看见」。

Token 成本应该怎么算,哪些数字不能直接搬

一份旧导出样本里,完整 system prompt 大约 36,700 个字符,粗略约 10K Token;其中一个 AGENTS.md 原文约 20,360 个字符。这些数字作为案例很有价值,它们说明项目说明可以成为最大块。但它们不是 Hermes 的固定开销。

最常见的换算公式是「英文大约 4 个字符一个 Token」。这只能做粗估,不能拿来结算。中文、代码、URL、JSON Schema、标点和模型的分词器都会让比例变化。尤其是工具 schema,里面常有重复字段名、枚举值和说明文本,字符数看着不大,Token 却未必少。

更可靠的做法是按来源做账,而不是急着算一个绝对数字:

1
2
3
总输入 = 系统提示词 + 工具 schema + 历史消息 + 本轮用户输入

系统提示词 = stable + context + volatile

对于成本,至少要同时看四件事:

  1. 每项占了多少 Token。
  2. 每项是否每次调用都会出现。
  3. provider 是否给重复前缀缓存,以及缓存读写分别如何计价。
  4. 这项内容是否真的改善了当前任务的成功率。

只盯住第一个数字容易做错优化。比如删掉一段短小的安全规则可能省不了什么,却增加工具误用概率;一个 15KB 的项目说明如果让模型每次都避免读错目录,可能比它的 Token 成本更划算。优化不是「越短越好」,而是删掉低价值、重复和与当前会话无关的内容。

项目文件的截断规则,以及它真正意味着什么

当前 prompt_builder.py 给项目上下文文件设计了可配置上限。默认基础上限是 20,000 个字符,但如果能获得模型的上下文窗口,会按以下近似规则计算动态上限:

1
上限 = max(20,000, min(上下文窗口 × 4 × 0.06, 500,000))

也就是说,20,000 不是所有模型的硬顶。用户在配置中显式设置 context_file_max_chars 时,显式值优先。源码中的「4 字符一个 Token」是英文启发式,只用于分配预算,不是中文 Token 的测量结果。

当内容超过上限,Hermes 保留前 70% 和后 20%,中间替换为提示标记,并告诉模型可以用 read_file 读取完整文件。以 20,000 为上限时,保留的是前 14,000 个字符和后 4,000 个字符;插入的标记本身还会占一点位置。因此它不是严格意义上的「输出刚好 20,000 字符」。

这个策略有明显取舍:文件开头通常有项目概况和常用命令,结尾常有测试或注意事项,二者比较可能有用;但中间的关键规范也可能被砍掉。看到截断警告时,正确反应不是立刻把上限调大,而是先问三个问题:

  • 这份文件是否把手册、历史记录和真正的操作规则混在一起了?
  • 能否把每次都必须遵守的十几条规则写进短 .hermes.md,把背景资料留给按需读取?
  • 当前任务真的需要这份完整说明吗?

如果答案是「需要」,再考虑增大上限或让模型显式读取原文件。把所有文档都塞进系统提示词,是最省人的整理时间、最浪费模型上下文的做法。

工具发现:筛选发生在 system prompt 之前

「工具发现」常被写成一笔模糊的 Token 成本,实际过程要具体得多。当前初始化流程大致是:

1
2
3
4
5
6
7
8
9
10
11
注册内置工具和插件

根据 enabled_toolsets / disabled_toolsets 展开工具集

对每个候选工具执行可用性检查 check_fn

生成最终 tool definitions,并记录 valid_tool_names

按最终工具集合构建相关的行为规则和 Skills 索引

把 system prompt、tools、历史消息发给模型 API

model_tools.py 里有一个值得记住的细节:enabled_toolsets 是白名单式选择,disabled_toolsets 在最后做减法。最终传给模型的是通过 check_fn 的工具 schema,而不是注册表里所有工具。可用性检查的结果还有短时间缓存,以免每次初始化都重复探测环境。

所以「注册了 51 个工具,某次只给模型 30 个」这种观察完全可能成立,但 51 和 30 本身没有普适性。MCP 服务是否连接、环境变量是否存在、插件是否加载、平台是否限制工具集、用户配置是否禁用工具,都会改变结果。

工具数量与成本的关系也不只是「一个工具一行字」。每个函数 schema 通常带名称、用途、参数、类型、必填字段、可能的枚举和示例。复杂工具的 schema 可能比普通 system prompt 段更贵。更少的工具通常意味着更低的输入和更少的选择干扰,但也可能让模型失去完成任务所需的能力。

有些 Hermes 场景会通过 tool_search 一类机制把延迟工具放在可检索目录里,先不把全部 schema 展开给模型。思路和 Skills 索引类似:把常驻说明换成按需加载。它适合工具很多但单次任务只用少数工具的环境;若模型频繁检索和加载同一批工具,收益会下降。

一个可以自己复现的排查流程

下面的流程不依赖抓包,也不要求你先会读 Python。目标是把「感觉上下文很大」变成一张可行动的清单。

第一步:固定一次观察对象

不要一边频繁改配置,一边比较不同会话的账单。先选一个具体任务和一个具体模型,例如「在这个仓库里解释一个报错」。记录日期、入口平台、工作目录、模型、启用的工具集和是否是新会话。否则两次差异可能只是模型或历史消息不同。

第二步:查当前目录会命中什么

在任务目录依次检查:

1
2
pwd
find . -maxdepth 1 \( -name '.hermes.md' -o -name 'HERMES.md' -o -name 'AGENTS.md' -o -name 'agents.md' -o -name 'CLAUDE.md' -o -name 'claude.md' -o -name '.cursorrules' \) -print

如果没有 .hermes.md,再从当前目录往 Git 根目录查看是否有它。注意不要把「仓库中存在 AGENTS.md」直接等同于「它被加载了」。优先级和当前工作目录决定最终结果。

接着看候选文件的体积和内容类型。一个好的项目上下文文件更像驾驶舱检查单:项目是什么、如何验证、哪些目录不要碰、有哪些硬约束。它不应该顺便塞进完整架构手册、几十条历史决策和每次发布记录。

第三步:区分四类常驻来源

可以按下面的表做自己的盘点:

来源 要检查的东西 常见问题 优先动作
身份与规则 SOUL.md、平台提示、模型专用规则 人格描述很长,内容相互矛盾 保留少量稳定边界,删掉一次性任务说明
项目上下文 .hermes.mdAGENTS.md 大而全、重复、被截断 写精简入口,背景材料按需读
记忆与画像 MEMORY.mdUSER.md、外部记忆 把日志和临时进度当记忆 只留未来还会影响判断的事实
工具与 Skills toolsets、插件、Skill 索引 低频工具和重复 Skill 堆积 先按场景缩小工具面,再清理索引

这个顺序有意把项目文件和记忆放在工具之前。对多数个人 Agent 来说,它们更容易无意膨胀,也更容易由用户自己控制。工具集涉及能力和权限,删之前要先确认任务不会因此失败。

第四步:做一次最小对照实验

一次只改一个变量。比如新建一个只包含必要规则的 .hermes.md,不要同时换模型、清空记忆、禁用十个工具。然后开一个新会话,用同样的问题比较:输入 Token、首 token 时间、工具调用次数和结果质量。

可以把结果写成这样:

版本 改动 输入 Token 首 token 时间 是否一次做对 备注
A 原项目文件 待测 待测 待测 基线
B 精简 .hermes.md 待测 待测 待测 只改项目上下文

「做对」比「Token 少」更重要。若 B 少了 20% 输入,却让模型每次都忘记测试命令或误改生成目录,那不是优化,是把成本转成了返工。

第五步:最后才动大刀

有了对照数据后,再做较大调整:为不同场景配置不同 toolset,归档低频 Skill,迁移臃肿记忆,或者改变工作目录策略。每次调整后保留一份可回退的文件。Agent 的上下文规则本身是程序的一部分,应该像改配置一样对待,而不是凭感觉删几段文字。

一个小例子:怎样写一份短而不空的 .hermes.md

很多人听到「精简」后,会把项目说明压成「遵循最佳实践」。这种句子几乎没有操作价值。好的短规则应该能在模型准备调用工具时改变选择。

下面是一份面向博客仓库的示意,不是通用模板:

1
2
3
4
5
6
7
# 本仓库工作说明

- 正文位于 `source/_posts/`,不要直接修改 `public/` 中的生成文件。
- 修改文章后,先检查 front matter、内部链接和 Markdown 代码块是否闭合。
- 只运行与本次改动相关的检查;不要顺手格式化全仓库。
- 未经确认,不修改主题配置、依赖版本或部署脚本。
- 输出时说明改了哪些文章,以及做过什么验证。

它只有几行,却回答了模型最容易犯错的几个问题:在哪里改、哪些文件别碰、怎么验证、什么属于越权。若项目还有复杂架构,把详细说明放在 docs/,并在这里给出文件路径和何时阅读的条件。这样模型需要细节时再读,不需要时不为整本手册付费。

与 OpenClaw 对照时,别把产品名当作结论

Hermes 和 OpenClaw 都会把身份、记忆、工作区规则和工具能力送给模型,因此两者都存在固定上下文成本。这个层面的对照是成立的。

但实现细节不能互相套用。本文确认的是 Hermes 在上述提交中的行为:项目上下文的优先级、动态截断、三段提示词装配、工具筛选和会话级缓存。OpenClaw 的工作区注入、命令和压缩行为应以它自己的版本化文档和源码为准。把一个系统的「八个文件」「默认 Token 数」贴到另一个系统上,通常只会制造看起来专业的错误。

我更愿意用一组通用问题比较 Agent,而不是背结论:

  1. 它把哪些信息全量常驻,哪些信息按需检索?
  2. 项目说明来自哪里,冲突时谁优先?
  3. 工具是全量 schema 还是延迟发现,权限在哪里收口?
  4. 历史压缩后,哪些约束还在,哪些只存在于过去的对话里?
  5. 运行日志是否能让用户看清当前到底注入了什么?

能回答这些问题,换到其他 Agent 框架也不会完全从零开始。

容易犯的几个判断错误

「上下文压缩会顺手把 system prompt 变小」

不一定。对话压缩主要处理历史消息和工具结果。当前 Hermes 可能在压缩节点重建 system prompt,但重建并不等于自动删掉 SOUL.md、项目文件、Skills 或工具 schema。只有你改变了这些来源或相关配置,固定部分才会明显变小。

「把所有规则放进 SOUL.md 最省事」

短期省事,长期会混乱。人格偏好、项目规则、用户画像和临时任务的变化速度不同。都塞进同一个文件,不仅难维护,也会让每个无关项目都背上同一堆指令。把规则放在与其生命周期相匹配的位置,通常更短也更可靠。

「项目文件越短,模型越聪明」

也不对。过短会把关键约束删没。真正该删的是重复、背景化、过期和无法执行的句子。保留能影响文件范围、验证命令、权限边界和交付格式的具体规则。

「工具越少越便宜,所以全关掉」

没有工具的 Agent 可能少花输入 Token,却需要用户反复提供文件内容和运行结果。合适的目标是任务所需的最小工具面。例如纯写作会话不必加载一堆部署与家居控制工具,排查线上问题却不该关掉必要的日志和终端能力。

「10K Token 就一定很贵」

要看模型、provider、缓存命中率、调用次数和任务价值。十分钟内连续做几十轮的编码任务,与一天只问一次问题,优化优先级并不一样。先测自己的真实账单和延迟,再决定是否值得花时间瘦身。

写在最后

Hermes 的 system prompt 不是一段神秘咒语,而是一份会话开工前的工作包。它把稳定身份、项目规则和当前用户状态分开,是为了让模型既能做事,也不至于每次从零认识你。代价是这些信息会占用输入上下文,内容失控后还会干扰模型判断。

对初学者最有用的习惯只有一个:每次觉得 Agent 又慢又贵又不听话时,先问「这一轮究竟给它看了什么」。先看项目上下文和记忆,再看工具和 Skills,最后用单变量对照实验验证。这样得到的不是一套只适用于 Hermes 的技巧,而是一种排查所有 Agent 上下文问题的方式。


参考资料