Chapter 10

Curator:技能库自我维护

让 Agent 创建的 skill 库不腐烂。状态机、归档策略、 auxiliary LLM review——这是 self-improving agent 的"图书管理员"。

本章约 4,800 字 阅读 ~15 分钟 关键词:lifecycle · auxiliary client · agent-created · pinned · archive

第 8 章我们看到 Agent 能自己创建 skill。问题是——一年下来你的 ~/.hermes/skills/ 可能堆了几百个,大部分不再被用、有些彼此重复、 有些有 bug。如果不管,技能库就会腐烂。Curator 是 Hermes 的解法。

10.1问题:技能库会腐烂

具体腐烂方式:

解决方案:定期 review、归档不活跃的、合并相似的、修破损的。 这事让 Agent 自己做——Curator 是这个想法的实现。

10.2设计原则

agent/curator.py 顶部的文档:

agent/curator.py:1-25
"""Curator — background skill maintenance orchestrator.

The curator is an auxiliary-model task that periodically reviews
agent-created skills and maintains the collection. It runs
inactivity-triggered (no cron daemon): when the agent is idle and
the last curator run was longer than ``interval_hours`` ago,
``maybe_run_curator()`` spawns a forked AIAgent to do the review.

Responsibilities:
  - Auto-transition lifecycle states based on derived skill activity timestamps
  - Spawn a background review agent that can pin / archive / consolidate /
    patch agent-created skills via skill_manage
  - Persist curator state (last_run_at, paused, etc.) in .curator_state

Strict invariants:
  - Only touches agent-created skills (see tools/skill_usage.is_agent_created)
  - Never auto-deletes — only archives. Archive is recoverable.
  - Pinned skills bypass all auto-transitions
  - Uses the auxiliary client; never touches the main session's prompt cache
"""

四条不变量决定 Curator 的性格

  1. 只动 agent-created skill。bundled / user-authored 不碰。 (第 8 章讲过 created_by: agent 这条元数据。)
  2. 从不自动删除,最重的操作是 archive。可恢复。
  3. pinned 跳过所有转换。用户钉过的就是要留的。
  4. 用 auxiliary client——绝不污染主 session 的 prompt cache。
谨慎的工程哲学 Curator 自己跑,但它从不删除用户数据。最坏后果是某个 skill 被归档到 .archive/ 目录——还能恢复。这种"自动化但可逆"的设计是可信 AI的关键。 对照那些"AI 帮你管理收件箱时直接删邮件"的产品。

10.3三态生命周期

每个 agent-created skill 有三个状态:

状态含义用户感受
active 正常使用,在 slash 补全菜单里 正常工作
stale 太久没用,从补全菜单里隐藏 看不到,但 session_search 仍能找到
archived 移到 .archive/,完全隐藏 需要 hermes curator restore 才能恢复

加上一个独立的 pinned 布尔,正交于三态:

tools/skill_usage.py:35-45
STATE_ACTIVE = "active"
STATE_STALE = "stale"
STATE_ARCHIVED = "archived"
_VALID_STATES = {STATE_ACTIVE, STATE_STALE, STATE_ARCHIVED}

# 每个 skill 在 ~/.hermes/skills/.usage.json 里的记录
# {
#   "created_at": "...",
#   "created_by": "agent" | "user",
#   "last_used_at": "...",
#   "use_count": 12,
#   "state": "active" | "stale" | "archived",
#   "pinned": true | false
# }

状态转换图

stateDiagram-v2
  direction LR
  [*] --> ACTIVE: skill 创建
  ACTIVE --> ACTIVE: 被使用
  ACTIVE --> STALE: 30 天没用
  STALE --> ACTIVE: 再次使用
(reactivate) STALE --> ARCHIVED: 90 天没用
(移到 .archive/) ARCHIVED --> ACTIVE: hermes curator restore note right of ARCHIVED 永不删除 可手动恢复 end note note left of ACTIVE pinned: true 的 skill 跳过所有自动转换 end note

10.4实际的转换代码

agent/curator.py 第 256–296 行:

