从一次 OpenClaw 请求抓包,聊聊 Skill 的运行原理

前言

最近 Skill 可以说火得一塌糊涂,社区和公司内部的相关讨论、落地实践都非常多。不过我翻了一圈文章,发现大部分都在讲"Skill 是什么"、"怎么写一个 Skill",比如 Skill 是个技能包概念、目录结构怎么组织、SKILL.md 怎么写、脚本怎么跑。但 Skill 底层到底是怎么跑起来的,却比较少人去聊。

换个角度看,Skill 门槛确实低,我也看到了一些同事直接把需求描述丢给 OpenClaw,完全靠 Vibe Coding 来实现 Skill,出了问题也让 AI 用自然语言修,确实也能正常跑起来。但在我看来,这样完成的 Skill 相对黑盒,容易"知其然而不知其所以然",不利于后续的维护与扩展。

这篇文章,打算通过一次真实的请求抓包,结合 OpenClaw 调用 Skill 的过程,把 Skill 从触发到执行的全流程拆开来看,最后再和 MCP / Tool 做个对比。

🦞 OpenClaw 如何注入 Skill

先从 OpenClaw 源码层面看起,看看 Skill 是怎么一步步被塞进 System Prompt 的。

整个注入链路就三步:扫描目录 → 格式化 XML → 拼入 Prompt。不要被源码吓到了,逻辑其实很直白。

1. 扫描目录,收集 Skill 元数据

入口是 loadSkillEntries,Agent 启动时调用这个函数,依次扫描多个 Skill 目录,按优先级从低到高合并(同名 Skill 高优先级覆盖低优先级):

typescript 复制代码
// src/agents/skills/workspace.ts(简化版)
function loadSkillEntries(
  workspaceDir: string,
  opts?: { config?: OpenClawConfig },
): SkillEntry[] {
  const loadSkills = (params: { dir: string; source: string }): Skill[] => {
    // 遍历子文件夹 → 读取 SKILL.md →
    // 解析 frontmatter 提取 name 和 description
    return [];
  };

  const bundledSkills = loadSkills({
    dir: bundledSkillsDir,
    source: 'openclaw-bundled',
  });
  const managedSkills = loadSkills({
    dir: managedSkillsDir,
    source: 'openclaw-managed',
  });
  const workspaceSkills = loadSkills({
    dir: workspaceSkillsDir,
    source: 'openclaw-workspace',
  });

  // 按优先级合并:bundled < managed < workspace
  const merged = new Map<string, 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 workspaceSkills) merged.set(skill.name, skill);

  // 返回 Skill 列表信息,包括 SKILL.md 的前置 YAML 元数据( name、description)
  return Array.from(merged.values()).map((skill) => ({
    skill,
    frontmatter: readSkillFrontmatterSafe({
      rootDir: skill.baseDir,
      filePath: skill.filePath,
    }),
    metadata: resolveOpenClawMetadata(frontmatter),
    invocation: resolveSkillInvocationPolicy(frontmatter),
  }));
}

到这里,所有 Skill 的元数据就收集齐了。

2. 格式化成 XML

拿到 Skill 列表后,formatSkillsForPrompt 方法会把它们格式化成 <available_skills> XML:

typescript 复制代码
// src/agents/skills/skill-contract.ts
function formatSkillsForPrompt(skills: Skill[]) {
  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');
}

3. 拼入 System Prompt

最后在 buildSkillsSection 中,把 XML 和匹配指令一起拼进 System Prompt:

