开源自托管项目 Hermes Agent:技能怎么装、怎么被检索
本文基于 hermes-agent 仓库 commit 2d40494(2026-07-29)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/NousResearch/hermes-agent 最新代码与文档为准。
读这个仓库的技能机制,第一件要放下的成见是「仓库里的 skills/ 就是运行时目录」——它不是。 hermes-agent(Nous Research 的开源自托管 Agent 项目,注意它跟同一家发布的 Hermes 开源模型系列不是一回事,也跟若干同名商标、同名库无关)把技能分成了两棵树:仓库里那棵是源码,安装时被播种到用户主目录下的另一棵,运行时只认后者。这个区分不是细节,它决定了你改完文件为什么没生效、你写的技能为什么别人克隆仓库拿不到、以及为什么有一批技能明明躺在仓库里却从来没在索引中出现过。
一、两棵树:源码那棵和生效那棵
仓库自带的技能写作规范里把这件事说得很直白。一份 SKILL.md 只有两个可能的家:一个是 ~/.hermes/skills/<可能有分类>/<名字>/SKILL.md,属于个人、不共享,由 skill_manage(action='create') 创建;另一个是仓库内的 skills/<category>/<name>/SKILL.md,会被提交、随包发布,而 skill_manage 的 create 动作不会写到这棵树里,得用写文件加 git add。
运行时那一侧在 tools/skills_tool.py 里写得更硬:技能目录解析自当前 profile 的 HERMES_HOME,注释直接标明所有技能都住在 ~/.hermes/skills/,是唯一的事实来源,安装时从随包的 skills/ 播种过去。播种逻辑在 tools/skills_sync.py,它维护一份清单文件,每行是「技能名:来源哈希」,然后按几种情况分别处理:清单里没有的算新技能,拷过去并记下哈希;随包版本没变就直接跳过,连用户那份都不读;随包变了而用户那份还等于当初的哈希,说明你没动过,可以安全更新;随包变了、用户那份也变了,就认定你做过定制,跳过不覆盖;清单里有、用户目录里没有,视为你主动删过,不再补回。
所以「改了没生效」通常有两种成因:你改的是仓库里的源码副本,而 Agent 读的是主目录那份;或者反过来,你改了主目录那份,于是这个技能从此被排除在随包更新之外。
第三批技能连播种都不参与。optional-skills/ 目录的说明只有短短一页,把定位讲清了:由 Nous Research 维护的官方技能,但默认不激活,随仓库发布却不会在安装时拷进 ~/.hermes/skills/,要通过 Skills Hub 自己捞:
hermes skills browse # browse all skills, official shown first
hermes skills browse --source official # browse only official optional skills
hermes skills search <query> # finds optional skills labeled "official"
hermes skills install <identifier> # copies to ~/.hermes/skills/ and activates
它给出的三条不默认激活的理由也很实在:小众集成(特定付费服务、专用工具)、实验性功能、以及需要大量前置准备(密钥、安装)的重型依赖。换句话说,默认那批是「装上就能用」的交集,可选那批是「你得先付出点什么」的并集。
数量上你自己就能数出来:skills/ 下 14 个顶层目录、共 70 份 SKILL.md(其中 skills/index-cache/ 不是技能分类,装的是几份索引 JSON);optional-skills/ 下 21 个分类目录、共 111 份 SKILL.md。也就是说仓库里躺着的技能,多数默认是不上场的。
二、装载分几级:从系统提示词里的一行到 references/
tools/skills_tool.py 的模块注释把这套设计称为渐进式披露,实际落地是四级,一级比一级贵:
第 0 级,系统提示词里的技能索引。 agent/prompt_builder.py 会扫出全部技能,按分类聚成「名字 + 描述」的清单铺进提示词。这里有个很容易吃亏的常数:agent/skill_utils.py 里 SKILL_PROMPT_DESC_LIMIT = 60,超长描述被截成前 57 个字符加上省略号。校验器允许描述最长 1024 字符,但那 1024 字符里只有前 57 个能进索引。仓库的写作规范因此要求描述以「Use when …」开头,并且触发条件必须在这个窗口内说完——它给的反例是把触发条件埋在一长句解释后面,截断后索引里只剩一句废话,模型再也不知道什么时候该调它。分类那一行的说明来自分类目录下的 DESCRIPTION.md,比如 skills/mlops/DESCRIPTION.md 只有一个 description 字段。
第 1 级,skills_list()。 只返回名字、描述、分类,外加一句提示让你去用 skill_view。它专门为省 token 而设计,同时也是描述被截断后你能看到完整文本的地方。
第 2 级,skill_view(name)。 返回完整的 SKILL.md,附带 tags、related_skills、以及一个 linked_files 结构,把这个技能目录下的 references/templates/assets/scripts 分门别类列出来。
第 3 级,skill_view(name, file_path)。 按需读某个附属文件。这也是为什么写作规范建议:总是要用的步骤放 SKILL.md,分支专用或篇幅大的材料塞进 references/,用指针引过去。规范还给了尺寸参考——software-development/ 下的同侪技能都在 8 到 14k 字符,超过 20k 就该拆。
检索路径上有几处值得单独记住的行为。名字查找同时支持目录名和 frontmatter 里的 name,因为 skills_list() 暴露的是后者;一旦本地技能目录和配置的外部目录里出现同名技能,skill_view 拒绝猜,直接返回全部候选路径并要求你用分类路径显式指定,系统提示词那边也会给两条记录都打上名字冲突的标记。插件提供的技能走带命名空间的限定名,形如 plugin:skill,返回内容前面会插一段横幅,把同一插件里的兄弟技能一并告诉模型。
装载还有一步文本预处理,在 agent/skill_preprocessing.py:${HERMES_SKILL_DIR} 和 ${HERMES_SESSION_ID} 两个模板变量默认替换,解析不出来的令牌故意原样留着方便排错;而形如 !`cmd` 的内联 shell 展开默认关闭,配置打开后才会以技能目录为工作目录跑 bash,有超时和输出长度上限。这个默认值的方向是对的——一次「查看技能」如果能顺手执行命令,那它就不再是读文档了。
站内已有几篇相邻的文章:pi 的技能加载、ECC 的技能检索、superpowers 的技能文件契约 讲的是另外几个项目,三种技能机制的横向对比 讲的是通用方法论;本篇不重复那些,只把 hermes-agent 这一个仓库落到实处——哪个文件、哪个常数、决定了你会踩到什么。
三、各部件与它们的位置
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 随包技能 | 安装时被播种进用户主目录的默认技能集 | skills/ | 刚装完,索引里出现的就是这批 |
| 分类说明 | 给索引里的分类补一句话描述 | skills/mlops/DESCRIPTION.md | 新增分类、或索引里分类名看不懂时 |
| 官方可选技能 | 随包发布但不默认激活,走 Hub 安装 | optional-skills/、optional-skills/DESCRIPTION.md | 需要某个小众集成或重依赖能力时 |
| 列出与查看 | skills_list / skill_view 两级返回,含附属文件清单 | tools/skills_tool.py | 想知道模型实际看到了什么 |
| 增删改 | skill_manage 六个动作,以及全部写入校验 | tools/skill_manager_tool.py | 让它把一次经验固化成技能 |
| 写作规范 | 在仓库里新增技能的格式、结构与自检清单 | skills/software-development/hermes-agent-skill-authoring/SKILL.md | 准备提交一个随包技能时 |
| 播种与更新 | 清单加哈希,判断能不能覆盖用户副本 | tools/skills_sync.py | 升级后发现某个技能没跟着变 |
| 装载预处理 | 模板变量替换、内联 shell 展开 | agent/skill_preprocessing.py | 技能正文里出现模板变量或反引号命令 |
| 索引与截断 | 把名字加短描述铺进每一轮提示词 | agent/prompt_builder.py、agent/skill_utils.py | 描述写长了被砍成省略号时 |
四、什么时候该自己写一个,以及校验器画的红线
tools/skill_manager_tool.py 的模块注释给了一个判断口径,比「觉得有用就写」清楚得多:技能是过程性记忆,捕捉的是某一类任务具体怎么做;而通用记忆是宽泛、陈述性的。工具描述里进一步列了几个触发时机——复杂任务成功了(调用五次以上)、克服了报错、用户纠正过的做法奏效了、发现了非平凡的工作流、或者用户明确要求记住一个流程。反过来,简单的一次性任务不必写;而如果你用了某个技能却撞上它没覆盖的坑,应该立刻打补丁而不是新建一个近亲。
选哪棵树也有明确分工:只对你自己有用的,让它创建到主目录那棵;要随仓库发布给别人的,写进 skills/<category>/<name>/,用文件写入加 git 提交,并且规范提醒 related_skills 会把两棵树合并解析——你可以从仓库内技能引用一个只存在于你主目录的技能,但别人克隆下来就断了,所以仓库内技能应尽量只引用仓库内技能。
写入时的硬约束都在同一个校验函数里,值得先记住再动手:名字最长 64 字符且只能是小写字母、数字、连字符、点、下划线,还得以字母或数字开头;描述必填、最长 1024 字符,而新建技能还额外要求描述能塞进 60 字符的提示词预算(编辑和补丁路径故意跳过这一条,好让存量的超长描述还能被慢慢改掉);frontmatter 必须以 --- 开头、正确闭合、能解析成 YAML 映射,且闭合之后正文不能为空;单份 SKILL.md 上限十万字符,单个附属文件上限 1 MiB。附属文件只允许落在四个子目录里:
ALLOWED_SUBDIRS = {"references", "templates", "scripts", "assets"}
规范里还有一处细节能省你一次困惑:它写的是任何前导空行或 BOM 都会导致校验失败,但校验器实际会先剥掉 UTF-8 BOM(注释写明是照顾 Windows 编辑器)。规范自己声明「事实来源是校验器」,所以这类地方以代码为准;同一份规范列举的仓库内分类清单也带着「用 ls skills/ 确认」的提醒,而现在两边确实对不齐——它列的 devops、data-science 之类只存在于 optional-skills/ 下。它甚至留了一条作者机器上的绝对路径当示例。这类漂移在高频迭代的仓库里很常见,别把文档当契约读。
五、边界与代价:这个设计放弃了什么
放弃了「文档即真相」。 技能索引、播种清单、发现缓存都是有状态的,文档只是其中一份可能落后的输入。写作规范自己就列了一条常见坑:当前会话看不到你刚加的技能,技能加载器在会话启动时初始化,只能新开会话验证;这不是 bug。发现层还有一层短缓存,tools/skills_tool.py 里 TTL 是 30 秒,签名由目录 mtime、禁用集合和平台共同构成——原地编辑一份 SKILL.md 只会改文件的 mtime,任何目录签名都看不见,所以那 30 秒是用来兜住这种改动的上限,不是即时生效的保证。
放弃了对技能内容的强隔离。 加载路径会扫一批提示注入特征串,命中之后只记一条警告日志,内容照样返回;文件不在受信目录里也是同样处理。仓库对此的态度写在配置项旁边:针对 Agent 自建技能的安全扫描默认关闭,理由是 Agent 本来就能通过终端工具走同样的代码路径且没有门禁,加扫描只增摩擦。这个判断在它的威胁模型里自洽,但含义你得接受——技能正文是会进上下文并影响行为的第三方指令,装一个来源不明的技能,等价于把一段没审过的指令放进每一轮对话。
明确不管的事情。 技能不解决权限:需要密钥的技能通过 frontmatter 声明必需的环境变量,缺了就返回「需要配置」的状态,交互式界面可以走安全输入,而消息平台那一侧压根没法安全地问你要密钥,只能给一句提示让你去本地补。技能也不负责把自己的依赖装到远端:远程后端(容器、托管沙箱、SSH 等)要求这些前置条件在远端环境里同样可用,工具只是把提示话说清。技能更不保证跨机一致——外部技能目录对自动维护是只读的,组织共享技能住在带令牌门禁的镜像目录里,本地删除会被下次同步拉回来。
常驻带来的代价要如实说。 这个项目的定位是常驻在你机器上、开终端执行命令、接你的聊天软件账号、往磁盘写文件、访问外部服务。技能层放大了这几条:一个技能可以自带 scripts/、可以在装载时展开内联 shell(如果你开了)、可以声明要往沙箱里挂凭据文件。它也确实带了不少约束——后台自我维护那条路径被拦得很死:不能写外部目录、不能改被固定的技能、不能碰随包和 Hub 安装的技能、不能动没被纳管的用户技能,改之前必须先真的读过目标文件,删除必须声明合并去向且走可恢复的归档而不是直接删。这些约束是好设计,但它们的存在本身也说明:一个能改自己技能的常驻程序,风险面是需要专门堵的。
六、上手与避坑清单
- 先看主目录,再看仓库。 会踩是因为
git log看着很新,Agent 行为却没变——你在仓库里读到的是源码副本,运行时读的是主目录那份。想确认模型手上到底是什么,用skill_view看返回内容,别看仓库文件。 - 别在仓库里用创建动作。 会踩是因为工具名字里没写它去哪;
skill_manage(action='create')落在主目录树,仓库树要靠写文件加提交。判断依据很简单:这个技能要不要被别人克隆到?要,就走仓库树。 - 描述的前 57 个字符当成唯一的一次机会。 会踩是因为校验器放你写到 1024 字符,索引却只显示前 57 个。写法就一条:触发条件顶格写,行为描述放后面,细节全进正文。这也是上下文预算在技能层最直接的一次体现。
- 改了随包技能就等于退出更新。 会踩是因为跳过是静默的——你的副本和来源哈希不一致,同步逻辑就认定这是定制,不再覆盖。要么接受这条分叉并自己维护,要么把改动提到仓库里去。
- 新加的技能当前会话看不到。 会踩是因为它像 bug,于是你开始反复改文件找原因。正确做法是新开会话验证,或者按确切路径直接查看;原地编辑还要留意那 30 秒的发现缓存。
- 同名会被拒绝而不是择一。 会踩是因为你在外部目录里放了一个跟本地同名的技能,然后按裸名字加载。返回的是候选清单和一句「不猜」,按分类路径写全,或者干脆改名。
- 附属文件路径写错会直接被挡。 会踩是因为你顺手放了个
docs/foo.md;允许的只有 references、templates、scripts、assets 四个目录,SKILL.md自己在技能根下另有豁免。 - 装第三方技能前把正文当代码审。 会踩是因为注入特征只记日志不拦人。审什么:有没有
scripts/、有没有内联 shell 片段、有没有声明要环境变量或凭据文件、有没有让你放宽某项配置。 - 可选技能不是「更好的技能」。 会踩是因为 Hub 里官方那批排在前面,看着像推荐位。它们不默认激活的三条理由——小众、实验性、重依赖——正好也是你装之前该逐条确认的。
收束
把这套机制压缩成一句:在这个仓库里,技能是一份带 frontmatter 的 Markdown,但技能系统是「两棵树 + 四级披露 + 一串校验红线 + 若干缓存」的组合。你要动手时,按这个顺序读文件最省时间:tools/skill_manager_tool.py 的模块注释和 _validate_frontmatter(写入端全部硬约束)、skills/software-development/hermes-agent-skill-authoring/SKILL.md(写作口径与自检清单,读时对着代码校正)、tools/skills_tool.py(模型实际拿到什么)、tools/skills_sync.py(升级会不会覆盖你)、agent/skill_preprocessing.py(装载时还会不会跑东西)。
顺手做一遍这四问的自检:这个技能该进哪棵树?描述的前 57 个字符够不够别人一眼判断该不该调?正文里有没有一句删掉之后行为不变的话?装载它需不需要任何我还没配的东西?四个都答得上来,再动手写。这个项目采用 MIT 许可证,LICENSE 署名 Nous Research,上面所有结论都能在仓库里当场核对——真要拍板,还是回去读代码。
本篇属于一个把开源常驻自托管 Agent 项目 Hermes Agent逐层拆开讲的系列,整体地图见 开源自托管 Agent 项目 Hermes Agent 是什么;沿着这条线往下,还可以看 开源自托管 Agent 项目 Hermes Agent 的定时任务怎么跑 和 自托管开源项目 Hermes Agent 的跨会话记忆由谁写入、何时写。