KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

04 · 技能与记忆:self-improving 闭环怎么真的转起来 — keel 龙骨

「self-improving」在 Hermes Agent 里不是一句口号,而是三层机制拼出来的:模型可以在对话中直接调用 skill_manage 创建/修改技能;每轮结束后有一个后台 fork 复盘这次对话,决定要不要沉淀;再往上还有一个 curator 定期做清理与合并。

「self-improving」在 Hermes Agent 里不是一句口号,而是三层机制拼出来的:模型可以在对话中直接调用 skill_manage 创建/修改技能;每轮结束后有一个后台 fork 复盘这次对话,决定要不要沉淀;再往上还有一个 curator 定期做清理与合并。

这一章回答:

  1. 一个「技能」在磁盘上是什么,frontmatter 有哪些字段;
  2. skill_manage 的六个 action 分别做什么,安全扫描卡在哪一步;
  3. 后台复盘 fork 为什么是「同一个 agent 的复制品」,它的工具白名单与 prompt 约束是什么;
  4. curator 的两条不变量如何防止「AI 自己把技能删光」;
  5. 记忆(MEMORY.md / USER.md)与跨会话检索(FTS5)各自负责什么。

一、技能 = 目录 + SKILL.md

技能的形态极简:一个目录,里面一个 SKILL.md,可选子目录(scripts/、references/、assets/)。加载侧的全部逻辑在 tools/skills_tool.py:

以仓库内置技能 skills/software-development/plan/SKILL.md 为例,frontmatter 是:

---
name: plan
description: Write a markdown plan to .hermes/plans/; no execution.
version: 2.0.0
author: Hermes Agent (writing-craft adapted from obra/superpowers)
license: MIT
platforms: [linux, macos, windows]
metadata:
  hermes:
    tags: [planning, plan-mode, implementation, workflow, design, documentation]
    related_skills: [subagent-driven-development, test-driven-development, requesting-code-review]
---

tools/skills_tool.py:28 的模块 docstring 把格式称为「agentskills.io compatible」,并在 :1667 说明读取顺序是先看 metadata.hermes.*(agentskills.io 约定),再回落顶层字段。同一个 docstring 还列出了 compatibility 等可选字段。

技能的来源也决定了它的信任等级:仓库内置 skills/(默认加载)、optional-skills/(默认不激活,靠 hermes skills install official/<category>/<skill> 安装,适配器在 tools/skills_hub.py 的 OptionalSkillSource),以及外部注册表下载的第三方技能。

二、skill_manage:六个 action

创建与修改技能的工具是 tools/skill_manager_tool.py:1542: skill_manage(action=...),action 值域为 create | patch | edit | delete | write_file | remove_file,对应实现:

action 实现
create tools/skill_manager_tool.py:908: _create_skill()
edit :1006: _edit_skill()
patch :1068: _patch_skill()
delete :1188: _delete_skill()
write_file :1296: _write_file()

安全扫描的钩子在 tools/skill_manager_tool.py:125: _security_scan_skill(skill_dir)。它并不是唯一的关卡:外部来源的技能在安装前还要过 tools/skills_guard.py——文件头注释写明这是「Security scanner for externally-sourced skills」,任何从注册表下载的技能都要过扫描,覆盖数据外泄、prompt injection、破坏性命令、持久化等类别,Finding.category 的值域是 exfiltration | injection | destructive | persistence | network | obfuscation;并且信任等级分级,其中 builtin 是「Ships with Hermes. Never scanned, always trusted」。

此外 tools/skill_linter.py 是非阻塞的检查器(文件头明确它与 skill_manager_tool.py::_validate_frontmatter 那个「hard validator」的分工),做 name 与目录是否一致、name 格式、description 长度这类风格检查。

三、后台复盘 fork:闭环真正闭合的地方

「在使用中改进」的实现不在 skill_manage 里,而在 agent/background_review.py。它的模块 docstring 第一行就把机制说清楚了:

Background memory/skill review — fork the agent to evaluate the turn.

流程是:每轮 run_conversation 结束后,spawn_background_review_thread(agent/background_review.py:1315)在一个 threading.Thread 里,把一个复制的 AIAgent 拉起来,喂给它本次对话的快照,问它「有没有技能/记忆该保存或更新」。

fork 的三个约束必须记牢:

