OpenClaw 设计与实现

第16章 技能系统

作者 杨艺韬 · 12,655 字 · 发布于 · 更新于

第16章 技能系统

上下文窗口是公共牧场——每个技能都想放更多的羊。把 53 个内建技能的正文全塞进提示是 213 KB,只放目录则不到 10 KB。这不是优化技巧,而是'公地悲剧'的系统性解法。

本章要点

  • 理解技能系统如何解决知识注入的"系统提示膨胀"问题
  • 掌握技能的剖析结构:SKILL.md 约定、六源加载与分层覆盖
  • 深入过滤管线:从发现到提示注入的完整链路
  • 理解技能安装安全、调用控制与生命周期管理
  • 实战演练:从零编写一个完整的技能包

部署方案解决了"在哪里跑"的问题,接下来要解决"怎么让 Agent 变得更聪明"。第 9 章的插件系统让平台可扩展,本章的技能系统则让 Agent 本身可扩展——不是改代码,而是给它"教材"。

16.1 知识注入问题

16.1.1 陈述性知识 vs. 程序性知识

问 Claude:"北京今天天气怎么样?"它会给你一个充满免责声明的模糊回答——"我无法获取实时数据,但通常三月份的北京……"。给它一个天气技能,结果截然不同——它直接调用 curl wttr.in/Beijing,返回精确的温度、湿度和未来三天预报。

差别在哪?大语言模型拥有广泛的陈述性知识("知道什么"),但缺乏程序性知识("怎么做")。哪个命令检查磁盘使用?某个 API 的参数格式是什么?GitHub CLI 怎么创建 PR?这些答案躺在 Stack Overflow 和 man pages 里,不在模型权重里。

技能系统弥合的,正是这道鸿沟——把"怎么做"的知识注入到"知道什么"的模型中。

如果说模型的权重是"通识教育",技能就是"职业培训"。通识教育让你知道世界上有锁这种东西,职业培训教你拿哪把钥匙开哪扇门。

16.1.2 为什么不把所有知识放进提示?

最朴素的方案是:把所有技能的完整指令放进系统提示。Agent 启动时加载所有知识,像一个读过所有手册的专家。

这个方案的致命缺陷是上下文窗口是寸土寸金的共享资源。技能指令、系统提示、对话历史、工具结果和用户消息,都在争夺同一块 token 预算。

让我们用仓库里的真实数据算一笔账。v2026.3.31 的 skills/ 目录下有 53 个内建技能:

$ find skills -name SKILL.md | wc -l
53
$ cat $(find skills -name SKILL.md) | wc -c
218444          # 约 213 KB

平均每个 SKILL.md 约 4 KB。如果把它们的正文全量塞进系统提示,光技能就要吃掉 213 KB 文本——按英文文本粗算是数万 token 的量级,而这还只是内建技能,不含用户自己装的和工作区里的。

更糟糕的是,大多数技能在大多数对话中都不会用到。如果用户在讨论代码审查,53 个技能中可能只有 github 和 coding-agent 两个相关,其余 51 个纯属白占地方。

口径:find skills -name SKILL.md | wc -l 与 cat $(find skills -name SKILL.md) | wc -c,基于 v2026.3.31 仓库。token 数与分词器相关,本专栏一律只给可复核的字节/文件数。

16.1.3 按需加载:图书馆模型

OpenClaw 对这一张力的回应,是技能系统最精妙的设计:按需加载。

graph LR
    subgraph "图 16-1:全量加载 vs. 按需加载"
        Full["全量加载<br/>53 个 SKILL.md 正文<br/>≈ 213 KB"]
        OnDemand["按需加载<br/>53 条目录项(名称 + 描述)<br/>≈ 10 KB<br/>+ 命中时才读正文"]
    end
    Full -.-> |"一个数量级"| OnDemand
    style Full fill:#c0392b,color:#fff
    style OnDemand fill:#1e8449,color:#fff

只有技能元数据(名称 + 描述)永久驻留在系统提示中,扮演"目录"的角色;完整指令仅在模型判断需要时,用 read 工具去读那个 SKILL.md 文件。同一批 53 个技能,正文合计 213 KB,而 frontmatter 里的 name + description 合计不到 10 KB——相差一个数量级。

这就像图书馆只在书架上放目录卡片,读者选定后才从书库取出原书。你不会把整个图书馆搬到办公桌上才开始工作——你只需要知道哪本书在哪个书架上。目录告诉你"这本书存在,关于什么",但不包含书的内容。

系统提示里那段目录长什么样?formatSkillsCompact 给出了最精简的形态:

The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its name.
When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.

<available_skills>
  <skill>
    <name>weather</name>
    <location>~/.openclaw/workspace/skills/weather/SKILL.md</location>
  </skill>
  ...
</available_skills>

注意那句 "Use the read tool to load a skill's file"——按需加载不需要专门的机制,它就是让模型用已有的 read 工具去读一个文件。这个设计的简洁程度容易被低估:没有技能加载 API、没有激活协议、没有状态机,只有一份文件清单和一个模型早就会用的工具。唯一的"协议"是一段文字规则:src/agents/system-prompt.ts 的 buildSkillsSection 会在目录外面包一节 ## Skills (mandatory),要求模型回复前先扫一遍目录,恰好一个技能明确适用时才去读它的 SKILL.md,且一开始最多只读一个。

关键概念:按需加载(On-Demand Loading) 技能系统的按需加载机制只在系统提示中放置技能的元数据摘要(名称 + 描述,紧凑模式下甚至只有名称 + 路径),完整的技能指令仅在模型判断需要时才通过 read 工具读取。这一设计解决了"公地悲剧"问题:上下文窗口是公共资源,每个技能都想占用更多空间,按需加载确保只有真正需要的技能才消耗宝贵的 Token 预算。

🔥 深度洞察:技能系统是生态位理论的工程实现

生态学中有个核心概念叫生态位(Ecological Niche):一片森林中,不同物种占据不同的生态位——有的在树冠层光合作用,有的在林下层分解落叶,有的在土壤中固氮。它们共享同一片有限的资源(阳光、水分、养分),但通过分工避免了零和竞争。OpenClaw 的技能系统面对的正是同一道难题:几十个技能共享一个有限的上下文窗口(相当于森林中有限的阳光),如果每个技能都试图"全量展开",就像所有树都长到同一个高度争夺阳光——结果是谁都长不好。按需加载的本质是让每个技能找到自己的生态位:元数据目录是"种子库",模型的选择是"自然选择",只有与当前任务匹配的技能才"萌发"并占据上下文空间。这不是工程优化——这是在有限资源上实现最大生物多样性的生存策略。

