Files
2026-06-17 17:32:26 +08:00

21 KiB

Skill 是什么?让 agent 学会"什么时候用什么工具"

本文以 OpenClaw 开源源码为例 (基于 2026.6.2 版本),结合实际代码说明 agent skill 系统的设计和工作原理。 涉及源码路径均可在仓库中直接查阅。

如果你跟着这个系列读到这儿: Function Call 讲了模型怎么调工具, MCP 讲了工具怎么从哪儿来, RAG 讲了怎么让模型看着资料回答。

但还有一个问题没解:给 agent 接了 100 个工具,它怎么知道 什么时候该用哪个? 这次不是 "知识不知道",而是 "方法不知道"。

本文回答这个问题。答案叫 Skill(技能)——它会借用一些 RAG 的 设计思想,但走出一个有意思的变体。


一个真实的麻烦

假设你正在做一个 AI 编程助手,接了一堆工具:ReadBashGrepEditgh(GitHub CLI 包装)、gitdockerkubectlnpmpytest、再加上 20 个 MCP server 暴露的工具。模型看到的 tool 列表有 80 个。

用户说:"帮我看看这个 PR 的评论"

模型该用哪个工具?是 gh pr view --comments?还是 git log --grep? 还是某个 MCP 工具?光看工具名和参数 schema,模型其实不知道你的团队 习惯用哪种方式

你可以这么做:

选项 A:把"怎么做"全塞进 system prompt

You are a coding assistant. When the user asks about PR comments,
use `gh pr view <pr-number> --comments --json`. When asked about
recent commits, use `git log --oneline -20`. When deploying, use
kubectl with our cluster context "prod-east"...

这能解决问题——但 10 条指令还行,100 条就崩了:system prompt 越来越长, 所有任务都付全套 token 成本,而且每条 instruction 都会跟其他 instruction 互相干扰

选项 B:让模型自己看 README

把所有工作流写成 markdown 文档,告诉模型 "需要时去读"。问题是:它 怎么知道现在该读哪份?80 个工具加 50 份操作文档,索引到底在哪儿?

选项 C:用 RAG 查相关文档

把所有文档向量化,每次任务先 retrieval(RAG 那一套)。 可行,但有两个问题:

  • 要维护一套向量库——切块、embedding、向量数据库,基础设施成本不小
  • 程序操作类的精确指令上,embedding 经常翻车——"PR 评论怎么查" 跟 "GitHub gh CLI 文档" 字面差很远,语义匹配可能拉到错的段落

对"知识检索"RAG 很合适,但对"流程指令"它有点重。

Skill 是第四个选项,而且很巧妙——它借了 RAG 的"先找再用"思路, 但把"找"这一步做得更轻


Skill 是什么

一句话:Skill 是一份 markdown 文档,描述"什么时候、怎么完成某类 任务",存在文件系统里,模型按需读取。

每个 skill 是一个目录,核心是带 YAML frontmatter 的 SKILL.md:

---
name: github
description: Interact with GitHub via gh CLI - view PRs, issues, comments, etc.
metadata: {"openclaw": {"requires": {"bins": ["gh"]}}}
---

When the user asks about a PR's comments, run:
  gh pr view <pr-number> --comments --json

When asked about recent commits in a repo, use:
  git log --oneline -20

If the user is asking about a closed PR, also check:
  gh pr view <pr-number> --json mergedAt,closedAt
...

就这。整个"能力"就是这份 markdown 加上目录结构。OpenClaw 仓库里有约 50 个内置 skill,每个都长这样——githubpytestdockerimage-lab 等等。


核心机制:渐进式披露(progressive disclosure)

最聪明的部分在这里。

模型的 prompt 里不放 skill 的正文,只放一个目录:

The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.

<available_skills>
  <skill>
    <name>github</name>
    <description>Interact with GitHub via gh CLI - view PRs, issues...</description>
    <location>/path/to/skills/github/SKILL.md</location>
    <version>3</version>
  </skill>
  <skill>
    <name>pytest</name>
    <description>Run and interpret Python tests...</description>
    <location>/path/to/skills/pytest/SKILL.md</location>
    <version>1</version>
  </skill>
  ...50 个 skill...
