【hermes】提示词的六层组装

$

上篇我们把对话存进了 SQLite,agent 退出后重启,对话还在。但有一个问题一直被推迟了——system prompt 从哪来?

回忆一下 S01 的最小循环。那段告诉大模型”你是一个什么样的助手”的 system prompt,是一个写死的字符串。能用吗?能用。但真实系统里,这段话要包含的信息远不止”你是一个助手”。

agent 需要知道自己是谁。用户有什么偏好。当前项目有什么规则。有哪些技能可以用。现在几点了。应该怎么调用工具。

这些信息来自不同的文件、不同的运行时状态。全写死在一个字符串里?改个人设要改代码,换个项目规则要改代码,加一个技能还要改代码。每改一处都要重新部署。

这篇拆开后:system prompt 到底是怎么来的。

第一个问题:agent 需要知道自己是谁

你新招了一个员工。入职第一天,他得知道几件事:我是谁?公司叫什么?我的岗位职责是什么?说话什么风格?

这些信息定义了这个人的”身份”。对 agent 来说也一样。

你的选择有哪些?

选择一:在代码里写死。system_prompt = "你是一个编程助手"。简单粗暴,但改一句话就得改代码、重新部署。

选择二:把身份定义放在一个独立的文件里。改文件就行,不用动代码。

Hermes 选了选择二。这个文件叫 SOUL.md,存在 ~/.hermes/ 目录下。

SOUL.md 的内容很简单,比如:

你是一个简洁、直接的编程助手。回答尽量简短。不要在每次回复结尾加总结。

如果 SOUL.md 不存在呢?用默认身份:”You are a helpful assistant.”——至少有个身份,不至于裸奔。

这就像公司的使命宣言。 它定义你是谁,但不告诉你具体怎么干活。具体的操作规范,后面几层会补上。

到这里,身份问题解决了。但光有身份不够。agent 还需要知道怎么干活。

第二个问题:告诉 agent 怎么干活

身份解决了”你是谁”。接下来是”你该怎么工作”。

agent 需要知道:有哪些工具可以用?工具调用的格式是什么?不同模型的工具调用方式有什么区别?

这些信息叫行为指导。它告诉模型使用工具的规范和行为边界。

为什么行为指导要单独一层?

因为不同模型的行为指导可能不同。Claude 的工具调用格式和 GPT 的不一样。如果你把人设和行为指导写在一起,换个模型就得连人设一起改。

分开之后,人设不动,只换行为指导那一层。

这就像公司的操作手册。 使命宣言说”我们做什么”,操作手册说”怎么做”。公司使命不常变,但操作流程会随着工具和系统升级而更新。

有了身份,有了操作规范。但 agent 还缺一样东西——记忆。

第三个问题:让 agent 记住事情

想象你有一个助手,每次对话都像第一次见面。你上次告诉他”我们项目用 pytest”,这次他又问你”你们用什么测试框架?”

这不行。

Hermes 用两个文件存记忆:MEMORY.md 存 agent 的长期记忆——它记住的事情、之前的结论、踩过的坑。USER.md 存用户偏好——你的编码习惯、沟通风格、常用工具。

这两个文件的内容会以快照形式注入 system prompt。每次启动时读一份快照,让 agent 带着记忆开始工作。

这就像你入职时 HR 给你的团队备忘录。 上次讨论的结论、老板的偏好、项目里的坑——都在里面。你不需要从头了解一切,因为有人帮你整理了前情提要。

到这里,agent 有了身份、有了操作规范、有了记忆。还差两样东西。

第四个问题:项目规则从哪来

不同项目有不同的规矩。Python 项目可能要求遵循 PEP 8,Node 项目可能要求用 ESLint。有的项目禁止修改 migrations 目录,有的项目要求每个函数必须有文档注释。

这些规则放在哪?放在项目目录的配置文件里。

但问题来了:Hermes Agent 支持好几种配置文件格式。

为什么要好几种?因为兼容性。用户可能从其他 agent 框架迁移过来,项目里已经有 CLAUDE.md 或 .cursorrules 了。Hermes Agent 直接读这些文件,不需要用户重写。

但多个文件同时存在怎么办?全部加载?

全部加载会出什么问题? 内容可能冲突。HERMES.md 说”用 pytest”,.cursorrules 说”用 Jest”。agent 听谁的?