16.1.4 按需加载的挑战

按需加载不是免费的午餐,它引入了一个新的复杂度:模型必须能从一两句话的描述中准确判断是否需要某个技能。

如果描述写得太笼统("适用于各种编程任务"),模型可能在不需要时加载它。如果描述写得太具体("仅适用于 Python 3.11 的 asyncio 调试"),模型可能在需要时错过它。描述的质量直接决定了按需加载的效果。

OpenClaw 通过规范化的描述格式来缓解这个问题:

仓库里 skills/weather/SKILL.md 的描述就是这个格式的范本(原文为英文):

description: "Get current weather and forecasts via wttr.in or Open-Meteo.
  Use when: user asks about weather, temperature, or forecasts for any location.
  NOT for: historical weather data, severe weather alerts, or detailed
  meteorological analysis. No API key needed."

翻成中文即:"通过 wttr.in 或 Open-Meteo 获取当前天气和预报。适用:用户询问任意地点的天气、温度或预报。不适用:历史天气数据、恶劣天气预警、专业气象分析。无需 API 密钥。"

💡 最佳实践:编写技能描述时,始终包含"适用(Use when)"和"不适用(NOT for)"两个维度。仓库里并非所有内建技能都这么写:53 个中描述含 "Use when" 的有 14 个,两者都写全的只有 weather、github、coding-agent 等少数几个——但这正是值得照做的范本。这帮助模型准确判断何时加载技能,避免误触发(浪费 Token)和漏触发(功能缺失)。

注意"适用"和"不适用"的明确界定——这不是随意的写作风格,而是帮助模型做精准判断的结构化信息。

16.2 技能的剖析

16.2.1 SKILL.md 的结构

每个技能是一个目录,包含必需的 SKILL.md 文件:

---
name: weather
description: "Get current weather and forecasts via wttr.in or Open-Meteo.
  Use when: ... NOT for: ..."
homepage: https://wttr.in/:help
metadata:
  openclaw:
    emoji: "☔"
    os: [linux, darwin]         # 支持的平台(键名是 os,不是 platform)
    requires:
      bins: [curl]              # 必须全部存在的二进制
      anyBins: [curl, wget]     # 存在其一即可
      env: [WEATHER_API_KEY]    # 需要的环境变量
      config: [channels.telegram.botToken]   # 需要为真值的配置路径
    install:                    # 缺依赖时怎么装(引导向导与网关的依赖安装用)
      - { id: brew, kind: brew, formula: curl, bins: [curl], label: "Install curl (brew)" }
---

# 天气技能
## 使用方法
```bash
curl wttr.in/Beijing         # 当前天气
curl wttr.in/Beijing?format=3 # 紧凑格式
```

## 注意事项
- 如果用户没有指定城市,使用 USER.md 中记录的位置
- 使用 `?format=j1` 获取 JSON 格式以便程序化处理

关键设计决策:只有 name、description 和文件路径进入系统提示。Markdown 正文仅在模型决定使用该技能、用 read 工具读取它之后才进入上下文。metadata.openclaw 下的字段用于资格过滤——在技能出现在目录之前就判断它是否适用于当前环境。

(上例是把各类字段拼在一起的示意,并非仓库里 skills/weather/SKILL.md 的原文——原文只声明了 emoji、requires.bins 与 install。真实文件里 metadata 常写成 JSON 风格的内联对象,例如 metadata: { "openclaw": { "emoji": "☔", ... } }——YAML 是 JSON 的超集,两种写法解析结果相同。)

16.2.2 为什么用 Markdown 而非代码?

这个设计选择引发了最多讨论。同类系统里,用代码(类、函数签名)或结构化配置来定义能力都很常见,为什么 OpenClaw 用纯文本的 Markdown 文件?

方案 A:Python/TypeScript 代码

class WeatherTool:
    def __init__(self):
        self.name = "weather"
        self.description = "Get weather forecasts"
    
    def execute(self, city: str) -> str:
        return subprocess.run(["curl", f"wttr.in/{city}"], capture_output=True).stdout

优点:类型安全,可编程,IDE 支持。缺点:需要编程能力,有运行时安全风险(代码可以做任何事),修改需要重新部署。

方案 B:JSON/YAML 配置

name: weather
description: "Get weather forecasts"
commands:
  - pattern: "weather {city}"
    exec: "curl wttr.in/{city}"

优点:声明式,安全。缺点:表达能力有限(复杂逻辑难以用 YAML 表达),不够灵活。

方案 C:Markdown(OpenClaw 的选择)

优点有三个层面:

  1. 门槛极低:能写 README 的人就能写技能。领域专家(网络管理员、数据分析师、运维工程师)无需学习 SDK 就能将自己的专业知识编码为技能。
  2. 不被系统直接执行:Markdown 文件是惰性的——它不是代码,加载器只解析 frontmatter,不会执行它。但这不等于无害:技能正文会被拥有 exec 等工具的 Agent 照着做,技能目录还可以附带脚本,所以恶意技能本质上是一种提示注入,实际危害取决于 Agent 的工具权限与执行审批策略(第 13 章)。防御上应当把第三方技能当作不受信内容:安装前审阅 SKILL.md 与附带脚本,并收紧工具权限。
  3. LLM 原生格式:技能是提示片段——设计目标是让 LLM 消费而非运行时解释器。Markdown 是 LLM 最自然的输入格式。格式与消费者的对齐消除了翻译层。

权衡:Markdown 缺乏类型检查和编程抽象。复杂的工作流难以用纯文本描述。OpenClaw 通过 command-dispatch: tool 和脚本引用(技能目录中可以包含辅助脚本)来部分弥补这个缺陷。

⚠️ 常见陷阱:SKILL.md 的 YAML Frontmatter 格式错误

SKILL.md 的 YAML frontmatter 必须以 --- 开头和结尾。常见错误:

<!-- ❌ 错误:缺少结束的 --- -->
---
name: my-skill
description: "这是我的技能"

# 技能正文
...

<!-- ⚠️ 不规范:description 中包含未加引号的冒号(严格 YAML 解析会失败) -->
---
name: my-skill
description: 这是我的技能: 很好用
---

<!-- ✅ 正确:冒号需要引号包裹 -->
---
name: my-skill
description: "这是我的技能: 很好用"
---