</available_skills>

每条只有几十个 token。模型看到这个目录后:

  1. 用户问 "帮我看看这个 PR 的评论"
  2. 模型扫一遍 description,"github" 这条匹配
  3. 模型调用 read 工具,读取那个 <location>
  4. 拿到 SKILL.md 正文,按指令执行 gh pr view ... --comments

只有匹配上的 skill 才付正文 token 成本。50 个 skill 在目录里大概几千 token,如果今天用了 github 和 pytest 两个,实际加载的正文也就 1000 tok。 对比"全塞进 system prompt"那种动辄几万 token 的方案,差距巨大。

这个设计有个名字叫 progressive disclosure——一开始只给最少必要 信息,需要细节时再加载。Anthropic 2025 年初提出的 Agent Skills 规范 就是基于这个思路。


OpenClaw 里是怎么落地的

讲完概念,看看 OpenClaw 源码里一次完整的 skill 调用从头到尾发生了什么。 这一节回答几个具体问题:skill 是怎么被找到的、prompt 长什么样、 模型怎么"决定"用某个 skill、它返回的内容里怎么标识这次调用

1. 加载:六类来源,优先级合并

agent session 启动时,OpenClaw 扫描六类目录(src/skills/loading/):

优先级 1(最高) │ <workspace>/skills          仅该 workspace
优先级 2       │ <workspace>/.agents/skills  仅该 agent
优先级 3       │ ~/.agents/skills            本机所有 agent
优先级 4       │ ~/.openclaw/skills          本机所有 agent
优先级 5       │ 内置 bundled(约 50 个)
优先级 6(最低) │ extraDirs + 插件携带

同名时高优先级覆盖低优先级——你可以在 workspace 里写一个 github 覆盖内置的 github,本地定制不动上游。

扫到的每个 SKILL.md 解析 YAML frontmatter,经过门控过滤(缺二进制 requires.bins、缺环境变量 requires.env、不在 agent allowlist 等) 被剔除掉。

活下来的合到一份 SkillSnapshot(src/skills/runtime/session-snapshot.ts) 固化在 session 上。会话中途用户改 skill 不影响进行中的对话——这跟 MCP catalog 缓存 是同样的"热路径不轮询"思路。

2. Prompt:实际的 XML 长什么样

prompt 由 formatSkillsForPrompt(src/skills/loading/skill-contract.ts:53) 生成。真实输出就是下面这段 XML(下面是从源码原文摘的):

The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.
If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.
When a skill file references a relative path, resolve it against the skill directory ...

<available_skills>
  <skill>
    <name>github</name>
    <description>Interact with GitHub via gh CLI - view PRs, issues, comments, releases</description>
    <location>/home/clz/.openclaw/skills/github/SKILL.md</location>
    <version>3</version>
  </skill>
  <skill>
    <name>pytest</name>
    <description>Run and interpret Python tests, debug failures</description>
    <location>/home/clz/.openclaw/skills/pytest/SKILL.md</location>
    <version>1</version>
  </skill>
  ...
</available_skills>

这段拼到 system prompt 里,跟 system instruction、工具列表(tools 字段)一起发给模型。这就是模型看到 skill 的全部——50 个 skill 也就几千 token,跟"全塞 skill 正文"动辄几万 token 差好几个数量级。

注意三点:

  1. 没有正文,只有 <location>(SKILL.md 绝对路径)
  2. <version> 用于会话内缓存失效——上轮和这轮版本不同时,模型 被指令要求重读
  3. prompt 里有句明确指令:"Use the read tool to load a skill's file when the task matches its description"。这一句就是全部 触发机制,没有别的代码逻辑

3. LLM 怎么"决定"使用某个 skill

完全是 LLM 自己的语义判断,没有任何代码做匹配

OpenClaw 这一侧:

  • 不算 embedding
  • 不做关键词索引
  • 不调用任何路由算法
  • 不告诉模型"现在该用 X skill"