agent/curator.py:256-296
def apply_automatic_transitions(now=None) -> Dict[str, int]:
    """Walk every agent-created skill and move active/stale/archived
    based on the latest real activity timestamp. Pinned skills are
    never touched. Returns a counter dict describing what changed."""
    from tools import skill_usage as _u

    if now is None:
        now = datetime.now(timezone.utc)
    stale_cutoff = now - timedelta(days=get_stale_after_days())
    archive_cutoff = now - timedelta(days=get_archive_after_days())

    counts = {"marked_stale": 0, "archived": 0,
              "reactivated": 0, "checked": 0}

    for row in _u.agent_created_report():
        counts["checked"] += 1
        name = row["name"]
        if row.get("pinned"):
            continue

        last_activity = _parse_iso(row.get("last_activity_at"))
        # 如果从来没活动过,用 created_at 作为锚
        anchor = last_activity or _parse_iso(row.get("created_at")) or now
        if anchor.tzinfo is None:
            anchor = anchor.replace(tzinfo=timezone.utc)

        current = row.get("state", _u.STATE_ACTIVE)

        if anchor <= archive_cutoff and current != _u.STATE_ARCHIVED:
            ok, _msg = _u.archive_skill(name)
            if ok:
                counts["archived"] += 1
        elif anchor <= stale_cutoff and current == _u.STATE_ACTIVE:
            _u.set_state(name, _u.STATE_STALE)
            counts["marked_stale"] += 1
        elif anchor > stale_cutoff and current == _u.STATE_STALE:
            # 又被用了 — 自动 reactivate
            _u.set_state(name, _u.STATE_ACTIVE)
            counts["reactivated"] += 1

    return counts
Listing 10.1 Curator 状态转换的核心逻辑

注意几个细节:

10.5触发:inactivity-based,不是 cron

什么时候跑 Curator?Hermes 的答案:"等用户闲下来"

agent/curator.py (简化)
def maybe_run_curator() -> None:
    """Called at session_end / idle detection. Decides whether to run."""
    state = load_curator_state()
    cfg = _load_config()

    if not cfg.get("enabled", True):
        return
    if state.get("paused"):
        return

    last_run = _parse_iso(state.get("last_run_at"))
    interval_hours = get_interval_hours()         # 默认 168 = 一周
    min_idle_hours = get_min_idle_hours()         # 默认 2 小时

    if last_run and (now - last_run).total_seconds() < interval_hours * 3600:
        return     # 还没到时间

    last_activity = _get_user_last_activity()
    if (now - last_activity).total_seconds() < min_idle_hours * 3600:
        return     # 用户最近还在用,别打扰

    # 满足条件 — 跑
    _run_curator()
    state["last_run_at"] = now.isoformat()
    save_curator_state(state)

关键设计:

为什么不用 cron Cron daemon 会持续占内存、需要安装服务、跨平台麻烦(macOS LaunchAgent vs Linux systemd)。 而 Curator 的需求是"偶尔跑一下"。 Inactivity-based 触发是"按需启动"的最朴素实现。

10.6Auxiliary LLM Review

除了无脑状态机,Curator 还会启动一个 review agent 做智能审查

这个 review agent 是个独立的 AIAgent 实例,用 auxiliary model(便宜小模型, 不是主对话用的旗舰),加载这些工具:

review_agent = AIAgent(
    model=cfg.get("curator.review_model", "claude-haiku-4-5"),
    enabled_toolsets=["skills"],   # 只能动 skill 相关工具
    skip_memory=True,                # 不污染主 memory
    skip_context_files=True,         # 不要 AGENTS.md 等
    quiet_mode=True,
    max_iterations=30,
)

它的 system prompt 大致是:

You are the Hermes Skills Curator. Review the following agent-created skills.

Active skills (last used ≥ 30 days ago, candidate for stale):
- data-processing-pipeline (last used 35 days ago, use_count: 2)
- csv-quick-summary       (last used 40 days ago, use_count: 1)

Recently archived (90+ days no use):
- (none)

Your tasks:
1. For each candidate, decide:
   - keep as active (if still useful)
   - mark stale
   - merge with similar skill
   - patch (fix broken references / outdated APIs)
2. Use skill_view to inspect content before deciding.
3. Use skill_manage(action="consolidate") to merge.
4. Use skill_manage(action="patch") to fix.
5. Output a brief summary.

Do NOT delete anything. Only archive. Pinned skills are off-limits.

这个 review agent 跑完,输出报告:

Curator Review · 2026-05-23
─────────────────────────────────────────
Reviewed 12 skills.

Consolidated:
- merged 'csv-quick-summary' + 'parse-csv-stats' → 'csv-analyze'

Patched:
- 'web-deep-research': updated outdated reference to web_search v1 → v2

Marked stale:
- 'old-mlflow-integration' (last used 67 days ago, low value)

Active: 9 skills remain.

报告写进 ~/.hermes/.curator_state,用户可以用 hermes curator status 看。

10.7backup:每次 review 前快照

Curator 可能改 / 归档 skill。如果它判断错了怎么办? Hermes 在每次 review 前打一个 tar.gz 快照:

