重读 OpenClaw:Skill 系统是如何工作的
深入浅出 OpenClaw Skill 系统:AI 助手网关的”说明书”是如何工作的
这是「重读 OpenClaw」系列的第二篇,通过 12 个问题,从零揭开 OpenClaw 中 Skill 的定位、加载、执行与协作全貌。
前言
最近在深入研究 OpenClaw——一个个人 AI 助手网关项目。它最让我着迷的设计是 Skill 系统。一开始我以为 Skill 就是”插件”,后来发现完全不是。它更像是一套”说明书体系“——Skill 负责告诉 AI 做什么、怎么做,而真正干活的是另一套 Tool 体系。
这篇文章以 Q&A 的形式,从最基础的概念开始,逐步深入到加载机制、执行路径、循环检测、插件协作,最后用一个完整场景把一切串起来。
一、入门:Skill 到底是什么?
Q1:一个 Skill 长什么样?
最简单的方式:打开一个真实的 Skill 文件看看。
skills/skill-creator/SKILL.md:
---
name: skill-creator
description: "Create, edit, audit, tidy, validate, or restructure AgentSkills and SKILL.md files."
---
# Skill Creator
Skills are compact triggerable workflows...
## Shape
skill-name/
SKILL.md ← 入口文件
scripts/ ← 可选脚本
references/ ← 可选参考文档
assets/ ← 可选资源文件
一个 Skill = 一个文件夹 + 一个 SKILL.md 文件。
-
• 文件头部的 ---包裹的是 YAML 元数据(name、description、是否可被用户调用等) -
• 下面是 指令正文(告诉 AI 遇到什么情况该怎么做)
核心认知:Skill 是纯文本的说明书,不是可执行代码。它不”运行”,而是被 AI “阅读后遵循”。
Q2:Skill 和 Tool 有什么区别?
这是最容易混淆的概念。用一个表格说清楚:
|
|
|
|
|---|---|---|
| 本质 |
|
|
| 作用 |
|
|
| 被谁使用 |
|
|
| 例子 | browser-automation/SKILL.md |
browser({ action: "open", url: "..." }) |
举个例子,browser-automation 这个 Skill 里写的是:
1. Check browser state: action="status"
2. Open tab: action="open", label="search"
3. Snapshot the page: action="snapshot"
4. Act: action="act" with a ref
这些指令教 AI 如何使用 browser 这个 Tool——按什么顺序调用、传什么参数、出错怎么恢复。Skill 本身不包含 browser 的实现代码。
一句话:Skill 是”菜谱”,Tool 是”锅和食材”。菜谱告诉你步骤,锅和食材真正把菜做出来。
Q3:那 Plugin 又是什么?三者什么关系?
Plugin 是集装箱,它可以同时打包 Skill 和 Tool,还可以打包更多东西。
看一个真实的 Plugin 声明——extensions/browser/openclaw.plugin.json:
{
"id": "browser",
"contracts": {
"tools": ["browser"]
},
"skills": ["./skills"],
"commandAliases": [{ "name": "browser" }]
}
这一个 Plugin 同时提供了:
-
• Tool: browser—— 真正可执行的浏览器自动化代码 -
• Skill: browser-automation/SKILL.md—— 教 AI 怎么用 browser 的说明书
而 Plugin 还能承载更多:Provider(模型提供商)、Channel(消息渠道)、Hook(生命周期钩子)、MCP Server……多达 20+ 种能力类型。
┌────────────────────────────────────────────┐
│ Plugin(集装箱) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Skills │ │ Tools │ │ 其他能力 │ │
│ │ 说明书 │ │ 可执行 │ │ Provider │ │
│ │ 纯文本 │ │ 代码 │ │ Channel │ │
│ │ │ │ │ │ Hook │ │
│ └──────────┘ └──────────┘ └────────────┘ │
└────────────────────────────────────────────┘
一句话:Plugin 是容器,Skill 是指令,Tool 是执行。三者不在一个维度上。
二、加载:Skill 怎么被发现和注入?
Q4:OpenClaw 从哪里找到 Skill?
这是整个系统最核心的问题。OpenClaw 从 7 个来源 扫描 Skill,按优先级从低到高:
📦 插件技能 → ~/.openclaw/plugin-skills/ (最低优先级)
📦 额外目录 → config 里配的 extraDirs
📦 内置技能 → openclaw 自带的 skills/ 目录
📦 托管技能 → ~/.openclaw/skills/ (从 ClawHub/Git 安装的)
📦 个人技能 → ~/.agents/skills/ (你自己写的)
📦 项目技能 → 项目根/.agents/skills/ (团队共享的)
📦 工作区技能 → 项目根/skills/ (最高优先级)
合并方式极其简单:用一个 Map<名字, 技能>,后面的覆盖前面的。
比如内置 skills/ 里有个 browser-automation,你的项目 <workspace>/skills/ 里也有一个同名但改过的版本。加载顺序是:
-
1. 先加载内置的 → Map 里有 browser-automation= 内置版 -
2. 再加载工作区的 → Map 里 browser-automation被覆盖成你的版本
结果:你的版本胜出。这就是”工作区覆盖内置”的全部秘密——不是复杂的优先级算法,就是 Map.set 的自然行为。
Q5:外部 Skill(插件、ClawHub、Git)是怎么进来的?
插件技能:插件在 openclaw.plugin.json 里声明 skills: ["./skills"],OpenClaw 启动时扫描插件目录,验证路径安全性,然后创建符号链接到 ~/.openclaw/plugin-skills/,之后走正常加载流程。
ClawHub 安装:openclaw skills install <name> → HTTP 下载解压 → 放到 ~/.openclaw/skills/。
Git 克隆安装:openclaw skills install <git-url> → git clone → 放到 ~/.openclaw/skills/。
关键点:不管技能从哪来,最终都是文件系统上的一个 SKILL.md 目录。进入文件系统后,加载管道一视同仁——不关心你是谁、从哪来的,只关心你有没有合法的 SKILL.md。
Q6:加载完后怎么让 AI 知道有哪些 Skill?
Skill 列表被渲染成 XML 块,注入到系统提示词中:
<available_skills>
<skill>
<name>browser-automation</name>
<description>Use when controlling web pages with the OpenClaw browser tool...</description>
<location>/home/user/.openclaw/plugin-skills/browser-automation/SKILL.md</location>
</skill>
<skill>
<name>gh-issues</name>
<description>Fetch GitHub issues, select candidates, spawn background fix agents...</description>
<location>/app/skills/gh-issues/SKILL.md</location>
</skill>
</available_skills>
关键指令就一句话:
“Use the read tool to load a skill’s file when the task matches its description.”
翻译:“当你觉得某个技能的描述和当前任务有关时,自己去读那个文件。”
这就是**延迟加载(lazy loading)**的精髓——菜单只放”菜名+简介”(一行),AI 需要时才去读”完整菜谱”(整个文件),避免 token 爆炸。
三、执行:Skill 被选中后怎么”运行”?
Q7:AI 选中 Skill 后,具体怎么执行?
有三种执行路径:
用户说话
│
├─ 普通聊天 → 【路径1:模型自动匹配】
│ AI 看到 <available_skills>
│ → 判断任务和某个 skill 匹配
│ → Read(SKILL.md)
│ → 按指令调用 Tool
│
└─ /skill名 → 【路径2:斜杠命令】
├─ 普通模式:改写用户消息,让 AI 处理
└─ Tool Dispatch:绕过 AI,直接执行工具
路径1:模型自动匹配(最常用)
用户说”帮我打开浏览器搜天气”,AI 的推理过程:
“这个任务涉及浏览器操作,而且是多步骤流程。
browser-automation的描述是 ‘Use when controlling web pages…’,完全吻合。我先读它的 SKILL.md。”
然后 AI 在一个推理回合里:
→ Read("browser-automation/SKILL.md") // 读到操作手册
→ browser({ action: "status" }) // 检查浏览器状态
→ browser({ action: "open", url: "https://baidu.com", label: "search" }) // 打开百度
→ browser({ action: "snapshot", targetId: "search" }) // 获取页面结构
→ browser({ action: "act", ... }) // 输入"天气"并搜索
Skill 不”运行”任何代码。 它改变的是 AI 的行为方式——读完后 AI 知道了正确的操作顺序、参数格式、错误恢复策略。
路径2A:普通斜杠命令
用户输入 /gh-issues --limit 5,消息在到达 AI 之前被拦截。OpenClaw 把用户消息改写为:
Use the "gh-issues" skill for this request.
User input:
--limit 5
然后这条改写后的消息 + 系统提示词(含 <available_skills>)一起发给 AI。AI 从改写消息中知道要用 gh-issues,从系统提示词中找到它的文件位置,然后 Read → 按指令执行。
路径2B:Tool Dispatch(绕过 AI)
如果 Skill 声明了 command-dispatch: tool 和 command-tool: message,用户输入 /my-status 时,OpenClaw 会:
-
1. 拦截消息 -
2. 识别 dispatch 配置 -
3. 直接调用 tool.execute()——AI 完全不知情
这个模式的特点:速度极快(不需要模型推理)、成本为零(不消耗 token),适合确定性操作。
Q8:Skill 能包含脚本吗?脚本怎么执行?
可以! Skill 的标准目录结构支持 scripts/ 文件夹。
例如 model-usage 这个 Skill,它的 SKILL.md 里写:
python {baseDir}/scripts/model_usage.py --provider codex --mode current
AI 读到后,做的事是:
AI: "需要运行 scripts/model_usage.py"
→ 调用 exec 工具:
{ "command": "python /path/to/skills/model-usage/scripts/model_usage.py --provider codex --mode current" }
→ exec 工具执行命令,返回 stdout/stderr
→ AI 看到结果,格式化展示给用户
关键点:脚本不是被 Skill 系统”自动执行”的。是 AI 读完 SKILL.md → 理解要做什么 → 主动调用 exec 工具 → 把脚本路径和参数传进去。脚本对框架来说完全是透明的——框架只知道 AI 调用了 exec 工具。
Q9:如果 Tool 调用名称写错或参数写错,怎么办?
错误被捕获后返回给模型,模型自我纠正。这是 ReAct 模式的核心优势。
但如果不加限制,模型可能陷入”犯错→重试→再犯错”的死循环。所以 OpenClaw 有5 层循环检测器:
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
magic_fix 反复调 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
检测原理:每个 tool call 被哈希为 工具名:参数哈希,执行结果也被哈希。循环检测器追踪最近 30 条历史,检查相同的 (工具名, 参数) 组合出现了多少次,结果是否一直相同(=没有进展)。
警告:tool call 仍然执行,但模型在返回结果里看到警告消息。阻断:tool call 被直接拦截,返回 { status: "blocked" },模型被迫放弃这条路。
Q10:ReAct 循环里会多次读取 Skill 吗?
可以,但通常不需要。系统提示词里写了:
“If a skill’s version differs from a previous turn, re-read its SKILL.md before using it.”
多次读取的场景:
-
• 版本变化:skill 文件被修改,版本号变了 -
• 上下文截断:长对话中上下文被压缩,模型忘记了 skill 内容 -
• 多 skill 协作:一个任务需要多个 skill
通常只读一次——读完内容就在上下文窗口里了。
Q11:我可以注册自定义 Tool 吗?
完全可以。 通过插件 API 的 registerTool 方法:
// 在插件中注册
api.registerTool({
name: "get_weather",
description: "Get current weather for a city",
inputSchema: {
type: "object",
properties: {
city: { type: "string", description: "City name" }
},
required: ["city"]
},
async execute(toolCallId, params) {
const { city } = params;
const weather = await fetch(`https://api.weather.com/${city}`).then(r => r.json());
return {
content: [{ type: "text", text: `${city}: ${weather.temp}°C` }]
};
}
});
Tool 需要满足的接口:
-
• name— 模型用这个名字来调用 -
• description— 告诉模型这个工具做什么 -
• inputSchema— 参数的 JSON Schema -
• execute— 实际执行函数
注册后,Tool 经过完整的策略管道(profile → provider → global → agent → group → sender → sandbox → subagent),最终出现在模型的工具列表中。
四、全景:Skill + Tool = ReAct 模式
Q12:所以整个体系就是 ReAct?
完全正确。 Skill 体系负责”说明书”(Reasoning),Tool 体系负责”执行”(Acting),两者通过 ReAct 循环联动。
┌──────────────────────────────────────────────────────┐
│ System Prompt │
│ ┌───────────────────┐ ┌─────────────────────────┐ │
│ │ <available_skills> │ │ Available Tools: │ │
│ │ 说明书目录 │ │ - read, write, edit │ │
│ │ name + description │ │ - exec, bash │ │
│ │ + location │ │ - browser, message │ │
│ └───────────────────┘ └─────────────────────────┘ │
└──────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ ReAct Loop │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ REASON │ ──→ │ ACT │ ──→ │ OBSERVE │ ──→ │
│ │ │ │ │ │ │ │
│ │ 读Skill │ │ 调Tool │ │ 看结果 │ │
│ │ 选Tool │ │ 填参数 │ │ 判成败 │ │
│ │ 定参数 │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ↑ │ │
│ └────────────────────────────────────┘ │
│ 循环直到任务完成 │
└──────────────────────────────────────────────────────┘
两个体系的职责边界:
|
|
|
|
|---|---|---|
| 本质 |
|
|
| 谁提供 |
|
|
| 加载时机 |
|
|
| AI 的角色 |
|
|
五、完整场景:从头到尾跑一遍
最后,用一个完整场景把所有概念串起来。
场景:用户在聊天里说 “帮我搜一下今天 AI 领域的新闻,整理成摘要发到 Discord”
第1步:消息到达,检查是否是斜杠命令
OpenClaw 收到消息 → 检查是否以 / 开头 → 不是 → 不是斜杠命令 → 正常发送给 AI。
第2步:AI 收到系统提示词(含 Skill 菜单)
AI 看到的 system prompt 里包含:
<available_skills>
<skill>
<name>tavily</name>
<description>Web search via Tavily API; search, extract...</description>
<location>/home/user/.openclaw/plugin-skills/tavily/SKILL.md</location>
</skill>
<skill>
<name>summarize</name>
<description>Summarize text, articles, or transcripts...</description>
<location>/app/skills/summarize/SKILL.md</location>
</skill>
<skill>
<name>discord</name>
<description>Discord message-tool ops: send/read/edit/delete...</description>
<location>/app/skills/discord/SKILL.md</location>
</skill>
</available_skills>
以及可用的 Tool 列表:read, exec, message, tavily_search, tavily_extract, …
第3步:AI 判断需要哪些 Skill(ReAct – Reason)
AI 分析用户请求:
“搜索新闻 → 匹配 tavily。整理摘要 → 匹配 summarize。发到 Discord → 匹配 discord。”
AI 在同一个推理回合输出多个 tool call:
→ Read("tavily/SKILL.md")
→ Read("summarize/SKILL.md")
→ Read("discord/SKILL.md")
第4步:OpenClaw 记录 Skill 使用
每次 Read 执行前,before-tool-call 钩子检查读取的文件路径是否命中某个 Skill 的 SKILL.md。命中了 → 发出 skill.used 诊断事件,记录技能名、来源、触发方式。
第5步:AI 读取 Skill 内容,按指令执行(ReAct – Act)
tavily 技能告诉 AI:
Use tavily_search with query parameter for web search.
Example: tavily_search({ query: "AI news today", max_results: 5 })
summarize 技能告诉 AI:
Use summarize.sh CLI tool to condense text.
Example: exec: summarize.sh --input /tmp/news.txt --format bullet
discord 技能告诉 AI:
Use message tool with channel: "discord".
Example: message({ action: "send", channel: "discord", to: "channel:123", message: "..." })
注意:这个技能声明了 allowed-tools: ["message"],只能用 message 工具
AI 按指令执行:
→ tavily_search({ query: "AI news 2026-06-11", max_results: 5 })
← 返回 5 条新闻结果
→ exec({ command: "summarize.sh --input /tmp/news.txt --format bullet" })
← 返回格式化的摘要
→ message({
action: "send",
channel: "discord",
to: "channel:123",
message: "📰 Today's AI News Summary:\n\n• ...\n• ...\n• ..."
})
← 发送成功
第6步:AI 观察结果,判断任务完成(ReAct – Observe)
所有 Tool 返回成功 → AI 判断任务完成 → 输出最终回复给用户:
“已为你搜索今天 AI 领域的最新新闻,摘要已发送到 Discord #general 频道!”
第7步:如果中途出错?
假设 AI 把 tavily_search 拼错成 tavily_seach:
→ tavily_seach({ query: "AI news" })
← Error: unknown tool: tavily_seach
AI 看到错误:"啊,拼错了,应该是 tavily_search"
→ tavily_search({ query: "AI news" }) // 纠正
← 正常返回结果
如果 AI 死不悔改,连续调用不存在的工具 10 次 → 循环检测器触发 unknown_tool_repeat → 硬阻断 → 模型被迫放弃。
这个场景涉及的全部组件:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
总结
OpenClaw 的 Skill 系统本质上是一套基于提示词工程的行为指导体系:
-
1. Skill 是说明书——纯文本的 SKILL.md,告诉 AI 遇到什么情况该怎么做 -
2. Tool 是执行器——TypeScript 函数,真正干活 -
3. Plugin 是集装箱——打包 Skill、Tool、Provider、Channel 等一切 -
4. 加载靠文件系统——7 个来源,Map 合并,后加载覆盖先加载 -
5. 注入靠提示词—— <available_skills>块注入系统提示词,AI 按需 Read -
6. 执行靠 ReAct——Reason(读 Skill 选 Tool)→ Act(调 Tool)→ Observe(看结果)→ 循环 -
7. 安全靠多层防护——allowed-tools 限制、5 层循环检测、工具策略管道
最精妙的设计是:Skill 从来不是”运行”的代码,它始终是一份被 AI 阅读后遵循的说明书。 这让 Skill 极其轻量(就是一个 Markdown 文件)、极其安全(没有代码执行风险)、极其灵活(任何人都可以写)。同时 Tool 体系提供了真正的执行能力,两者通过 ReAct 模式无缝协作。
夜雨聆风