模型这一侧拿到 prompt 之后:

  1. 看到用户问题 "帮我看看 PR 123 的评论"
  2. <available_skills> 里几十条 <description>
  3. 用语义判断:"github" 这条 description 提到了 PR 和 comments,匹配
  4. 决定调 read 工具去读 <location> 指向的文件

整个"路由智能"完全是 LLM 推理出来的。OpenClaw 只负责把候选目录端 端正正放好。

推论:description 的写法直接决定 skill 会不会被触发——它是 skill 作者手里最重要的"路由表项"。写得含糊("GitHub stuff")模型 就匹配不到;写得准确("Interact with GitHub via gh CLI - view PRs, issues, comments")模型几乎必中。

4. LLM 返回的内容里有什么

没有"特殊的 skill 调用类型"。模型返回的就是普通的 tool_use—— 一个 read 工具调用,参数是 SKILL.md 路径:

{
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "text",
      "text": "我用 github skill 来查 PR 评论。"
    },
    {
      "type": "tool_use",
      "id": "toolu_01abc...",
      "name": "Read",
      "input": {
        "file_path": "/home/clz/.openclaw/skills/github/SKILL.md"
      }
    }
  ]
}

注意 name"Read",不是 "skill_github""invoke_skill" 之类的特殊名字。Skill 触发本质上就是一次 file read,跟模型读其他 文件没有任何机制上的区别。

OpenClaw 这一侧收到 tool_use 后:

  1. 在 tool 列表里找到 Read,调它的 execute
  2. 返回 SKILL.md 全文给模型
  3. 模型在下一轮看到正文,按指令开始执行真正的工作 (gh pr view 123 --comments --json)

整个流程完全复用了 Function Call 的标准机制——这是 progressive disclosure 设计的另一个优点:不需要新协议、不需要 provider 支持 特殊字段、模型不需要任何专门的训练。

5. 走完一遍完整时序

Session 启动
  │
  ├─ 扫六类目录,加载 SKILL.md frontmatter
  ├─ 门控过滤,剔除不满足 requires 的
  ├─ 固化 SkillSnapshot
  └─ formatSkillsForPrompt() → <available_skills> XML 片段

用户发问:"帮我看看 PR 123 的评论"
  │
  ├─ OpenClaw 组装请求:
  │     system: "...<available_skills>...</available_skills>"
  │     tools:  [Read, Write, Bash, ...]
  │     user:   "帮我看看 PR 123 的评论"
  │
  └─ 发给 LLM

LLM 第 1 轮返回:
  │
  └─ tool_use: Read(file_path="/path/to/skills/github/SKILL.md")

OpenClaw 执行 Read,返回 SKILL.md 正文

LLM 第 2 轮返回:
  │
  ├─ "我用 github skill,执行 gh pr view"
  └─ tool_use: Bash(command="gh pr view 123 --comments --json")

OpenClaw 执行 Bash,返回 JSON 结果

LLM 第 3 轮返回:
  │
  └─ 用自然语言把 PR 评论整理给用户

整个机制非常简洁:两次额外 tool_use 往返(一次 read SKILL.md、 一次执行 SKILL.md 里指的命令),换来零基础设施 + 任意可扩展的能力库

6. 一个有意思的细节:为啥不直接塞 SKILL.md 内容

读到这里你可能会想:"既然要 read 一次拿全文,为啥不在 prompt 里 直接给正文,省一次往返?"

OpenClaw 的设计就是为了避这个开销:

  • 50 个 skill 的正文加起来可能上万 token,每轮都付这个钱(prompt cache 命中也得算 hash + 重新走 prefill)
  • 用户这一轮可能根本不用任何 skill(普通问候/闲聊),正文白付
  • 多一次 read 往返,对比"每轮付全部 token",在大多数场景下都更划算

进一步,OpenClaw 在 prompt 预算紧张时还会更激进地压缩目录 (applySkillsPromptLimits)——只保留 name + location,丢掉 description。 即使在这种最差情况下,模型仍然知道"有这些 skill 存在",可以试着读 进来看看。