typescript 复制代码
// src/agents/system-prompt.ts
function buildSkillsSection(params: {
  skillsPrompt?: string;
  readToolName: string;
}): string[] {
  return [
    '## Skills (mandatory)',
    'Before replying: scan <available_skills> <description> entries.',
    `- If exactly one skill clearly applies: read its SKILL.md at <location> with \`${params.readToolName}\`, 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.',
    params.skillsPrompt,
  ];
}

后续每一轮会话中,这段 SkillsSection 会作为 System Prompt 的一部分被 LLM 读取。LLM 拿到用户输入后,扫描 <available_skills> 中每个 Skill 的 description 做语义匹配,命中后调用 read Tool 读取完整 SKILL.md,再用 exec Tool 执行 Skill 脚本。这个过程,在下面的抓包数据里可以看得比较清晰。

🔍 从抓包请求看 LLM 如何运行 Skill

分析抓包之前,先回顾一下标准 Skill 的目录结构:

text 复制代码
skill-name/
├── SKILL.md       # 必需:核心指令文件,包含 YAML 元数据
├── scripts/       # 可选:可执行脚本(Python、Bash 等)
├── references/    # 可选:供 SKILL.md 按需读取的参考文档
└── assets/        # 可选:用于生成结果的模板、图标等资源

下面以某个活动管理 Skill(redbrick-activity)在 OpenClaw 运行中的请求抓包为例。用户可以在飞书上通过自然语言(如"查询 xxx 活动列表")对活动进行增删查改,以下是对应 SKILL.md 的简化版本:

markdown 复制代码
---
name: redbrick-activity
description: |
  管理 GMS 红砖平台(redbrick)活动的全生命周期:创建、查询、搜索、修改、删除、下线活动,以及绑定 GitLab 仓库。
  当用户提到以下内容时使用:
  - "创建活动"、"新建活动"、"修改活动"、"删除活动"、"下线活动"
  - "查询活动"、"搜索活动"、"活动列表"
  - 提到"红砖"、"redbrick"、"GMS"
  不要用于非红砖平台的活动管理、通用项目管理、与活动无关的查询。
metadata:
  version: 1.0.0
---

# 红砖活动管理 Skill

通过对话方式管理 GMS 红砖平台活动的全生命周期。

## 核心规则

...相关鉴权规则,此处省略...

## 能力清单

### 能力 1:搜索活动列表

执行脚本:

```bash
bun run {skill-path}/scripts/main.ts search_activity \
  --domain "{domain}" --token "{token}" --app-id "{appId}" \
  [--keyword "{keyword}"] \
  [--online-status {1|2|3|4|5}] \
  [--formal-filter {0|1}] \
  [--page-size {n}] [--page-index {n}]
```

参数说明详见 `references/api-params.md`。

### 能力 2:查询活动详情

执行脚本:

```bash
bun run {skill-path}/scripts/main.ts get_activity \
  --domain "{domain}" --token "{token}" --app-id "{appId}" \
  --activity-id "{activityId}"
```

...其余能力(创建、修改、删除、下线活动,绑定仓库),此处省略...

为了方便理解,我对抓包到的原数据做了翻译和简化,核心流程保持不变。

json 复制代码
{
  "messages": [
    // 1. System Prompt,包含 Skill 列表 XML:<available_skills>
    {
      "role": "system",
      "content": "你是一个运行在 OpenClaw 内部的个人助手。\n\n## 工具说明\n...[省略通用系统运行规则]...\n\n## 技能(Skills)------必须执行\n在回复用户之前:扫描 <available_skills> 中每个 skill 的 <description> 条目。匹配后使用 `read` 工具读取 SKILL.md 并严格遵循其中的指引执行。\n...[省略执行约束与路径解析规则]...\n\n<available_skills>\n <skill>\n  <name>redbrick-activity</name>\n  <description>红砖活动管理 Skill,提供创建、查询、搜索、修改、删除、下线活动能力...[省略description]</description>\n  <location>~/.openclaw/workspace/skills/redbrick-activity/SKILL.md</location>\n </skill>\n</available_skills>"
    },

    // 2. 用户输入
    { "role": "user", "content": "查询1781活动列表" },

    // 3. 第一轮 Tool Call:LLM 决定读取 SKILL.md
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "callaam7tgj3602f4thlxpkka9gk",
          "type": "function",
          "function": {
            "name": "read",
            "arguments": "{\"path\":\"/Users/tom/.openclaw/workspace/skills/redbrick-activity/SKILL.md\"}"
          }
        }
      ]
    },

    // 4. read 返回 SKILL.md 完整内容
    {
      "role": "tool",
      "tool_call_id": "callaam7tgj3602f4thlxpkka9gk",
      "content": "redbrick-activity skill.md 内容...[包含具体参数和执行脚本说明,篇幅过长,省略详细内容]"
    },

    // 5. 第二轮 Tool Call:LLM 决定执行脚本
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "callui9xd13vcv1edumyjq31uf5z",
          "type": "function",
          "function": {
            "name": "exec",
            "arguments": "{\"command\":\"bun run /Users/tom/.openclaw/workspace/skills/redbrick-activity/scripts/main.ts search_activity --domain 'https://game.bytedance.net' --app-id '1781'\"}"
          }
        }
      ]
    },

    // 6. 脚本执行结果
    {
      "role": "tool",
      "tool_call_id": "callui9xd13vcv1edumyjq31uf5z",
      "content": "【活动列表】共 32 条结果(第 1 页,每页 5 条)...[省略详细内容]"
    },

    // 7. LLM 整理输出,回复用户
    {
      "role": "assistant",
      "content": "✅ 已成功查询到活动列表(共32条,当前显示第1页)...[省略详细内容]"
    }
  ],

  //定义 tools
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "read",
        "description": "Read the contents of a file...[省略详细说明]",
        "parameters": {
          "type": "object",
          "properties": {
            "path": { "type": "string" },
            // ...[省略其他参数]
          }
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "exec",
        "description": "Execute shell commands...[省略详细说明]",
        "parameters": {
          "type": "object",
          "properties": {
            "command": { "type": "string" },
            // ...[省略其他参数]
          }
        }
      }
    }
  ]
}

结合下面的时序图会更清晰:

sequenceDiagram participant U as 用户 participant A as Agent 框架 participant LLM as LLM participant FS as 文件系统 participant S as Skill 脚本 U->>A: "查询 1781 活动列表" A->>LLM: messages + tools + system_prompt(含 Skill 清单) Note over LLM: 扫描 Skill 清单
匹配到 redbrick-activity LLM->>FS: read(SKILL.md) FS-->>LLM: SKILL.md 完整内容 Note over LLM: 解析规则
收集参数 → 生成命令 LLM->>S: exec(bun run main.ts search_activity ...) S-->>LLM: 格式化的活动列表 LLM-->>A: 整理后的回复 A-->>U: "✅ 查询到 32 条活动..."

可以看到,LLM 查询活动列表的完整路径是:扫描 Skill 清单 → 匹配 redbrick-activity → read SKILL.md → exec 脚本

顺便提一下,不少文章都提到 Skill 相比 MCP 的一大优点是减少上下文长度。从抓包数据也能印证这一点,Skill 的 name 和 description 虽然每轮都会载入,但 MCP 还会额外载入所有参数及其描述;MCP 数量多的时候,Token 占用会明显膨胀。而 Skill 按需加载的特性(仅在匹配时才载入 SKILL.md、reference、assets 等资源),节约 Token 的效果就比较明显了。

再看 Skill 的本质

通过上面的分析,基本可以得出一个结论:Skill 并非一种新技术,本质上是"新瓶装旧酒"。但这并不意味着可以否定它的价值,通过工程化的封装,Skill 将 Prompt 工程与 Tool Calling / Function Calling 的优势很好地结合在一起,形成了一种灵活、即写即用的"技能包"新形态。

注:OpenAI 最初将模型调用外部函数的机制命名为 Function Calling,后来统一为 Tool Calling,其他厂商也沿用了这一叫法。本文中两者指同一个东西,不做区分。

Skill = 结构化 Prompt + Tool Calling

Skill 的构成可以拆成两层:

  • 定义层SKILL.md 是一份写给 LLM 的结构化 Prompt,告诉它"遇到什么场景、按什么步骤执行、参数怎么填"
  • 执行层 :Agent 平台提供的内置 Tool ------ read 负责按需加载 SKILL.mdexec 负责执行脚本完成实际操作

换句话说,Skill 本身只定义"做什么"和"怎么做",真正的加载和运行交给平台侧的通用 Tool 来完成。

Skill 是 ReAct 的工程化封装

ReAct(Reasoning + Acting) 是 Shunyu Yao 等人 2022 年提出的范式,核心思想是让 LLM 交替进行推理(Thought)与行动(Action),每步行动后根据观察结果(Observation)再决定下一步------即"想一步、做一步、看一步"。

回顾前面的抓包结果,Skill 的运行天然符合这个循环:

text 复制代码
Thought:      用户要查活动列表,扫描 Skill 清单后匹配到 redbrick-activity
Action:       调用 read,读取 SKILL.md
Observation:  获得执行规则和 CLI 模板

Thought:      解析规则,收集 domain、appId、token 等参数
Action:       调用 exec,执行 search_activity
Observation:  获得活动列表

Thought:      数据已获取,整理格式
Action:       输出最终回复

可以看到,运行步骤是一个个 Thought → Action → Observation 循环。相比纯推理,ReAct 赋予了模型"边想边做边看"的自我纠错能力,一定程度上减少了幻觉和跑偏。

不过,Skill 模式下的 ReAct 和标准 ReAct 有一个关键差异:Action 的自由度

标准 ReAct 中,模型需要自行决定调用哪个工具、传什么参数,这是一道"开放式问答";而 Skill 模式下,SKILL.md 提前给出了 CLI 模板和参数说明,Action 变成了"填空题"------模型只需从用户输入中提取值、填入预定义模板。自由度越低,确定性越高,这正是"工程化封装"对 ReAct 最核心的约束。

Skill vs MCP

MCP(Model Context Protocol,模型上下文协议)是 Anthropic 在 2024 年底推出的开放协议,目标是标准化 AI 模型与外部工具、数据源之间的连接方式,目前已成为 Agent 生态中工具接入的事实标准。

Skill 和 MCP 在功能上有一定互补,但也存在替代性。什么时候该用 Skill,什么时候该用 MCP?我从技术实现的角度来分析一下。

MCP 的核心还是 Tool Calling

MCP 的核心能力 Tools 是基于 LLM 的 Tool Calling 来实现的(部分 Agent 在模型不支持 Tool Calling 时,会改为将工具定义嵌入 prompt),它还标准化了数据的读取(Resources)和交互的模板(Prompts)。

为了方便对比,先看一个用原生 Tool Calling 实现 redbrick-activity 的例子:

typescript 复制代码
import { OpenAI } from 'openai';

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: 'https://api.openai.com/v1',
});

// 定义工具:告诉 LLM 可以查询活动信息
const activityTool = {
  type: 'function',
  function: {
    name: 'search_activity',
    description: '根据关键字和状态搜索红砖活动',
    parameters: {
      type: 'object',
      properties: {
        keyword: { type: 'string', description: '活动关键字' },
        status: { type: 'number', description: '活动状态:1-在线 2-下线' },
      },
      required: ['keyword'],
    },
  },
};

// 工具实现:实际调用红砖 API 获取活动数据
async function search_activity(parameters) {
  const result = {
    total: 1,
    list: [
      { id: 123, name: parameters.keyword, status: parameters.status || 1 },
    ],
  };
  return JSON.stringify(result);
}

async function main() {
  const messages = [
    { role: 'system', content: '你是红砖活动运营助手' },
    { role: 'user', content: '请帮我搜索一下在线的春节活动。' },
  ];

  // 第一次请求:LLM 决定是否调用工具
  const response = await client.chat.completions.create({
    messages,
    tools: [activityTool],
    model: 'gpt-4-turbo',
  });
  const message = response.choices[0].message;

  // 如果 LLM 决定调用工具,获取参数并执行
  if (message.tool_calls?.length > 0) {
    const toolCall = message.tool_calls[0];
    const parameters = JSON.parse(toolCall.function.arguments);
    const result = await search_activity(parameters);

    // 第二次请求:将工具执行结果发回 LLM,获取最终回复
    const finalResponse = await client.chat.completions.create({
      messages: [
        ...messages,
        message,
        { role: 'tool', tool_call_id: toolCall.id, content: result },
      ],
      model: 'gpt-4-turbo',
    });
    console.log(finalResponse.choices[0].message.content);
  }
}

main();

如果用 MCP 来封装,Server 端代码如下,这里额外补充 Resources、Prompts 的使用例子:

typescript 复制代码
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
  name: 'redbrick-activity-mcp',
  version: '1.0.0',
});

// 注册 Tool(暴露执行能力):搜索活动
server.tool(
  'search_activity',
  '根据关键字和状态搜索红砖活动',
  {
    keyword: z.string().describe('活动关键字'),
    status: z.number().optional().describe('活动状态:1-在线 2-下线'),
  },
  async ({ keyword, status }) => {
    const result = {
      total: 1,
      list: [{ id: 123, name: keyword, status: status || 1 }],
    };
    return {
      content: [{ type: 'text', text: JSON.stringify(result) }],
    };
  },
);

// 注册 Resource(暴露静态/只读数据):活动创建的默认配置模版
server.resource(
  'create-activity-json',
  'redbrick://config/create-activity.json',
  async (uri) => {
    const defaultData = {
      act_name: '',
      start_time: '',
      end_time: '',
    };
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: 'application/json',
          text: JSON.stringify(defaultData),
        },
      ],
    };
  },
);

// 注册 Prompt(暴露交互模板):标准的创建活动指令
server.prompt(
  'create_activity_template',
  '引导用户创建一个标准红砖活动的提示词模板',
  { theme: z.string().describe('活动主题方向') },
  ({ theme }) => ({
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: `我想创建一个主题为"${theme}"的红砖活动。\n请先调用 read_resource 获取 redbrick://config/create-activity.json 里的默认配置,然后再结合我的主题,调用 create_activity 工具完成创建。`,
        },
      },
    ],
  }),
);

