1. Skills 是什么 #
如果把 MCP 比作「万能插头」(连数据库、连 API),Skills 更像是操作说明书:告诉 AI「遇到某类任务时,按哪几步做、输出什么格式」。
例如「根据 git 变更写 commit message」:你可以把步骤写在 SKILL.md 里,而不是每次对话都重复一长段提示词。
2. 学前准备 #
2.1 YAML Frontmatter 是什么 #
SKILL.md 文件开头常用一对 --- 包住一小段 YAML 配置(叫 frontmatter),后面才是 Markdown 正文。YAML 在这里只需会键值对:
name: commit-message
description: 根据 git 变更生成提交说明下面正文里写具体步骤。Node 里可用 gray-matter 库自动拆开「头 + 正文」。
2.2 技能放在哪个目录 #
| 位置 | 路径 | 用途 |
|---|---|---|
| 项目级 | <项目根>/.claude/skills/<技能名>/SKILL.md |
跟仓库走,团队共享 |
| 用户级 | ~/.claude/skills/<技能名>/SKILL.md |
你个人所有项目可用 |
每个技能一个文件夹,里面必须有 SKILL.md。Cursor 编辑器使用 .cursor/skills/,思路相同,路径不同;本教程示例用 .claude/skills。
2.3 渐进式加载 #
- 启动时:只把每个技能的
name、description放进系统提示(很短)。 - 需要时:再读取
SKILL.md全文(或用户输入/commit-message时注入)。
这样不会一上来就把所有技能正文塞进上下文。
3. Skills 与 MCP 怎么配合 #
| Skills | MCP | |
|---|---|---|
| 本质 | 「怎么做」的流程与规范 | 「连出去」的工具与数据 |
| 典型 | 代码审查清单、写 commit 的步骤 | 读数据库、调 HTTP、跑外部服务 |
| 本教程 | 用 Node 读 SKILL.md |
见 mcp.md |
常见组合:MCP 取数 → Skill 规定怎么分析、怎么输出。
4. SKILL.md 长什么样 #
下面是一份可直接放进项目的示例(路径:.claude/skills/commit-message/SKILL.md)。第 6 节的脚本也会自动创建同名文件,你也可手写。
---
name: commit-message
description: 根据 git 变更生成 commit message。用户要提交说明、commit message 时使用。
---
1. 先在工作区执行 `git status` 与 `git diff` 了解变更。
2. 用中文写一行 conventional commits 风格标题,必要时加简短条目。
3. 若无变更,说明没有可提交内容。说明: name、description 供 AI 判断「要不要用这个技能」;下面的编号列表是真正指令,按需加载。
5. 动手:拆开 Frontmatter #
保存为 01-frontmatter-demo.js,node 01-frontmatter-demo.js 即可。用极简解析帮助理解结构(正式项目建议用 gray-matter)。
// 模拟一份 SKILL.md 的全文(真实场景下从文件读取)
const raw = `---
name: commit-message
description: 根据 git 变更生成提交说明
---
1. 先执行 git status 与 git diff。
2. 写一行中文 conventional commits 标题。
`;
// 用正则拆出 --- 之间的 YAML 与之后的正文(教学用)
function splitFrontmatter(text) {
// 匹配开头的 --- ... --- 块
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
// 若没有 frontmatter,整段当作正文
if (!match) {
return { meta: {}, body: text.trim() };
}
// 逐行解析 name: value(仅支持简单键值)
const meta = {};
match[1].split('\n').forEach((line) => {
const colon = line.indexOf(':');
if (colon > 0) {
const key = line.slice(0, colon).trim();
const val = line.slice(colon + 1).trim();
meta[key] = val;
}
});
return { meta, body: match[2].trim() };
}
// 执行拆分
const { meta, body } = splitFrontmatter(raw);
// 打印元数据
console.log('meta:', meta);
// 打印正文前 40 字
console.log('body 预览:', body.slice(0, 40) + '...');6. 安装依赖并准备示例技能目录 #
在练习目录执行:
npm init -y
npm install gray-matter保存为 02-init-skill.js,运行后会创建 .claude/skills/commit-message/SKILL.md。
// 引入 path,拼接目录
const path = require('path');
// 引入 fs/promises,异步写文件
const fsp = require('fs/promises');
// 技能目录:当前项目下的 .claude/skills/commit-message
const skillDir = path.join(process.cwd(), '.claude', 'skills', 'commit-message');
// SKILL.md 完整路径
const skillFile = path.join(skillDir, 'SKILL.md');
// 要写入的 SKILL.md 内容(含 frontmatter + 正文)
const content = `---
name: commit-message
description: 根据 git 变更生成 commit message。用户要提交说明、commit message 时使用。
---
1. 先在工作区执行 git status 与 git diff 了解变更。
2. 用中文写一行 conventional commits 风格标题,必要时加简短条目。
3. 若无变更,说明没有可提交内容。
`;
// 自执行异步函数
(async () => {
// 递归创建目录
await fsp.mkdir(skillDir, { recursive: true });
// 写入文件
await fsp.writeFile(skillFile, content, 'utf8');
// 提示成功
console.log('已创建:', skillFile);
})();7. 技能加载模块 skills.js #
把下面保存为 skills.js(与 02-init-skill.js 同目录)。它扫描项目级与用户级技能目录,并提供 loadSkills、getSkill、enrichSystem、parseSlash。
// 路径处理
const path = require('path');
// 异步文件 API
const fsp = require('fs/promises');
// 用户主目录(放个人技能)
const os = require('os');
// 解析 SKILL.md 的 YAML frontmatter
const matter = require('gray-matter');
// 先扫项目目录,再扫用户目录;同名时后者覆盖前者
const SKILL_DIRS = [
path.join(process.cwd(), '.claude', 'skills'),
path.join(os.homedir(), '.claude', 'skills'),
];
// 内存中的技能表:name -> { name, description, body }
const skills = new Map();
// 解析原始 SKILL.md 字符串
function parseFrontmatter(raw) {
const { data, content } = matter(raw);
return { meta: data, body: content.trim() };
}
// 扫描某个 skills 根目录下的子文件夹
async function scanDir(root) {
let entries;
try {
entries = await fsp.readdir(root, { withFileTypes: true });
} catch {
return;
}
for (const ent of entries) {
if (!ent.isDirectory()) continue;
try {
const raw = await fsp.readFile(
path.join(root, ent.name, 'SKILL.md'),
'utf8',
);
const { meta, body } = parseFrontmatter(raw);
const name = (meta.name || ent.name).trim();
skills.set(name, {
name,
description: meta.description || name,
body,
});
} catch {
/* 无 SKILL.md 则跳过 */
}
}
}
// 重新加载所有技能
async function loadSkills() {
skills.clear();
for (const dir of SKILL_DIRS) await scanDir(dir);
return [...skills.values()];
}
// 按名称取技能
function getSkill(name) {
return skills.get(String(name || '').replace(/^\//, '').trim()) || null;
}
// 在基础 system 提示后追加技能目录(仅 name + description)
function enrichSystem(base) {
if (!skills.size) return base;
const lines = [...skills.values()].map((s) => `- ${s.name}: ${s.description}`);
return (
base +
'\n\nSkills(匹配时再加载正文,或用户输入 /技能名):\n' +
lines.join('\n')
);
}
// 解析 /commit-message 补充说明 形式的输入
function parseSlash(line) {
const t = line.trim();
if (!t.startsWith('/')) return null;
const rest = t.slice(1);
const sp = rest.indexOf(' ');
const cmd = (sp === -1 ? rest : rest.slice(0, sp)).trim();
const skill = getSkill(cmd);
if (!skill) return null;
return { skill, args: sp === -1 ? '' : rest.slice(sp + 1).trim() };
}
module.exports = { loadSkills, getSkill, enrichSystem, parseSlash };8. 演示:加载目录、拼 system、解析斜杠 #
先运行 node 02-init-skill.js,再运行 03-demo.js。
// 引入第 7 节的技能模块
const {
loadSkills,
getSkill,
enrichSystem,
parseSlash,
} = require('./skills');
// 主流程
(async () => {
// 扫描并加载所有 SKILL.md
const list = await loadSkills();
console.log('已加载技能数:', list.length);
list.forEach((s) => console.log(' -', s.name, ':', s.description));
// 模拟启动时只把「目录」放进 system
const baseSystem = '你是编程助手。';
const fullSystem = enrichSystem(baseSystem);
console.log('\n=== enrichSystem 结果(节选)===');
console.log(fullSystem);
// 模拟用户输入斜杠命令
const slash = parseSlash('/commit-message 本次只改 README');
if (slash) {
console.log('\n=== 斜杠解析 ===');
console.log('技能:', slash.skill.name);
console.log('用户补充:', slash.args);
console.log('将注入的正文:\n', slash.skill.body);
}
// 模拟按需 getSkill(等同 readSkill 工具)
const one = getSkill('commit-message');
if (one) {
console.log('\n=== getSkill 正文长度 ===', one.body.length, '字符');
}
})().catch((err) => {
console.error(err);
process.exit(1);
});预期: 能看到 commit-message 的描述、拼好的 system 片段,以及斜杠解析出的正文。
9. 在真实 Agent 里怎么用 #
在完整的 claude.js(见 2.claudecode.md 第 12 节)里,通常还会:
main()开头await loadSkills(),再enrichSystem更新发给模型的 system。- 注册工具
readSkill:模型传入{ skill: "commit-message" }时返回body。 - 用户输入
/commit-message时,用parseSlash把技能正文拼进 user 消息。
你不必一次写完 Agent;先把本教程的 加载 + 斜杠 + 按需取正文 跑通即可。
10. 在 Cursor 里使用 Skills #
Cursor 把个人技能放在 ~/.cursor/skills/<技能名>/SKILL.md,项目技能放在 .cursor/skills/。格式同样是 frontmatter + Markdown 正文。创建方式可参考 Cursor 自带的 create-skill 指引;与本文 .claude/skills 路径不同,文件写法一致。
11. 常见误区 #
11.1 把整份 SKILL.md 每次都塞进 system #
正文可能很长,应只放 description 列表,正文在匹配后或 /技能名 时再加载。
11.2 name 与文件夹名不一致 #
建议 meta.name 与目录名一致(如都叫 commit-message),避免斜杠命令和 getSkill 对不上。
11.3 万事皆 Skill #
只调一个固定 HTTP 接口、没有流程说明时,直接写代码或 MCP 即可,不必强行包一层 Skill。
12. 知识点速查 #
| 主题 | 记住这一句 |
|---|---|
| Skills 是什么 | 可复用的「怎么做」说明书(SKILL.md) |
| 必备文件 | 每个技能文件夹里的 SKILL.md + frontmatter |
| 省 Token | 启动只加载 name/description,正文按需读 |
| 项目路径 | .claude/skills/<名>/SKILL.md |
| Node 解析 | gray-matter 拆 YAML 与正文 |
| 用户快捷 | /技能名 [补充说明] → parseSlash |
| 与 MCP | Skill 定流程,MCP 连外部能力 |