前言
最近 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" },
// ...[省略其他参数]
}
}
}
}
]
}
结合下面的时序图会更清晰:
匹配到 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.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 时:
- 调用
resources/list→ 发现skill://redbrick-activity/SKILL.md这个资源 - 通过
skill://前缀识别出"这是一个 Skill" - 需要时调用
resources/read→ 拿到完整的 SKILL.md 内容 - 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 触发不准、执行跑偏的问题?欢迎评论区聊聊你的经验 👋