_REVIEW_MAX_ITERATIONS = 16(agent/background_review.py:48)是复盘的迭代预算——比主循环的 90 小得多,因为它只需要判断「写什么」,不需要做复杂调查。

prompt 里有两条明显的「鼓励写」与「鼓励改」的指令:agent/background_review.py:261 写着「ACTIVE — most sessions produce at least one skill update, even if …」,:289 要求「…new learning, PATCH that one first. It is the skill that was in…」。也就是说:默认假设是「本次会话应该有产出」,且优先 PATCH 本会话实际加载过的技能,而不是新建一个同主题的重复技能。

这里还有一个划算的优化(agent/background_review.py:33-45):复盘默认跑在主模型上("auto")并全量重放会话——因为已经在 prompt cache 里,读缓存便宜。只有用户把复盘路由到另一个更便宜的模型时,才改为重放一份紧凑 digest,因为换模型后缓存 key 不同,全量重放只会冷写一遍。

四、curator:只增不减的治理层

后台复盘会不断产出技能,长期会积累垃圾。治理层是 agent/curator.py,它自己的 docstring(agent/curator.py:11-17)把边界写成了不变量:

- Spawn a background review agent that can pin / archive / consolidate / ...
- Only touches agent-created skills (see tools/skill_usage.is_agent_created)
- Never auto-deletes — only archives. Archive is recoverable.

两条不变量各防一类事故:只动 agent 自己创建的技能,防的是把用户手写的技能归档掉;只 archive 不 delete,防的是不可逆损失——归档是「移动到 ~/.hermes/skills/.archive/」,可恢复,且注释 agent/curator.py:454 明确把归档称为「the maximum destructive action」。

状态机在 apply_automatic_transitions()(agent/curator.py:305):按 stale_after_days / archive_after_days 把技能在 active / stale / archived 之间移动,并统计 marked_stale / archived / reactivated / checked / seeded。其中有几处人为的仁慈:

run_curator_review()(agent/curator.py:1511)跑一个 LLM 复盘,其 fork 的预算是 max_iterations=9999(agent/curator.py:1947)——和后台复盘的 16 形成极端对比:curator 要做的是批量分类与合并,属于「慢但一次性」的工作。配套还有 agent/curator_backup.py。

遥测与来源追踪是三份 sidecar:tools/skill_usage.py 写 ~/.hermes/skills/.usage.json(skill_usage.py:86 给出路径;use_count / view_count / patch_count 在 :169 与 :647-657,状态字段 state / pinned);来源在 tools/skill_provenance.py;账本在 tools/skill_ledger.py。.usage.json 的读写用跨进程锁保护(skill_usage.py:91)。

五、记忆:MEMORY.md 与 USER.md

记忆层是 tools/memory_tool.py,核心类 tools/memory_tool.py:148: class MemoryStore,注册名 memory。它维护两份文件:

工具 action 值域是 add | replace | remove | batch(:1161 处理 batch)。写入前会做快照净化(_sanitize_entries_for_snapshot,:233-234),说明记忆内容会被注入 system prompt 的一部分,因此不能当作可信文本。

跨会话的对象化建模走插件化路线:抽象接口是 agent/memory_provider.py:104: class MemoryProvider(ABC),管理器是 agent/memory_manager.py:364: class MemoryManager,通过 inject_memory_provider_tools()(agent/memory_manager.py:110)按需把 provider 的工具注入。plugins/memory/ 下有 honcho(dialectic Q&A / semantic search / peer cards)、mem0、supermemory、hindsight、byterover、openviking、holographic、retaindb 等实现。

两条 nudge 间隔在 agent/agent_init.py 里定义,默认都是 10:agent._memory_nudge_interval = 10(:1763,可由配置 nudge_interval 覆盖,:1778)与 agent._skill_nudge_interval = 10(:1879,可由 skills.creation_nudge_interval 覆盖,:1882)。技能侧在 agent/conversation_loop.py:1976 每轮自增 agent._iters_since_skill,达到间隔就提醒模型「该沉淀了」。

六、跨会话检索与技能注入

