摘要 :OpenClaw 默认只能对话;Skills 把「领域说明书」以 目录 + SKILL.md(YAML 头信息 + 正文) 的形式挂进工作区,让模型在需要时再用 read 工具 拉取全文,从而在不动内核的前提下扩展行为边界。本篇对齐仓库 D1 文档与 Agent Skills 社区规范,说明 Built-in Tools / Skills / Plugins 的分层;并基于本地源码 固定 commit 走读 监听目录 → 解析 SKILL.md → 拼装 <available_skills> 提示块 → 会话快照与热更新 的链路,顺带澄清 ClawHub 安装产物 (skills/<slug>/、.clawhub/lock.json)与「技能目录」的关系。
关键词 :OpenClaw;Skills;SKILL.md;ClawHub;会话快照;渐进式披露;源码走读;Agent Skills
姊妹篇 :全系列术语与读源码约定见 OpenClaw 深度解析与源代码导读 · 第1篇:系列导读------术语、版本与读源码方法。
源码版本说明 :下文引用路径均相对于 openclaw/openclaw 仓库根目录;本地阅读使用的 commit 为 0dd4958bc8a78d26b3b526b1f2e63b15110c64a2 (2026-04-11,请以该 SHA 为准对照行号;若你跟踪 main,请自行 git checkout 后再核对)。
关于 Skills 的基本概念,我们之前也出过一系列的博客,参考如下:
| 顺序 | 文章 | 核心内容(通俗说) |
|---|---|---|
| 1 | LangGraph 7 · 技能 Skills | 技能是什么 :文件夹 + SKILL.md,符合 Agent Skills;智能体走 发现 → 选择 → 加载 → 使用,能力不写死在代码里。 |
| 2 | LangGraph 17 · 三级加载与 Token 优化 | 渐进披露(Progressive Disclosure) :L1 只看元数据、L2 拉全文说明、L3 再按需读 references/,在规模化技能库时省 token。 |
| 3 | LangGraph 18 · Skill 四种形态 | 形态与路由:Inline / File-based / External / Meta;任务执行与「技能创作」类需求可以分岔到不同子图。 |
| 4 | LangGraph 19 · SkillToolset | 工具化加载 :list_skills / get_skill_details / load_skill_resource,由 Agent 自主决定何时拉菜单、读详情、打开附录。 |
| 5 | LangGraph 20 · Meta Skills | 让系统自己写技能 :按规范生成 SKILL.md → 静态校验 → 有限轮 LLM 修复 → 落盘 → discover_skills 注册校验;并可与第 19 篇的三工具执行路径放在同一张图里用意图路由切换。 |
| 6 | LangGraph 21 · Skills 6 · 从意图到可上线技能 | 创作流水线 :Capture Intent 收成结构化规格 → 撰写 SKILL.md → 静态校验 + 自动修复 小循环 → evals 冒烟 (Executor 按技能作答、Judge 按 rubric 打分)→ 不通过则 refine 大修订 → 落盘 + discover_skills ;把「格式合规」推进到「可测、可迭代、能上线」。 |
1 为什么要单独讲 Skills?
可以把 OpenClaw 想成一家餐厅:Gateway 负责排号与前厅调度,Brain 是主厨的脑子,Hands 是真炒菜的手。Skills 则像「预制酱料包 + 菜谱卡片」 ------不改造灶具(内置工具),也不把整条供应链搬进后厨(MCP 插件),而是告诉模型:在什么场景下、去读哪一份说明、相对路径如何解析到磁盘上的真实文件。
💡 理解要点 :Skills 解决的是 「如何把人类可维护的操作知识,安全地交给模型按需加载」 ;它不是又一个「函数注册表」,而是 以文档为边界的扩展单元。
2 OpenClaw 里的 Skills 是什么?
OpenClaw 通过 与 Agent Skills 兼容的技能目录 教模型如何使用工具。每个技能是一个文件夹,内含带 YAML frontmatter 的 SKILL.md 与说明正文;运行时再从多个根目录 发现 → 过滤 → 把「目录级摘要」写进系统侧提示 ,需要细节时由模型用 read 打开完整文件。
这与「手机上的 App」类比仍然贴切:
- 每个技能是自包含的扩展,描述能力边界与使用方式。
- 可独立安装、更新、移除;ClawHub 提供社区分发(类似 npm 之于 JavaScript 包)。
- 技能通常落在工作区的
skills/下(另有用户级、项目级、捆绑包等来源,见下一节)。
🔍 实际例子 :为团队写一个「内部 API 调用规范」技能:frontmatter 里写清 name / description,正文里写步骤、错误码、示例 curl;模型只在任务命中描述时才去读全文,避免每轮对话都把几千字塞进上下文。
3 三种能力类型:Built-in Tools / Skills / Plugins
| 类型 | 含义 | 典型来源 |
|---|---|---|
| Built-in Tools | 核心能力(读文件、Shell、web_fetch 等) |
随 OpenClaw 安装提供 |
| Skills | 以 SKILL.md 为主的原生扩展 |
工作区自建、ClawHub 安装、捆绑技能等 |
| Plugins | 基于 MCP 的集成 | 任意兼容 MCP 的服务端 |
对多数用户而言,Skills 是把「操作知识」产品化的主路径;Plugins 更适合把外部系统以工具形态接进来。官方文档在 Skills(OpenClaw) 中把 位置(从哪加载) 与 可见性(是否进 allowlist) 分成两维控制------后文在「多 Agent」一节会点到 agents.defaults.skills。
4 渐进式披露:为什么「只给目录」是对的?
Agent Skills 文档 把 渐进式披露(progressive disclosure) 概括成三层:
- 目录层:会话开始时只看到技能名与简短描述(控制 token)。
- 指令层 :任务匹配时再用 read 打开完整
SKILL.md。 - 资源层:脚本、参考文档、附件按需在后续步骤加载。
🔍 实际例子(真实技能 seo-checklist) :这是一个放在技能目录里的 「SEO 审查说明书」 ------当用户要检查或优化 博客文章、落地页等内容的搜索引擎表现时,模型按技能约定工作:先用 YAML 头里的 name / description 判断任务是否属于「SEO 场景」 ;命中后再打开 SKILL.md,按清单逐项看 标题长度与关键词位置、元描述、H2/H3 层级、首段与关键词密度、段落可读性、内外链 等;若还需要更细的规则(例如关键词研究流程、E-E-A-T、技术 SEO、链接策略),再按需 读取同目录下的 references/seo-guidelines.md,避免一上来就把长附录塞进系统提示。某些编排里正文会写「用资源加载工具读附录」;在 OpenClaw 里,对附录文件再执行一次 read(解析为技能目录内的绝对路径) 即可,语义仍是 先读 SKILL.md、再读附录。
-
L1(目录层) :提示里只出现
seo-checklist+ 一句description(「SEO 优化检查清单...当用户需要检查或优化内容的 SEO 时使用」),模型用它判断要不要进入 SEO 审查流程,不必 预载 checklist 全文。py--- name: seo-checklist description: SEO 优化检查清单,适用于博客、网页等内容。涵盖标题、元描述、标题层级、关键词布局、可读性等。当用户需要检查或优化内容的 SEO 时使用。 --- -
L2(指令层) :判定相关后,read 打开完整
SKILL.md,按章节化步骤产出审查结论或改写建议。py# SEO 检查清单 当用户请求检查或优化内容的 SEO 时,请先使用 load_skill_resource 工具读取 `references/seo-guidelines.md` 中的详细指南,并按以下 checklist 逐项审核: ## 1. 标题(Title) - 长度 50-60 字符 - 主关键词靠近开头 - 避免堆砌关键词 ## 2. 元描述(Meta Description) - 长度 150-160 字符 - 包含行动号召(CTA) - 自然融入主关键词 ## 3. 标题层级(H2/H3) - 清晰的层级结构 - 2-3 个标题中包含目标关键词 - 避免跳级(如 H2 直接接 H4) ... -
L3(资源层) :仅当需要细则时,再 read
references/seo-guidelines.md(关键词研究、内容质量、技术 SEO 等),把 token 花在「真的要用到附录」的那一刻。py# SEO 详细指南(L3 资源) 本文件为 seo-checklist 技能的补充参考,仅在需要更细致规则时加载。 ## 关键词研究 - 使用工具(如 Google Keyword Planner)识别搜索量适中、竞争度可控的词 - 长尾关键词通常更容易排名 - 关注用户意图:信息型、交易型、导航型 ## 内容质量 - E-E-A-T:Experience, Expertise, Authoritativeness, Trustworthiness - 原创、深入、对用户有价值 - 定期更新以保持时效性 ...
AgentScope 多智能体文档 也用「Skills (Progressive Disclosure)」一词强调:不要把所有技能全文一次性塞进系统提示 。OpenClaw 的实现与此一致:默认注入的是 XML 形态的 <available_skills> 列表 ,并明确提示模型用 read 加载技能文件(见 §8.3「formatSkillsForPrompt」)。
💡 理解要点 :Skills 的性能与成本之间的关系,本质是 「摘要进提示、正文走工具」;若你把 20 个技能的正文都手工贴进 system prompt,就退化成「巨型静态提示词工程」,失去了扩展平面的意义。
4.1 Skills 在 Router → Brain 流程中的实际使用(OpenClaw 视角)
渐进式披露不是静态概念,而是在 Router → Brain → 回复 的完整数据流中分阶段按需触发的。以下结构图展示了 Skills 各部分内容在何时、如何被使用:
Brain 层:LLM 推理
L3: Resources(资源层)
L2: Instructions(指令层)
L1: Catalog(目录层)
磁盘层:Skill 包结构
我需要 weather skill
需要调用 API
需要查阅文档
返回结果
skills/weather/
**SKILL.md**
YAML frontmatter + Markdown body
scripts/
query_api.py
references/
api-docs.md
只读 **frontmatter**
name + description + version
技能目录
<available_skills>
读取 **SKILL.md body**
去掉 frontmatter 的正文
注入 System Prompt
作为技能上下文
读取 references/
执行 scripts/
LLM 推理
输出工具调用
read_skill / exec_script
三阶段渐进式使用(与 Router/Brain 流程对应):
| 阶段 | 使用到的 Skill 部分 | 触发时机 | 目的 | Token 控制 |
|---|---|---|---|---|
| L1 Catalog | SKILL.md 的 frontmatter(YAML 头部) |
每次请求初始化(Brain 组装 Prompt 时) | 让 LLM 知道"有哪些技能可用" | 极小(仅 name + description,约 500 tokens) |
| L2 Instructions | SKILL.md 的 body(正文,去掉 frontmatter) |
LLM 决定使用该 Skill 时 (输出 read_skill 工具调用) |
告诉 LLM"如何使用这个技能" | 中等(技能说明书,约 2000 tokens) |
| L3 Resources | references/ 文档、scripts/ 脚本 |
执行阶段按需加载 (exec_script 或再次 read) |
提供额外的参考资料或可执行代码 | 按需(引用文件大小,或脚本返回结果) |
实际工作流程示例(用户输入:"查北京天气,如果下雨提醒我今晚关窗"):
Step 1: Router 接收消息
├─ 判定:需要 Brain 处理(非命令、非简单问候)
↓
Step 2: Brain 初始化(L1 - Catalog)
├─ 扫描 skills/ 目录,读取每个 SKILL.md 的 frontmatter
├─ 生成 <available_skills> XML(仅含 name + description)
└─ 注入 System Prompt(Token:~500)
↓
Step 3: LLM 第一轮推理
├─ 输入:System Prompt + <available_skills> + 用户消息
├─ LLM 决策:"我需要 weather-query 和 reminder 两个 skill"
└─ 输出工具调用:{ tool: "read_skill", args: { skill: "weather-query" } }
↓
Step 4: 加载 Skill 详情(L2 - Instructions)
├─ 读取 weather-query/SKILL.md 的 body(完整说明书)
└─ 注入到当前上下文(Token:~2000)
↓
Step 5: LLM 第二轮推理
├─ 现在 LLM 知道 "怎么查天气"
└─ 输出工具调用:{ tool: "exec_script", args: { script: "query_api.py", params: "北京" } }
↓
Step 6: 执行与加载资源(L3 - Resources)
├─ 执行 scripts/query_api.py 北京
├─ 返回结果:{ "city": "北京", "weather": "rain", "temp": "18°C" }
└─ 结果返回给 LLM
↓
Step 7-8: 设置 reminder(重复 L2/L3 流程)→ 生成最终回复
📎 与第4篇 Router 的关联 :Router 决定"消息是否进 Brain";一旦进入 Brain,上述 L1/L2/L3 流程自动触发。disable-model-invocation: true 的 Skills 不会出现在 L1 的 <available_skills> 中,只能通过用户显式命令 (如 /weather 北京)触发,详见 §8.3。
5 技能从哪些目录来?(官方优先级)
以下与 D1 文档一致,便于你在磁盘上「按图索骥」(高优先级在前):
<workspace>/skills → <workspace>/.agents/skills → ~/.agents/skills → ~/.openclaw/skills(托管/共享)→ 捆绑技能 → skills.load.extraDirs(最低)
多 Agent 时,每个 Agent 有自己的 workspace ,因此 <workspace>/skills 天然是 按 Agent 隔离 的;共享机器级技能则落在 ~/.openclaw/skills 或通过 extraDirs 挂载公共包。详见 docs/tools/skills.md 的 Locations and precedence。
6 ClawHub 与技能生命周期(安装 / 更新 / 移除)
6.1 ClawHub 是什么?
ClawHub 是 OpenClaw 的公开技能注册表 (另有组织仓库 openclaw/clawhub)。可以把它想成 「面向 Agent 技能的 npm」 :社区上传技能包,你用命令行或对话里的指令 搜索、安装、升级、卸载,而不必自己维护下载链接与解压脚本。
官方说明见仓库 D1:Skills 文档 · ClawHub 一节(含 openclaw skills ... 与独立 clawhub CLI 的分工)。
6.2 安装:对话里怎么敲、终端里怎么敲?
常见入口有两类(不同客户端可能只暴露其中一种,以你当前 UI/文档为准):
- 对话(slash)里安装------适合「正在聊天、顺手装一个」:
text
/skills install @author/skill-name
- 本机 CLI(官方文档主推,适合脚本与 CI):
bash
openclaw skills install <skill-slug>
安装完成后,磁盘上通常会发生什么?
- 在 当前 Agent 的 workspace 下出现
<workspace>/skills/<slug>/(或等价布局) 的技能目录,内含SKILL.md(及脚本、参考文档等附件)。 - 若该技能需要 API Key、开关、渠道白名单 等配置,产品侧往往还会在
openclaw.json(默认路径多为~/.openclaw/openclaw.json,可用$OPENCLAW_CONFIG_PATH覆盖)里登记一项,便于 启用/禁用 与 注入环境变量。示例(结构与官方叙述一致,字段以你本机生成结果为准):
json
{
"skills": {
"@niceperson/brave-web-search": {
"enabled": true,
"config": {
"apiKey": "${BRAVE_API_KEY}"
}
}
}
}
💡 理解要点 :SKILL.md 教模型「怎么用」 ;openclaw.json 里那一段教运行时「给不给你用、密钥从哪来」------两条线经常同时出现,但职责不同。
6.3 更新:单包与「一键全更」
对话里:
text
/skills update @author/skill-name
/skills update --all
终端里 (与 D1 一致):openclaw skills update ...、openclaw skills update --all 等。
语义上可以记三条:
- 单技能更新 :从注册表(或上游源)拉取 该 slug 的最新包 ,覆盖
<workspace>/skills/<slug>/下文件;openclaw.json里该项的enabled、config等通常保留(除非你手动改过同步策略)。 - 全部更新 :对锁文件/已安装集合里登记的多个技能 逐个重复上述过程。
- 更新前 :尽量阅读 changelog / 权限说明;技能升级可能改行为、加网络依赖或破坏既有工作流。
6.4 移除
对话里:
text
/skills remove @author/skill-name
语义上:删除该技能目录 ,并在配置里 去掉对应 skills 条目(具体以你客户端生成的 diff 为准),避免模型继续看到已卸载能力。
6.5 一次操作会动到哪些地方?(小结表)
下面把 「用户操作 → 落盘 → 模型何时看见」 压成一张表。第三列刻意 不写 旧叙述里常见的聚合文件 SKILLS.md :在 openclaw/openclaw 当前 main 技能管线 中,模型侧可见性 来自 扫描各 SKILL.md + 拼 <available_skills> + 会话 skillsSnapshot (见 §8.3~§8.5);安装器写入的 .clawhub/lock.json 则服务 版本与复现。
| 操作 | 对工作区(skills/ 等) |
对 openclaw.json(若使用) |
对「下一轮模型是否看见」 |
|---|---|---|---|
| install | 新增 <workspace>/skills/<slug>/(含 SKILL.md) |
常新增 skills 子项(enabled、config...) |
文件一落地 ,watcher 会 bump 快照版本;同会话 下一用户轮 起可重建 skillsSnapshot(§8.5) |
| update | 覆盖该 slug 目录内容 | 多数情况下保留 原 config / enabled |
同上:内容变 → 版本 bump → 快照可刷新 |
| remove | 删除目录 | 删除对应配置项 | 目录消失 → 版本 bump → 新快照里不再列出该技能 |
6.6 源码侧:安装路径、锁文件与 SKILL.md 变体
src/agents/skills-clawhub.ts 把 ClawHub 安装目录 规范到 <workspace>/skills/<slug>/ ,解压后调用 ensureSkillRoot :为兼容历史包名,会依次探测 SKILL.md / skill.md / skills.md / SKILL.MD ;若都不存在则抛错 downloaded archive is missing SKILL.md ------规范文件名仍是 SKILL.md ,其余仅为兼容。(节选,commit 0dd4958bc8a78d26b3b526b1f2e63b15110c64a2)
ts
// openclaw/src/agents/skills-clawhub.ts --- ensureSkillRoot
async function ensureSkillRoot(rootDir: string): Promise<void> {
for (const candidate of ["SKILL.md", "skill.md", "skills.md", "SKILL.MD"]) {
if (await fileExists(path.join(rootDir, candidate))) {
return;
}
}
throw new Error("downloaded archive is missing SKILL.md");
}
锁文件与来源元数据 :<workspace>/.clawhub/lock.json 记录已安装 slug 与版本时间;各技能目录下可有 .clawhub/origin.json (与 skills-clawhub.ts 前半部类型一致),用于 供应链审计与「从哪一版 ClawHub 来」 。这与运行时 formatSkillsForPrompt 拼出来的 <available_skills> 、以及会话里缓存的 skillsSnapshot.prompt 仍是 两条线 :前者回答 「装了什么、能不能复现」 ,后者回答 「这一轮提示里贴哪张菜单」。
7 LangGraph 式 Skills 案例演示与 OpenClaw 对照
在讲 OpenClaw 源码(§8)之前,本节把 「Agent Skills 规范 + 发现 → 选择 → 加载 → 使用」 落到一个 可下载、可运行 的工程里:技能全部是 「子目录 + SKILL.md」 ;Python 里用 skill_loader.py 拆成四个函数,main.py 按固定顺序调用;需要 LLM 时走 use_skill_with_llm (LangChain ChatPromptTemplate → ChatOpenAI → StrOutputParser)。这与 LangGraph 系列里「先把数据流讲清楚,再决定要不要画成 StateGraph」 的教法一致------本示例甚至没有强制引入图编排 :流水线本身就是一张 有向无环图(DAG),便于你对照 OpenClaw 里「谁负责发现、谁负责注入提示、谁负责 read」。
示例源代码 :LangGraph Skills 示例。解压后请以包内 demo_codes/README.md 为准对照目录结构;下文文件名与职责与该包一致。
💡 理解要点 :本节的定位是 教学脚手架 ------一个技能根目录 + 一个 loader + 一个入口脚本 ;不是 OpenClaw Gateway 的产品形态。读完 §7.9 再进入 §8,可避免把 SKILLS.md 等旧叙述误当成内核契约。
7.1 架构总览(Mermaid)与每个节点在干什么
下面这张图与示例代码中的 数据流 一一对应:从左到右,磁盘上的技能包 → 四次纯函数/链式调用 → 终端上的自然语言输出。
skills_library/
discover_skills
select_skill_for_task
load_skill
use_skill_with_llm
输出结果
各节点含义(读图时请 从左到右 跟随数据形态变化):
| 节点 | 名称(代码中) | 输入 / 输出(直觉) | 职责一句话 |
|---|---|---|---|
| A | skills_library/ |
输入:无(磁盘根);输出:若干子目录 | 技能包仓库 :每个子文件夹 必须 含 SKILL.md (YAML frontmatter + Markdown 正文);可从 SkillMD、Anthropic anthropics/skills 等来源解压放入。 |
| B | discover_skills |
入:skills_root(可选);出:List[Dict] 元数据列表 |
发现(L1) :只读每个 SKILL.md 的 frontmatter ,得到 name、description 等;不读正文,控制 token,与渐进披露第一层对齐。 |
| C | select_skill_for_task |
入:用户任务字符串 + 上一步列表;出:一个 skill_name 或 None |
选择 :在「已发现的技能名集合」里做 路由 。示例用 关键词表 (如「总结」→ summarize);生产可换成 LLM 分类器 或 embedding 相似度。 |
| D | load_skill |
入:skill_name;出:(full_content, body) |
加载(L2) :读取 完整 SKILL.md ;body 为 去掉 frontmatter 后的正文,准备塞进 LLM。若目录不存在则返回空串,由入口逻辑报错退出。 |
| E | use_skill_with_llm |
入:skill_name、skill_instructions(即 body)、user_input 等;出:模型生成的字符串 |
使用 :把 「技能说明书 + 用户任务」 填进 system / user 模板,调用 ChatOpenAI 执行;API Key 等可由 config_parser 从 .env 注入。 |
| F | (标准输出) | 入:模型返回;出:人眼可读答案 | 呈现 :main.py 或 notebook 打印;无状态,不持久化会话。 |
与 OpenClaw 的粗粒度映射 (细节以 §8 为准):A≈多根 skills/ 目录 ;B≈**local-loader + workspace 拼 catalog**;C≈模型自行决定 read 哪个 SKILL.md (外加 agents.*.skills allowlist);D≈read 工具打开路径 ;E≈Brain 里那一轮 tool/LLM 循环 ;F≈通道回包。
7.2 技能库节点:demo_codes/skills_library/
- 路径 :资源包内
demo_codes/skills_library/。 - 约定 :一级子目录 = 一个技能 ;每个子目录下
SKILL.md为硬入口文件名(与 Agent Skills 一致)。 - 准备技能包 :按包内
skills_library/README.md,从 SkillMD、Anthropic 官方仓库等下载 ZIP 或复制子文件夹到上述目录(例如summarize/、pdf/)。
7.3 发现节点:discover_skills(只抬「菜单」,不上菜)
作用 :遍历技能根下子目录,仅解析 frontmatter ,跳过无 SKILL.md 或解析失败的目录;返回的列表即 「当前机器上可选技能索引」。
python
def discover_skills(skills_root: Optional[Path] = None) -> List[Dict[str, str]]:
root = Path(skills_root) if skills_root else DEFAULT_SKILLS_LIBRARY
if not root.is_dir():
return []
result: List[Dict[str, str]] = []
for path in sorted(root.iterdir()):
if not path.is_dir():
continue
skill_md = path / SKILL_FILENAME # 通常为 SKILL.md
if not skill_md.is_file():
continue
raw = skill_md.read_text(encoding="utf-8", errors="replace")
meta = _parse_frontmatter(raw)
if meta.get("name"):
result.append(meta)
return result
典型输出形态 (节选):打印多行 - name: description 前 60 字...,便于人眼确认 L1 元数据是否加载成功。
7.4 选择节点:select_skill_for_task(把自然语言路由到技能名)
作用 :给定用户任务文本,在 已发现 的技能里挑出 一个 name 。示例实现刻意 简单、可预测 :先 小写 + strip ,再按 关键词 → 目标技能名 扫描;命中则返回该名;否则 退回列表第一个 (教学上避免「无技能可选」卡死,生产应显式处理 None)。
python
keywords: List[Tuple[List[str], str]] = [
(["总结", "摘要", "summarize", "压缩", "汇总"], "summarize"),
(["pdf", "文档"], "pdf"),
(["doc", "word", "docx"], "docx"),
(["协作", "coauthor", "文档协作"], "doc-coauthoring"),
]
# ... 命中则返回对应 discovered 项中的 name;否则返回 discovered[0]
🔍 实际例子 :任务写「请总结下面这段话...」→ 命中「总结」类关键词 → 若库中存在 summarize 技能则选中它;若你库里只有 doc-coauthoring,则可能落到 默认第一个 ------这正是 关键词路由的局限 ,也解释为何生产常改用 LLM 选择。
7.5 加载节点:load_skill(按需读全文,拆出正文)
作用 :在 已选定 skill_name 之后,才打开 skills_library/<skill_name>/SKILL.md 全文;返回 (full, body) ,其中 body 去掉 YAML 头,供下游 只把「可执行说明」塞进 prompt,减少无关节点污染 system 槽位。
python
def load_skill(skill_name: str, skills_root: Optional[Path] = None) -> Tuple[str, str]:
root = Path(skills_root) if skills_root else DEFAULT_SKILLS_LIBRARY
skill_md = (root / skill_name) / SKILL_FILENAME
if not skill_md.is_file():
return "", ""
try:
full = skill_md.read_text(encoding="utf-8", errors="replace")
except OSError:
return "", ""
body = full
m = FRONTMATTER_PATTERN.match(full)
if m:
body = full[m.end() :].strip()
return full, body
若文件不存在或读失败,示例返回空串并由 入口脚本 raise SystemExit(...) 提示用户检查路径------这是 「加载失败 = 硬错误」 的显式风格,便于初学者排障。
7.6 使用节点:use_skill_with_llm(LangChain 把「说明书 + 用户话」交给模型)
作用 :把 load_skill 得到的 body 作为 技能说明 ,与用户任务一起交给 ChatOpenAI ;依赖 langchain-openai / langchain-core (未安装则返回友好提示字符串)。API Key / BASE_URL / MODEL 可由 config_parser.skills_config 从 .env 读取,与主示例其它篇一致。
python
prompt = ChatPromptTemplate.from_messages([
("system", "你正在使用名为「{skill_name}」的 Agent Skill。请严格按照以下技能说明执行用户请求。\n\n技能说明:\n{skill_instructions}"),
("user", "{user_input}"),
])
chain = prompt | llm | StrOutputParser()
return (chain.invoke({...}) or "").strip()
💡 理解要点 :这一步等价于 OpenClaw Brain 里「把 read 到的 SKILL 正文并入上下文再让模型推理」 ------差别在于:这里是 单次链式调用 ;OpenClaw 里往往还有 工具循环、沙箱、多轮会话。
7.7 入口节点 main.py 与配套文件
| 文件 | 节点角色 | 说明 |
|---|---|---|
main.py |
编排器(Orchestrator) | 严格顺序 :discover_skills → select_skill_for_task → load_skill → use_skill_with_llm;支持 python main.py "任务..." 从命令行传入任务。 |
skill_loader.py |
B~E 的实现载体 | 含发现、选择、加载、使用的全部 Python 逻辑;可与单元测试直接对打。 |
config_parser.py |
配置适配 | 从 .env 读取 OPENAI_API_KEY / DASHSCOPE_API_KEY、BASE_URL、MODEL,供节点 E 使用。 |
requirements.txt |
依赖清单 | langchain-openai、langchain-core、python-dotenv 等。 |
main.ipynb |
交互式走读 | 逐步打印「已发现技能」「选中技能」「正文长度」等,适合课堂演示。 |
README.md |
人类文档 | 与 CSDN 包 内说明互补,以本地解压版本为准。 |
7.8 如何运行(与示例 README 一致)
- 进入
demo_codes,创建虚拟环境并pip install -r requirements.txt。 - 配置
.env(至少一种模型 Key;可选BASE_URL、MODEL)。 - 按 §7.2 在
skills_library/下放入至少一个合规技能目录。 - 执行
python main.py或python main.py "你的任务描述";或打开main.ipynb逐步运行节点 B~E。
7.9 与 OpenClaw 运行时对照:一张表纠正常见误读
下面左列概括 本教学脚手架 的设定,右列对齐 OpenClaw 当前主线的真实行为 (以 openclaw/openclaw 的 src/ + D1 docs/tools/skills.md 为准)。其中关于 SKILLS.md 的一行,专门回应一些旧对照表 里的写法:它们常把 OpenClaw 的「能力可见性」说成 「安装器维护一个总表 SKILLS.md」 ------这与 当前内核实现 并不一致,读源码时应以 「扫描各 SKILL.md + 拼 <available_skills> +(可选)会话 skillsSnapshot」 为准(详见 §8)。
| 维度 | LangGraph 式小演示(「四门功课」脚手架) | OpenClaw 运行时 |
|---|---|---|
| 技能形态 | 文件夹 + SKILL.md(YAML frontmatter + 正文) |
主流一致;部分技能包还可带 manifest / 脚本等(以具体技能与 D1 为准) |
| 技能根目录 | 示例里常固定为单一 skills_library/ |
多根路径 + 显式优先级 :<workspace>/skills、<workspace>/.agents/skills、~/.agents/skills、~/.openclaw/skills、捆绑包、skills.load.extraDirs 等(§5) |
| 注册 / 配置 | 往往 没有 与内核同级的全局 JSON;「有没有子目录」≈「装没装」 | openclaw.json 中的 skills/agents.defaults.skills/agents.list[].skills 等,管 启用、密钥、按 Agent 的 allowlist(§6) |
| 能力可见性 | discover_skills() 的返回列表(打印或内存),不承诺落盘成某个总表文件 |
运行时拼接 formatSkillsForPrompt / formatSkillsCompact 的 <available_skills> XML ,并写入会话 skillsSnapshot.prompt 缓存;不应 简单映射为「必须存在根目录聚合 SKILLS.md」 |
| 发现→选择→加载→使用 | 四函数显式调用,顺序由 main.py 写死 |
同思路、不同挂载点 :发现/过滤在 workspace.ts + local-loader.ts ;加载由模型 read;Gateway 把「目录摘要」提前注入提示(§8) |
| 安装 / 更新 / 移除 | 多为 手工拷贝/删除 子目录 | /skills ... 或 openclaw skills ... + ClawHub ;并写 .clawhub/lock.json / origin.json(§6) |
| 热更新 | 通常需 重新跑脚本 或重启进程才再 discover |
ensureSkillsWatcher + bumpSkillsSnapshotVersion ,让会话快照在 后续用户轮 可失效重建(§8.5) |
8 源码:从「磁盘上的 SKILL.md」到「会话里的技能快照 Snapshot」
8.1 只监听技能入口文件
src/agents/skills/refresh.ts 里,resolveWatchTargets 明确注释:技能由 SKILL.md 定义,只监听这些文件,避免监听无关大目录耗尽文件描述符:
ts
// openclaw/src/agents/skills/refresh.ts --- resolveWatchTargets(节选)
function resolveWatchTargets(workspaceDir: string, config?: OpenClawConfig): string[] {
// Skills are defined by SKILL.md; watch only those files to avoid traversing
// or watching unrelated large trees (e.g. datasets) that can exhaust FDs.
const targets = new Set<string>();
for (const root of resolveWatchPaths(workspaceDir, config)) {
const globRoot = toWatchGlobRoot(root);
targets.add(`${globRoot}/SKILL.md`);
targets.add(`${globRoot}/*/SKILL.md`);
}
return Array.from(targets).toSorted();
}
resolveWatchPaths 则把 skills/、.agents/skills/、~/.openclaw/skills、~/.agents/skills、skills.load.extraDirs、插件技能目录 等拼成监听根集合(同文件 resolveWatchPaths)。
💡 理解要点 :热更新不是「监听整个 workspace」,而是 围绕技能入口文件的稀疏监听。
8.2 本地加载:openVerifiedFileSync + frontmatter + 最小必填字段
src/agents/skills/local-loader.ts 在读取 SKILL.md 时使用 openVerifiedFileSync ,并校验 真实路径落在技能根内 ;随后解析 frontmatter,并要求 name 与 description 非空,否则该目录不算有效技能:
ts
// openclaw/src/agents/skills/local-loader.ts --- loadSingleSkillDirectory(节选)
function loadSingleSkillDirectory(params: {
skillDir: string;
source: string;
rootRealPath: string;
maxBytes?: number;
}): Skill | null {
const skillFilePath = path.join(params.skillDir, "SKILL.md");
const raw = readSkillFileSync({
rootRealPath: params.rootRealPath,
filePath: skillFilePath,
maxBytes: params.maxBytes,
});
if (!raw) {
return null;
}
let frontmatter: Record<string, string>;
try {
frontmatter = parseFrontmatter(raw);
} catch {
return null;
}
const fallbackName = path.basename(params.skillDir).trim();
const name = frontmatter.name?.trim() || fallbackName;
const description = frontmatter.description?.trim();
if (!name || !description) {
return null;
}
// ... 后续组装 Skill 对象
这与 Agent Skills 规范里对 元数据可发现性 的要求同向:没有描述,就不该出现在自动发现列表里,否则模型只能瞎猜何时加载。
8.3 拼进模型上下文的 XML:formatSkillsForPrompt
src/agents/skills/skill-contract.ts 中的 formatSkillsForPrompt 负责把技能数组渲染成 与上游 Agent Skills 格式化器字节对齐 的 XML(文件头注释写明意图),并再次强调 用 read 工具加载 与 相对路径相对于技能目录解析:
ts
// openclaw/src/agents/skills/skill-contract.ts --- formatSkillsForPrompt(节选)
export function formatSkillsForPrompt(skills: Skill[]): string {
if (skills.length === 0) {
return "";
}
const lines = [
"\n\nThe following skills provide specialized instructions for specific tasks.",
"Use the read tool to load a skill's file when the task matches its description.",
"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>",
];
for (const skill of skills) {
lines.push(" <skill>");
lines.push(` <name>${escapeXml(skill.name)}</name>`);
lines.push(` <description>${escapeXml(skill.description)}</description>`);
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
lines.push(" </skill>");
}
lines.push("</available_skills>");
return lines.join("\n");
}
同目录下的 compact-format.test.ts 用 @mariozechner/pi-coding-agent 的上游实现做对齐测试------说明 OpenClaw 把自己定位在 「兼容生态的事实标准」 上,而不是另起一套提示语文案。
8.4 Token 预算:全量 → 紧凑(仅 name+location)→ 截断
当技能很多时,src/agents/skills/workspace.ts 的 applySkillsPromptLimits 会依次尝试:
- 保留 完整
formatSkillsForPrompt; - 超长则降级为
formatSkillsCompact(去掉 description); - 仍超长则 二分前缀 找到能塞进预算的最大子集,并附加
openclaw skills check审计提示。
这保证了 「先丢描述、再丢条目」 的降级顺序,与渐进式披露理念一致:宁可少给摘要,也不要静默把技能整个消失而不留痕迹。
下列摘自 OpenClaw 仓库 src/agents/skills/workspace.ts (与篇首 pin 的 commit 0dd4958bc8a78d26b3b526b1f2e63b15110c64a2 一致):先看 formatSkillsCompact (仅保留 name + location),再看 applySkillsPromptLimits 如何在「全量 XML 放不下 → 紧凑仍放不下 → 二分截断前缀」之间切换;最后在 resolveWorkspaceSkillPromptState 里拼接 openclaw skills check 提示行。
ts
// formatSkillsCompact --- 无 description,先保住「还有哪些技能」
export function formatSkillsCompact(skills: Skill[]): string {
if (skills.length === 0) return "";
const lines = [
"\n\nThe 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>",
];
for (const skill of skills) {
lines.push(" <skill>");
lines.push(` <name>${escapeXml(skill.name)}</name>`);
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
lines.push(" </skill>");
}
lines.push("</available_skills>");
return lines.join("\n");
}
const COMPACT_WARNING_OVERHEAD = 150;
function applySkillsPromptLimits(params: { skills: Skill[]; config?: OpenClawConfig }): {
skillsForPrompt: Skill[];
truncated: boolean;
compact: boolean;
} {
const limits = resolveSkillsLimits(params.config);
const total = params.skills.length;
const byCount = params.skills.slice(0, Math.max(0, limits.maxSkillsInPrompt));
let skillsForPrompt = byCount;
let truncated = total > byCount.length;
let compact = false;
const fitsFull = (skills: Skill[]): boolean =>
formatSkillsForPrompt(skills).length <= limits.maxSkillsPromptChars;
const compactBudget = limits.maxSkillsPromptChars - COMPACT_WARNING_OVERHEAD;
const fitsCompact = (skills: Skill[]): boolean =>
formatSkillsCompact(skills).length <= compactBudget;
if (!fitsFull(skillsForPrompt)) {
if (fitsCompact(skillsForPrompt)) {
compact = true;
} else {
compact = true;
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);
truncated = true;
}
}
return { skillsForPrompt, truncated, compact };
}
ts
// resolveWorkspaceSkillPromptState --- 截断/紧凑时追加审计提示,再选择 formatter
const truncationNote = truncated
? `⚠️ Skills truncated: included ${skillsForPrompt.length} of ${resolvedSkills.length}${compact ? " (compact format, descriptions omitted)" : ""}. Run \`openclaw skills check\` to audit.`
: compact
? `⚠️ Skills catalog using compact format (descriptions omitted). Run \`openclaw skills check\` to audit.`
: "";
const prompt = [
remoteNote,
truncationNote,
compact ? formatSkillsCompact(skillsForPrompt) : formatSkillsForPrompt(skillsForPrompt),
]
.filter(Boolean)
.join("\n");
8.5 会话首包与快照版本:session-updates.ts
这一节回答一个朴素问题:技能目录里的 SKILL.md 被人改了、或新装了一个技能之后,正在聊天里的 Agent 什么时候会「看见」新目录? 若每一轮对话都重新扫描整个 skills/ 树并重新拼一大段 XML,磁盘 I/O 和 CPU 会浪费 ;若只在进程启动时扫一次,热更新又没了 。OpenClaw 的做法是:把「当时拼好的技能提示块」缓存在会话条目里,叫 skillsSnapshot;再用一个单调递增的「技能快照版本号」判断缓存是否过期。
8.5.1 三个名词,先对齐
| 名词 | 你可以把它想成... | 在代码里做什么 |
|---|---|---|
| Session(会话) | 一条「和用户连着聊」的上下文线,对应会话存储里的一条记录 | SessionEntry 里除了 sessionId、消息指针等,还可以挂 skillsSnapshot |
skillsSnapshot |
一张已经算好的「技能菜单贴纸」 :里面有拼进系统侧的 prompt 字符串 (即 §8.4 那套 XML/紧凑格式 + 可能的截断提示)、技能名列表、以及生成时的 version 等 |
由 buildWorkspaceSkillSnapshot 算出来,写回会话条目,后续轮次能 复用 |
快照 version |
菜单贴纸左上角印的批次号;磁盘上任意技能相关变更会把「批次号」加大 | 读 getSkillsSnapshotVersion(workspaceDir) ;变更时由 watcher 等路径调用 bumpSkillsSnapshotVersion (见 src/agents/skills/refresh-state.ts) |
💡 理解要点 :skillsSnapshot 不是 SKILL.md 原文缓存 ,而是「已经格式化、可能已截断、准备塞进模型上下文的那一段说明文字 」的快照;模型真要读某技能细节,仍然走 read 打开文件(§8.3)。
8.5.2 ensureSkillsWatcher:谁在「盯着」技能文件?
src/agents/skills/refresh.ts 里的 ensureSkillsWatcher 会在 每个 workspace 上挂一层 稀疏文件监听 (主要看各技能根下的 SKILL.md / */SKILL.md,见 §8.1)。当文件有增删改,监听逻辑会触发 bumpSkillsSnapshotVersion({ workspaceDir, ... }) ------等价于通知全进程:「这个工作区的技能集合变了,批次号 +1」。
src/auto-reply/reply/session-updates.ts 导出的 ensureSkillSnapshot (注意名字里带 Skill)在每一轮需要技能快照时 先调用 ensureSkillsWatcher,保证:你改文件的那一刻起,版本号已经准备好;下一轮会话逻辑就能发现「贴纸过期」。
8.5.3 何时重算快照?shouldRefreshSnapshotForVersion + 过滤器
核心判断在 ensureSkillSnapshot 里(节选如下;逻辑以你 pin 的 commit 为准):
ts
// src/auto-reply/reply/session-updates.ts --- ensureSkillSnapshot(节选)
const snapshotVersion = getSkillsSnapshotVersion(workspaceDir);
const existingSnapshot = nextEntry?.skillsSnapshot;
ensureSkillsWatcher({ workspaceDir, config: cfg });
const shouldRefreshSnapshot =
shouldRefreshSnapshotForVersion(existingSnapshot?.version, snapshotVersion) ||
!matchesSkillFilter(existingSnapshot?.skillFilter, skillFilter);
const buildSnapshot = () =>
buildWorkspaceSkillSnapshot(workspaceDir, {
config: cfg,
agentId: sessionAgentId,
skillFilter,
eligibility: { remote: remoteEligibility },
snapshotVersion,
});
shouldRefreshSnapshotForVersion(缓存版, 当前版)(refresh-state.ts):若 当前版 > 缓存版 ,说明自上次生成快照以来 至少发生过一次「技能变更事件」 ,必须 重扫并重拼prompt。特殊地,若当前版为0而缓存版已大于0,也会刷新------避免「版本号被重置」后仍误用旧贴纸。matchesSkillFilter:若本轮对话携带的skillFilter(例如按通道只允许部分技能)与快照里记录的不一致,即使磁盘没变 也要重建------否则 Agent 会看到 不该暴露 的技能菜单。
8.5.4 「首轮对话」和「后续轮次对话」分别发生什么?
仍看 ensureSkillSnapshot 的分支(可与上节节选连读):
-
isFirstTurnInSession === true(本会话的第一轮用户消息)若会话条目里还没有
skillsSnapshot,或shouldRefreshSnapshot为真,就buildSnapshot(),并把结果连同systemSent: true等写回 会话存储 (persistSessionEntryUpdate)。直观理解:新会话的「第一声铃」要把系统侧该准备的东西(含技能菜单贴纸)一次性备齐。 -
后续轮次
若发现 已有
skillsSnapshot且 不需要shouldRefreshSnapshot,就 继续用旧贴纸 ,避免每轮扫盘。若 需要刷新 (版本号变了或 filter 变了),即便不是首轮,也会在
!isFirstTurnInSession分支里 再次persistSessionEntryUpdate,把新快照写回会话------于是 下一轮模型读到的会话元数据里已经是新菜单。 -
测试快速路径
若环境变量
OPENCLAW_TEST_FAST=1,该函数会直接返回、不写会话、不挂 watcher ------单元测试里另有用例覆盖快照行为,正文读者只需知道:生产路径才会走完整逻辑。
8.5.5 和 §8.4 的关系(把拼图合上)
- §8.4 解决的是:「贴纸」上能写多少字 (
maxSkillsPromptChars、紧凑、截断)。 - §8.5 解决的是:「贴纸」何时重印(版本号 + 会话持久化 + 首轮/后续轮写回策略)。
合起来:技能文件一改 → watcher bump 版本 → 下轮 ensureSkillSnapshot 发现版本落后 → 调用 buildWorkspaceSkillSnapshot 重算(内部再走 §8.4 预算逻辑)→ 新 prompt 写入 skillsSnapshot 。因此 一般不必重启 Gateway 才能看到新技能;除非你的客户端始终不触发会话更新路径(那属于集成问题,而非 Skills 子系统设计目标)。
🔍 实际例子 :你在 <workspace>/skills/ 下新增一个 my-corp/SKILL.md 并保存;chokidar 触发 bump;同一 Agent 同一会话里 下一笔用户消息 到来时,shouldRefreshSnapshotForVersion 为真,会话文件里的 skillsSnapshot.prompt 被换成带新技能条目的 XML------用户感知为「下一轮它就认识了新技能」。
会话层
§8.5 版本层(何时重印)
§8.4 预算层(贴纸内容)
bump
当前版 > 缓存版
或 filter 变化
版本未变
内部调用
生成
是首轮
后续轮次
无需刷新
需要刷新
maxSkillsPromptChars
紧凑格式 / 截断逻辑
chokidar watcher
监听 skills/ 目录
全局版本号
snapshotVersion
shouldRefreshSnapshotForVersion
对比缓存版 vs 当前版
isFirstTurnInSession?
首轮对话
skillsSnapshot.prompt
XML 技能菜单贴纸
persistSessionEntryUpdate
持久化到会话存储
用户修改 SKILL.md
或新增技能文件
下一笔用户消息到来
ensureSkillSnapshot
调用 buildWorkspaceSkillSnapshot
继续用旧贴纸
新的 skillsSnapshot.prompt
buildSnapshot()
systemSent: true
已有快照?
需要刷新?
再次 persistSessionEntryUpdate
写回新快照
模型读取 XML 菜单
感知新技能
9 安全与治理:把第三方技能当不可信代码
官方 Skills 文档 Security notes 强调:第三方技能应视为不可信代码 ;工作区发现路径需落在配置根内;Gateway 背书的依赖安装路径会跑 危险代码扫描 (critical 默认拦截等)。这与前文 openVerifiedFileSync + realpath 守卫 形成纵深:读文件 与跑安装脚本两道门槛。
🔍 实际例子 :从 ClawHub 装技能前,先在仓库网页或解压目录里读一遍 SKILL.md 与脚本;生产环境优先沙箱执行、最小化 API Key 暴露面------Skills 再方便,也不改变「供应链 = 攻击面」这一事实。
10 和本篇相关的后续篇目
- 第5篇 Brain :工具循环里 read 如何真正打开
SKILL.md、多轮上下文如何增长。 - 第6篇 Hands:沙箱与执行面如何承接技能文档里描述的命令。
- 第10篇 多 Agent :
agents.list[].skills替换式 allowlist 与「默认不限制」的差异(见 D1 文档示例)。 - 第14篇 安全与成本:供应链、密钥与按 Agent 选模型。
11 本篇小结
- 概念上 :Skills = Agent Skills 兼容目录 + 渐进式披露;与 Built-in、Plugins 分层清晰。
- 对照上 :§7 以 Mermaid 架构图 + 节点表 走读
skills_library/→ discover → select → load → use**(示例源码见参考文献第 8 条),§7.9 专门纠正旧对照里 **SKILLS.md` = OpenClaw 可见性 的简化说法。 - 产品上 :ClawHub 负责 安装/版本/锁文件 ;原生 CLI
openclaw skills ...与 UI 技能页共享同一套后端能力叙述(以文档为准)。 - 源码上 (§8 起):稀疏监听
SKILL.md→ 安全读取与 frontmatter → XML 目录注入 → 预算降级 → 会话快照 构成主链路;本地加载器以SKILL.md为硬文件名,与 ClawHub 解压兼容层(skills.md等)略有分工。 - 澄清 :在当前
openclaw/openclaw仓库的src/实现中,并未出现名为SKILLS.md的聚合清单文件生成逻辑 ;「技能对模型可见」主要来自 运行时生成的提示文本 /skillsSnapshot.prompt。若你在其它工具链里看到SKILLS.md叙述,请 以本仓库 S0+D1 为准 做概念对齐,避免混用两套机制。
下一篇(第3篇) :Gateway ------长驻控制面、单端口多协议、单实例锁与启动链;见 03_OpenClaw_Gateway.md。
12 参考文献与链接
- OpenClaw 主仓库:https://github.com/openclaw/openclaw
- OpenClaw 文档(D1,Skills):https://github.com/openclaw/openclaw/blob/0dd4958bc8a78d26b3b526b1f2e63b15110c64a2/docs/tools/skills.md
- Agent Skills 规范(D3,社区规范):https://agentskills.io/specification
- Agent Skills 概念页(渐进式披露,D3):https://agentskills.io/what-are-skills
- 如何把 Skills 接进自有 Agent(D3):https://agentskills.io/integrate-skills
- AgentScope 文档中的 Skills / Progressive Disclosure(D3,类比阅读):https://java.agentscope.io/en/multi-agent/skills.html
- ClawHub:https://clawhub.ai;仓库:https://github.com/openclaw/clawhub
- 与本篇 §7 配套的 Skills 四门功课 可运行示例(ZIP,CSDN):https://download.csdn.net/download/zyctimes/92728873
- Skills 系列博客1:LangGraph 7 · 技能 Skills
- Skills 系列博客2:LangGraph 17 · 三级加载与 Token 优化
- Skills 系列博客3:LangGraph 18 · Skill 四种形态
- Skills 系列博客4:LangGraph 19 · SkillToolset
- Skills 系列博客5:LangGraph 20 · Meta Skills
- Skills 系列博客6:LangGraph 21 · Skills 6 · 从意图到可上线技能