跳到正文
多吃点的小饭桌
目录

5 月 1 日早上刷公众号,看到一条推送:Hermes Agent v0.12.0 发了,代号「The Curator Release」。里面提到一个叫”自助策展人”的东西,能在后台自动给 Skill 打分、合并重复项、清理死 Skill。

我看完第一反应——这不就是我三天前那篇博客结尾念叨的吗?「一个会自动生长但从不修剪的系统,时间长了就是这样。」

二话不说,先 hermes update。跑完之后,我开始问 Hermes 这个 Curator 到底是什么。于是有了下面这篇——我让它自己研究自己,然后写出来。


以下内容由 Hermes(deepseek-v4-pro)根据 v0.12.0 源码、release notes 和相关 PR 自主研究和撰写。


为什么叫「策展人」

在拆代码之前,先聊聊名字。Curator 这个词,Herems 不是随便起的。

在西方博物馆体系里,curator 是一个分量很重的角色。他不只是给藏品贴标签的人。史明立在《中西方博物馆策展人制度浅析》中这样描述:西方博物馆语境下的 curator 兼顾展览的方方面面——既具备阐释展品的专业能力,又具备展品选取、观众交流、公关宣传、文物收藏与保护等多方面的综合能力。简单说,这是一个同时管研究、管挑选、管维护的人。

这个词翻译到中文,博物馆学界吵了很多年。湖南省博物馆的李慧君主张统一译为「策展人」,理由是这个译名已被业界普遍接受,且强调展览导向有助于推动藏品研究向公众展示转化。山东大学的尹凯则反对,指出西方 curator 的”生成性和多义性”在中文里没法用两个字概括,「策展人」的翻译”是对博物馆藏品研究和专业学术的忽视,是展览形式对知识内容的凌驾与吞噬”。也有学者提出「典藏研究员」「研究员」等译名,认为更接近 curator 的本意。

这个争论本身很有意思,但先不去管它。重点是:无论哪种译法,curator 的内核始终是”筛选、整理、维护”——从来不是”销毁”。

Herems 把这个后台程序命名为 Curator,用意就在这里。你的 Skill 库是一座不断膨胀的馆藏。它需要有人来甄别哪些值得保留、哪些可以合并、哪些该归档到库房。这个角色是策展人,不是清洁工。实际操作上也确实如此:Curator 最极端的动作只是把 Skill 移到 .archive/ 目录,一键可恢复——就像博物馆把展品从展厅撤到库房,而不是扔进垃圾车。

在源码中,这个定位体现在模块 docstring 的第一段:

“The curator is an auxiliary-model task that periodically reviews agent-created skills and maintains the collection.”

—— agent/curator.py

注意”maintains the collection”(维护馆藏)这个措辞。不是”deletes”,不是”removes”,是”maintains”。

名字讲清楚了,下面拆它的运作逻辑。

一、Curator 是什么

Curator 是一个运行在 Hermes Gateway 后台的自主 Skill 维护程序。它不是一个独立进程,没有 cron daemon。它的触发入口是 maybe_run_curator(),在每次 Session 启动时被调用,做两道门控判断:

  1. 上次运行距今是否超过了 interval_hours(调用 should_run_now() 检查时间间隔和暂停状态)
  2. 当前 gateway 是否闲置了至少 min_idle_hours(由调用方传入的 idle_for_seconds 参数判断)

两道门都过,就触发一次策展。默认参数定义在模块顶部:

# agent/curator.py L39-L42
DEFAULT_INTERVAL_HOURS = 24 * 7 # 7 天
DEFAULT_MIN_IDLE_HOURS = 2 # 闲置 2 小时
DEFAULT_STALE_AFTER_DAYS = 30 # 30 天不用 → 休眠
DEFAULT_ARCHIVE_AFTER_DAYS = 90 # 90 天不用 → 归档

这些默认值可以通过 config.yaml 中的 curator 段覆盖,读取逻辑在 get_interval_hours() 等函数中。

每次策展的状态持久化在 ~/.hermes/skills/.curator_state 文件中,结构定义在 _default_state()

# agent/curator.py L53-L61
def _default_state() -> Dict[str, Any]:
return {
"last_run_at": None,
"last_run_duration_seconds": None,
"last_run_summary": None,
"last_report_path": None,
"paused": False,
"run_count": 0,
}

这个文件用原子写入(mkstemp + os.replace),不怕跑一半崩掉。