跟 RAG 有什么不一样

上一篇刚讲完 RAG,你可能觉得这俩思路很像——都是 "先找相关的、再让模型用"。核心思想确实是 RAG 那一套 progressive disclosure 的延伸,但 Skill 在关键一步上换了实现:

RAG Skill
索引 embedding 向量库 目录 + 文本 description
匹配 cosine similarity(数学) 模型读 description 做语义判断(LLM)
加载 检索器自动选 top-k 模型自己决定调 read 工具
基础设施 向量数据库 + embedding 模型 文件系统 + 一个 read 工具
适合什么 大规模、模糊查询、长尾知识 中等数量、明确分类、操作指令

关键差别在 "找" 这一步谁来做:

  • RAG 让向量库来找——准确但要基础设施(embedding 服务 + 向量数据库), 适合海量、模糊、跨文档的知识检索
  • Skill 让模型自己来找——把目录直接塞进 prompt,让模型读 description 做语义匹配,省掉向量化整套链路

为什么 Skill 敢省掉向量化?因为它的场景跟 RAG 不一样:

  • RAG 要在几万段文档里找几个相关段——向量化是必要的,模型扫不过来
  • Skill 通常就 50~100 条——目录占几千 token,模型完全扫得过来

简言之:RAG 适合"知识检索",Skill 适合"流程路由"。规模小、目标 明确的场景,直接让 LLM 充当"路由器"比搭一套向量库更轻、更准。

这是个有意思的工程权衡:当 LLM 自己够聪明时,有些中间层就可以 省掉


"路由智能"留给模型,"可用性控制"留给代码

OpenClaw 的实现(src/skills/)有一个清晰的分工:

代码这一层管"哪些可用"——会被加载进目录的 skill 必须通过这些过滤:

  • 缺少必需的二进制(requires.bins: ["gh"] 但本机没装 gh)→ 排除
  • 缺少必需的环境变量(requires.env: ["GITHUB_TOKEN"])→ 排除
  • 平台不符(requires.os: ["macos"] 但跑在 Linux 上)→ 排除
  • 不在当前 agent 的 allowlist 里 → 排除

模型这一层管"用不用、用哪个"——目录里活下来的 skill,选哪个完全是 模型的语义判断。代码不去做关键词匹配、不去算 embedding、不去做任何 "推荐"。

这个分层很重要:

  • 代码擅长确定性检查(二进制装没装、env 设没设)
  • 模型擅长语义判断("用户说的'PR 评论'对应 github 这个 skill")

让各自做自己擅长的事。


description 是 skill 作者最重要的产物

既然模型靠 description 做匹配,description 写得好不好直接决定 skill 会不会被触发

好的 description:

description: Interact with GitHub via gh CLI - view PRs, issues,
comments, releases, run workflows, manage labels.

涵盖了关键名词("PR"、"issue"、"comment")和动词("view"、"run"、 "manage"),用户问任何相关问题模型都能匹配上。

差的 description:

description: GitHub stuff

模型看到这个根本不知道你 skill 里写了什么。用户问 "看 PR 评论" 时模型 不会想到调它。

这是 skill 工程的核心技能——不是写代码,而是 写出让模型能精确匹配的 英文描述


版本失效:会话中如何感知 skill 变化

<available_skills> 里每条带一个 <version>,prompt 里有句指令:

If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.

会话开始时,OpenClaw 固化一份 SkillSnapshot(src/skills/runtime/), 中途用户改了 SKILL.md,新的 version 会进到目录里,模型看到版本变了 就重读正文——缓存失效靠版本号传导,而不是每轮都重读

这跟我们之前在 MCP 内部实现 里看到的 "catalog 缓存 + listChanged 失效" 是同一个套路:热路径缓存,显式信号 触发刷新


还有几个有意思的设计

1. prompt 预算超限时怎么办

50 个 skill 写得详细的话,目录可能上万 token。OpenClaw 有梯度退化 (applySkillsPromptLimits):

