OpenClaw Skill系统评测:LLM工作流按需学习指南
摘要
Skill系统通过SKILL md文件为LLM提供操作手册,采用元数据与正文分离、资格过滤仅暴露可用
场景:AI 如何精准调用对应命令?
向 OpenClaw 提问:「帮我查一下上海今天的天气。」

AI 随即生成 curl "wttr.in/Shanghai?format=3" 命令,执行后正确返回天气数据。
但这里隐藏着一个关键机制问题:LLM 作为语言模型,并非天生知道查天气需要 wttr.in,也不了解管理 GitHub PR 依赖 gh CLI,更别提控制 Spotify 要用 spotify-player 这类专属工具。
因此,必然存在某种机制让 LLM“学会”这些技能。然而,若将 50 个工具的完整文档一股脑塞进系统提示,文档体积会瞬间撑爆上下文窗口。
这正是 Skill 系统要解决的核心矛盾:
- 文档规模瓶颈:50+ 工具各有详尽文档。全部预加载,LLM 的上下文根本容纳不下。
- 工具可用性校验:
ghCLI 未安装、spotify-player环境变量缺失——将这些不可用工具暴露给 LLM,只会引发无效调用和错误。 - 工作流标准化需求:工具的使用方法必须让 LLM 精确理解并严格遵循,而非靠“猜测”运作。
- 用户体验优化:用户更倾向于输入
/weather 上海快捷触发,而非每次都书写完整自然语言描述。
一、SKILL.md:面向 LLM 的文档规范
为何选用 Markdown 而非代码?
Skill 并非传统程序——它本质上是一份“给 LLM 阅读的操作指南”。LLM 最擅长处理自然语言与 Markdown,因此最佳格式是携带 YAML frontmatter 的 Markdown 文件。
每个 skill 对应一个目录,核心文件为 SKILL.md:
---name: weatherdescription: "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."metadata:{ "openclaw": { "emoji": "?️", "requires": { "bins": ["curl"] } } }---# Weather Skill## When to Use✅ **USE this skill when:**- "What's the weather?"- "Will it rain today/tomorrow?"## Commands```bash# One-line summarycurl "wttr.in/London?format=3"
文件拆分为两大区块。首先是 frontmatter(机器解析部分):
```typescript// src/agents/skills/types.tstype OpenClawSkillMetadata = {always?: boolean; // 绕过资格检查,强制包含emoji?: string; // 视觉展示primaryEnv?: string; // 主要依赖的环境变量requires?: {bins?: string[]; // 必需的可执行文件anyBins?: string[]; // 满足其中之一即可env?: string[]; // 必需的环境变量config?: string[]; // 必需的配置键};install?: SkillInstallSpec[]; // 依赖安装方式};
frontmatter 包含两个关键字段。其一是 description,相当于系统提示中唯一的“代言人”,LLM 凭借这一行决定是否启用该 skill。其二是 metadata.openclaw.requires.bins,声明依赖哪些可执行文件;运行时若缺失,整个 skill 会从系统提示中自动移除。
接着是 正文(LLM 阅读部分),详细说明“何时启用”“何时禁用”、命令模板和注意事项。这段内容不会预置在系统提示中,仅在 LLM 主动读取时才会载入上下文。
这种分离机制是整套系统的设计核心:元数据供机器解析,正文供 LLM 学习,摘要则处于中间层传递决策信号。
二、多来源发现与优先级(workspace.ts)
问题:skill 从哪些来源获取?
一个用户可能同时拥有系统内置 skill、自行安装 skill 以及项目级 skill。所有这些技能都需要被系统发现,且同名时必须遵循明确的覆盖规则。
loadSkillEntries() 按优先级从低到高扫描六个来源:
extra(openclaw.yml 中 skills.load.extraDirs 指定)< bundled(核心内置,代码库 skills/ 目录,随 OpenClaw 发布)< managed(~/.openclaw/skills/,用户通过 openclaw skills install 安装的)< agents-skills-personal(~/.agents/skills/,个人全局 skill)< agents-skills-project(工作区 .agents/skills/,项目级 skill)< workspace(工作区 skills/,最高优先级)
优先级通过 Map 实现——后赋值的会覆盖先赋值的:
// src/agents/skills/workspace.tsconst merged = new Map<string, Skill>();for (const skill of extraSkills)merged.set(skill.name, skill);for (const skill of bundledSkills)merged.set(skill.name, skill);for (const skill of managedSkills)merged.set(skill.name, skill);for (const skill of personalAgentsSkills) merged.set(skill.name, skill);for (const skill of projectAgentsSkills)merged.set(skill.name, skill);for (const skill of workspaceSkills)merged.set(skill.name, skill);
换言之,若在项目中放入 skills/github/SKILL.md,则会完全取代系统内置的 github skill,而非合并。用户可为特定项目定制任意 skill 的行为。
嵌套目录探测
resolveNestedSkillsRoot() 采用精巧的启发式逻辑:若 dir/skills/*/SKILL.md 存在,则认定 dir/skills 为实际 skills 根目录。这样一来,~/.openclaw/skills/ 目录下既能直接存放 github/SKILL.md,也能容纳包含 skills/ 子目录的完整工具包——两种结构均可正确识别。
三、资格过滤:仅暴露可用的 skill
问题:gh CLI 未安装,是否还应向 LLM 展示 GitHub skill?
shouldIncludeSkill() 在加载后执行运行时资格检查:
// 检查 requires.bins:这些可执行文件是否存在?// 检查 requires.anyBins:至少有一个存在吗?// 检查 requires.env:这些环境变量是否已设置?// 检查 requires.config:配置文件中是否有这些键?// 检查 os:当前操作系统是否匹配(如仅 macOS 的 skill)// always: true → 跳过所有检查,强制包含
若未安装 gh,requires.bins: ["gh"] 检查失败,GitHub skill 直接从列表中移除——LLM 的系统提示中根本不会出现任何相关条目。
过滤之后还有第二步:剔除那些 disable-model-invocation: true 的 skill。此类 skill 只能通过 /命令 显式触发,LLM 自主决策时无法感知它们的存在。
资格上下文:远端信息
SkillEligibilityContext.remote 支持注入远端节点的状态:
type SkillEligibilityContext = {remote?: {platforms: string[];hasBin: (bin: string) => boolean; // 目标节点是否包含 curl?hasAnyBin: (bins: string[]) => boolean;note?: string;};};
当 Agent 在远端 Node Host 上执行时,资格检查针对的是目标节点的环境,而非 Gateway 所在机器。例如,远端 Linux 服务器上安装有 gh 而本地 Mac 没有,GitHub skill 依然会呈现给 LLM。
四、渐近式披露:系统提示仅包含摘要
问题:150 个 skill 的完整文档有多大?
假设每个 SKILL.md 平均 2000 字节,150 个 skill 总计 300KB 纯文本——远超大多数模型的上下文窗口承受能力。
解决方案是渐近式披露:系统提示中只存放每个 skill 的三个字段——name、description、location,正文留到 LLM 决定使用时才读取。
formatSkillsForPrompt() 将过滤后的 skill 列表格式化为:
<a vailable_skills><skill><name>weathername><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.description><location>~/.openclaw/skills/weather/SKILL.mdlocation>skill><skill><name>githubname><description>GitHub operations via gh CLI: issues, PRs, CI runs, code review.Use when: (1) checking PR status or CI, (2) creating/commenting on issues...description><location>~/.openclaw/skills/github/SKILL.mdlocation>skill>a vailable_skills>
注意 location 字段中的路径:/Users/alice/.openclaw/skills/weather/SKILL.md 被压缩为 ~/.openclaw/skills/weather/SKILL.md。该细节由 compactSkillPaths() 实现,每个路径约节省 5-6 个 token,150 个 skill 合计可节省 600-900 token。
Token 预算控制
// src/agents/skills/workspace.tsconst DEFAULT_MAX_SKILLS_IN_PROMPT = 150;const DEFAULT_MAX_SKILLS_PROMPT_CHARS = 30_000;const DEFAULT_MAX_SKILL_FILE_BYTES = 256_000;// 超出字符限制时,采用二分搜索定位最大可容纳前缀if (!fits(skillsForPrompt)) {let lo = 0, hi = skillsForPrompt.length;while (lo < hi) {const mid = Math.ceil((lo + hi) / 2);if (fits(skillsForPrompt.slice(0, mid))) lo = mid;else hi = mid - 1;}skillsForPrompt = skillsForPrompt.slice(0, lo);}
五、系统提示中的元指令:引导 LLM 正确使用
问题:LLM 拿到 skill 列表后,清楚自己该怎么做吗?
仅有列表还不够——LLM 还需要明确的行为规则。buildSkillsSection() 的职责是将列表与指令一并注入系统提示:
// src/agents/system-prompt.tsfunction buildSkillsSection(params: { skillsPrompt?: string; readToolName: string }) {return ["## Skills (mandatory)","Before replying: scan
该指令的设计包含几个关键要点:
(mandatory)标记为“强制”,意味着 LLM 每次回复前都必须扫描,而非“偶尔参考”。- 明确指定使用
read工具加载处的 SKILL.md,LLM 无需猜测路径。 - “never read more than one skill up front” 规则防止 LLM 一次性读取所有可能相关的 skill,从而避免大量 token 浪费。
- “then follow it” 表示读取后必须遵循其中的指示,而非仅作参考。
最终效果是什么?用户提问「查一下上海天气」→ LLM 扫描摘要 → 匹配到 weather skill 的 description → 调用 read("~/.openclaw/skills/weather/SKILL.md") → 读取完整工作流 → 成功执行 curl "wttr.in/Shanghai?format=3"。
整个过程中,LLM 是主动参与者,而非被动执行脚本的机器。Skill 系统通过“摘要 + 路径”为 LLM 提供恰好够用的决策信息,完整内容仅在真正需要时才加载。
六、/命令:用户显式触发路径
问题:用户更想输入 /weather 上海 而非自然语言
buildWorkspaceSkillCommandSpecs() 扫描所有 user-invocable: true 的 skill(默认为 true),然后为消息平台注册斜杠命令:
// src/auto-reply/skill-commands.ts// /weather → weather skill// /github → github skill// 冲突时自动追加 _2 后缀
命令名还会经过规范化处理:
function sanitizeSkillCommandName(raw: string): string {return raw.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32); // Discord 限制:命令名最长 32 字符}
两种触发模式
用户发送 /weather 上海 后,系统查找 weather 对应的 SkillCommandSpec,然后沿以下两条路径之一执行:
模式一:经过 LLM(默认)
/weather 上海→ resolveSkillCommandInvocation() 识别命令→ 将 "weather 上海" 作为用户消息注入会话→ LLM 正常处理(仍会读取 SKILL.md 并做出决策)
模式二:确定性工具分发(command-dispatch: tool)
若 SKILL.md 的 frontmatter 中声明了:
command-dispatch: toolcommand-tool: execcommand-arg-mode: raw
那么触发将完全绕过 LLM:
/weather 上海→ dispatch.kind === "tool"→ 直接调用 exec 工具,args = "上海"(原样转发)→ LLM 不参与任何决策
这种模式对“输入明确、工具已知、无需推理”的场景非常高效,执行速度更快,且行为完全可预测。
七、沙盒环境下的 skill 同步
当 Agent 在 Docker 沙盒中运行时,skill 文件需要从宿主机同步至容器:
// src/agents/skills/workspace.tsexport async function syncSkillsToWorkspace(params: {sourceWorkspaceDir: string; // 宿主机工作区targetWorkspaceDir: string; // 容器内工作区}) {// 1. 加载宿主机 skill 列表// 2. 清空容器内的 skills/ 目录// 3. 将每个 skill 目录 cp 进容器// 4. 路径安全检查(防止路径遍历攻击)}
同步完成后,容器内的 read 工具读取的是容器内 SKILL.md 副本,而非宿主机路径。resolveSandboxPath() 确保每个 skill 目录名安全,不会通过 ../.. 等名称逃逸到容器外部。
小结:渐近式披露驱动的 LLM 工作流
Skill 系统的核心,其实是一个简洁的设计理念:不将文档转化为代码,而是将文档本身教给 LLM,让 LLM 按文档行事。
| 阶段 | 机制 | 目的 |
|---|---|---|
| 发现 | 六来源扫描 + Map 优先级覆盖 | 允许用户/项目覆盖系统内置 skill |
| 过滤 | bins/env/os 资格检查 | 仅向 LLM 暴露当前环境真正可用的 skill |
| 摘要注入 | name + description + location,字符预算控制 | 以最小 token 开销让 LLM 做出决策 |
| 元指令 | ## Skills (mandatory) + read 工具路径 | 告知 LLM 如何利用这些信息 |
| 渐近式披露 | LLM 决策后主动调用 read(SKILL.md) | 完整文档仅在真正需要时进入上下文 |
| /命令 | buildWorkspaceSkillCommandSpecs() 注册斜杠命令 | 用户显式触发,绕过自然语言推理 |
| 确定性分发 | command-dispatch: tool | 执行路径完全绕过 LLM |
这套设计让 skill 作者只需编写 Markdown,无需了解 LLM 推理细节、工具注册或消息平台差异——一个 SKILL.md 文件,就能使 AI 严格遵循作者的意图行动。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。