两种错误的后果不同。缺少结束的 --- 时整块 frontmatter 取不到,技能因为没有 description 被静默跳过——你不会看到报错,只是技能不出现在目录中。冒号那一种,严格 YAML 解析确实会失败,但 OpenClaw 会退回逐行解析兜底(src/markdown/frontmatter.ts 的 parseFrontmatterBlock),单行的值通常仍能读出;只是别指望每个解析器都这么宽容,加引号才稳妥。如果新添加的技能"不生效",首先检查 frontmatter 格式。

⚠️ 常见陷阱:技能描述过于笼统导致误触发

按需加载依赖模型从一两句话的描述中判断是否需要加载技能。描述太笼统会导致不必要的加载,浪费上下文空间:

# ❌ 过于笼统:几乎任何请求都可能触发
description: "帮助用户完成各种任务"

# ✅ 精确:明确适用和不适用场景
description: "通过 GitHub CLI 管理 PR 和 Issue。
  适用:创建 PR、查看 CI 状态、合并 PR、创建 Issue。
  不适用:代码编写、Git 本地操作(commit/push/branch)。"

16.3 六源加载与分层覆盖

16.3.1 六个来源的设计动机

OpenClaw 从六个来源加载技能,每个有不同优先级(从低到高):

graph BT
    subgraph "图 16-2:技能加载优先级(低→高)"
        Extra["1. openclaw-extra<br/>(extraDirs / 插件目录)"]
        Bundled["2. openclaw-bundled<br/>(内建核心技能)"]
        Managed["3. openclaw-managed<br/>(已安装的技能)"]
        Personal["4. agents-skills-personal<br/>(个人定制)"]
        Project["5. agents-skills-project<br/>(项目级技能)"]
        Workspace["6. openclaw-workspace<br/>(工作区技能)"]
    end
    Extra --> Bundled --> Managed --> Personal --> Project --> Workspace
    style Workspace fill:#1e8449,color:#fff
    style Extra fill:#95a5a6,color:#333

为什么需要六个来源?不同来源服务不同的定制需求:

来源 目录 用途
extra 配置项 skills.load.extraDirs 列出的目录,外加插件声明的技能目录 额外挂载的技能(优先级最低)
bundled 安装包自带的 skills/ 核心技能(weather、github、coding-agent)
managed 状态目录下的 skills/(默认 ~/.openclaw/skills) 本机所有工作区共享的技能
personal ~/.agents/skills 跨项目通用的个人技能
project <工作区>/.agents/skills 特定项目的技能(如"这个项目的部署流程")
workspace <工作区>/skills 当前工作区的技能(最高优先级);openclaw skills install 从 ClawHub 装的技能也落在这里

16.3.2 完全替换而非合并

高优先级来源完全替换同名的低优先级技能——没有合并。这是刻意的设计决策。

考虑替代方案:合并覆盖——高优先级来源只覆盖它定义的字段,低优先级的其他字段保留。这听起来更灵活,但引入了歧义:如果 bundled 的 weather 技能有 5 个命令,workspace 的 weather 技能只定义了 2 个,合并后有几个?7 个?还是 2 个?如果 workspace 想删除 bundled 的某个命令怎么办?

完全替换虽然不灵活,但是可预测且可调试的——如果 workspace 有 weather,那就完全忽略其他来源的 weather。运营者永远知道某个技能来自哪里,不需要在脑中模拟合并逻辑。

只在"越接近用户越优先"这一点上像 CSS 级联:CSS 是逐属性合并的,这里则是整份替换。

16.3.3 路径逃逸防护

每个技能的真实路径(符号链接解析后)必须位于其根目录内。这防止恶意技能通过符号链接引用系统文件:

skills/
  evil-skill/
    SKILL.md → /etc/passwd   # 被跳过,并写一条 warn 日志

实现是 tryRealpath() + isPathInside():把候选路径和根目录都解析成真实路径再比对,不在根目录内就跳过。注意它不是"静默忽略"——warnEscapedSkillPath() 会打一条 Skipping skill path that resolves outside its configured root.,并把 source、rootDir、原始路径与解析后的真实路径一并记下。这个区别很重要:静默丢弃会让排障变成猜谜,而这条日志直接告诉你"哪个技能、从哪来、指向了哪里"。

这是典型的路径遍历防御——在 Web 安全中常见,在技能系统中同样必要。同一条防线上还有配额:单个根目录下只按名称排序取前 DEFAULT_MAX_SKILLS_LOADED_PER_SOURCE = 200 个子目录去加载,子目录数超过 DEFAULT_MAX_CANDIDATES_PER_ROOT = 300 时还会打一条"可疑地大"的告警,防止有人往技能目录里塞十万个空文件夹拖垮启动。

16.4 过滤管线:从发现到提示

16.4.1 资格检查链

从文件系统加载到出现在系统提示中,每个技能通过多重资格检查:

图 16-3:技能资格过滤管线(简化)

flowchart TD
    Load["从文件系统加载"] --> ConfigCheck{"配置启用?"}
    ConfigCheck -->|"否"| Reject1["过滤掉"]
    ConfigCheck -->|"是"| OSCheck{"操作系统匹配?"}
    OSCheck -->|"否"| Reject2["过滤掉"]
    OSCheck -->|"是"| BinCheck{"必需二进制文件存在?"}
    BinCheck -->|"否"| Reject3["过滤掉"]
    BinCheck -->|"是"| EnvCheck{"环境变量满足?"}
    EnvCheck -->|"否"| Reject4["过滤掉"]
    EnvCheck -->|"是"| Available["✅ 进入系统提示目录"]
    style Available fill:#1e8449,color:#fff
    style Reject1 fill:#c0392b,color:#fff
    style Reject2 fill:#c0392b,color:#fff
    style Reject3 fill:#c0392b,color:#fff
    style Reject4 fill:#c0392b,color:#fff

实现集中在 src/agents/skills/config.ts 的 shouldIncludeSkill(),它依次检查:

  1. skills.entries.<key>.enabled === false → 直接排除
  2. skills.allowBundled 白名单(只作用于内建技能,配了就只放行名单内的)
  3. evaluateRuntimeEligibility()——按 metadata.openclaw 里的声明判定:
    • os:平台匹配(并会把已连接的 macOS 远程节点一并纳入考虑,见 16.4.3)
    • requires.bins:全部二进制都要存在;requires.anyBins:存在其一即可(远程 macOS 节点上的二进制也算)
    • requires.env:环境变量可满足(三种途径,见下)
    • requires.config:指定的配置路径为真值
    • always: true:跳过 requires.* 各项检查;但 os 在它之前判定,平台不匹配仍会被排除

这条过滤链的核心思想是:不要给 Agent 看它无法使用的技能。如果天气技能需要 curl 但系统上没安装,把这个技能放进提示只会让 Agent 尝试后失败——浪费 token、浪费时间,还可能让 Agent 进入错误恢复循环。