你的选择有哪些?

选择一:全部加载,让模型自己判断。模型可能会困惑,而且浪费上下文窗口。

选择二:定一个优先级,只用优先级最高的那个文件。

Hermes 选了选择二。优先级链是这样的,

第一优先级:HERMES.md(从当前目录往上找,一直找到 git 根目录)。第二优先级:AGENTS.md(只看当前目录)。第三优先级:CLAUDE.md(只看当前目录)。第四优先级:.cursorrules(只看当前目录)。

只用第一个找到的。 找到 HERMES.md 就不看后面的了。

这就像公司制度冲突时的裁决规则。 国家法律大于行业标准,行业标准大于公司制度,公司制度大于部门惯例。只用最高优先级的那个,不会全部执行——否则互相矛盾。

还有一个细节:每个文件最多读 20,000 个字符。 超出的部分直接截断。

为什么?防止一个巨大的配置文件吃掉整个上下文窗口。一个 50KB 的 AGENTS.md 如果全部塞进去,留给实际对话的空间就所剩无几了。

就像每个部门的文件最多 20 页。 再重要的部门,也不能把整本手册撑到 500 页,让别人的内容没地方放。

到这里,所有来源都齐了。最后一个问题:拼好之后怎么办?

第五个问题:拼一次就够了

六层内容都收集齐了。最直觉的做法:每次调 API 都重新读文件、重新拼装。

为什么不应该每次都重新组装?

两个原因。

第一,浪费。SOUL.md 不会每次调用之间变化,HERMES.md 也不会。每次都重新读文件、重新拼字符串,做的是无用功。

第二,更关键的原因——Anthropic 的 prompt caching 会失效。 Anthropic 的 API 有一个优化:如果 system prompt 在多轮对话之间保持不变,它可以缓存这份 prompt,减少计算量,降低费用。但前提是 prompt 内容不能变。你每次重新组装,哪怕内容一样,只要有一个字节不同,缓存就失效了。

所以 Hermes 的做法是:组装一次,缓存下来。 同一个 session 的所有 API 调用,复用同一份 system prompt。

什么时候会重新组装?只有上下文压缩的时候。对话太长触发压缩(S05 会讲),压缩后需要重建 system prompt,这时候才清除缓存、重新拼装。

这就像复印机。 入职手册装订好了,复印一份给每次会议用。你不会每次开会都重新排版印刷一本。

System Prompt 六层组装:从 SOUL.md 到时间戳,按顺序拼装成一条完整字符串
System Prompt 六层组装:从 SOUL.md 到时间戳,按顺序拼装成一条完整字符串

Hermes 的独特设计:Gateway 续接 session 时从 SQLite 读 prompt

这里有一个精妙的设计。回忆 S03——Gateway 模式下,每条消息都会创建一个新的 AIAgent 实例。如果每个新实例都重新组装 system prompt,会出什么问题?

上一轮的 agent 可能在对话中修改了 MEMORY.md。新实例重新读 MEMORY.md,组装出来的 prompt 就和上一轮不一样了。Anthropic 的 prompt cache 前缀匹配失败,缓存失效,多花钱。

所以 Hermes 在第一次组装好 system prompt 之后,把它存到了 SQLite 的 session 表里。后续的新实例直接从数据库读缓存,不重新组装。

这保证了同一个 session 里,不管经过了多少轮、MEMORY.md 被改了多少次,发给 API 的 system prompt 始终一致。

Prompt 缓存策略:首次组装后存入 SQLite,Gateway 后续实例直接读取,保证 prompt 跨实例一致
Prompt 缓存策略:首次组装后存入 SQLite,Gateway 后续实例直接读取,保证 prompt 跨实例一致

第六个问题:临时指令怎么办

缓存解决了”稳定复用”的问题。但现实中有时候需要临时追加指令。

比如 Gateway 场景下,某一次 API 调用需要额外加一条系统指令——”这次回复请用英文”。这条指令只在这一次调用中有效,不应该写进永久缓存。

Hermes 的做法:临时指令拼在缓存后面,不存到 SQLite。



effective_system = cached_prompt + "\n\n" + ephemeral_prompt

缓存不动,临时指令只在当次调用时生效。

