从一次 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 清单<br/>匹配到 redbrick-activity LLM->>FS: read(SKILL.md) FS-->>LLM: SKILL.md 完整内容 Note over LLM: 解析规则<br/>收集参数 → 生成命令 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.md,exec 负责执行脚本完成实际操作

换句话说,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_activity、delete_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 触发不准、执行跑偏的问题?欢迎评论区聊聊你的经验 👋

相关推荐
深蓝AI40 分钟前
12000 名工程师的高盛,只用四个月就收回了交易员的 AI 编程权限
人工智能·程序员
wangruofeng5 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
怕浪猫5 小时前
这个十几个agent 协作项目值得关注
人工智能·面试·github
wangruofeng5 小时前
给前端开发者的 100 个设计参考网站
前端·设计
starzy19907 小时前
深入理解Scala隐式类:扩展方法机制
开发语言·人工智能·scala
深蓝AI7 小时前
为什么 agent 的 demo 都很惊艳,上了生产就不靠谱
人工智能·程序员
数字新视界7 小时前
信创动环监控在机房环境管理中的智能化应用与实践分析
大数据·人工智能·物联网·信息与通信·动环监控系统
岁月宁静8 小时前
DeepSeek Harness 实战与架构解析:让 Agent 从“会聊”到“能交付”
人工智能·python·agent
星栈8 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技8 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端