// 启动 MCP 服务器
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main();

MCP 做的事情本质上是把工具的注册、数据的暴露和调用的模板标准化,用 JSON-RPC 2.0 协议统一了 Server 和 Client 之间的通信方式------让任何 MCP Server 暴露的 Tool、Resource 都能被任何 MCP Client 发现和调用。

不管 Client 内部用原生 Tool Calling 还是 Prompt 模式,最终送给 LLM 的数据结构是一样的:函数名、一句话描述、参数 Schema。当 Agent 接入上述 MCP Server 后,发给 LLM 的 tools 字段大致如下:

json 复制代码
{
  "messages": [
    { "role": "system", "content": "你是人工智能助手" },
    { "role": "user", "content": "帮我搜索一下包含'元旦'的红砖活动" }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "search_activity",
        "description": "根据关键字和状态搜索红砖活动",
        "parameters": {
          "type": "object",
          "properties": {
            "keyword": { "type": "string", "description": "活动关键字" },
            "status": {
              "type": "number",
              "description": "活动状态:1-在线 2-下线"
            }
          },
          "required": ["keyword"]
        }
      }
    }
  ]
}

从 LLM 视角看差异

回顾前面的抓包数据,Skill 模式下 LLM 收到的信息要丰富得多:System Prompt 里有 name + description(触发匹配),匹配后通过 read 加载完整 SKILL.md(执行规则、CLI 模板、参数优先级、二次确认逻辑...),需要时还能继续加载 references/ 下的详细参考数据。