16.4.2 环境变量的三种满足方式

环境变量需求可通过三种方式满足:

  1. 直接设置:process.env.WEATHER_API_KEY 已存在。
  2. 技能配置注入:运营者在 ~/.openclaw/openclaw.json 中配置 skills.entries.weather.env.WEATHER_API_KEY。
  3. API 密钥快捷方式:技能在元数据里声明 primaryEnv: "WEATHER_API_KEY",运营者只需配 skills.entries.weather.apiKey——系统自动把它当作那个环境变量。

对应的判定逻辑就写在 shouldIncludeSkill 里:

hasEnv: (envName) =>
  Boolean(
    process.env[envName] ||
    skillConfig?.env?.[envName] ||
    (skillConfig?.apiKey && entry.metadata?.primaryEnv === envName),
  ),

第三种方式是一个精巧的用户体验优化——大多数技能只需要一个 API 密钥,apiKey 比手动配置环境变量映射更直觉。

16.4.3 远程节点的动态资格

一个特别有趣的设计:技能的资格可以随Node 连接状态动态变化。

macOS 专属技能 → 默认不资格
MacBook 伴侣应用连接 → 变为资格
MacBook 断开 → 恢复不资格

在 v2026.3.31 上,参与这项判定的只有支持 system.run 的 macOS 节点(src/infra/skills-remote.ts 的 getRemoteSkillEligibility):只要连着一台这样的节点,平台就额外算作 darwin,节点上探测到的二进制也能满足 requires.bins;提示里还会附一句说明,让模型经 exec host=node 到那台节点上执行。Android 等其他节点不影响技能资格。Agent 的"知识"就这样与它当下能调用的设备保持同步。

16.5 自适应格式降级

16.5.1 当目录太大时

预算不是无限的。src/agents/skills/workspace.ts 里定义了五个默认上限(都可通过 skills.limits.* 覆盖):

默认常量 值 含义
DEFAULT_MAX_CANDIDATES_PER_ROOT 300 单个技能根目录下子目录数超过此值即视为可疑并告警
DEFAULT_MAX_SKILLS_LOADED_PER_SOURCE 200 每个来源最多加载多少个技能
DEFAULT_MAX_SKILLS_IN_PROMPT 150 最多多少个技能进入提示
DEFAULT_MAX_SKILLS_PROMPT_CHARS 30,000 技能提示块的字符预算
DEFAULT_MAX_SKILL_FILE_BYTES 256,000 单个 SKILL.md 的体积上限

当技能目录超出这个字符预算时,系统不是简单截断(可能在中间截断一个技能的描述),而是自动通过三层降级:

第一层(完整):名称 + 描述 + 位置(由上游 @mariozechner/pi-coding-agent 的 formatSkillsForPrompt 生成)。

第二层(紧凑):只剩名称 + 位置,描述整块省掉。这一层由本地的 formatSkillsCompact() 生成,输出是 XML 形态而非列表(src/agents/skills/workspace.ts):

<available_skills>
  <skill>
    <name>weather</name>
    <location>~/.openclaw/workspace/skills/weather/SKILL.md</location>
  </skill>
</available_skills>

(块前还有三行说明,告诉模型「用 read 工具按需加载」以及「相对路径要按 SKILL.md 所在目录解析」。frontmatter 写了 disable-model-invocation: true 的技能在这一步被整体过滤掉。)

第三层(截断):二分搜索确定紧凑格式能容纳的最大技能前缀

// src/agents/skills/workspace.ts:597-607 — applySkillsPromptLimits(if/else 压成了一行)
let lo = 0;
let hi = skillsForPrompt.length;
while (lo < hi) {
  const mid = Math.ceil((lo + hi) / 2);
  if (fitsCompact(skillsForPrompt.slice(0, mid))) { lo = mid; } else { hi = mid - 1; }
}
skillsForPrompt = skillsForPrompt.slice(0, lo);

降级顺序背后有一个明确的价值判断,源码注释说得很清楚:// Full format exceeds budget. Try compact (name + location, no description) to preserve awareness of all skills before dropping any.——宁可让模型看到所有技能的名字,也不要让它完全不知道某些技能的存在。丢失描述只是让判断变难,丢失条目则是让能力凭空消失。

一旦降到紧凑或截断层,调用方都会在提示前面加一句警告(紧凑层说明描述被省略,截断层还会写明"包含了 N 个中的 M 个"),因此紧凑格式的预算里预留了 COMPACT_WARNING_OVERHEAD = 150 个字符——模型至少要知道"这份目录是不完整的"。

16.5.2 路径压缩

路径压缩把冗长的绝对路径换成以 ~ 开头的短路径:

// src/agents/skills/workspace.ts:46 — compactSkillPaths
filePath: s.filePath.startsWith(prefix) ? "~/" + s.filePath.slice(prefix.length) : s.filePath,

做的事情很朴素:把 home 目录前缀换成 ~。之所以安全,是因为模型认得 ~,而 read 工具也会把它展开回真实路径。源码注释给出的量级是"每个技能路径省约 5–6 token,N 个技能合计约 400–600 token"。

这看似微不足道,但记住:技能目录是系统提示的常驻部分——每一轮对话都要重发一遍。常驻开销的优化和一次性开销的优化,性价比完全不是一回事。

16.5.3 这是"公地"问题

自适应降级的深层动机是上下文窗口作为公地(Commons)的经济学。

上下文窗口是一块有限的共享资源,多个"消费者"(系统提示、技能目录、对话历史、工具输出、用户消息)竞争使用。如果技能目录不自我约束地占用空间,它会"挤出"其他消费者——对话历史更早遭到截断,工具输出压缩更激进,用户消息的上下文更短。

这与经济学中的"公地悲剧"类似:每个消费者都想最大化自己的空间,但如果每个都这样做,整体效率下降。自适应降级是技能系统作为"负责任的公民"的自我调节——在空间充裕时提供完整信息,在空间紧张时自动精简,确保不牺牲其他消费者的利益。

16.6 快照、缓存与热重载

16.6.1 不可变快照

SkillSnapshot 是不可变的时间点视图(第17章称之为"不可变快照协调"模式)。为什么不直接读取文件系统?

问题:每个回合都重新扫描六个来源、逐个解析 frontmatter、做资格判定,开销不小;而且一次运行中途若有人改了 SKILL.md,同一回合里前后看到的技能目录就可能不一致。

