Chapter 08

Skills:程序性记忆

从 Voyager 在 Minecraft 里建立 skill library, 到 Hermes 用 SKILL.md 让 Agent 越用越聪明。这一章讲"程序性记忆"是什么、为什么这么设计。

本章约 5,900 字 阅读 ~25 分钟 关键词:Voyager · SKILL.md · procedural memory · cache-preserving injection

从这一章开始我们进入 Hermes 最有意思的部分——学习闭环。一个好 Agent 不应该每次 都从零开始,它应该把过去的成功解法存下来下次用。 这就是"程序性记忆"——区别于"情景记忆"("我们上次聊过什么"),程序性记忆是 "做某类事情的方法"

8.1认知科学的两种记忆

这个区分来自认知科学。人脑有两种长期记忆:

陈述性记忆 (Declarative)程序性记忆 (Procedural)
例子 "巴黎是法国首都" "骑自行车的肌肉记忆"
能用语言描述? 很难——"做了就会"
提取方式 主动回忆 遇到情境自动启动
形成方式 看到 + 编码 重复练习

对 LLM Agent 来说,这两种都很重要,但实现机制完全不同

8.2Voyager:让 Skill Library 成为研究范式

程序性记忆在 Agent 上的开山之作是 Voyager。我们之前提过一次,这里详细看:

Foundational Paper
Voyager: An Open-Ended Embodied Agent with Large Language Models
Wang et al. · arXiv:2305.16291 · NeurIPS 2023

NVIDIA 团队在 Minecraft 里跑 GPT-4 Agent。三个核心组件:

  1. Automatic Curriculum:根据当前进度提议下一个"刚好够难"的任务。
  2. Skill Library:每次完成新任务时,把解法(一段 JavaScript 代码) 存进 library。Library 里的代码索引向量化,下次任务来时按相似度检索。
  3. Iterative Prompting:执行报错时把错误信息送回 GPT-4 改代码,直到通过。

成绩:unique items 拿到 3.3 倍、移动距离 2.3 倍、tech tree milestone 解锁速度 15.3 倍。 更重要的是:在一个全新的 Minecraft 世界里,Voyager 能用学来的 skill解决新任务, 而对照组从零开始。

Voyager 教会业界的事

  1. Skill 是"可执行的",不是描述性 prompt。Voyager 的 skill 是真的 JavaScript 函数。
  2. Skill Library 是向量化检索。任务描述 → 找最相似 skill → 直接调用或参考。
  3. 每个 skill 在使用中 iterative refine。错了不要扔,要改。
  4. Skill 抵抗 catastrophic forgetting:因为 skill 是 explicit storage,不是模型权重。

Hermes 的 Skill 系统继承了这些思想,但选择了一个更"人类友好"的表达:

8.3Hermes 的 Skill 是什么

Hermes 的 Skill 不是可执行代码,而是一份 Markdown 文档—— SKILL.md。它告诉 LLM "在什么情况下、按什么步骤、用什么工具"。

看一个真实的例子。skills/apple/apple-reminders/SKILL.md

skills/apple/apple-reminders/SKILL.md
---
name: apple-reminders
description: "Apple Reminders via remindctl: add, list, complete."
version: 1.0.0
author: Hermes Agent
license: MIT
platforms: [macos]
metadata:
  hermes:
    tags: [Reminders, tasks, todo, macOS, Apple]
prerequisites:
  commands: [remindctl]
---

# Apple Reminders

Use `remindctl` to manage Apple Reminders directly from the terminal.
Tasks sync across all Apple devices via iCloud.

## Prerequisites

- **macOS** with Reminders.app
- Install: `brew install steipete/tap/remindctl`
- Grant Reminders permission when prompted
- Check: `remindctl status` / Request: `remindctl authorize`

## When to Use

- User mentions "reminder" or "Reminders app"
- Creating personal to-dos with due dates that sync to iOS
- Managing Apple Reminders lists
- User wants tasks to appear on their iPhone/iPad

## When NOT to Use

- Scheduling agent alerts → use the cronjob tool instead
- Calendar events → use Apple Calendar or Google Calendar
- Project task management → use GitHub Issues, Notion, etc.

## Quick Reference