~/.hermes/skills/.backups/
├── 2026-05-16_curator.tar.gz
├── 2026-05-09_curator.tar.gz
├── 2026-05-02_curator.tar.gz
└── ...

每个 backup 是整个 skills/ 目录的快照。用户随时可以:

hermes curator rollback 2026-05-16
# 把 skills/ 还原到那个时间点的状态

backup 保留策略:默认 4 周。可在 config.yaml 改。

10.8用户怎么干预

Hermes 的 hermes curator CLI 给用户完全控制:

命令作用
hermes curator status看上次 run 时间、转换结果
hermes curator run立即跑一次(绕过 interval / idle 检查)
hermes curator pause暂停自动 review
hermes curator resume恢复自动 review
hermes curator pin <name>钉住一个 skill,免疫所有转换
hermes curator unpin <name>取消钉住
hermes curator archive <name>手动归档
hermes curator restore <name>恢复归档 skill
hermes curator prune清理 90 天前的 archived(用户确认后)
hermes curator backup手动打快照
hermes curator rollback <date>还原快照

10.9配置

~/.hermes/config.yaml:

curator:
  enabled: true                  # 默认 ON
  interval_hours: 168            # 一周一次
  min_idle_hours: 2              # 用户至少闲 2 小时
  stale_after_days: 30           # 30 天没用变 stale
  archive_after_days: 90         # 90 天没用归档
  review_model: claude-haiku-4-5 # review 用的便宜模型
  backup:
    enabled: true
    retain_weeks: 4              # 快照保留 4 周

三个数字(30 / 90 / 168)是经验值——Hermes 团队跑了几个月得出的。 你可以根据自己使用频率调整。

10.10横向对照:哪些产品有 Curator 类似机制

产品类似机制
Cursor "@codebase" 索引会定期重建,但无 skill 概念
Claude Code 没有 skill 自动归档;CLAUDE.md 完全人工管理
Voyager skill library 不归档,但 iterative refinement 时会被改写
Letta (MemGPT) archival memory 有 paging,但无显式 review 工作流
Hermes 完整的"创建 → 使用 → 归档 → 备份 → 恢复"循环

Hermes 的 Curator 是"长寿 Agent 生态"的一个独特实践。如果你计划让 Agent 运行 数月甚至数年,必须有这种机制。否则 skill 库迟早爆炸。

10.11Curator 哲学:Pruning Is Caring

Curator 体现了一个深刻的工程理念:

An agent that only learns but never forgets is a hoarder. Real intelligence requires pruning.
本书作者(对 Hermes 设计的总结)

三个学习闭环组件的协作:

flowchart TB
  S["Skill
创建"]:::pillar M["Memory
回忆"]:::pillar C["Curator
修剪"]:::pillar S --> SA["把新经验写成 SKILL.md"] M --> MA["搜索相关历史
注入 volatile"] C --> CA["定期审查 · 归档 · 合并
防止技能库膨胀腐烂"] SA --> Short["短期收益
这次任务能用"]:::good MA --> Short SA --> Long["长期健康
半年后仍能用"]:::good CA --> Long classDef pillar fill:#f5ede0,stroke:#8b1538,color:#8b1538,font-weight:bold classDef good fill:#ecf3eb,stroke:#2f5d3a,color:#2f5d3a

没有 Curator,前两件事会让你的 Agent 慢慢被自己的"经验"压垮。 这就是为什么 Hermes 这么强调"create + recall + prune"是同一个闭环。

10.12本章带走的

章末练习

  1. Easy 为什么 Curator 必须用 auxiliary client,不能用主 session 的 LLM?
  2. Easy 默认 stale = 30 天、archive = 90 天。如果你的 Agent 用户每天用一次,这些值合适吗? 如果一年用一次呢?讨论怎么调。
  3. Medium Curator 现在只动 agent-created。如果用户也希望 "user-authored" 的旧 skill 被自动归档怎么办?设计一个 opt-in 机制让用户允许 Curator 管理他自己的 skill。
  4. Medium Curator 用 LLM review 时,可能误判——把有用的 skill 标为 stale。 设计一个反馈循环:用户 reactivate 一个被错判的 skill 时,怎么让 Curator "学到"这个 skill 不该标 stale?
  5. Hard 把 Curator 的思想扩展到 memory:写一份"Memory Curator"设计文档。 考虑:什么数据该 forget?怎么 forget(向量库还是 SQLite)?什么时候 forget? 约 500 字。
  6. Hardagent/curator_backup.py。它用 tarfile 打快照。 讨论:如果用户在 Curator review 跑到一半时强制重启 Hermes,会发生什么? backup 完整吗?skill 状态一致吗?需要什么 transactional 保证?