方案:会话第一个回合构建快照并存进会话记录(src/auto-reply/reply/session-updates.ts),之后的回合直接复用;只有技能快照版本号前进了(见下两节),下一个回合才重建。一个回合之内,技能目录始终是同一份。

16.6.2 热重载

chokidar 文件监视器检测 SKILL.md 变更,默认 250ms 防抖(src/agents/skills/refresh.ts,可通过 skills.load.watchDebounceMs 调整;开关是 skills.load.watch)。变更检测到时,快照版本提升,下一个 Agent 回合获取更新的技能集——无需重启网关。

值得一提的是防抖参数用了两次:先传给 chokidar 的 awaitWriteFinish.stabilityThreshold,等的是"文件大小不再变化",而不只是"事件停了一会儿";事件到达后再用同样时长的 setTimeout 合并连发的变更,最后才提升版本号。文本编辑器保存文件时通常会产生多次写入事件(写临时文件、重命名、改权限),大文件的写入本身也可能被分成多次;用稳定性阈值才能真正避免读到写了一半的文件。

16.6.3 缓存策略

技能缓存靠版本号失效,而不是给快照设过期时间。版本号按工作区各记一份(另有一个全局版本,取两者较大者);每次监视到变更就提升一次,取值是 Date.now(),但保证单调递增——若时钟没走,就在旧值上加 1。消费者(Agent 回合)比较会话里快照的版本与当前版本,快照版本较旧就重建。

这样做的好处是判断干脆:不存在"快照是否已经过期"的模糊地带,只看版本有没有前进。

16.7 技能安装与安全

16.7.1 统一安装接口

这里要先把两件容易混淆的事分开。

其一,安装技能本身。openclaw skills install <slug> 从 ClawHub 把技能装进当前工作区的 skills/ 目录,支持 --version <version> 指定版本、--force 覆盖同名技能;配套的还有 openclaw skills search(搜 ClawHub)和 openclaw skills update [--all]。此外 skills 子命令下还有三个只读命令:list(--eligible 只看可用的、-v 显示缺失的依赖)、info <name>、check(谁就绪、谁缺东西)。

其二,安装技能所需的依赖。这才是下面五种安装方式的用武之地——它们写在 SKILL.md 的 metadata.openclaw.install 数组里,描述"这个技能需要的二进制该怎么装":

kind 必填字段 适用场景
brew formula(写 cask 也行,见下) macOS 命令行工具
node package Node.js 包
go module Go 工具
uv package Python 工具
download url 直接下载(可带 archive / extract / stripComponents / targetDir)

src/agents/skills/frontmatter.ts 里的 parseInstallSpec 对每一种都做了必填字段校验(src/agents/skills/frontmatter.ts:167-181 五个 if (spec.kind === ... && !spec.X) return undefined)——缺 formula 的 brew 规格、缺 url 的 download 规格会被直接丢弃,而不是留到运行时才炸。唯一一处放宽是 brew:cask 会被当成 formula 的别名收下(src/agents/skills/frontmatter.ts:131-134,仅在没写 formula 时生效),所以只写 cask 的规格照样能过校验。默认用哪个 Node 包管理器由 skills.install.nodeManager 决定(npm / pnpm / yarn / bun),skills.install.preferBrew(默认 true)则控制 brew 可用时是否优先选 brew 规格。

16.7.2 安装前安全扫描

插件安装、以及技能的依赖安装(src/agents/skills-install.ts 在执行 metadata.openclaw.install 之前先扫描技能自身目录)都会经过安全扫描(src/plugins/install-security-scan.ts),发现的问题按三级严重度归类:

严重级别 处理方式
critical 阻断安装,返回 security_scan_blocked 并列出具体命中项
warn 放行但打警告
info 只计入扫描摘要

只有 critical 会真正拦下安装(扫描本身出错也会以 security_scan_failed 拦下);逃生舱是 dangerouslyForceUnsafeInstall——名字本身就是一次劝阻。扫描不是万能的:Markdown 是自然语言,恶意意图可以用无穷多种方式表达。但它拦住了最常见的危险模式,筑起合理的第一道防线。还要注意覆盖范围:在 v2026.3.31 上,openclaw skills install 从 ClawHub 下载技能的路径(src/agents/skills-clawhub.ts)并不调用这项本地扫描——对第三方技能,安装后、启用前自己审阅一遍 SKILL.md 和附带脚本,仍是必要的一步。

16.7.3 环境变量注入的引用计数

环境变量注入使用引用计数确保多个技能共享一个环境变量时只设置一次,所有引用释放时恢复原始值。

技能 A 需要 API_KEY → 引用计数 = 1, 设置 process.env.API_KEY
技能 B 也需要 API_KEY → 引用计数 = 2, 不重设
技能 A 的注入撤销 → 引用计数 = 1, 保持
技能 B 的注入撤销 → 引用计数 = 0, 恢复原始值(或清除)

关键安全特性:getActiveSkillEnvKeys() 使 ACP 拉起外部编码代理时能从子进程环境中剥离技能注入的密钥,防止凭证泄露到子进程里(源码注释举的例子是 OPENAI_API_KEY 漏给 Codex CLI)。

此外,系统永不覆盖已存在的进程环境变量。如果用户已显式设置 process.env.API_KEY,技能的环境注入不会覆盖它——用户的显式配置优先。

16.8 调用:两维控制矩阵

技能的可调用性由 frontmatter 顶层的两个独立布尔值控制(内部分别解析为 userInvocable 与 disableModelInvocation),组合出四种行为:

user-invocable disable-model-invocation 行为
true false 默认:用户 /skill-name + 模型自动发现
true true 仅用户命令,对模型隐藏
false false 无斜杠命令,模型从提示中发现
false true 完全隐藏(内部使用)

这个矩阵的设计动机是:某些技能只有在用户明确请求时才应触发。例如,"删除所有邮件"技能若由模型自动发现,可能因用户的模糊指令而意外触发。在 frontmatter 里写 disable-model-invocation: true 确保它只在用户显式输入 /delete-emails 时才执行。

16.8.1 直接工具分发

高级技能可在 frontmatter 里声明 command-dispatch: tool 并用 command-tool 指明一个工具,从而绕过 LLM——用户输入 /<命令> 参数 时,原始参数串被原样交给那个工具执行(OpenClaw 内置工具或插件提供的工具),结果文本直接作为回复,无需模型推理。没写 command-tool 的话,这个声明会被忽略。这有两个好处:

  1. 零 token 成本:不调用模型 API。
  2. 低延迟:不等待模型推理,耗时只取决于工具本身。