会话检索工具是 tools/session_search_tool.py(注册名 session_search,注册点在 :1302),底层是 SQLite FTS5。四种模式在文件头(:8-25)列明:query 走 FTS5 并按会话血缘去重、sessions 只列元数据不做 FTS、window 只取锚点附近 ±N 条消息、其余模式各有形状。索引实现是 hermes_state_search.py:_sanitize_fts5_query()(:1179)负责把用户输入里的 FTS5 特殊字符(_FTS5_SPECIAL_CHARS,:45)剥掉或加引号,避免裸输入进 MATCH 直接抛错;CJK 查询走独立的 trigram 表(:1848,messages_fts_cjk / messages_fts_trigram),因为默认 unicode61 tokenizer 切不开中文。

会话库本体是 hermes_state.py:3093: class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin),get_messages() 在 :10138,search_sessions() 在 :11047,异步版本 AsyncSessionDB 在 :13072。恢复用 hermes -c/--continue(hermes_cli/main.py:1823: _resolve_continue_arg)、hermes sessions,以及独立工具 hermes_cli/session_recovery.py(inspect_session_database() :365、recover_session_database() :1533)。过大时被硬闸拦住——hermes_state.py:135: class SessionResumeTooLargeError(ValueError)。

最后是技能如何进入上下文。agent/skill_commands.py:630: build_skill_invocation_message(...) 构造注入消息。关键决定写在 AGENTS.md:420:

- Skill slash commands: `agent/skill_commands.py` scans `~/.hermes/skills/`,
  injects as **user message** (not system prompt) to preserve prompt caching

注入成 user message 而不是 system prompt,是为了不破坏前缀缓存——system prompt 一变,整段缓存就失效。

代码地图

机制 位置 要点
技能目录解析 tools/skills_tool.py: _skills_dir 第 148 行;~/.hermes/skills,安装时由内置 skills/ 播种
frontmatter 解析 tools/skills_tool.py: _parse_frontmatter 第 556 行;metadata.hermes.* 优先,回落顶层字段(:1667)
技能发现 tools/skills_tool.py: _find_all_skills 第 673 行;扫 ~/.hermes/skills + 外部目录 + 项目目录
列技能 / 看技能 tools/skills_tool.py: skills_list / skill_view 第 804 行 / 第 1072 行
技能管理入口 tools/skill_manager_tool.py: skill_manage 第 1542 行;create / patch / edit / delete / write_file / remove_file
创建/编辑/补丁 tools/skill_manager_tool.py: _create_skill 第 908 行;另有 _edit_skill(:1006)、_patch_skill(:1068)、_delete_skill(:1188)、_write_file(:1296)
写入前扫描 tools/skill_manager_tool.py: _security_scan_skill 第 125 行
外部技能扫描 tools/skills_guard.py: scan_skill 类别 exfiltration/injection/destructive/persistence/network/obfuscation(:78);builtin 永不扫描(:12)
风格检查 tools/skill_linter.py: LintFinding 第 104 行;非阻塞,与 _validate_frontmatter 的硬校验分工
后台复盘 fork agent/background_review.py 模块 docstring 第 1 行「fork the agent to evaluate the turn」;工具白名单仅 memory + skill(:12-13)
复盘预算 agent/background_review.py: _REVIEW_MAX_ITERATIONS 第 48 行,值 16
复盘线程 agent/background_review.py: spawn_background_review_thread 第 1315 行;threading.Thread
复盘 prompt 倾向 agent/background_review.py: 261 / :289 第 261 行「most sessions produce at least one skill update」;第 289 行优先 PATCH 本会话用过的技能
curator 不变量 agent/curator.py 模块 docstring 第 16 行只动 agent 创建的技能;第 17 行「Never auto-deletes — only archives」
自动状态流转 agent/curator.py: apply_automatic_transitions 第 305 行;active/stale/archived,never-used 有宽限(:359)
curator 复盘 agent/curator.py: run_curator_review 第 1511 行;fork 预算 max_iterations=9999(:1947)
技能遥测 tools/skill_usage.py: 86 / :169 ~/.hermes/skills/.usage.json;use_count / view_count / patch_count / state / pinned
记忆存储 tools/memory_tool.py: MemoryStore 第 148 行;MEMORY.md(:223)与 USER.md(:224)
记忆动作 tools/memory_tool.py: memory_tool 第 1056 行;add / replace / remove / batch
记忆 provider 接口 agent/memory_provider.py: MemoryProvider 第 104 行;ABC
记忆管理器 agent/memory_manager.py: MemoryManager 第 364 行;注入入口 inject_memory_provider_tools(:110)
nudge 间隔 agent/agent_init.py 第 1763 / 1879 行 _memory_nudge_interval = 10、_skill_nudge_interval = 10
技能 nudge 触发 agent/conversation_loop.py: 1976 agent._iters_since_skill += 1
会话检索工具 tools/session_search_tool.py: session_search 第 1065 行;注册在第 1302 行;FTS5
FTS5 查询净化 hermes_state_search.py: _sanitize_fts5_query 第 1179 行;特殊字符集在 :45
会话库 hermes_state.py: SessionDB 第 3093 行;get_messages(:10138)、search_sessions(:11047)、AsyncSessionDB(:13072)
恢复硬闸 hermes_state.py: SessionResumeTooLargeError 第 135 行
技能注入方式 agent/skill_commands.py: build_skill_invocation_message 第 630 行;按 AGENTS.md:420 以 user message 注入以保 prompt cache

