技能系统:Agent 如何把经验变成可复用的程序化记忆
从声明式 SKILL.md 到自动化经验积累,Hermes 如何让 Agent 越用越聪明
系列:通过 Hermes 探秘 Agent 工程 · 补遗篇 关联篇:记忆系统:跨会话持久化的工程实现
问题:记住了"是什么",但忘了"怎么做"
第 5 篇聊过 Hermes 的记忆系统——MEMORY.md 记事实,USER.md 记偏好。它们是陈述性记忆——你知道什么。
但 Agent 完成一个复杂任务后,最有价值的不是"我知道什么",而是"我怎么做成的":
- 第一次用 Docker 部署应用时踩过的坑和最终成功的命令序列
- 为项目编写特定格式的 API 文档时的最佳实践
- 在 CI 流程里正确配置测试覆盖率的步骤
这是程序化记忆——关于"怎么做"的知识。
Hermes 的技能系统就是为此设计的:把成功的做法编码成可复用的程序化记忆,以后遇到类似任务时自动加载、自动执行。
技能是什么
一个技能就是一个目录,核心是一个 SKILL.md 文件:
~/.hermes/skills/
├── docker-deploy/
│ ├── SKILL.md # 主指令(必须)
│ ├── references/
│ │ └── troubleshooting.md # 参考文档
│ └── templates/
│ └── Dockerfile.tpl # 模板文件
├── api-docs/
│ └── SKILL.md
└── code-review/
├── SKILL.md
└── references/
└── checklists.md
SKILL.md 使用 YAML frontmatter + Markdown 主体:
---
name: docker-deploy
description: 用 Docker 部署 Python web 应用的最佳实践,包含常见坑点排查
version: 1.2.0
---
# Docker 部署 Python Web 应用
## 触发条件
当用户要求:
- "用 Docker 部署这个应用"
- "帮我写 Dockerfile"
- "把这个服务容器化"
...
frontmatter 有严格的长度限制:description ≤ 1024 字符,name ≤ 64 字符——这些不是随意定的,它们对应 LLM 上下文窗口里每个技能的 token 预算。
三个工具:skills_list、skill_view、skill_manage
技能系统的三个工具共用 registry.register(self, name, toolset, schema, handler, check_fn) 注册到 skills toolset。
skills_list — 列出所有可用技能
schema:
description: "List available skills (name + description). Use skill_view(name) to load full content."
parameters:
category (string, optional): 按分类过滤
返回 JSON:
{
"success": true,
"count": 12,
"categories": ["devops", "coding", "writing"],
"skills": [
{"name": "docker-deploy", "description": "用 Docker 部署 Python web 应用...", "category": "devops"},
{"name": "api-docs", "description": "编写 REST API 文档的最佳实践...", "category": "writing"},
...
],
"hint": "Use skill_view(name) to see full content, tags, and linked files"
}
只返回 name + description 的元数据——几十个技能加起来可能只有 1000 字符,消耗极少的 token。
skill_view — 加载完整的技能内容
schema:
description: "Skills allow for loading information about specific tasks and workflows,
as well as scripts and templates. Load a skill's full content or access
its linked files (references, templates, scripts)..."
parameters:
name (string, required):
description: "技能名称。对于插件提供的技能,使用 'plugin:skill' 格式
(如 'superpowers:writing-plans')"
file_path (string, optional):
description: "技能目录内的文件路径(如 'references/api.md'、
'templates/config.yaml'、'scripts/validate.py')。
省略则返回主 SKILL.md 内容。"
调用链:
- 路径安全检查 — 拒绝
..遍历、绝对路径、Windows drive path - 插件分发 — 如果 name 包含
:(如plugin:skill),查插件注册表 - 三策略查找 — ① 直接路径匹配 → ② 递归目录名匹配 → ③ frontmatter
name字段匹配 - 碰撞检测 — 多个目录匹配同一个 name → 拒绝并提示歧义
- 平台过滤 —
frontmatter.platforms不匹配当前 OS → 跳过 - 环境过滤 —
frontmatter.requires不满足 → 跳过
返回 JSON:
{
"success": true,
"name": "docker-deploy",
"content": "...完整的 SKILL.md 内容...",
"linked_files": {
"references": ["troubleshooting.md", "security-checklist.md"],
"templates": ["Dockerfile.tpl", "docker-compose.yml.tpl"],
"scripts": ["validate.sh"]
},
"tags": ["docker", "deployment", "devops"],
"related_skills": ["ci-cd", "kubernetes"]
}
每次成功的 skill_view 还会触发遥测:更新 view_count 和 use_count——这是 Curator 判断"哪些技能在活跃使用"的依据。
skill_manage — 创建、更新、删除技能
schema:
description: "Manage skills (create, update, delete). Skills are your procedural
memory — reusable approaches for recurring task types..."
parameters:
action (string, enum): create | patch | edit | delete | write_file | remove_file
name (string): 技能名(≤64 字符,小写+连字符)
content (string): create/edit 时的完整 SKILL.md 内容
old_string / new_string (string): patch 时的查找替换
replace_all (boolean): patch 是否替换全部匹配
category (string): create 时的分类目录
file_path (string): 辅助文件的路径
file_content (string): write_file 时的文件内容
create 的动作:
- 创建
~/.hermes/skills/<category>/<name>/目录结构 - 写入 SKILL.md(自动添加 frontmatter)
- 可选写入 references/、templates/、scripts/、assets/ 辅助文件
- 如果开启了
skills.guard_agent_created(默认关闭),运行安全扫描
patch 的动作: 在 SKILL.md 或指定文件里做查找替换——使用模糊匹配(9 种策略容忍空白差异),是增量更新的首选方式。
delete 的动作有三层路径验证:
- 拒绝符号链接/junction 重定向
- 拒绝解析后不在任何 skills_root 目录下的路径
- 拒绝删除 skills_root 本身(防止递归删除整个 skills/ 树)
渐进式披露:token 预算的艺术
第 1 篇提到过,system prompt 是"有价商品"——每添加一个字就消耗一个 token 预算。技能系统的渐进式披露在两个层面解决这个问题:
层面一:System Prompt 里的 Skill Index
build_skills_system_prompt() 在 system prompt 里注入一份紧凑的技能索引:
## Skills (mandatory)
Before replying, scan the skills below. If a skill matches or is even partially relevant
to your task, you MUST load it with skill_view(name) and follow its instructions.
Err on the side of loading — it is always better to have context you don't need
than to miss critical steps, pitfalls, or established workflows.
<available_skills>
devops: DevOps and infrastructure skills
- docker-deploy: 用 Docker 部署 Python web 应用的最佳实践...
- kubernetes: 在 Kubernetes 上部署和管理容器化应用...
writing: Documentation and writing skills
- api-docs: 编写 REST API 文档的最佳实践...
</available_skills>
Only proceed without loading a skill if genuinely none are relevant to the task.
这份索引只包含 name(≤64 字符)+ description(≤1024 字符)。10 个技能可能 ~2000 字符,50 个字符可能 ~8000 字符。
关键设计决策:system prompt 里的索引不是过滤,而只是提示。LLM 看到索引后,自己判断该调不调 skill_view。没有任何代码逻辑会说"这个技能不在 system prompt 索引里所以禁止加载"——skill_view 和 skills_list 都可以按名字或路径找到任何技能,包括在索引之外的。
编码模式下还有个优化:与 coding 无关的分类会被降级为 names-only——只显示技能的 name 列表,不显示 description。这可以减少非 coding 上下文下的 token 消耗:
social-media [names only]: twitter-linkedin, wechat-article, xiaohongshu
注:names-only 的分类仍然可以被加载——只是系统 prompt 里节省了描述文本。
层面二:两层缓存避免重复计算
系统 prompt 里的技能索引构建有一套两层缓存:
L1 — 进程内 LRU cache:
cache_key = (
skills_dir path,
external_dirs list,
available_tools (permutation invariance),
available_toolsets (permutation invariance),
platform hint,
disabled_skill_names,
compact_categories,
)
CWD 变了、工具集变了、平台变了 → cache 失效。Gateway 有多个 agent session 共用同一个进程,所以这个 cache 必须记录 platform 维度——保证 Telegram session 和 Discord session 的技能索引是独立的。
L2 — 磁盘快照:
~/.hermes/skills/.skills_prompt_snapshot.json
包含所有技能的元数据(name, description, platforms, conditions)和目录结构。冷启动时先尝试加载磁盘 snapshot;如果 mtime/size manifest 不匹配(技能文件被修改过),再走全文件扫描。
磁盘 snapshot 让 Gateway 冷启动时不需要遍历所有 SKILL.md 文件——直接读 JSON 解析即可。
层面三:按需展开
三层渐进式的 token 消耗:
| 层级 | 调用 | 返回内容 | 典型 token 消耗 |
|---|---|---|---|
| L1 | System prompt | name + description 索引 | ~100/token × SKILL.md |
| L2 | skill_view(name) | 完整 SKILL.md + linked_files 目录 | ~2K-10K |
| L3 | skill_view(name, file_path) | 单个文件的内容 | ~500-5K |
L1 层和 L2 层之间的关键差异是 linked_files 目录——skill_view 返回的 JSON 里包含一个 linked_files 字段,告诉 Agent 这个技能还附带哪些 references/templates/scripts。Agent 可以自己判断是否需要进一步加载。
工程启示
1. 程序化记忆 vs 陈述性记忆
MEMORY.md 记事实,SKILL.md 记流程。这是一个清晰的认知科学映射——人类大脑的陈述性记忆(海马体)和程序性记忆(基底节)是分离的。Agent 工程里,把"世界知识"和"操作知识"分开存储,能显著提升检索效率。
2. 渐进式披露是 Agent 的标准技能
skills_list → skill_view → skill_view(file_path=...),三层对应 token 消耗的阶梯。系统 prompt 里的索引相当于"书架上的书脊"——提供选择信号但不消耗内容预算。所有需要把"大量信息"暴露给 LLM 的场景(工具定义、文档检索、推荐系统),都应该采用这种书脊 → 目录 → 正文的渐进模式。
3. 声明式技能比代码插件更安全
SKILL.md 是 Markdown 文档,不是 Python 代码。它只能"告诉" LLM 怎么做,不能自己执行。这让技能的安全风险从"任意代码执行"降级为"prompt injection"——后者已经有系统的防御机制(安全扫描、隔离安装、信任分级)。
4. Agent 的自我进化
技能创建 = Agent 把成功的做法固化成可执行记忆。这是 Agent 自主进化的最小可行形式——不需要 fine-tuning,不需要复杂的训练管线,只需要 Agent 在完成任务后多走一步:把成功做法写成 SKILL.md。
5. 缓存维度设计
build_skills_system_prompt 的 cache key 包含了 platform hint + compact_categories + available_toolsets——这三个维度是 Gateway 多 session、多平台、多变 posture 场景下真正的"变化维度"。这个 cache key 设计值得参考:不只是"内容变了才 cache 失效",而是"消费上下文变了才 cache 失效"。