把两者放在一起看:

text 复制代码
MCP 给 LLM 的信息:
  tools: [{ name, description, parameters: { Schema } }]
  ──────────────────────────────────────────────────────
  "search_activity"
  "根据关键字和状态搜索红砖活动"
  { keyword: string, status?: number }
  → 一共 ~50 tokens,LLM 自行决定何时调用、如何处理结果

Skill 给 LLM 的信息:
  System Prompt: [{ name, description }]          ← 每轮携带,~50 tokens
      ↓ 匹配后按需加载
  SKILL.md: 完整执行规则 + CLI 模板               ← ~2000 tokens
      ↓ 需要时继续加载
  references/: 详细参考数据                       ← 按需
  → LLM 按预定义规则执行,自由度被约束

这个差异可以明确两者的本质分工:

  • MCP 是原子操作层。一个 Tool = 一个函数,暴露的是"能做什么"(能力),不管"怎么用好"(流程)。类比 API 接口文档。
  • Skill 是业务编排层。一个 Skill = 多步骤工作流,定义的是"遇到什么场景、按什么步骤、用什么参数、出错怎么办"。类比 SOP 操作手册。

用前面的 redbrick-activity 来具象化:

text 复制代码
MCP 层(原子操作):
  search_activity(keyword, status)
  get_activity(id)
  create_activity(body)
  delete_activity(id)
  → 4 个独立的 Tool,各自只知道自己的参数