这种"LLM 旁路"在高频、低复杂度的操作中非常有价值——同一个查询每天发十次,每次都走 LLM 推理不仅浪费,还增加了不必要的延迟。

16.9 技能生命周期:从创建到退役

16.9.1 五个阶段

每个技能经历可预测的生命周期:

阶段1:创建。 某人(运营者、领域专家,或 Agent 自身通过自我改进)编写一个 SKILL.md 文件。此时技能只是一个文件,系统尚未加载它。

阶段2:发现。 文件监视器检测到新文件(或启动时已存在)。技能进入加载管线:frontmatter 解析、资格检查、与更高优先级来源的去重。

阶段3:活跃使用。 技能的元数据出现在系统提示目录中。模型选中它时,用 read 工具读取完整的 SKILL.md 正文,正文作为工具结果进入对话。技能开始影响 Agent 行为。

阶段4:演进。 基于经验修改技能——也许某个命令语法有误,或者发现了更好的方法。热重载无缝拾取变更。

阶段5:退役。 技能变得过时(它描述的 API 已废弃,它依赖的工具已卸载)。从技能目录中删除它,文件监视器捕获到删除后,下一个回合的目录里就不再有它。

整个生命周期完全通过文件系统发生——没有数据库,没有注册 API,没有部署管线。文件系统就是技能数据库。 这种简洁性是刻意的设计选择:它意味着技能可以用 cp、mv、rm 和 git 管理——每个运营者都已经熟悉的工具。

16.9.2 自我改进的技能

一个特别强大的模式:Agent 可以创建和修改自己的技能。当 Agent 第一次成功完成一个复杂任务(例如,部署到新平台),它可以:

  1. 提取它通过试错发现的程序。
  2. 使用 write 工具将其写为 SKILL.md。
  3. 放置到工作区技能目录中。
  4. 热重载立即拾取。
  5. 下一次请求类似任务时,技能已可用——不需要再次试错。

这创造了一个正反馈循环:Agent 在做过的任务上越来越好。随着时间推移,工作区积累了机构知识——不是人类必须阅读的文档,而是 Agent 直接消费的操作知识。

哲学含义是深刻的:技能系统将 Agent 的工作区变成了一种学习记忆。技能是 Agent 的长期程序性记忆,跨会话甚至跨 Agent 实例持久化。

16.10 与其他系统的关联

技能系统不是孤立的——它与 OpenClaw 的多个子系统紧密关联:

与上下文引擎(第5章):技能目录占用系统提示空间,自适应降级确保它不过度消耗上下文窗口。

与工具系统(第10章):技能通过 command-dispatch: tool 可以直接调用工具。技能还可以在 requires.bins 中声明依赖的工具二进制文件。

与安全系统(第13章):技能安装扫描利用安全系统的模式检测。技能的环境变量注入需要与凭证管理系统协调。

与 Node 系统(第11章):远程节点的连接状态影响技能的资格判定。网关不在 macOS 上时,macOS 专属技能只在有 macOS 节点连接时可用。

与 Cron 系统(第12章):定时任务触发的 Agent 回合也可以使用技能——凌晨的巡检作业可能需要 healthcheck 技能的知识。

graph TB
    subgraph "图 16-4:技能系统的跨模块关联"
        Skills["技能系统<br/>知识注入"] --> Context["上下文引擎<br/>占用/让步"]
        Skills --> Tools["工具系统<br/>直接调用"]
        Skills --> Security["安全系统<br/>安装扫描"]
        Node["Node 系统<br/>动态资格"] --> Skills
        Cron["Cron 系统<br/>定时调用"] --> Skills
    end
    style Skills fill:#1e8449,color:#fff

16.11 技能系统的能力清单

同类框架的能力边界变化很快,逐项打勾的横向对比表在写下的那一刻就开始过期。这里改列 OpenClaw 技能系统自身的特性与出处,读者可据此对照自己手边的方案:

特性 实现 源码位置
按需加载 提示里只放目录,正文靠 read 工具按需读 formatSkillsCompact @ src/agents/skills/workspace.ts
多源覆盖 6 级来源,完全替换而非合并 src/agents/skills/workspace.ts 的 precedence 注释
资格过滤 os / bins / anyBins / env / config 五类声明 shouldIncludeSkill @ src/agents/skills/config.ts
自适应降级 完整 → 紧凑 → 二分截断 applySkillsPromptLimits @ src/agents/skills/workspace.ts
预算上限 5 个可配置默认值(150 个 / 30,000 字符 等) src/agents/skills/workspace.ts
热重载 chokidar + 250ms 写入稳定阈值 src/agents/skills/refresh.ts
安装安全扫描 critical 阻断,warn/info 放行并记录 src/plugins/install-security-scan.ts
环境变量注入 引用计数 + 不覆盖既有变量 + 子进程剥离 src/agents/skills/env-overrides.ts
格式 Markdown + YAML frontmatter src/agents/skills/frontmatter.ts

技能系统在门槛和上下文效率上的优势是结构性的:Markdown 让非程序员也能创建技能,按需加载让大量技能共存而不耗尽上下文。代价同样是结构性的——没有类型检查,没有编程抽象。需要运行时逻辑的场景,那是插件系统(第 9 章)的领地。

16.12 实战推演:从零编写一个技能

理论讲够了,让我们动手。本节将完整推演如何从零创建一个 OpenClaw 技能——一个 Git 仓库状态检查技能。通过这个过程,你将亲身体验"文件系统即数据库"的简洁哲学。

16.12.1 需求分析

我们要创建一个技能,让 Agent 能够:

  • 检查当前工作目录的 Git 状态
  • 显示未提交的变更、分支信息、最近的提交日志
  • 支持常见的 Git 操作指导

16.12.2 第一步:创建 SKILL.md

在工作区的技能目录下创建文件:

mkdir -p ~/.openclaw/workspace/skills/git-status

创建 ~/.openclaw/workspace/skills/git-status/SKILL.md:

---
name: git-status
description: "Check Git repository status, show uncommitted changes, branch info, and recent commits.
  Use when: user asks about git status. NOT for: rebase, cherry-pick, conflict resolution."
metadata: { "openclaw": { "emoji": "📊", "requires": { "bins": ["git"] } } }
---

# Git Status Skill — 快速查看仓库状态

## When to Use
✅ "What's the git status?" / "Any uncommitted changes?" / "What branch am I on?"
❌ 复杂 merge/rebase → 终端直接操作

## Commands
```bash
git status --short --branch && echo "---" && git log --oneline -5  # 概览
git diff --stat                 # 未暂存变更
git diff --cached --stat        # 已暂存变更
git status --branch --porcelain=v2  # 推送/拉取状态
```