\`\`\`bash
remindctl                    # Today's reminders
remindctl today
remindctl tomorrow
remindctl week
remindctl overdue
remindctl all
remindctl 2026-01-04         # Specific date
remindctl add "Buy milk" --due tomorrow
remindctl done <id>
\`\`\`

解剖这份 skill:

区块作用
YAML frontmatter元数据:name、description、platforms 限制、依赖命令
简介段落告诉模型这个 skill 大致干啥
Prerequisites系统要求 / 安装指令 / 权限要求
When to Use触发条件,这是最关键的一块
When NOT to Use反触发条件,防误用
Quick Reference具体命令例子

When to Use / When NOT to Use 是 Skill 的灵魂。LLM 看到这两块,就知道 在用户说"提醒我明天 9 点开会"时调用,在用户说"5 分钟后通知我"时不调用(那个应该走 cronjob)。

8.4Skill 被发现:scan_skill_commands

用户敲 /apple-reminders 启动 skill 之前,Hermes 必须先发现它。 扫描器在 agent/skill_commands.py:263-326

agent/skill_commands.py:263-326 (节选)
def scan_skill_commands() -> Dict[str, Dict[str, Any]]:
    """Scan ~/.hermes/skills/ and return /command → skill info."""
    global _skill_commands, _skill_commands_platform
    _skill_commands_platform = _resolve_skill_commands_platform()
    _skill_commands = {}

    from tools.skills_tool import (
        SKILLS_DIR, _parse_frontmatter,
        skill_matches_platform, _get_disabled_skill_names,
    )
    from agent.skill_utils import (
        get_external_skills_dirs, iter_skill_index_files,
    )

    disabled = _get_disabled_skill_names()
    seen_names = set()

    # 扫多个目录:本地 + 外部
    dirs_to_scan = []
    if SKILLS_DIR.exists():
        dirs_to_scan.append(SKILLS_DIR)
    dirs_to_scan.extend(get_external_skills_dirs())

    for scan_dir in dirs_to_scan:
        for skill_md in iter_skill_index_files(scan_dir, "SKILL.md"):
            # 跳过 .git / .archive 等
            if any(p in {'.git', '.archive'} for p in skill_md.parts):
                continue

            content = skill_md.read_text(encoding='utf-8')
            frontmatter, body = _parse_frontmatter(content)

            # 平台不匹配跳过(macOS-only skill 在 Linux 上不显示)
            if not skill_matches_platform(frontmatter):
                continue

            name = frontmatter.get('name', skill_md.parent.name)
            if name in seen_names or name in disabled:
                continue

            # 把 "Apple Reminders" → /apple-reminders
            cmd_name = name.lower().replace(' ', '-').replace('_', '-')
            cmd_name = _SKILL_INVALID_CHARS.sub('', cmd_name)

            _skill_commands[f"/{cmd_name}"] = {
                "name": name,
                "description": frontmatter.get('description', ''),
                "skill_md_path": str(skill_md),
                "skill_dir": str(skill_md.parent),
            }
            seen_names.add(name)

    return _skill_commands

设计要点:

  1. 多目录扫描:内置 skills/、用户 ~/.hermes/skills/、外部 ~/.hermes/external_skills/
  2. 平台过滤platforms: [macos] 的 skill 只在 macOS 上显示。
  3. 禁用列表:用户可以在 config 里 skills.disabled: [foo] 显式禁用。
  4. 命名空间:名字 → kebab-case slash command。"Apple Reminders"/apple-reminders
  5. 同名优先:先扫到的赢。本地覆盖外部,外部覆盖内置。

结果是个全局 dict _skill_commands,TUI 的 tab 补全直接读它。

8.5Skill 被加载:作为 User Message

这是 Hermes Skill 系统最聪明的设计。回顾第 5 章: system prompt 不能改,会破 cache。但我们想动态启用 skill 怎么办?

答案:把 skill 作为 user message 追加,不动 system promptagent/skill_commands.py:160-183

agent/skill_commands.py:160-183
def _build_skill_message(
    loaded_skill, skill_dir, activation_note,
    user_instruction="", runtime_note="", session_id=None,
) -> str:
    """Format a loaded skill into a user/system message payload."""
    from tools.skills_tool import SKILLS_DIR

    content = str(loaded_skill.get("content") or "")

    # 模板变量替换({{config.foo}} 等)
    skills_cfg = _load_skills_config()
    if skills_cfg.get("template_vars", True):
        content = _substitute_template_vars(content, skill_dir, session_id)

    # Inline shell 展开 — 实验功能,默认关
    if skills_cfg.get("inline_shell", False):
        timeout = int(skills_cfg.get("inline_shell_timeout", 10) or 10)
        content = _expand_inline_shell(content, skill_dir, timeout)

    parts = [activation_note, "", content.strip()]
    # 拼好后作为 user 消息追加
    return "\n".join(parts)

activation_note 长这样:

[IMPORTANT: The user has invoked the "apple-reminders" skill, indicating they want
you to follow its instructions for the upcoming request. Read the skill content
below carefully and apply it to your next response.]

# Apple Reminders

Use `remindctl` to manage Apple Reminders directly from the terminal.
...

这条 user message 被追加到 messages 末尾:

messages = [
    {"role": "system", "content": "...(stable + context + volatile)"},
    {"role": "user", "content": "你好"},
    {"role": "assistant", "content": "嗨..."},
    {"role": "user", "content": "[Skill activated: apple-reminders]\n# Apple Reminders\n..."},
    # ↑ 新加的 — 不动前面任何一条
]

cache 命中分析:

这就是 Hermes 工程的核心智慧 把"上下文相关的能力"放进 user message 而不是 system prompt。 System prompt 像操作系统镜像,user message 像应用程序。装应用不该重启系统。 这一招让 Hermes 能动态加载 skill 而不付 cache miss 代价。

对比:错误做法

如果你天真地用"system prompt 加 skill":

# BAD
def load_skill(skill_name):
    agent.system_prompt += f"\n\n--- Skill: {skill_name} ---\n{SKILL_MD_CONTENT}"
    # 立刻破 cache

下次 API call,前缀完全变了。整个 cache 报废。一个简单 turn 可能成本 ×10。 这就是为什么 Hermes 选择"以 user message 注入"——不要碰前缀

8.6Skill 的高级特性

模板变量

SKILL.md 里可以写 {{config.something}},加载时被替换:

# Send Email via SMTP

Use SMTP server at {{config.smtp.host}}:{{config.smtp.port}}.
Auth user: {{config.smtp.user}}

变量来源是 config.yamlskills.config.<key>

Inline Shell 展开(实验功能)

SKILL.md 里的 $(command) 块在加载时被替换成 shell 输出:

# Project Context

Current branch: $(git branch --show-current)
Recent commits:
$(git log --oneline -5)

默认关(inline_shell: false),有安全顾虑。打开后注意: 这相当于让 SKILL.md 在加载时执行任意命令。只用于自己写的 skill。

Skill 附带 scripts

真正复杂的逻辑不该让 LLM 每次重新生成。把它存成脚本:

~/.hermes/skills/research/web_scrape/
├── SKILL.md                # 决策手册
├── scripts/
│   ├── extract_main.py    # 提取主内容的复杂 BeautifulSoup 逻辑
│   └── dedupe_urls.py
└── references/
    └── selectors.json     # 常见网站的 CSS 选择器

SKILL.md 里只说 "用 scripts/extract_main.py 提取主内容",LLM 会用 execute_codeterminal 工具执行这个脚本。Hermes 在 execute_code 工具的 working directory 里把 scripts/ 加入 PATH。

为什么用脚本而不是让模型每次写 稳定性——同样的逻辑每次生成可能有细微差异,调试困难。 token 成本——一段 200 行的 BeautifulSoup 代码每次都生成是浪费。 测试——脚本可以单独跑 unit test,工具调用没法。 用脚本是程序性记忆的"实弹",SKILL.md 是"使用手册"

8.7Skill 的来源与生态

Hermes 的 skills 有三个来源:

① 内置 (skills/)

仓库自带的,~30 个分类:

skills/
├── apple/                  apple-reminders, imessage
├── research/               web-deep-research, fact-check
├── devops/                 ci-cd-status, k8s-debug
├── data-science/           pandas-eda, sklearn-baseline
├── software-development/   conventional-commit, code-review
├── mcp/                    mcp-server-init
├── mlops/                  mlflow-experiment, training-monitor
└── ...

② 用户自创 (~/.hermes/skills/)

你随时可以扔一个 SKILL.md 在这个目录下,Hermes 重启自动发现。 或者让 Agent 自己用 skill_manage(action="create") 工具创建——这是核心 self-improving 机制。

③ Skills Hub (远程仓库)

Hermes 维护一个公开 skill 索引在 agentskills.io。 用户用 hermes skills install <name> 装。

这个标准被多个 Agent 框架支持(Claude Desktop、Aider、Cursor)—— 写一份 SKILL.md 可以被所有兼容工具用上。这是"skill 互操作性"方向,Hermes 团队主推。

8.8Agent 自己创建 Skill

最有趣的部分:Agent 自己学新本事并把它存下来。 Hermes 提供 skill_manage 工具:

# LLM 决定创建一个新 skill
skill_manage(
    action="create",
    name="data-processing-pipeline",
    skill_md="""---
name: data-processing-pipeline
description: 'Generic CSV-to-Parquet ETL with pandas/polars.'
version: 1.0.0
author: Hermes Agent
metadata:
  hermes:
    created_by: agent
    tags: [data, etl, pandas]
---

# Data Processing Pipeline

When the user asks to clean a CSV and produce Parquet output:

## Steps

1. Use read_file to inspect the CSV schema.
2. Use execute_code to run polars (faster than pandas for > 1GB):
   ```python
   import polars as pl
   df = pl.read_csv(input_path, ignore_errors=True)
   df.write_parquet(output_path, compression='zstd')
   ```
3. Verify with df.describe() ...
""",
)

触发条件由 LLM 自己判断——通常在 system prompt 里有这样一段建议:

After completing a non-trivial task that required figuring out a workflow, consider whether the pattern is general enough to be saved as a skill. Use skill_manage(action="create", ...) when you think future you would benefit from the recipe.
system_prompt 里的 skill creation hint

关键元数据:created_by: agent——标记这是 Agent 自创, 为下章要讲的 Curator 提供归属信息。Curator 只管理 agent-created skill。

8.9Hermes Skill vs Voyager Skill

VoyagerHermes
Skill 是什么 可执行 JavaScript 函数 Markdown 决策手册(可附脚本)
检索方式 向量相似度 显式 /skill-name + LLM 自己 skill_view
iterative refine 报错回灌 → LLM 改代码 Curator 定期审查 + LLM skill_manage(action="patch")
使用环境 Minecraft(封闭世界) 用户自己的电脑(开放世界)
可读性 代码——只机器懂 Markdown——人和机器都懂、能编辑
互操作性 专属 Voyager 实现 跨 Agent 框架(Claude/Cursor/Hermes)

Hermes 这个设计的取舍:放弃 Voyager 的全自动化,换来用户可审计、可编辑、可分享。 对生产用户来说,"Agent 自己写了什么 skill"是必须可看的;不可读的代码会让人不敢信。

8.102025.12 — Skill 成为跨厂家开放标准

本章前面把 SKILL.md 当作"Hermes 内部约定"讲。2025 年 12 月一个事件改写了它的地位

Industry Standard
Anthropic Agent Skills Specification (Open Standard)
Anthropic · 2025-12 · github.com/anthropics/skills

Anthropic 把 SKILL.md 格式作为开放标准发布并开源 reference 实现。 格式核心:YAML frontmatter + Markdown body + 可选 scripts/ 目录。 OpenAI 在两周内宣布 Codex CLI 和 ChatGPT 采用同一格式。 这是继 MCP 之后第二个跨厂家 Agent 标准。

标准化之后的生态形态

2026 年 5 月时,Agent Skills 生态已经稳定下来,主要节点:

角色例子定位
标准 owner Anthropic anthropics/skills spec + reference skills(PDF / PPTX / DOCX 等)
免费 marketplace agentskills.io Hermes 早期支持的、社区免费 skill 索引
商业 marketplace agensi.io · skillsmp.com 付费 skill;作者拿 80% 分成
Host(支持加载) Claude Desktop / Claude Code / Codex CLI / ChatGPT / Cursor / Zed / Hermes 同一份 SKILL.md 在每个 host 都能用
分发协议 Claude Plugin Marketplace(/plugin 三合一:MCP server + Skill + plugin

Hermes 的 SKILL.md 是否兼容标准?

几乎完全兼容——因为 Hermes 的格式就是参考标准(之一)。 Hermes 维护者参与了 standard 早期讨论。差异点不到 5%:

实战建议 写新 skill 时避开 Hermes 私有字段(metadata.hermes.config 等)。 这样这份 SKILL.md 可以扔给 Claude Code 或 ChatGPT 直接用,不光在 Hermes 里跑。 Skill 互操作性是这个领域接下来最值钱的资产。

Agent Skills 与 MCP 的分工

同期 Anthropic 还推 MCP。两者经常被混淆,区别看一眼就清楚:

MCP ServerAgent Skill
形态 外部进程(JSON-RPC) SKILL.md + scripts 文件
提供 Tools / Resources / Prompts 决策手册 + 可选脚本
调用方式 JSON-RPC tools/call 把 SKILL.md 注入到 LLM context
典型例子 GitHub MCP(封装 GitHub API) conventional-commit 风格手册
状态保持 进程持续运行,可有状态 无状态,文件只读
语言 任意(Python / TS / Rust / Go) Markdown + 任意脚本语言
分发 NPM / Pip / Binary Git repo / marketplace tarball

记忆口诀MCP 是"接什么外部系统",Skill 是"怎么用这些系统"。 一个 Excel skill 配 Excel MCP server——前者教 LLM "什么时候要做透视表", 后者提供 "如何调 Excel API 真的去做"。

Voyager → 标准化:六年的路

把整条路线压成时间线:

timeline
  title Procedural Memory 在 Agent 中的演进
  section 概念期
    2023.05 : Voyager : 可执行代码 + 自动课程
    2023.10 : MemGPT : OS 类分层记忆
  section 工程化
    2024.06 : Hermes 原型 : SKILL.md 内部约定
    2025.01 : agentskills.io : 第一个跨工具免费 marketplace
    2025.06 : Anthropic Context Engineering : 把 skill 正名为"程序性记忆"工程实践
  section 标准化
    2025.12 : Anthropic Agent Skills spec : 开放标准发布
    2026.01 : OpenAI Codex CLI + ChatGPT : 采用同一格式
    2026.02 : agensi.io / skillsmp.com : 付费 marketplace 上线 (80/20 分账)
    2026.04 : Pre-built skills : Anthropic 自发 PowerPoint / Excel / Word / PDF skill

下一节我们看在标准化之后,怎么写一份跨工具兼容的 SKILL.md。

8.11SKILL.md 的写作规范

Hermes 的 AGENTS.md 里有八条 skill 写作硬规则。这些规则是 PR 评审标准:

  1. description ≤ 60 字符,一句话,句号结尾。 避免营销词。
  2. 引用工具时用 backtick,且必须是 Hermes 原生工具或 MCP server 名。 不要写 "use grep",写 "use search_files"
  3. platforms gating 要和实际脚本 import 一致。 POSIX-only 的代码必须声明 platforms。
  4. author 写真人,不写 "Hermes Agent"。
  5. section 顺序固定:标题 → 简介 → When to Use → Prerequisites → How to Run → Quick Reference → Procedure → Pitfalls → Verification。
  6. 复杂逻辑放 scripts/,不要让 LLM inline 写。
  7. 测试在 tests/skills/test_<skill>_skill.py,只用 stdlib + pytest + unittest.mock。
  8. .env.example 添加要清晰隔离,不污染周围内容。
学到了写作的方法学 上面 8 条本质都是"让 LLM 容易理解、让人容易审计、让 PR 容易合并"。 你自己写 skill 时也照做。写 skill 是给 LLM 写说明书, 跟写代码注释类似——细节越具体越好。

8.12代码深挖:模板变量与 inline shell

第 8.6 节提到 SKILL.md 支持模板变量 + inline shell 展开。看实际的两个核心函数:

8.12.1 substitute_template_vars:极简白名单

agent/skill_preprocessing.py:10-60 (摘要)
# 只允许 ${HERMES_SKILL_DIR} 和 ${HERMES_SESSION_ID} 两个 token。
# 未匹配的 token 保留原样,方便作者 debug。
_SKILL_TEMPLATE_RE = re.compile(r"\$\{(HERMES_SKILL_DIR|HERMES_SESSION_ID)\}")

# inline shell:!`cmd`(非贪婪,不跨行)
_INLINE_SHELL_RE = re.compile(r"!`([^`\n]+)`")

# 输出上限 — 防一条流氓命令塞爆 context
_INLINE_SHELL_MAX_OUTPUT = 4000


def substitute_template_vars(content, skill_dir, session_id):
    """Replace ${HERMES_SKILL_DIR} / ${HERMES_SESSION_ID} in skill content.

    Only substitutes tokens for which a concrete value is available —
    unresolved tokens are left in place so the author can spot them.
    """
    skill_dir_str = str(skill_dir) if skill_dir else None

    def _replace(match):
        token = match.group(1)
        if token == "HERMES_SKILL_DIR" and skill_dir_str:
            return skill_dir_str
        if token == "HERMES_SESSION_ID" and session_id:
            return str(session_id)
        return match.group(0)        # 没值就留原样

    return _SKILL_TEMPLATE_RE.sub(_replace, content)

没有 {{config.foo}} 这种通用配置插值——只有 SKILL_DIR 和 SESSION_ID 两个变量。 原因:配置插值会引入"用户改了一个 config 就让所有 skill 行为变"的隐式耦合。 SKILL.md 应该自包含,所以 Hermes 限制极严。

8.12.2 run_inline_shell:timeout-only 安全

agent/skill_preprocessing.py:63-121 (摘要)
def run_inline_shell(command, cwd, timeout):
    """Execute a single inline-shell snippet and return its stdout (trimmed).

    Failures return a [inline-shell error: ...] marker, not raise."""
    try:
        completed = subprocess.run(
            ["bash", "-c", command],
            cwd=str(cwd) if cwd else None,
            capture_output=True, text=True,
            timeout=max(1, int(timeout)),
            check=False,
        )
    except subprocess.TimeoutExpired:
        return f"[inline-shell timeout after {timeout}s: {command}]"
    except FileNotFoundError:
        return "[inline-shell error: bash not found]"
    except Exception as exc:
        return f"[inline-shell error: {exc}]"

    output = (completed.stdout or "").rstrip("\n")
    if not output and completed.stderr:
        output = completed.stderr.rstrip("\n")
    if len(output) > _INLINE_SHELL_MAX_OUTPUT:
        output = output[:_INLINE_SHELL_MAX_OUTPUT] + "...[truncated]"
    return output
无沙箱,只有 timeout "safety layer 只有 timeout 和 graceful error"——任何 bash 命令都直接 subprocess.run()。 没有 allowlist、没有 chroot、没有 seccomp。 这是故意的设计:inline shell 是给"你自己写的 SKILL.md"用的,不该跑别人不可信的 SKILL.md。 如果安装了第三方 skill,Hermes 配置项 skills.inline_shell 默认关,需要 opt-in。

8.13本章带走的

章末练习

  1. Easy 为什么 Skill 必须作为 user message 注入,而不是 system prompt?用 50 字回答。
  2. Easy 程序性记忆和陈述性记忆的核心区别是什么?分别给一个 Agent 应用场景。
  3. Medium 给一个具体场景:你天天用 kubectl logs 看某个特定 namespace 的 pod 日志, 要按时间、grep 关键字、过滤掉 healthcheck。写一份完整的 SKILL.md,遵守八条写作规则。
  4. Medium Voyager 的 "iterative prompting" 在 Hermes 里对应什么机制? (提示:考虑 Curator 和 skill_manage(action="patch")
  5. Hard Hermes 的 Skill 没有"自动检索"——LLM 不会"在 100 个 skill 里挑相关的"。 为什么这种设计权衡值得?设想一个 Agent 有 500 个 skill 时会怎样。 然后想想:如何加一个"显式向量检索"机制而不破 cache?
  6. Hardtools/skill_usage.py(如果你下载了 Hermes 源码)。它存 ~/.hermes/skills/.usage.json 里。 解释 created_by: agent 标记从哪里写入、被谁读取。这条数据链路对下章的 Curator 极重要。