导航菜单

  • 1.claudecode
  • 2.claudecode
  • readline
  • iconv-lite
  • mcp
  • skills
  • gray-matter
  • modelcontextprotocol
  • 1. Skills 是什么
  • 2. 学前准备
    • 2.1 YAML Frontmatter 是什么
    • 2.2 技能放在哪个目录
    • 2.3 渐进式加载
  • 3. Skills 与 MCP 怎么配合
  • 4. SKILL.md 长什么样
  • 5. 动手:拆开 Frontmatter
  • 6. 安装依赖并准备示例技能目录
  • 7. 技能加载模块 skills.js
  • 8. 演示:加载目录、拼 system、解析斜杠
  • 9. 在真实 Agent 里怎么用
  • 10. 在 Cursor 里使用 Skills
  • 11. 常见误区
    • 11.1 把整份 SKILL.md 每次都塞进 system
    • 11.2 name 与文件夹名不一致
    • 11.3 万事皆 Skill
  • 12. 知识点速查

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 渐进式加载 #

  1. 启动时:只把每个技能的 name、description 放进系统提示(很短)。
  2. 需要时:再读取 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 节)里,通常还会:

  1. main() 开头 await loadSkills(),再 enrichSystem 更新发给模型的 system。
  2. 注册工具 readSkill:模型传入 { skill: "commit-message" } 时返回 body。
  3. 用户输入 /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 连外部能力
← 上一节 readline
下一节 没有下一节 →

访问验证

请输入访问令牌

Token不正确,请重新输入