16.12.3 解剖 Frontmatter

让我们逐字段分析这个 SKILL.md 的 frontmatter——name、description 对应上游 @mariozechner/pi-coding-agent 的 Skill 类型,metadata.openclaw 下的字段对应 src/agents/skills/types.ts 中的 OpenClawSkillMetadata:

name: git-status          # 技能的唯一标识符,用于去重和覆盖
description: "..."        # 出现在系统提示的技能目录中(紧凑模式下会被省略)
metadata:
  openclaw:
    emoji: "📊"           # 在技能目录中的视觉标识
    requires:
      bins: ["git"]       # 资格检查:系统必须有 git 二进制文件

关键设计洞察:description 是技能最重要的字段,它决定了模型何时激活这个技能。提示里的技能块由 src/agents/skills/workspace.ts 的 buildWorkspaceSkillsPrompt 生成——完整模式调用上游 @mariozechner/pi-coding-agent 的 formatSkillsForPrompt(名称 + 描述 + 路径),预算吃紧时降级为本地的 formatSkillsCompact(只剩名称 + 路径)。正文无论哪种模式都不进提示。因此 description 必须精确描述"何时使用"和"何时不使用"——而且要做好它在紧凑模式下根本不会出现的准备,那时模型只能靠技能名判断。

requires.bins 触发 src/agents/skills/config.ts 中 shouldIncludeSkill 函数的资格检查。如果系统没有安装 git,这个技能将不会出现在目录中——Agent 永远不会知道它的存在。这比"加载了但报错"更优雅。

16.12.4 第二步:验证技能加载

技能创建后,热重载会自动拾取。验证方法:

# 方法一(最直接):用 CLI 查
openclaw skills list --eligible        # 只列出依赖齐全、当前可用的技能
openclaw skills list -v                # 顺带显示每个技能缺什么
openclaw skills info git-status        # 看这个技能的详情
openclaw skills check                  # 谁就绪、谁缺依赖

# 方法二:查日志
openclaw logs --limit 200 | grep git-status

如果一切正常,紧凑模式下它在系统提示里长这样:

<skill>
  <name>git-status</name>
  <location>~/.openclaw/workspace/skills/git-status/SKILL.md</location>
</skill>

(完整模式下还会带上 description。)如果它没出现,最常见的两个原因是:frontmatter 缺失、未闭合或取不到 description(技能根本没被加载),或者 requires.bins 里声明的二进制在这台机器上不存在——openclaw skills check 能一眼区分这两种情况:前者在列表里找不到,后者会被列为缺依赖。

16.12.5 第三步:添加引用文件(进阶)

对于复杂的技能,你可能需要附带参考文件。创建 references/ 目录:

mkdir -p ~/.openclaw/workspace/skills/git-status/references

创建 references/common-workflows.md:

# Common Git Workflows

## Feature Branch Workflow
1. `git checkout -b feature/xxx`
2. Make changes, commit frequently
3. `git push origin feature/xxx`
4. Create PR

## Hotfix Workflow
1. `git checkout -b hotfix/xxx main`
2. Fix the issue
3. `git push origin hotfix/xxx`
4. Create PR targeting main

在 SKILL.md 正文中引用:

## Reference Files
- See `references/common-workflows.md` for standard Git workflows

机制:这里没有专门的路径解析代码,靠的是提示里的那条规则——紧凑目录前的说明要求模型"技能文件引用相对路径时,以 SKILL.md 所在目录为基准解析成绝对路径再用"(src/agents/skills/workspace.ts:550)。模型据此把 references/common-workflows.md 拼成技能目录下的绝对路径,再用 read 工具按需读取——这与技能正文的按需加载哲学一脉相承。

16.12.6 第四步:添加脚本(进阶)

对于需要执行脚本的技能,创建 scripts/ 目录:

mkdir -p ~/.openclaw/workspace/skills/git-status/scripts
cat > ~/.openclaw/workspace/skills/git-status/scripts/full-report.sh << 'EOF'
#!/bin/bash
echo "=== Git Repository Report ==="
echo "Branch: $(git branch --show-current)"
echo "Remote: $(git remote -v | head -1)"
echo ""
echo "=== Status ==="
git status --short
echo ""
echo "=== Recent Commits ==="
git log --oneline -10
echo ""
echo "=== Stash ==="
git stash list
EOF
chmod +x ~/.openclaw/workspace/skills/git-status/scripts/full-report.sh

在 SKILL.md 中引用脚本:

## Full Report Script
Run `{baseDir}/scripts/full-report.sh` for a comprehensive repository overview.
`{baseDir}` is this skill's directory (the folder containing this SKILL.md).

注意 exec 的工作目录是 Agent 工作区而不是技能目录,裸写 scripts/full-report.sh 会在工作区里找这个脚本、找不到。仓库内建技能的惯例是写成 {baseDir}/scripts/...(如 skills/video-frames/SKILL.md),明确告诉模型要以技能目录为基准拼出绝对路径。

16.12.7 第五步:覆盖内建技能

假设 OpenClaw 已经内建了一个 git-status 技能,但你想自定义它的行为。六级来源的优先级机制让这变得简单:

# workspace(优先级最高)覆盖 openclaw-bundled
# 只需在工作区中创建同名技能即可
~/.openclaw/workspace/skills/git-status/SKILL.md              # 你的版本,胜出
/usr/lib/node_modules/openclaw/skills/git-status/SKILL.md     # 内建版本,被完全忽略

关键行为:这是完全替换,不是合并——你的整个 SKILL.md 取代内建版本。src/agents/skills/workspace.ts 里的合并顺序注释写得很明白:// Precedence: extra < bundled < managed < agents-skills-personal < agents-skills-project < workspace。

顺带一提,如果你只是想关掉某些内建技能而不是覆盖它们,有更省事的办法:skills.entries.<key>.enabled: false 关单个,skills.allowBundled 给内建技能整体设白名单。

16.12.8 完整目录结构

最终的技能包结构:

skills/git-status/
├── SKILL.md              # 必需:技能定义(frontmatter + 正文)
├── references/           # 可选:参考文档
│   └── common-workflows.md
└── scripts/              # 可选:可执行脚本
    └── full-report.sh

这就是全部。没有 package.json,没有编译步骤,没有注册 API。文件系统即接口。cp 即部署,rm 即卸载,git 即版本管理。

16.12.9 为什么这比代码插件更好(在这个场景下)