梯度 行为 代价
正常 name + description + location + version
maxSkillsInPrompt 截断技能数量 后面的不可见
超字符预算 降级 compact:只剩 name + location 丢 description,模型只能按名字猜
再超 截断 + 注入警告 提示用户 openclaw skills check

哪怕最差情况,模型也至少知道有这个 skill 存在,可以试着用 read 工具看看。这是 graceful degradation 的好例子——情况越糟,体验越糟, 但永远不彻底崩。

2. 绕过模型的两条路径

有时候你不想让模型自己决定:

  • disable-model-invocation: true → 不进目录,模型自主用不到;只能 用户敲 /skill-name 触发
  • command-dispatch: tool → 斜杠命令连模型都不经过,直接分发到 注册工具(确定性操作)

这是给敏感操作或者高频操作准备的——比如 /deploy 你绝对不希望模型 "自己判断时机",必须人触发。

3. Skill Workshop:agent 提案 + 人审

agent 在工作中发现"这个流程值得做成 skill"时,不直接写 SKILL.md, 而是产出一份提案进队列。用户审批(openclaw skills workshop apply <id>) 后才写进 skill 目录。

这是 "agent 可以建议、人类拥有最终所有权" 原则的体现——你不会希望模型 偷偷改自己的指令集。

4. 安装是供应链问题

skill 可以从多个来源安装:ClawHub 注册表(类似 npm)、Git 仓库、本地 目录、zip 上传。每个来源都被视为 不可信代码:

  • ClawHub 上架前过安全扫描(VirusTotal / ClawScan / 静态分析)
  • 安装时本地可以配 security.installPolicy 跑自定义策略命令,fail-closed
  • openclaw skills verify 拿信任信封做校验,适合接入 CI 做供应链门禁

skill 不是"代码"但也不只是"文档"——它能让模型执行任意命令, 所以安全模型按代码标准来。


Skill 跟 native tool / MCP tool 是什么关系

你可能会想:"模型已经有 tool 了,skill 是不是又一层抽象?"

不是的。skill 不是 tool,它控制模型怎么用现有的 tool

  • Tool(包括 native、plugin、MCP)是"能做什么"——读文件、跑命令、调 API
  • Skill 是"什么时候做、怎么做"——遇到这种用户请求,该按什么顺序调哪些工具

类比一下:

  • Tool 是厨房里的刀、锅、铲——基础能力
  • Skill 是菜谱——告诉你做番茄炒蛋要先打蛋还是先炒西红柿

你完全可以只有 tool 没有 skill,模型靠常识也能用。但如果有领域特定的 习惯(我们公司 PR 用 squash 合并 / 我们的数据库用这个连接字符串 / 这个 API 错误码 200 其实是失败)——这些是模型训练数据里没有的东西, 就需要 skill 来教。


总结:为什么 Skill 是个好主意

回到开头那个问题:给 agent 接了 100 个工具,它怎么知道什么时候该 用哪个?

            塞进 system prompt              全部 token 成本
                  ↑                              ↓
  Skill ←─────────┴─ progressive disclosure ─────┘  + 模型语义匹配
                  ↓                              ↑
            按需 read 加载                只付匹配上的成本

Skill 在三个轴上找到了平衡:

极端 1 极端 2 Skill 在哪儿
信息密度 全部塞进 prompt(贵) 全部不给(模型不知道有) 只给目录,正文按需加载
路由智能 代码决定(僵硬) 模型决定(可能瞎选) 代码筛可用性,模型选用哪个
演化方式 硬编码(改要发版) 模型自改(危险) 文件系统 + workshop 人审

最关键的洞察是:目录这个抽象足够便宜,可以放大量条目;模型的语义 匹配能力足够强,可以按 description 准确路由;文本格式足够灵活,可以 表达任意流程指令。三者结合,得到了一套零基础设施、可扩展、可治理 的"agent 能力库"。

这跟 LSP 把"语言能力"抽出来变成独立服务、跟 MCP 把"工具能力"抽出来变成独立 server 是同样的思路—— 把可复用的智能抽到通用的接口背后。Skill 抽的是"流程指令"。


想深入了解?