Skill 层(业务编排):
  1. 从上下文继承 domain / appId(参数优先级规则)
  2. 收集参数,缺失则追问用户
  3. 危险操作触发 HARD STOP 二次确认
  4. 调用对应脚本执行
  5. 格式化结果 + 给出下一步建议
  → 一套完整的执行 SOP,约束了 LLM 的每一步决策

试想如果只有 MCP 的 4 个原子 Tool,Agent 需要自己"悟"出这套流程------该先继承上下文还是直接问用户要参数?删除前要不要确认?结果用什么格式展示?这些全靠 LLM 自由发挥。有了 Skill,这些决策被预定义在 SKILL.md 中,Agent 按手册来就行。

什么时候用 MCP,什么时候用 Skill

从前面的分析可以看出,MCP 和 Skill 其实不在一个层面------MCP 暴露的是原子操作(一个 Tool = 一个函数),Skill 编排的是业务流程(一个 Skill = 多步骤工作流)。

回到 redbrick-activity 的例子,search_activitydelete_activity 这些原子操作适合用 MCP 暴露,而"查询前先继承上下文、删除前要二次确认、结果要格式化展示"这套流程适合用 Skill 定义。

两者的差异整理成表格:

维度 MCP Skill
封装粒度 原子操作(一个 Tool = 一个函数) 业务流程(一个 Skill = 多步骤工作流)
谁来写 服务提供方(高德 / 飞书 / 腾讯云等官方) 业务使用方(团队 / 个人)
关注点 "怎么连上这个系统" "怎么用好这个系统完成任务"
标准化程度 高(JSON-RPC 2.0 + 统一 Schema + 官方 SDK) 低(约定大于标准,本质是 Markdown)
动态分发 支持(npx/uvx 实时执行、SSE 远程连接等) 需手动下载至本地 Skill 目录
跨平台互操作 ✅ 写一次,任何兼容 Client 可用 ⚠️ 格式基本统一,但注册、发现与执行机制各异