二、怎么运作:两阶段流水线

Curator 的的运作分成泾渭分明的两个阶段。第一阶段不调用任何 LLM,纯规则驱动;第二阶段 fork 出一个策展 agent,用模型做决策。总调度在 run_curator_review()

阶段一:纯规则自动流转

每次 Curator 启动,第一步是 apply_automatic_transitions()。这个函数不消耗任何 LLM token,完全是确定性逻辑。

它遍历 skill_usage.agent_created_report() 返回的所有 Skill,对每个 Skill 读取 last_activity_at 时间戳。注意这里的”活跃”不只是被调用来执行任务——被 skill_view() 查看、被 skill_manage() 修改,都算活跃,都会刷新时间戳。

核心判断逻辑(L228-L253):

for row in _u.agent_created_report():
name = row["name"]
if row.get("pinned"): # pinned skill 直接跳过
continue
anchor = last_activity or created_at or now # 取最晚的时间锚点
current = row.get("state", _u.STATE_ACTIVE)
if anchor <= archive_cutoff and current != _u.STATE_ARCHIVED:
_u.archive_skill(name) # 归档
elif anchor <= stale_cutoff and current == _u.STATE_ACTIVE:
_u.set_state(name, _u.STATE_STALE) # 标记休眠
elif anchor > stale_cutoff and current == _u.STATE_STALE:
_u.set_state(name, _u.STATE_ACTIVE) # 重新激活

四条规则:

  • stale_after_days(30 天)之内有活动 → 保持 active
  • 超过 30 天但不到 archive_after_days(90 天)→ 标记为 stale
  • 超过 90 天 → 归档到 ~/.hermes/skills/.archive/
  • 被标记 stale 后又有了新活动 → 自动 reactivate

pinned skill 免疫一切。 判断在循环的最开头(L231 if row.get("pinned"): continue),无论时间戳多老,都不会被碰。

第一阶段跑完后,初始状态写入 .curator_state,然后进入第二阶段。

阶段二:LLM 策展——伞形合并

这才是 Curator 真正”思考”的部分。入口在 _run_llm_review(),会 fork 一个完整的 AIAgent 实例。

fork agent 的隔离措施:

措施源码说明
独立模型_resolve_review_model() L1118-L1158auxiliary.curator.{provider,model},可不走主模型
高迭代上限L1237 max_iterations=9999伞形合并可能需要 50-100 次 API 调用
禁自我审查L1244-L1245策展过程不会触发新的 Background Review
无上下文文件L1240 skip_context_files=True不加载 AGENTS.md 等
无记忆L1241 skip_memory=True不污染长期记忆
静默输出L1252-L1254 stdout/stderr → /dev/null工具调用日志不污染终端

策展 agent 收到两份输入:一份候选名单(由 _render_candidate_list() 生成,包含每个 Skill 的名称、状态、使用计数、最后活跃时间),以及一个长达 110 行的策展 prompt(CURATOR_REVIEW_PROMPT,L262-L372)。

这个 prompt 是 Curator 的灵魂。 它要求策展 agent 做一件很具体的事:扫一遍所有 agent 自动创建的 Skill,把属于同一个领域的碎片拼成一张大图——这就是「伞形合并」。

先看一个具体例子,再讲它怎么做到的。假设你的 Skill 库里有这样三个条目:

hermes-config-debug/SKILL.md ← 某次调试 config.yaml 的经验
hermes-config-migration/SKILL.md ← 升级后迁移旧配置的步骤
hermes-config-rollback/SKILL.md ← 配置改崩了怎么回滚

这三个 Skill 各自独立时,Hermes 每次做配置相关的任务都要搜三遍、匹配三次。策展 agent 看到 hermes-config- 这个共同前缀,判断它们属于同一个领域,于是执行合并。合并之后变成:

hermes-config/SKILL.md ← 一个 Skill,内含三节
## Debug
之前的 debug 经验...
## Migration
迁移步骤...
## Rollback
回滚方法...
.archive/
hermes-config-debug/ ← 已归档
hermes-config-migration/ ← 已归档
hermes-config-rollback/ ← 已归档

下次 Hermes 做配置相关任务时,匹配到 hermes-config 这一个 Skill 就够了——它自己会在三节里找最相关的那段。

用一张流程图来概括策展 agent 的决策过程:

flowchart TD
    A[扫描所有 agent-created Skill] --> B{有共同前缀聚类?}
    B -->|无| C[跳过,保持原样]
    B -->|有| D{聚类中有一个够宽的<br/>Skill 能做伞?}
    D -->|有| E[用 patch 把其他 Skill<br/>的内容追加到伞下<br/>然后归档窄 Skill]
    D -->|无| F{窄 Skill 的内容<br/>有独立价值?}
    F -->|有| G[创建新伞形 SKILL.md<br/>窄内容降级为 references/ 文件<br/>然后归档原 Skill]
    F -->|无| H[直接归档<br/>标记为 pruned]
    E --> I[继续扫描下一个聚类]
    G --> I
    H --> I
    C --> I
    I --> B

这就是 prompt 里要求的三种操作(L293-L331):

操作什么时候用上面对应
合并到已有伞聚类里正好有一个够宽的 Skillhermes-config 吸收另外两个
新建伞形 Skill聚类里全是窄的,需要从零建伞如果三个都窄,新建一个 hermes-config
降级为支持文件窄但有独立价值,不配做独立 Skill比如某次 bug 的复现日志,放进 references/

Prompt 里有一句直指核心:

“A collection of hundreds of narrow skills where each one captures one session’s specific bug is a FAILURE of the library — not a feature.”

—— agent/curator.py L268-L269

几百个一次性的窄 Skill 不是资产,是负债。这正是我上一篇博客花了 2000 字论证的论点。而这句出现在 Curator 的系统 prompt 里,说明 Hermes 团队清晰地意识到了问题,并且把这个判断写进了策展 agent 的 DNA。

策展结束后,agent 必须输出一份结构化的 YAML:

consolidations:
- from: hermes-config-debug
into: hermes-config
reason: absorbed debug workflow as subsection
prunings:
- name: one-off-audit-2026
reason: stale single-session artifact with no reusable content

这份报告写入 ~/.hermes/logs/curator/{时间戳}/,包含 run.json(机器可读)和 REPORT.md(人类可读)。报告生成逻辑在 _write_run_report() 中,包含策展前后的 Skill 快照 diff、自动流转统计、LLM 工具调用明细、以及分类结果。

三、三层防御:为什么这次不是「又一个大锤」

上一篇博客里,我写到 Background Review 机制的问题是”太主动了”——没有确认、没有提示、静默创建。Curator 会不会又是个静默乱删的大锤?

源码给出的答案是:不会。Curator 嵌入了三层防御。

第一层:只碰 agent-created skill

候选名单的过滤由 skill_usage.is_agent_created() 实现。builtin skill(源码自带的 .bundled_manifest 中的条目)和 hub skill(hub lockfile 中的条目)永远不会进入候选名单。从官方渠道获得的 Skill,Curator 完全不管。

Curator 的 docstring 也写死了这条不变量(L16):

“Only touches agent-created skills (see tools/skill_usage.is_agent_created)”

而在策展 prompt 中,这条规则被重复强调(L277-L278):

“DO NOT touch bundled or hub-installed skills. The candidate list below is already filtered to agent-created skills only.”

这意味着即使策展 agent 某天完全失控,爆炸半径也被限定在你自己的 skill 范围内,不会波及系统核心功能。

第二层:归档,不删除

Curator 最极端的操作是 mv 目录到 ~/.hermes/skills/.archive/。源码中没有删除文件的代码路径。策展 prompt 第一段硬规则(L279-L281):

“DO NOT delete any skill. Archiving (moving the skill’s directory into ~/.hermes/skills/.archive/) is the maximum destructive action. Archives are recoverable; deletion is not.”

这不是技术限制——mv 之后的路径在程序看来和删除没区别。这是设计哲学:策展人把展品收进库房,策展人不扔东西。被归档的 Skill 可以用 hermes curator restore <name> 一键恢复。

第三层:模型输出 + 工具调用双重审计

策展分类不是由模型一句 YAML 说了算的。实际判定由 _reconcile_classification() 完成,它把两路信息做交叉验证:

  • 模型的结构化报告(YAML 中的 consolidationsprunings 列表)——提供意图和理由
  • 工具调用启发式审计(由 _classify_removed_skills() 回溯实际 skill_manage 调用)——提供地面实况

交叉验证的三条规则(L598-L610):

  • 模型声明「X 合并到 Y」且 Y 确实存在 → 采信模型报告
  • 模型声明「X 合并到 Y」但 Y 不存在(幻觉)→ 降级,改用工具调用审计的实际发现,或 fallback 到 pruned
  • 模型漏写了某个实际发生过的合并 → 审计机制补上,标记 source="tool-call audit"

