上篇我们把对话存进了 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,这时候才清除缓存、重新拼装。
这就像复印机。 入职手册装订好了,复印一份给每次会议用。你不会每次开会都重新排版印刷一本。

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 始终一致。

第六个问题:临时指令怎么办
缓存解决了”稳定复用”的问题。但现实中有时候需要临时追加指令。
比如 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 机制。你组装一次、保持稳定,上游才能缓存命中。做产品也一样——你的设计决策不只看自己系统内部,还要考虑上下游的协作效率。
下一篇,我们拆开”上下文压缩”——对话太长怎么办?不能每次都把完整历史塞给模型。