简单来说:高德提供地图查询、飞书提供消息发送、腾讯云提供云资源管理------这些服务提供方不关心调用方的业务逻辑,用 MCP 暴露标准化的原子操作就够了。而业务使用方要在这些原子操作之上编排自己的流程,参数怎么收集、危险操作怎么拦截、结果怎么展示,这些只有自己清楚,适合写成 Skill。

Skill 的跨平台互操作现状

Skill 的文件格式已经高度统一。Anthropic 发起的 Agent Skills 开放标准SKILL.md + YAML frontmatter)目前已被许多编程 Agent 采纳,包括 Claude Code、Cursor、Trae、VS Code / GitHub Copilot、Windsurf、OpenAI Codex 等主流客户端,基础格式(name + description + Markdown body)已兼容。

但目前的 Agent Skills 规范存在两个主要痛点

痛点一:Skill 作为静态文件分发,缺乏动态分发能力

当前规范中,Skill(SKILL.md + 配套脚本)以文件包的形式整体下发,Agent 从本地磁盘的特定目录扫描和加载。问题在于,这套流程完全是静态的------没有类似 MCP SSE 那样的动态推送机制:

  • 你接入了一个第三方 MCP 服务,Tools 通过协议动态提供,随时可用;但配套的 Skill 只能手动下载、解压、放到指定目录,Agent 才能识别。
  • 一旦服务方更新了业务流程或脚本逻辑,你本地的 Skill 文件就过期了,必须再次手动下载替换,无法自动同步。

痛点二:没有标准化的发现与分发机制

目前社区虽然出现了不少 Skill 市场(如 skillsmp.com),但这些都是"场外交易"------得自己去搜索、下载、解压、放到正确的目录。整个过程完全脱离 MCP 协议体系,没有一个统一的、程序化的方式让 Agent 自动发现"有哪些 Skill 可用"。

打个比方:MCP 已经给你修好了一条高速公路(标准化连接协议),Tools 可以在这条路上跑,但 Skills 却只能靠人肉快递,得自己开车去仓库取货,再搬回家。

为了解决这些痛点,MCP 官方在今年成立了 Skills Over MCP 工作组(Working Group),核心目标就是让 Skill 也能通过 MCP 协议进行发现、分发和消费

那么,MCP 规范打算怎么解决?

工作组在调研了社区多个独立实现(NimbleBrain、skilljack-mcp、FastMCP 3.0 等)后,目前的主流方案是:复用 MCP 已有的 Resources 原语 + 标准化 skill:// URI 约定,让 MCP Server 直接通过网络下发 Skill。零协议变更,不新增任何 MCP 方法,完全基于已有能力实现。

核心思路一句话概括:Server 在暴露 Tools 的同时,把对应的 Skill 也作为 Resource 一起暴露出去

以 TypeScript 为例,一个 MCP Server 只需要这样做:

typescript 复制代码
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
  name: 'redbrick-activity-mcp',
  version: '1.0.0',
});

// 注册 Skill 为 Resource,使用 skill:// URI
server.resource(
  'redbrick-activity-skill',
  'skill://redbrick-activity/SKILL.md',
  async (uri) => {
    // 动态从远程仓库或统一注册中心拉取最新的 Skill
    const response = await fetch(
      'https://registry.my-org.com/skills/redbrick-activity/SKILL.md',
    );
    const skillContent = await response.text();

    return {
      contents: [
        { uri: uri.href, mimeType: 'text/markdown', text: skillContent },
      ],
    };
  },
);

// 正常注册 Tools(原子操作)
server.tool(
  'search_activity',
  '根据关键字和状态搜索红砖活动',
  {
    keyword: z.string().describe('活动关键字'),
    status: z.number().optional().describe('活动状态:1-在线 2-下线'),
  },
  async ({ keyword, status }) => {
    const result = {
      total: 1,
      list: [{ id: 123, name: keyword, status: status || 1 }],
    };
    return {
      content: [{ type: 'text', text: JSON.stringify(result) }],
    };
  },
);

// 启动 MCP 服务器
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main();

