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 定期做清理与合并。
这一章回答:
- 一个「技能」在磁盘上是什么,frontmatter 有哪些字段;
skill_manage的六个 action 分别做什么,安全扫描卡在哪一步;- 后台复盘 fork 为什么是「同一个 agent 的复制品」,它的工具白名单与 prompt 约束是什么;
- curator 的两条不变量如何防止「AI 自己把技能删光」;
- 记忆(
MEMORY.md/USER.md)与跨会话检索(FTS5)各自负责什么。
一、技能 = 目录 + SKILL.md
技能的形态极简:一个目录,里面一个 SKILL.md,可选子目录(scripts/、references/、assets/)。加载侧的全部逻辑在 tools/skills_tool.py:
tools/skills_tool.py:148: _skills_dir()—— 解析出~/.hermes/skills(注释:所有技能住在这里,安装时由仓库内置skills/播种)。tools/skills_tool.py:556: _parse_frontmatter()—— 切出 YAML frontmatter 与正文。tools/skills_tool.py:673: _find_all_skills()—— 递归扫描,除~/.hermes/skills外还包括外部目录(agent.skill_utils.get_external_skills_dirs)与项目级目录(get_project_skills_dirs)。tools/skills_tool.py:804: skills_list()/:1072: skill_view()—— 对应注册名skills_list/skill_view。
以仓库内置技能 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 的三个约束必须记牢:
- 继承父 agent 的实时运行时(provider、model、base_url、凭据、已缓存的 system prompt),因此命中同一份前缀缓存、用同一套鉴权。
- 工具白名单只含 memory 与技能管理工具,其余在运行时一律拒绝(docstring
:12-13)。 - 绝不触碰主对话与 prompt cache(docstring
:7)。
_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。其中有几处人为的仁慈:
never_used(use_count == 0)的技能有宽限期(:359-365),理由是「a use=0 skill is absence of evidence, not evidence of staleness」。- 被 cron 引用的技能不参与清理(
_cron_referenced_skills(),:290;prompt 里:462要求「DO NOT archive or prune any skill markedcron=yes」)。 - 从
stale回到active是允许的(:393-394),被用过就复活。
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。它维护两份文件:
MEMORY.md:agent 自己的笔记(环境事实、项目知识等),见memory_tool.py:223。USER.md:关于你的画像(偏好、沟通风格等),见memory_tool.py:224。
工具 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 检测。
自测题
_find_all_skills()除了~/.hermes/skills还扫描外部目录与项目目录。请说明这个设计在「团队共享技能」与「第三方技能隔离」两个场景下各自的风险,以及tools/skills_tool.py:1465-1468那两条警告分别对应哪种风险。- 后台复盘的 fork 声明「绝不触碰主对话与 prompt cache」。请指出它靠什么手段做到这一点(提示:看它继承了什么、又回避了什么),以及如果它把结果直接写回主对话会破坏什么。
- 复盘 prompt 要求「优先 PATCH 本会话加载过的技能」,而不是「新建一个技能」。请从
tools/skill_usage.py的遥测字段出发,说明这条指令为什么能减少技能碎片化。 - curator 的两条不变量是「只动 agent 创建的技能」与「只 archive 不 delete」。请分别构造一个场景说明:如果去掉其中任意一条,会发生什么用户可感知的损失。
build_skill_invocation_message把技能注入成 user message。请说明这会让「技能正文里的指令」与「用户的指令」在冲突时产生什么后果,以及系统目前靠什么机制缓解(提示:注意skills_tool.py里对受信目录外的检查与 injection 检测)。