维度 代码形态的扩展(如 OpenClaw 插件,第 9 章) 文件技能(SKILL.md)
创建门槛 需要 TypeScript 编程能力 只需会写 Markdown
部署方式 安装 + 加载进程 放文件 + 热重载
版本管理 包管理器 就是文件,git 跟踪即可
覆盖方式 fork + 改代码 同名文件按来源优先级覆盖
上下文成本 工具定义常驻上下文 目录常驻,正文按需
适用场景 需要运行时逻辑的复杂工具 操作知识和流程指导

权衡是真实的:技能系统不适合需要复杂运行时逻辑的场景(比如维持一个有状态的第三方连接、解析复杂响应)。那是插件系统(第 9 章)的领地。技能系统的最佳战场是操作知识的编码——"如何做某事"的程序性知识。两者也不是二选一:技能可以通过 command-dispatch: tool 把某个斜杠命令直接接到插件提供的工具上。

16.13 关键源码文件

文件 用途
src/agents/skills.ts 技能系统入口——导出所有核心功能
src/agents/skills/workspace.ts 技能发现、加载和提示构建
src/agents/skills/config.ts 技能配置解析和资格检查
src/agents/skills/env-overrides.ts 环境变量注入和引用计数
src/agents/skills/types.ts 核心类型定义(SkillEntry、SkillSnapshot)
src/agents/skills-install.ts 统一安装接口
src/auto-reply/skill-commands.ts 技能斜杠命令的列举与解析(command-dispatch: tool 的执行在 src/auto-reply/reply/get-reply-inline-actions.ts)
src/agents/skills/refresh.ts chokidar 监视与快照刷新
src/agents/skills/frontmatter.ts frontmatter 解析与元数据/安装规格校验
src/agents/skills-status.ts 依赖就绪度与缺失项报告
src/plugins/install-security-scan.ts 安装前安全扫描
src/config/types.skills.ts skills.* 配置类型

16.14 本章小结

OpenClaw 的技能系统通过模块化知识注入框架将通用 Agent 转变为领域专家。按需加载把常驻提示开销压在"一份目录"的量级——仓库里 53 个内建技能,正文合计 213 KB,目录不到 10 KB——而且这个开销还有 30,000 字符的硬预算兜底,不会随技能数量无限膨胀。六级源优先级让用户覆盖任何内建行为而不修改原始文件。自适应格式降级优雅处理上下文窗口的资源约束。Markdown 格式降低了技能创建的门槛,让领域专家可以直接将知识编码为技能。

核心洞察:上下文窗口是公地——一块有限的共享资源,技能、系统提示、对话历史和用户消息都在竞争使用。技能系统的整个架构——从按需加载到路径压缩到三层降级——都是在践行"负责任的资源消费者"角色。每个设计决策都在问:"如何以最小 token 成本提供最大操作知识?"

这种对稀缺资源的深刻感知,是 OpenClaw 技能系统与简单的"把一切放进提示"方法的根本区别。它不是技术上的聪明——而是经济学上的明智。当你管理一块所有消费者共享的有限资源时,节制不是限制,而是让更多参与者能够共存的条件。就像交响乐团——每件乐器都能独奏很响,但真正的美只在大家控制音量、各守声部时才会涌现。

上下文窗口是 Agent 系统最贵的不动产——每一个 token 都是寸土寸金。

技能系统体现了全专栏五大设计哲学中的三条:约定优于配置(Convention over Configuration)——系统自动发现和注册 SKILL.md 文件,无需任何配置;渐进式复杂度(Progressive Disclosure)——从简单的 Markdown 文件到完整的技能包(含脚本、引用文件、环境变量),每一层都是可选的;通道无关(Channel-agnostic)——技能不关心消息来自哪个通道,同一个技能在所有通道上表现一致。

至此,我们已经逐一拆解了 OpenClaw 的每一个核心子系统。下一章将从"分析零件"转向"提炼模式"——当你退后三步,纵观这些子系统的设计,会发现七个反复出现的架构模式。理解这些模式,不仅能帮你读懂 OpenClaw,更能指导你设计自己的 Agent 系统。

🛠️ 试一试:编写一个最简技能

创建你的第一个 OpenClaw 技能只需一个 Markdown 文件:

# 1. 创建技能目录
mkdir -p ~/.openclaw/workspace/skills/hello-check

# 2. 编写 SKILL.md —— 这就是整个技能的定义
cat > ~/.openclaw/workspace/skills/hello-check/SKILL.md << 'EOF'
---
name: hello-check
description: "Quick system health check — reports uptime, memory, and load average.
  Use when: user asks for a health check or system status.
  NOT for: application-level metrics or log analysis."

metadata: { "openclaw": { "emoji": "🩺", "os": ["linux"] } }
---

# Hello Check Skill

When the user asks for a health check or system status, run these commands
and present the results in a friendly summary:

## Commands

```bash
# System uptime
uptime

# Memory usage
free -h

# Load average (already in uptime output, but highlight it)
cat /proc/loadavg
```

## Output Format

Summarize as:
- 🕐 **Uptime**: <days/hours>
- 💾 **Memory**: <used>/<total> (<percentage>%)
- 📊 **Load**: <1min> / <5min> / <15min>

Keep it concise. One short paragraph max.
EOF

# 3. 确认它被识别到(CLI 直接读盘)。网关侧 skills.load.watch 默认开启,
#    下一回合即可用;若关掉了监视,开新会话或重启网关:openclaw gateway restart
#    (示例命令 free、/proc/loadavg 只在 Linux 上有,所以元数据里限定了 os)
openclaw skills info hello-check

# 4. 测试:向 Agent 发送 "do a health check"
#    模型从目录里看到这个技能,用 read 工具读取 SKILL.md,再照着执行

💡 观察要点:注意你没有写任何代码——整个技能就是一个 Markdown 文件。这就是"文件系统即数据库"理念的最直接体现。尝试改写 description(尤其是"适用/不适用"两句),观察 Agent 的技能匹配行为如何变化——这是整个按需加载机制唯一的输入信号。


思考题

  1. 概念理解:技能系统为什么采用"约定优于配置"(SKILL.md)而非程序化注册的方式?这种设计对技能的可移植性和社区共享有什么影响?
  2. 实践应用:为一个"客户支持"Agent 设计一套技能体系——需要包含 FAQ 检索、工单创建、情感分析和升级到人工的能力。每个技能的 SKILL.md 应该如何定义触发条件和能力边界?
  3. 开放讨论:技能系统本质上是在上下文窗口中动态注入知识。这种"按需注入"模式与 RAG(检索增强生成)有什么异同?它们能否互补?

📚 推荐阅读