这样一来,当 Client(Agent)连接到这个 Server 时:

  1. 调用 resources/list → 发现 skill://redbrick-activity/SKILL.md 这个资源
  2. 通过 skill:// 前缀识别出"这是一个 Skill"
  3. 需要时调用 resources/read → 拿到完整的 SKILL.md 内容
  4. Agent 按照 Skill 指令编排 Tool 调用

整个过程中,Skill 和 Tools 由同一个 Server 原子化提供、版本绑定。Server 更新了业务流程,Agent 下次拉取就是最新的 Skill 规则,彻底解决了手动同步的问题。

更进一步,MCP 已有的 resources/subscribe 机制还能实现实时推送------Server 在 Skill 内容变化时主动通知 Client,Client 无需轮询就能拿到最新版本。

你可以把这套方案理解为:接入服务时,不仅动态拿到了"零件"(Tools),还把"最新版说明书"(Skills)一起拿到了,而且说明书还支持"自动推送更新"。

text 复制代码
┌──────────────────────────────────────────────────────────────────┐
│                        Agent 运行时                               │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │             Skill(业务编排层)                              │  │
│  │                                                            │  │
│  │  ┌─────────────────┐  通过 MCP Resources 协议动态下发         │  │
│  │  │  skill://        │  ← resources/list 发现                │  │
│  │  │  SKILL.md        │  ← resources/read 按需加载            │  │
│  │  │                  │  ← resources/subscribe 实时推送更新    │  │
│  │  └─────────────────┘                                       │  │
│  │                                                            │  │
│  │  执行规则 + CLI 模板 + 参数优先级                              │  │
│  │  → 约束 LLM 行为,提升确定性                                   │  │
│  └──────────────────────────┬─────────────────────────────────┘  │
│                             │ 调用                               │
│  ┌──────────────────────────▼─────────────────────────────────┐  │
│  │             MCP(标准连接层)                                │  │
│  │                                                            │  │
│  │  原子 Tool + JSON-RPC + 统一 Schema                         │  │
│  │  → 跨平台互操作,标准化接口                                    │  │
│  │                                                            │  │
│  │  Tools 和 Skills 由同一个 Server 提供,版本一致                │  │
│  └────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘

值得一提的是,这套方案目前还处于实验阶段(Extensions Track SEP 草案),社区在实验中也发现了一些挑战,比如模型不会主动去读 Skill 内容、Skill 遵从度会随上下文增长而衰减等。但方向已经明确:让 Skill 从"静态本地文件"进化为"Server 端动态提供的、可发现、可订阅的资源",彻底实现"原子操作"与"业务编排"在同一个通信标准下的闭环。

对 Skill 开发的一些体感

分析完原理,回头看 Skill 的开发,在某些点上会有更深的体感。

description 的质量决定触发率

从注入链路可以看到,System Prompt 中只包含 name 和 description,LLM 做匹配时唯一能参考的就是 description。它不是给人看的文档说明,而是给 LLM 看的触发条件

写 description 要注意几点:写具体的触发关键词,覆盖用户可能的表述方式("查询活动"、"活动列表"、"搜索活动"),包含业务专有名词("红砖"、"redbrick"、"GMS"),同时写明不适用的场景来减少误触发。像"管理活动的工具"这种模糊描述,LLM 几乎无法判断什么时候该触发。

yaml 复制代码
description: |
  管理 GMS 红砖平台(redbrick)活动的全生命周期:创建、查询、搜索、修改、删除、下线活动,以及绑定 GitLab 仓库。
  当用户提到以下内容时使用:
  - "创建活动"、"新建活动"、"修改活动"、"删除活动"、"下线活动"
  - "查询活动"、"搜索活动"、"活动列表"
  - 提到"红砖"、"redbrick"、"GMS"
  不要用于非红砖平台的活动管理、通用项目管理、与活动无关的查询。

SKILL.md 的结构影响执行质量

前面分析过,Skill 对标准 ReAct 最核心的约束就是把 Action 从"开放式问答"变成"填空题"。这个"填空题"能不能填对,很大程度取决于 SKILL.md 怎么写。

最关键的一点:给出 CLI 模板。比起"调用 main.ts 脚本,传入 domain、token 和 appId 参数"这种自然语言描述,直接给出命令模板让 LLM "填空",准确率要高得多:

bash 复制代码
bun run {skill-path}/scripts/main.ts search_activity \
  --domain "{domain}" --token "{token}" --app-id "{appId}"

