IronClaw 学习笔记 05:Skills 系统
SKILL.md 格式兼容、多目录发现、按需加载模式、三种设计流派对比
上一篇我们给了 Agent 安全防护。但一个安全的 Agent 如果只会做固定的几件事,就像一个只会背课文的学生。
🎯 这一篇要解决什么问题?
Phase 04 之后,tiny-claw 的能力是硬编码的——所有工具在启动时注册,所有行为在编译时决定。想让 Agent 做新的事?改代码、重编译。
但在 Cursor 里,你只需要写一个 Markdown 文件扔到 skills/ 目录里,Agent 就能学会新技能——审查代码、操作数据库、合并 PR、搜索日志。这就是 Skills 系统的威力。
真实场景:
┌─────────────────────────────────────────────────────────────┐
│ skills/ 目录里放着多个技能: │
│ │
│ db-query/SKILL.md → "Query databases using CLI tools" │
│ ship/SKILL.md → "One-click create and merge PR" │
│ log-search/SKILL.md → "Search application logs" │
│ deploy-check/SKILL.md → "Check deployment status" │
│ ... │
│ │
│ 用户说: "帮我查一下 users 表的数据" │
│ Agent 扫描所有技能的描述 → │
│ db-query/SKILL.md 匹配 → 读取完整内容 → │
│ 按照里面的步骤连接数据库、执行查询、输出结果 │
│ │
│ 这一切不需要改代码,只需要写 Markdown。 │
└─────────────────────────────────────────────────────────────┘
问题是:目前至少有三种 Agent Skills 实现——IronClaw、Cursor/Trae、OpenClaw。它们的 SKILL.md 格式和加载机制各有差异。tiny-claw 应该兼容哪个?
答案是:兼容 Cursor 格式作为基线,借鉴 OpenClaw 的门控机制,参考 IronClaw 的信任模型。
🧠 三种实现流派对比
在写 tiny-claw 之前,先对比三种现有实现,理解它们的设计选择。
SKILL.md 格式
| Cursor/Trae | OpenClaw | IronClaw | |
|---|---|---|---|
| Frontmatter | name + description | name + description + metadata.openclaw | name + description + activation |
| Body | Markdown 提示词 | Markdown 提示词 | Markdown 提示词 |
| 激活关键词 | 无(靠 description) | 无(靠 description) | keywords + patterns + tags |
| 门控 | 无 | requires.bins/env/config | requires.bins/env/config |
| 安装规格 | 无 | install (brew/node/go) | 无 |
| 必需字段 | name, description | name, description | name |
Cursor 的格式最简单——只有 name 和 description 两个字段。OpenClaw 在此基础上加了 metadata.openclaw 做门控和安装。IronClaw 加了 activation 做精确匹配。
技能选择机制
这是三种实现最大的分歧点:
┌─────────────────────────────────────────────────────────────┐
│ 技能选择:两种根本不同的策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 策略 A:代码评分选择 (IronClaw) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户消息 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ prefilter_skills() │ │
│ │ • 关键词精确匹配 → +10 分 │ │
│ │ • 关键词子串匹配 → +5 分 │ │
│ │ • 标签匹配 → +3 分 │ │
│ │ • 正则匹配 → +20 分 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 按分数排序 → 取 Top-N → 将 body 注入系统提示词 │ │
│ │ │ │
│ │ 优点: 零 LLM 调用、确定性、可调试 │ │
│ │ 缺点: 每个技能要精心配 keywords/patterns,维护成本高 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 策略 B:列出 + 按需读取 (Cursor / OpenClaw) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 启动时 │ │
│ │ • 扫描所有 SKILL.md → 提取 name + description │ │
│ │ • 构建 <available_skills> 列表 → 放入系统提示词 │ │
│ │ │ │
│ │ 每条消息 │ │
│ │ • LLM 扫描 <available_skills> 的 description │ │
│ │ • 如果匹配 → 用 read 工具读取 SKILL.md 完整内容 │ │
│ │ • 按照技能指令执行 │ │
│ │ │ │
│ │ 优点: 零配置、description 就够、LLM 自己判断最合适的 │ │
│ │ 缺点: 依赖 LLM 判断力、技能列表占用系统提示词空间 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Cursor 和 OpenClaw 都选了策略 B——让 LLM 自己决定用哪个技能。这很合理:
- LLM 比关键词匹配更懂语义——"帮我看看这个 PR" 能匹配 "code-review" 技能,即使没配关键词
- 零维护成本——写好
description就行,不需要穷举触发词 - 按需读取省 token——只有真正用到的技能才被读入上下文
IronClaw 选策略 A 有它的道理——在聊天机器人场景中 LLM 调用是收费的,确定性选择避免了额外开销。但对个人助手来说,策略 B 更实用。
发现目录
| Cursor/Trae | OpenClaw | IronClaw | |
|---|---|---|---|
| 项目级 | .cursor/skills/ | workspace/skills/ | workspace/skills/ |
| 用户级 | ~/.cursor/skills/ | ~/.openclaw/skills/ | ~/.ironclaw/skills/ |
| 安装级 | ~/.cursor/skills-cursor/ | 多个插件目录 | ~/.ironclaw/installed_skills/ |
| 优先级 | 用户 > 项目 | 后来源覆盖先来源 | Trusted > Installed |
目录布局都是 <skill-name>/SKILL.md——这一点是统一的。
上下文注入方式
┌─────────────────────────────────────────────────────────────┐
│ 上下文注入:Eager vs Lazy │
├─────────────────────────────────────────────────────────────┤
│ │
│ IronClaw (Eager): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ system prompt: │ │
│ │ ... 基础提示词 ... │ │
│ │ <skill name="rust-reviewer" trust="trusted"> │ │
│ │ (完整的 SKILL.md body,可能数千字) │ │
│ │ </skill> │ │
│ │ <skill name="git-helper" trust="trusted"> │ │
│ │ (又一个完整的 body) │ │
│ │ </skill> │ │
│ │ │ │
│ │ 每条消息都带着匹配到的技能全文 → 占用上下文窗口 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Cursor / OpenClaw (Lazy): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ system prompt: │ │
│ │ ... 基础提示词 ... │ │
│ │ ## Skills │ │
│ │ <available_skills> │ │
│ │ <skill> │ │
│ │ <name>db</name> │ │
│ │ <description>Query PostgreSQL...</description> │ │
│ │ <location>skills/db/SKILL.md</location> │ │
│ │ </skill> │ │
│ │ <skill> │ │
│ │ <name>ship</name> │ │
│ │ <description>One-click PR...</description> │ │
│ │ </skill> │ │
│ │ ... (只有 name + description,几十字) │ │
│ │ </available_skills> │ │
│ │ │ │
│ │ 模型判断需要哪个 → 用 read 工具读取对应 SKILL.md │ │
│ │ → 只有真正用到的技能占用上下文 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Lazy 模式的优势在于可扩展性——Cursor 支持 150 个技能同时列出,因为每个只占一行名字 + 描述。IronClaw 的 Eager 模式在 3-5 个技能时可行,但到 150 个就不可能了。
📦 SKILL.md 格式:兼容设计
tiny-claw 的 SKILL.md 格式目标:一份文件,三个平台都能用。
最小格式(Cursor 兼容)
---
name: my-skill
description: What this skill does. Use when user says X or wants to do Y.
---
# My Skill
## Instructions
Step-by-step guidance...
这是 Cursor 的标准格式,也是 tiny-claw 的基线。只需 name + description 两个字段。
扩展格式(OpenClaw 兼容门控)
---
name: github
description: "GitHub operations via gh CLI..."
metadata:
{
"openclaw": {
"requires": { "bins": ["gh"] },
"install": [{ "id": "brew", "kind": "brew", "formula": "gh" }]
}
}
---
tiny-claw 解析 metadata.openclaw.requires 做运行时门控(检查 bins/env),但不处理 install 规格。
字段说明
| 字段 | 必需 | 来源 | 说明 |
|---|---|---|---|
name | 是 | Cursor | 技能标识符,小写字母/数字/连字符,≤64 字符 |
description | 是 | Cursor | 技能描述,包含 WHAT + WHEN,≤1024 字符 |
metadata.openclaw.requires.bins | 否 | OpenClaw | 必须在 PATH 上的二进制 |
metadata.openclaw.requires.env | 否 | OpenClaw | 必须存在的环境变量 |
为什么不用 IronClaw 的 activation 字段?
IronClaw 的 activation.keywords/patterns/tags 是为代码评分设计的。tiny-claw 采用 Cursor/OpenClaw 的策略——让 LLM 根据 description 自行判断。所以不需要这些字段。
但 tiny-claw 的解析器会忽略不认识的字段,不会报错。这意味着带 activation 字段的 IronClaw SKILL.md 也能在 tiny-claw 中加载——只是 activation 字段不会生效。
description 的写法
description 是技能选择的唯一依据——LLM 通过它判断技能是否适用。好的 description 需要:
| 原则 | 好例子 | 坏例子 |
|---|---|---|
| 包含 WHAT | "Query databases using CLI tools" | "Database helper" |
| 包含 WHEN | "Use when user says /db" | (缺少触发条件) |
| 第三人称 | "Processes Excel files and generates reports" | "I can help you with Excel" |
| 包含触发词 | "mentions PR merge, or says merge pr" | "handles PRs" |
🔍 多目录发现
tiny-claw 扫描多个目录,兼容 Cursor 的路径约定:
┌─────────────────────────────────────────────────────────────┐
│ 技能发现路径 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 优先级(后来源覆盖先来源,同名技能取后者): │
│ │
│ 1. 用户级技能 │
│ ~/.tiny-claw/skills/ (手动放置) │
│ │
│ 2. 项目级技能 │
│ <workspace>/skills/ (项目共享,最高优先级) │
│ │
│ 复用外部技能:通过软链接植入 │
│ ln -s ~/.cursor/skills/db skills/db │
│ ln -s ~/.cursor/skills/ship ~/.tiny-claw/skills/ship │
│ │
│ 布局: │
│ <dir>/ │
│ ├── skill-a/ │
│ │ └── SKILL.md │
│ ├── skill-b/ │
│ │ ├── SKILL.md │
│ │ ├── reference.md (可选,技能可以引用) │
│ │ └── scripts/ (可选,工具脚本) │
│ └── ... │
│ │
│ 同名技能: 后发现的覆盖先发现的。 │
│ 项目级 > 用户级 │
│ │
└─────────────────────────────────────────────────────────────┘
为什么用软链接而不是直接扫描外部目录?
tiny-claw 只扫描自己的两个目录,不主动读取 ~/.cursor/skills/ 等外部路径。用户通过 ln -s 软链接按需引入,好处是:
- 显式控制——你选择哪些技能植入,而不是全部加载
- 不耦合——不依赖其他工具的目录约定
- 零迁移成本——一条
ln -s命令即可复用已有技能
门控检查
对于带 metadata.openclaw.requires 的技能,tiny-claw 在发现阶段做门控:
发现 github/SKILL.md
→ 解析 requires.bins: ["gh"]
→ which gh → 找到 /usr/local/bin/gh → ✅ 通过
→ 加入可用技能列表
发现 docker-ops/SKILL.md
→ 解析 requires.bins: ["docker"]
→ which docker → 未找到 → ❌ 跳过
→ 日志: "Skill 'docker-ops' skipped: binary 'docker' not found"
门控不满足的技能直接跳过,不出现在 <available_skills> 列表中。这比运行时报错"找不到命令"体验好得多。
🚀 按需加载:List → Read → Execute
这是整个 Skills 系统的核心模式,来自 Cursor 和 OpenClaw 的实践:
┌─────────────────────────────────────────────────────────────┐
│ Skills 工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ═══ 启动时 ═══ │
│ │
│ Step 1: 扫描目录 → 发现 SKILL.md 文件 │
│ Step 2: 解析 frontmatter (name + description) │
│ Step 3: 门控检查 (bins/env) │
│ Step 4: 构建 skills prompt: │
│ │
│ <available_skills> │
│ <skill> │
│ <name>db-query</name> │
│ <description>Query databases using CLI tools. │
│ Use when user says "/db".</description> │
│ <location>/Users/me/project/skills/db/SKILL.md</loc> │
│ </skill> │
│ <skill> │
│ <name>ship</name> │
│ <description>One-click create and merge PR. │
│ Use when user says "/ship".</description> │
│ <location>/home/me/.tiny-claw/skills/ship/SKILL.md</l> │
│ </skill> │
│ ... (每个技能只占几行) │
│ </available_skills> │
│ │
│ ═══ 每条消息 ═══ │
│ │
│ Step 5: 系统提示词中包含: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ## Skills │ │
│ │ Scan <available_skills> descriptions. │ │
│ │ If one matches: read its SKILL.md, then follow it. │ │
│ │ If none match: do not read any SKILL.md. │ │
│ │ │ │
│ │ <available_skills>...</available_skills> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 6: LLM 判断 → 调用 read_file 读取匹配的 SKILL.md │
│ Step 7: LLM 按照 SKILL.md 中的指令执行任务 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 关键: SKILL.md 的完整内容只在被选中时才进入上下文。 │ │
│ │ 20 个技能的列表只占 ~2000 字符。 │ │
│ │ 而一个技能的完整内容可能 5000+ 字符。 │ │
│ │ Lazy 比 Eager 省了 10 倍上下文空间。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
系统提示词中的 Skills 指令
OpenClaw 的实现非常精炼(参考 system-prompt.ts):
## Skills (mandatory)
Before replying: scan <available_skills> <description> entries.
- If exactly one skill clearly applies: read its SKILL.md at <location> with read_file, then follow it.
- If multiple could apply: choose the most specific one, then read/follow it.
- If none clearly apply: do not read any SKILL.md.
Constraints: never read more than one skill up front; only read after selecting.
<available_skills>
...
</available_skills>
关键设计点:
- "mandatory"——强制 LLM 在回复前先扫描技能列表
- "never read more than one skill up front"——防止 LLM 贪婪地读所有技能,浪费 token
- "only read after selecting"——先判断再读,不是先读再判断
read_file 工具的角色
在这种模式下,read_file 工具不仅用于读文件,还是 Skills 系统的核心组件——它是 LLM 获取技能完整内容的唯一通道。tiny-claw 已有 read_file 工具(Phase 01),所以这里零新增工具就能实现 Skills。
📝 tiny-claw Phase 05 的设计决策
| 维度 | IronClaw | Cursor/OpenClaw | tiny-claw (Phase 05) |
|---|---|---|---|
| SKILL.md 格式 | activation 字段 | name + description | Cursor 格式 + OpenClaw 门控 ✅ |
| 选择机制 | 代码评分 (prefilter) | LLM 自行判断 | LLM 自行判断 ✅ |
| 注入方式 | Eager(body 注入上下文) | Lazy(列表 + 按需读取) | Lazy ✅ |
| 发现目录 | 3 个 ironclaw 目录 | Cursor/OpenClaw 目录 | 兼容 Cursor 路径 ✅ |
| 门控检查 | bins/env/config | bins/env/config + OS | bins/env ✅ |
| 信任模型 | Trusted/Installed | 无 | 暂不实现 |
| 工具衰减 | 只读白名单 | 无 | 暂不实现 |
| 技能限制 | max_active + max_tokens | maxSkillsInPrompt (150) | max_skills_in_prompt (100) |
关键决策:
- 兼容 Cursor 格式——绝大多数用户已有 Cursor 技能文件,零迁移成本
- Lazy 加载模式——更省 token,支持更多技能,借助已有的
read_file工具 - 门控从 OpenClaw 借鉴——
metadata.openclaw.requires.bins/env在加载时检查 - 安全模型推迟——先让技能能用起来,Trust/Attenuation 后续再加
- 不实现注册表——只支持本地文件,在线安装后续再做
🧪 动手试试
Phase 05 之后 tiny-claw 能加载和使用技能。试试这些场景:
| 场景 | 操作 | 观察点 |
|---|---|---|
| 软链接复用技能 | ln -s ~/.cursor/skills/db skills/db | 软链接的技能出现在列表中 |
| 创建项目技能 | 在 skills/my-skill/SKILL.md 写一个技能 | /skills 命令看到新技能 |
| 触发技能 | 发送匹配 description 的消息 | Agent 读取 SKILL.md 并按指令执行 |
| 门控检查 | 在 SKILL.md 中设 requires.bins: ["nonexistent"] | 技能不出现在可用列表中 |
| 多技能共存 | 准备 10+ 个技能 | 列表正确显示,只有匹配的被读取 |
| 同名覆盖 | 项目级和用户级有同名技能 | 项目级覆盖用户级 |
📝 本篇总结
| 理解项 | 描述 |
|---|---|
| 三种流派 | IronClaw(代码评分)、Cursor/OpenClaw(LLM 自选) |
| SKILL.md 格式 | name + description 两个必需字段(Cursor 兼容) |
| 门控扩展 | metadata.openclaw.requires.bins/env(OpenClaw 兼容) |
| description 写法 | 第三人称、包含 WHAT + WHEN、具体触发词 |
| 多目录发现 | tiny-claw 用户级 → Cursor 用户级 → 项目级(后覆盖前) |
| Lazy 加载 | 列表只放 name + description + location,LLM 按需读 |
| 系统提示词 | <available_skills> XML 列表 + Skills 指令段落 |
| read_file 复用 | 不需新工具,现有 read_file 就是技能加载通道 |
| 门控检查 | bins/env 不满足 → 技能从列表移除,不会运行时报错 |
| 兼容性 | 同一份 SKILL.md 在 Cursor、OpenClaw、tiny-claw 都能用 |
核心洞察
1. LLM 是最好的技能选择器
IronClaw 精心设计了关键词评分算法来选择技能。但 Cursor/OpenClaw 证明了一个更简单的方案——让 LLM 自己读 description 来决定。LLM 理解语义,关键词匹配只是文本比较。一个好的 description 比一堆 keywords 更有效。
2. Lazy > Eager
把匹配的技能全文塞进系统提示词(Eager)是直觉做法,但 Lazy 模式(只列出名字和描述,按需读取)在工程上更优——支持 100+ 技能、节省 token、利用已有工具。当 LLM 有 read_file 能力时,没必要提前把所有可能需要的内容都塞进上下文。
3. 生态兼容比创新更重要
tiny-claw 可以设计一个更"先进"的 SKILL.md 格式,但兼容 Cursor 的简单格式意味着用户能直接复用已有的 20+ 个技能。在 AI 工具生态中,兼容性就是功能。
下一篇:IronClaw 学习笔记 06:多渠道 + Web —— Channel trait 统一抽象、Web Gateway 设计、SSE/WebSocket 实时推送。