这就像会议前的临时便签。 “今天的会议额外注意这个事项”——它贴在会议室白板上,开完就撕。不会印进员工手册里。

第七个问题:记忆为什么分两条路

前面讲了 MEMORY.md 和 USER.md 的记忆注入。但 Hermes 还支持外部记忆提供者——通过 plugin 接入的第三方记忆服务。

这两种记忆的注入方式不同。

内置记忆(MEMORY.md / USER.md)→ 注入到 system prompt。因为这些内容相对稳定,不会每次调用都变。

外部记忆(plugin)→ 注入到 user message,不进 system prompt。

为什么外部记忆不进 system prompt?

因为外部记忆的内容每轮可能不同。它会根据用户当前的问题去检索相关记忆,每次返回的内容可能不一样。如果放进 system prompt,system prompt 就每轮都在变,缓存就失效了。

这就像两种信息源。 内置记忆是写进员工手册的公司文化——稳定、长期、不常变。外部记忆是每次开会前发的参考资料——每次不同,针对当次议题。参考资料不会印进手册里,因为下次开会就换了。

到这里,system prompt 的完整组装流程就清楚了。

复盘:六层组装,各管各的事

回头看,S01 到 S04 拼在一起,完整流程是这样的:

程序启动时,先读 SOUL.md 确定人设。然后加载行为指导,告诉模型怎么使用工具。接着读记忆快照,让 agent 带着前情提要开始工作。再加载技能索引,让 agent 知道有哪些高级能力可用。然后按优先级链读项目配置,确定当前项目的规则。最后加上时间戳和模型信息。

六层内容收集到一个列表里,用换行符拼接成一条完整的 system prompt、缓存起来,后续所有 API 调用复用同一份。

六层各管各的事:

内容来源更新频率
1 人设SOUL.md~/.hermes/很少改
2 行为指导工具使用规范、模型指导代码内置随版本更新
3 记忆MEMORY.md + USER.md~/.hermes/agent 运行时可能更新
4 技能已安装技能索引技能目录安装/卸载技能时变
5 项目配置HERMES.md 等项目目录项目规则变化时改
6 时间戳当前时间 + 模型信息运行时每次启动时生成

和前三篇对比,变化集中在 prompt 层,其他层纹丝不动:

能力S01+S02+S03+S04
system prompt硬编码字符串六层动态组装
改人设改代码改 SOUL.md
换项目规则改代码改 HERMES.md
prompt 缓存组装一次复用
核心循环不变不变
工具系统不变不变
存储层不变不变

初学者最容易犯的错:

一,把所有来源拼成一个巨大的硬编码字符串。改一处就要改代码。应该让每个来源独立维护、按顺序拼装。

二,每轮 API 调用都重新组装。浪费时间,还会破坏 prompt cache。应该组装一次,缓存复用。

三,加载全部项目配置文件而不是只用优先级最高的。同时加载 HERMES.md 和 AGENTS.md 和 .cursorrules,内容可能冲突或重复。

四,把 system prompt 放在 messages 列表里。system prompt 应该在每次 API 调用时临时拼在前面,不应该作为 messages 的一部分存到 SQLite。否则它会被上下文压缩、被重复、被持久化成历史消息。

最后,三个能带走的判断。

一,分层组装优于硬编码。 不是因为”分层更优雅”,而是因为每个来源的更新频率不同、维护者不同。人设很少改,项目规则经常变,记忆运行时在变。分层让每个来源独立变化,互不影响。做产品也一样——把变化频率不同的东西解耦,系统才容易维护。

二,优先级链优于全部加载。 多个配置文件可能冲突。只用优先级最高的一个,避免内容矛盾。做产品也一样——规则冲突时,需要一个明确的裁决机制,而不是”全部执行看看效果”。

三,缓存复用优于每次重建。 不只是性能考虑,更是为了配合 API 提供方的 prompt cache 机制。你组装一次、保持稳定,上游才能缓存命中。做产品也一样——你的设计决策不只看自己系统内部,还要考虑上下游的协作效率。

下一篇,我们拆开”上下文压缩”——对话太长怎么办?不能每次都把完整历史塞给模型。

📚 Hermes Agent 源码精读 · 系列第 04 篇

← 上一篇:03-会话持久化

| 下一篇:05-上下文压缩