继续第一篇的修复任务。你让 Hermes 修一个失败测试,中途补了一句:“这个仓库里不要直接修改生成文件。” 几天后,它又遇到同一个目录。你真正希望它做的,不只是把这句话原样保存,而是先认出它是什么:这是当前任务的临时限制, 某个仓库的长期规则,还是你在所有项目里的个人偏好?三种答案听起来相近,写错位置后的行为却完全不同。
如果把它写进本轮 transcript,下一轮未必主动看见;写进 USER.md,其他仓库也可能被同一条规则约束;
写进项目的 AGENTS.md,它会跟随仓库,但只适合真正属于这个项目的约定;写成 Skill,则把一句禁止规则误装成了操作流程。
所以“让 agent 记住”之前,必须先回答这条信息适用于哪个范围、能活多久、以后在什么场景下应该出现。
这一篇要解决的问题。 读完后,你应该能拿到任意一条信息,先不用源码名,判断它是规则、偏好、证据、流程还是一次性状态;然后再说明它为什么应该进入 system prompt 的某一层、Memory、SessionDB、Skill,或者只留在 transcript。
本文根据 2026 年 9 月 20 日核实的固定源码解释实际读写行为。三层 system prompt、会话快照和真实历史是实现事实; 文中的信息分类方法是根据这些机制整理出的使用判断。工具指导规定哪些事实应该写入,不代表文件存储能自动证明事实的长期价值。
一、先别急着选文件:先判断这条信息是做什么的
初学者最容易从文件名开始想:“这是不是应该写进 Memory?”更稳妥的顺序恰好相反。先问四个问题,再选存储位置。
| 先问什么 | “不要改生成文件”的可能答案 | 它决定什么 |
|---|---|---|
| 它在约束谁? | 只约束这个仓库,或约束用户的所有项目。 | 项目 Context 与用户画像不能互换。 |
| 它要活多久? | 只到本轮结束,或以后每次进入仓库都成立。 | 一次性 transcript 与长期状态不能互换。 |
| 它是事实、证据还是方法? | 偏好是一条小事实;失败日志是证据;重新生成文件是一套方法。 | Memory、SessionDB 与 Skill 不能互换。 |
| 下次什么时候需要看见? | 进入仓库就要遵守,还是遇到同类任务时才按需加载。 | 决定它应常驻 prompt,还是检索后再进入本轮模型输入。 |
这四问建立了一个简单顺序:先确定作用范围,再确定寿命,然后区分事实、证据和方法,最后决定读取时机。 Hermes 源码里的层级和存储位置,都是在实现这个顺序。下面先从模型每轮最先接触的 system prompt 开始。
二、system prompt 不是一个可以随便塞东西的大袋子
调用模型时,最前面需要一段说明告诉它“你是谁、能做什么、当前工作区有哪些规则”。这段说明叫 system prompt。 如果把身份、工具说明、项目文件、用户偏好、当前日期和临时检索结果全部随手拼进去,它当然也能运行;问题是每一轮只要有一小块变化, 前面的字节就可能跟着改变,模型看到的规则来源也越来越难追踪。
Hermes 的
build_system_prompt_parts
先把材料放进三个有顺序的层级,再一次性连接。这里的顺序不是“谁更重要”的排名,而是“谁更可能跨会话保持不变”的排名。
开头的“指令装配台”把这些关系画成了上下两条路:三层材料在工作台上合成会话级 system prompt; turn-only 召回走下方独立通道,只进入本轮模型输入。下面再逐层解释每一块为什么待在那里。
2.1 stable、context、volatile 分别保护什么
stable 位于最前面,放身份、通用工具指导、任务完成规则和较稳定的运行提示。它保护的是可复用前缀:
只要这部分字节不变,上游 provider 就更容易复用已经计算过的 prompt cache。
context 放当前工作区相关的材料,依次包括调用方传入的 system message、项目 Context 文件,再接 coding workspace snapshot 与工作区指导。
它不像身份那样跨所有会话稳定,但在一次项目会话里通常不会每轮变化。把它单列出来,也能看清一条规则究竟来自用户调用方还是仓库文件。
volatile 放“下一次重建时更可能变化”的内容:Skill 索引、MEMORY.md、USER.md、外部 memory provider block、
冻结的插件段、会话起始时间与运行环境提示。源码把它放在最后,因此即使重建时尾部改变,前面的稳定前缀仍有机会被复用。注意:volatile 不等于每轮刷新。
build_system_prompt
会把完整结果缓存在当前 AIAgent 上,通常一场会话只构建一次;压缩使缓存失效时会重新读取 Memory,恢复时则优先复用持久化的 prompt 字节,只有兼容性检查要求时才重建。
return {
"stable": "\n\n".join(stable_parts),
"context": "\n\n".join(context_parts),
"volatile": "\n\n".join(volatile_parts),
}
这也解释了一个看似反常的现象:本轮刚用 memory 工具写入的新偏好已经落盘,但当前会话的 system prompt 不会立刻被改写。 这是有意的快照语义,不是写入失败。长期状态的“磁盘真相”和当前模型正在使用的“会话快照”是两份不同生命周期的数据。
2.2 项目 Context 先选类别,再合并目录规则
仓库同时存在 HERMES.md、AGENTS.md 和 CLAUDE.md 时,Hermes 仍只选择第一个有内容的项目规则类别:
先找 .hermes.md/HERMES.md,再找 AGENTS 目录链,然后是当前目录的 CLAUDE.md/claude.md,最后是 Cursor rules。
build_context_files_prompt 的 or 链限制的是类别,并不承诺只读一个文件;
用户配置目录中的 SOUL.md 仍是独立身份来源。
选中 AGENTS 类别后,目录发现逻辑 从 Git 根目录走到当前目录,按根到深层顺序合并规则。
每个目录优先取首个非空且可读取的 AGENTS.override.md、AGENTS.md 或 agents.md;重复内容去重,并保留来源标签。
没有 Git 根目录时只检查当前目录,避免把任意父目录文件变成项目指令。
仓库根目录 AGENTS.md:不要直接修改生成文件
packages/api/AGENTS.override.md:本目录运行 schema 生成器
当前目录:packages/api
未命中 HERMES 规则 → 选中 AGENTS 类别
根目录规则 → api 目录规则 → 合成项目 Context
同目录的 AGENTS.md 被非空 override 文件遮蔽;CLAUDE 类别不再加载
这个例子保护的是“共享规则在前,局部规则随后补充”的目录关系。仓库约定应留在选中的项目 Context 中,随代码评审; 不应复制进每场会话都加载的用户 Memory。这里的合并是文本组装,实际是否遵守规则仍取决于模型及工具执行约束。
2.3 turn-only 为什么不属于第四层 system prompt
修测试时,外部 memory provider 可能临时找回一条相关记录,插件也可能给当前请求补一段上下文。这些材料只服务本轮判断。
Hermes 没有给 system prompt 增加“第四层”,而是由
compose_user_api_content
把它们附到当前 user message 的 API 副本上。
存储里的用户原话仍然是“修复失败测试”;发送给模型的副本可以是“修复失败测试 + 本轮召回”。Hermes 还把实际发送的字节保存为
api_content sidecar,后续重放时使用同一份内容,相关写入在
回合准备尾部。
这样既保住真实 transcript,也避免同一条历史消息在下一轮以另一组临时材料重新渲染。
三、长期状态不是一本记忆库,而是三种不同承诺
现在把视线从“模型这次看到什么”移到“以后怎样找回来”。用户偏好、失败日志和可复用流程都能帮助未来任务, 但它们分别承诺三件不同的事:一个小事实以后仍成立,一段历史确实发生过,一套方法遇到同类任务还能复用。
3.1 Memory:小、稳定、进入下一份会话快照的事实
Hermes 的内置 Memory 不是无限向量库,而是两份有字符预算、可直接审计的文件。MEMORY.md 保存跨任务的环境事实和长期约定,
USER.md 保存用户偏好与沟通方式。默认预算仍分别为 2200 和 1375 个字符。当前 工具指导 进一步要求:事实应适用于每场会话,而不只是“以后可能有用”。
某类任务的偏好、纠正与踩坑经验应进入对应 Skill;用户身份、跨任务的稳定环境事实和没有更具体归属的长期约定,才适合 Memory。
MemoryStore
同时维护 live entries 与 frozen snapshot。会话开始时,磁盘条目经过威胁模式扫描后形成 snapshot;system prompt 读取 snapshot。
本轮 add 或 replace 修改 live entries 并落盘,但不会偷偷改动已经交给模型的 snapshot。
写入的拒绝条件也很明确:空内容、注入或泄露模式、字符超限、文件读取失败都会被拒绝;重复添加返回成功但不重复写入;replace 遇到多个不同匹配时不会猜。
这些保护集中在
add 与 replace。
长期事实越小,写入越保守,日后越容易看懂它为什么存在。
3.2 SessionDB:保存发生过什么,不替历史下结论
几天后你问“上次失败的测试命令是什么”,需要的是原始证据,不是一句概括。Hermes 把用户消息、assistant 消息、工具调用与结果保存到 SQLite session DB,
再由 session_search 使用 FTS5 查找。工具内部不再调用模型,所以返回的是数据库中的真实消息窗口,而不是又一次生成的摘要。
discovery 路径先调用 db.search_messages,再按 session lineage 去重。默认 detail="adaptive" 只为最高排名结果展开完整窗口与首尾 bookends,其他结果保持紧凑;detail="full" 才全部展开。见
_discover。
紧凑结果保留 match_message_id,可以继续用 around_message_id 定位上下文。当前仍在模型上下文里的同谱系记录通常被排除;已经压缩归档或因会话重置离开当前上下文的记录仍可被找回。
3.3 Skills:保存以后怎样做,而不是过去做过什么
如果这次修复让 agent 学会了“生成文件出错时,先修改 schema,再运行生成器,最后验证 diff”,这已经不再是一条事实,而是一套步骤。
Skills 就是 Hermes 的程序性记忆:普通 Skill 在 system prompt 中只放简短索引,模型判断匹配后再用 skill_view 加载完整正文,避免每次都携带所有流程;显式预加载的固定 Skill 是例外。
最新的
build_skills_system_prompt
通过进程内 LRU 与磁盘 snapshot 两层缓存构造索引;这只是“怎样快速构造 Skill 目录”,不是按 LRU 淘汰 Skill 内容。
Skill 的生命周期与 Curator 会在下一篇解释,这里只要记住:LRU 管的是 prompt 索引缓存,不是知识价值排序。
skill_manage
向模型公开 operations 数组:create、patch、delete,以及支持文件的 write_file/remove_file;普通写操作失败会回滚触及的 Skill;delete 必须是本次调用唯一操作。旧的单操作参数仍为历史记录和待审批重放保留兼容,完整重写可以使用 patch 的 content 形式。成功后清掉索引缓存,但不会每次写入都强行重排当前会话 prompt。
假设生成流程正文和配套检查说明必须一起更新,可以提交一组操作。下面是有效的调用形状,省略已有 Skill 的其他内容;写之前仍应读取目标文件。
{
"operations": [
{"action": "patch", "name": "regenerate-api", "old_string": "运行生成器", "new_string": "修改 schema 后运行生成器"},
{"action": "write_file", "name": "regenerate-api", "file_path": "references/verification.md", "file_content": "检查生成 diff,并运行 API 测试。"}
]
}
如果第二项失败,第一项也不应留下半次更新。批量写入实现 持有全部目标锁直到提交或回滚;若文件系统导致回滚失败,会保留快照供恢复,因此仍要检查错误结果。审批开启时暂存也不等于内容已经写入。Memory 同样支持按最终字符预算检查的原子 operations 批,便于一次替换旧事实并添加新事实。
四、把同一句话放进五个位置,行为会怎样改变
现在做一次反事实练习。同样是“不要直接修改生成文件”,位置不同,系统对它作出的承诺也不同。
| 放置位置 | 适用条件 | 下次怎样出现 | 放错的代价 |
|---|---|---|---|
| 项目 Context | 这是仓库级规则,所有贡献者与任务都应遵守。 | 进入该工作区的新会话构建 prompt 时加载。 | 多个规则文件并存可能让权威来源混乱。 |
USER.md |
这是用户跨项目都成立的稳定偏好。 | 下一份用户画像 snapshot 进入 volatile 尾部。 | 仓库局部规则会错误扩散到别的项目。 |
| SessionDB | 需要证明用户何时说过、当时上下文是什么。 | 需要时通过 session_search 检索。 |
如果只存总结,原始措辞和上下文会消失。 |
| Skill | 已经形成“怎样安全重新生成”的可复用步骤。 | 索引匹配后按需加载完整流程。 | 一句偏好会被误写成笨重、难触发的流程。 |
| Transcript only | 它只约束当前任务,结束后不应持续生效。 | 留作恢复与审计,不主动常驻未来 prompt。 | 过早持久化会把临时要求硬化成长期规则。 |
五、一次完整判断:从用户原话走到正确保存位置
把前面的规则压缩成一次可复述的判断。用户在修复任务中说“这次不要改生成文件”,首先把原话保存进 transcript,保证任务中断也能恢复。 runtime 把它放进本轮模型输入,前台据此改 schema 并运行生成器。交付后,系统不应该因为出现了“以后”两个字就自动写 Memory。
接下来才检查范围:如果仓库已经把同一约束写进权威 Context 文件,无需复制;如果用户明确说这是所有项目的稳定偏好,可以提炼成一条短
USER.md 事实;如果真正可复用的是生成与验证步骤,应更新对应 Skill;如果都不是,就只保留历史。SessionDB 已经承担了“以后还能找到原话”的职责,
所以“不写长期状态”不等于“彻底忘记”。
先保存原话,完成当前任务;
再问它属于哪个范围;
事实进 Memory,证据留 SessionDB,方法进 Skill;
没有长期价值,就只留在 transcript。
这段判断不是 Hermes 源码里的单个 if 函数,而是本文根据每个组件能观察到的读写行为整理出的阅读方法。
它的价值在于把“记不记”拆成两个问题:真实历史一定要保存;是否再提炼成长期状态,则需要更严格的写入条件。
六、结论:状态设计的核心是下一次何时可见
到这里,几个容易突兀的名词已经连成一条路线。system prompt 决定会话开始时模型带着哪些指令;项目 Context 提供仓库权威规则; Memory 提供短小稳定事实;SessionDB 保存可检索的真实证据;Skills 保存按需加载的流程;turn-only 材料只进入本轮 user message 副本。
它们的区别不只是文件格式,而是不同承诺:谁能写、什么时候读、当前会话会不会立即看见、失败时哪份数据仍然可信。 以后再遇到“这个要不要记住”,最有用的问题不是“写进哪块 memory”,而是下一次在什么场景下,谁应该让模型看见它。
不过,知道经验应该落进哪本账,还没有解释它怎样从一次已经完成的任务里被提炼出来。 下一篇会从 finalizer 开始,区分 background review、nudge 与周期 Curator 的触发与写入时机,并解释后台工作怎样与前台回答分开。