类似的思路还有:用表格定义参数(比大段文字更容易被 LLM 准确解析)、明确优先级规则(上下文继承 > 用户提供 > 主动追问,避免 LLM 自由发挥)。本质上都是在降低 Action 的自由度,让执行更确定

references/ 是第二层懒加载

SKILL.md 本身已经按需加载了,但如果内容太长(大量参数枚举、域名映射等),同样会浪费上下文。把参考数据拆到 references/ 目录下,SKILL.md 中只写引用,就实现了两级懒加载:

text 复制代码
Level 1: System Prompt 中只有 name + description(~50 tokens)
   ↓ 命中时加载
Level 2: SKILL.md 核心指引(~2000 tokens)
   ↓ 需要时加载
Level 3: references/ 详细参考数据(按需)

用户只是查询活动时,LLM 不需要读取创建活动的参数默认值文档。只加载当前任务需要的上下文,这是 Skill 上下文效率的关键。

给危险操作加 HARD STOP

前面提到,ReAct 的 Observation 能在多步任务中纠错:查询参数填错了,Observation 返回空结果,LLM 可以修正后重试。但这种纠错能力有一个隐含前提------操作本身是可重试的

删除、下线这类操作不属于这个范畴。用户说"删除活动 1234",LLM 在 Thought 阶段决定执行删除后,就会直接进入 Action 调用 exec(delete_activity --id 1234)------这是一个单步不可逆操作,没有"试错后重来"的空间。问题不在于 Observation 纠错太晚,而在于标准 ReAct 流程中 Thought → Action 是无缝衔接的,中间没有人类介入点,LLM 的判断就是最终决策。

对这类操作,应该在 SKILL.md 中定义二次确认机制(HARD STOP):要求 LLM 先展示操作摘要("即将删除活动 ID: 1234,名称: xxx,确认删除请回复'确认'"),等用户明确回复后才执行 exec。

text 复制代码
标准 ReAct:    Thought → Action → Observation
                         ↑ 直接执行,无门控

HARD STOP:     Thought → [人工确认] → Action → Observation
                         ↑ 插入审批节点

本质上,HARD STOP 是在 Thought → Action 之间插入一个人工审批节点,把"LLM 自主决策"变成"LLM 提议 + 人类批准"。对于不可逆操作,这层门控不是可选的优化,而是必要的安全边界

写在最后

回到开头的问题,Skill 底层到底是怎么跑的?拆开来看其实并不复杂:本质就是结构化 Prompt + Tool Calling 的工程化封装,通过 SKILL.md 把 LLM 的 Action 从"开放式问答"收敛为"填空题",再借助 ReAct 循环完成多步执行。理解了这一层,不管是写 Skill、调 Skill,还是排查问题,都能从"知其然"走向"知其所以然"。

至于 Skill 和 MCP 的关系,两者不在一个层面上竞争,更多是互补------MCP 管"怎么连上",Skill 管"怎么用好"。而随着 Skills Over MCP 工作组的推进,两者未来大概率会在同一个协议体系下闭环,到时"手动搬运 Skill 文件"这件事应该就可以告别了。

你在实际开发中有没有遇到 Skill 触发不准、执行跑偏的问题?欢迎评论区聊聊你的经验 👋

相关推荐
慕斯fuafua2 小时前
CSS——弹性盒子
前端·css
穿过锁扣的风2 小时前
从零吃透 YOLO:从 v1 到 v2,实时目标检测的完整进化之路
人工智能·yolo·目标检测
M ? A2 小时前
Vue Transition 组件转 React:VuReact 怎么处理?
前端·javascript·vue.js·经验分享·react.js·面试·vureact
湘美书院--湘美谈教育2 小时前
湘美书院人工智能启示录:AI会是人类的造梦师吗?
大数据·人工智能·深度学习·神经网络·机器学习
东坡肘子2 小时前
从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI -- 肘子的 Swift 周报 #132
人工智能·swiftui·swift
小江的记录本2 小时前
【分布式】分布式一致性协议:2PC/3PC、Paxos、Raft、ZAB 核心原理、区别(2026必考Raft)
java·前端·分布式·后端·安全·面试·系统架构
军军君012 小时前
【人工智能/AI】项目实战一:AI产品汇总(非完全)
人工智能·ai
huangql5202 小时前
CSS布局 (三):浮动——从文字环绕到多列布局
前端·javascript·css
kishu_iOS&AI2 小时前
神经网络 —— 搭建神经网络(实例)
人工智能·深度学习·神经网络