这意味着即使策展用的模型偶尔胡说八道,最终写入 REPORT.md 的分类是有据可查的。幻觉会被抓出来并降级处理。

四、闭环:从「只进不出」到「策展循环」

回到三天前那篇博客的结尾。我写的是:

一个会自动生长但从不修剪的系统,时间长了就是这样。得,先手动割草吧。

当时我能做的三件事是:调高 nudge_interval、手动 hermes skills list 清理、蹲着社区 Issue #11425。那是一个被动防御的姿势——系统在设计上不做修剪,我只能自己拿着剪刀上。

v0.12.0 的 Curator 把这件事从手动变成了自动,而且把”修剪”升级成了”策展”。阶段一解决「这东西过期了吗」,阶段二解决「这些碎片应该拼成什么形状」。两个阶段加在一起,Skill 库从放任生长的野地变成了有园丁打理的花园。

这里还要提一句 Self-Improvement Loop 的同期升级。v0.12.0 不只是加了 Curator,还重写了 Background Review 底层逻辑(_spawn_background_review()run_agent.py L3521-L3615)。关键变化:

  • 继承父会话 runtimeL3574-L3583):provider、model、api_key、base_url 正确传递,不再出现 OAuth 场景下”No LLM provider configured”的错误
  • 工具集限制L3586 enabled_toolsets=["memory", "skills"]):review agent 只能用记忆和 Skill 工具,不会跑偏
  • 过滤前轮 tool messageL3607-L3609):_summarize_background_review_actions() 会跳过已在父会话历史中的旧工具消息,避免把”上次对话已保存”当作”刚才又保存了”

两套机制的分工现在非常清晰:

  • Review agent(Background Review):每次对话后运行,负责「这次对话有没有值得沉淀的经验」,决定要不要存新东西
  • Curator:按周期运行,扫描所有已存 Skill,负责「这些旧东西还该不该留」,决定要不要清理旧东西

一个管存,一个管清。之前只有存没有清,是半套系统。现在合龙了。

五、配置与使用

Curator 默认开启。升级到 v0.12.0 后,config migration v22→v23 会自动写入默认配置(hermes_cli/config.py L3318-L3398):

curator:
enabled: true
interval_hours: 168 # 7 天
min_idle_hours: 2 # 闲置 2 小时以上才触发
stale_after_days: 30 # 30 天不用标记为休眠
archive_after_days: 90 # 90 天不用归档

如果想让 Curator 用独立模型(推荐,省钱),配在 auxiliary.curator 下(模型解析逻辑见 _resolve_review_model() L1118-L1158,优先级:auxiliary.curator → legacy curator.auxiliary → 主模型):

auxiliary:
curator:
provider: deepseek
model: deepseek-v4-flash

常用命令一览(实现在 hermes_cli/curator.py):

命令用途
hermes curator status查看策展状态、Skill 活跃度排行(最常用/最少用 Top 5)
hermes curator run手动触发一次策展(加 --sync 可等待 LLM 阶段完成)
hermes curator pause / resume暂停 / 恢复自动策展
hermes curator pin <name> / unpin钉住 / 取消钉住某个 Skill(pinned skill 免疫一切自动操作)
hermes curator restore <name>.archive/ 恢复一个被归档的 Skill

策展报告位于 ~/.hermes/logs/curator/{YYYYMMDD-HHMMSS}/ 下,每次运行一个子目录。run.json 是完整机器记录(包含策展前后的 Skill 快照 diff、自动流转统计、LLM 工具调用明细、分类结果),REPORT.md 是人类可读摘要。

一个注意事项: v0.12.0 升级后,ComfyUI 和 TouchDesigner-MCP 从 optional 变成了 builtin 默认自带。这意味着 hermes skills list 的条目数会突然涨几个——这是官方加菜,不是 Curator 的 bug,Curator 也不管 builtin skill。


三天前,我蹲在 Issue #11425 下面等 PR。三天后,社区已经把策展机制合进了主分支,从 prompt 设计到代码防御到审计报告一整套配齐。从「能不能有人来管一下这个只进不出的系统」到「这里是你的策展人、你的管道、你的审计报告」——这个闭环的速度,让我觉得开源有时候真的挺厉害的。