关键取舍

技能只是「目录 + SKILL.md」,代价是元数据校验只能靠约定与 linter。
没有 schema 注册表、没有编译期校验,frontmatter 靠 _parse_frontmatter 容错解析 + _validate_frontmatter 硬校验 + skill_linter 风格检查三道软硬不一的门。收益是技能可以用任何编辑器手写、可以直接被 git 管理、天然兼容 agentskills.io 生态;代价是格式错误往往在运行期才暴露。

后台复盘用的是「复制一个 AIAgent」而不是「另起一个轻量管道」,代价是资源开销与白名单维护成本。
fork 继承 provider、model、凭据、缓存前缀,因此命中同一份 prompt cache——这是「全量重放也便宜」的前提。代价是复盘本身仍是一次完整的 agent 运行(预算 16 轮),且「只允许 memory + skill 工具」这条白名单必须随工具新增而维护。

复盘 prompt 明确要求「大多数会话至少产出一条技能更新」,代价是可能写出低价值技能。
这是把「闭环必须转起来」放在「产出必须全部高质」之前的取舍。补偿手段是下游的 curator:低价值技能会被标 stale 继而 archive,而因为 archive 可恢复且只动 agent 自建技能,误伤的代价被压到很低。

curator 只 archive 不 delete,代价是磁盘与索引会长期保留已归档内容。
「Never auto-deletes」+「Archive is recoverable」换来的是 AI 自治过程中不可能造成不可逆损失。配套的复杂度是 archive 态必须参与状态机(reactivated 计数就是证据)、hermes update 重播种时要靠抑制列表保持归档状态(agent/curator.py:196-197),以及 never_used 的宽限期规则。

技能注入用 user message 而非 system prompt,代价是注入内容与用户输入混在同一通道。
为保住 prompt cache,build_skill_invocation_message 的结果按 AGENTS.md:420 作为 user message 注入。收益是缓存前缀稳定、长会话成本显著降低;代价是技能正文与真实用户消息在语义上不再分层——因此 agent/skills_tool.py:1465-1468 才对「技能文件位于受信目录之外」发警告,并保留 injection 检测。

自测题

  1. _find_all_skills() 除了 ~/.hermes/skills 还扫描外部目录与项目目录。请说明这个设计在「团队共享技能」与「第三方技能隔离」两个场景下各自的风险,以及 tools/skills_tool.py:1465-1468 那两条警告分别对应哪种风险。
  2. 后台复盘的 fork 声明「绝不触碰主对话与 prompt cache」。请指出它靠什么手段做到这一点(提示:看它继承了什么、又回避了什么),以及如果它把结果直接写回主对话会破坏什么。
  3. 复盘 prompt 要求「优先 PATCH 本会话加载过的技能」,而不是「新建一个技能」。请从 tools/skill_usage.py 的遥测字段出发,说明这条指令为什么能减少技能碎片化。
  4. curator 的两条不变量是「只动 agent 创建的技能」与「只 archive 不 delete」。请分别构造一个场景说明:如果去掉其中任意一条,会发生什么用户可感知的损失。
  5. build_skill_invocation_message 把技能注入成 user message。请说明这会让「技能正文里的指令」与「用户的指令」在冲突时产生什么后果,以及系统目前靠什么机制缓解(提示:注意 skills_tool.py 里对受信目录外的检查与 injection 检测)。

进入 keel 阅读