Cursor 中自动加载 Skill 的技术实现解析

AgenticCoding·十二月创作之星挑战赛 10w+人浏览 457人参与

Cursor 中自动加载 Skill 的技术实现解析

本文深入剖析 cursor-skills CLI 工具的设计与实现,探讨如何构建一个面向 AI Agent 的技能管理系统。

在这里插入图片描述

Why: 为什么需要自动加载 Skill

问题背景

在 AI 辅助编程的时代,Cursor 等智能 IDE 依赖上下文来理解项目并执行任务。然而,我们面临几个核心问题:

1. 上下文碎片化

开发者积累了大量可复用的工作流、代码模板和领域知识,但这些内容散落在各处:

  • 项目文档
  • 个人笔记
  • 历史对话记录
  • 团队 Wiki

AI Agent 每次启动都是"失忆"状态,无法有效利用这些积累。

2. 提示词管理困境

优秀的提示词是宝贵资产,但管理成本高:

  • 手动复制粘贴效率低下
  • 版本控制困难
  • 跨项目共享不便
  • 难以在团队间标准化

3. Agent 能力边界模糊

AI Agent 不知道自己"能做什么"。没有一个清晰的能力声明机制,导致:

  • 用户不知道可以请求什么
  • Agent 无法主动提供帮助
  • 复杂工作流难以触发

解决思路

我们需要一个系统来:

  1. 声明式定义技能 - 用结构化文件描述 Agent 的能力
  2. 自动发现与加载 - Agent 启动时自动获取可用技能列表
  3. 按需读取内容 - 只在需要时加载完整技能定义,节省 Token
  4. 分层管理 - 支持全局技能和项目级技能的优先级覆盖

What: Skill 系统的核心概念

技能文件结构

每个 Skill 是一个目录,核心是 SKILL.md 文件:

.cursor/skills/
├── figma2code/
│   ├── SKILL.md           # 技能定义(必需)
│   ├── scripts/           # 执行脚本(可选)
│   └── templates/         # 模板资源(可选)
└── code-review/
    └── SKILL.md

SKILL.md 结构:

---
name: figma2code
description: Convert Figma designs to React components
---

## 工作流程

1. 获取 Figma 设计稿 JSON
2. 解析设计结构
3. 生成 React 组件代码

## 使用方式

调用 `scripts/generate.js` 并传入 Figma URL...

双层技能系统

系统支持两个技能存储位置:

层级路径作用域优先级
项目级.cursor/skills/当前项目
全局级~/.cursor/skills/所有项目

当同名技能存在于两个位置时,项目级技能优先。这允许:

  • 全局定义通用技能
  • 项目级覆盖或扩展特定行为

AGENTS.md 作为能力声明

AGENTS.md 是 AI Agent 的"能力清单",位于项目根目录:

<!-- SKILLS_TABLE_START -->
<available_skills>
<skill>
<name>figma2code</name>
<description>Convert Figma designs to React components</description>
<location>project</location>
</skill>
<skill>
<name>code-review</name>
<description>Automated code review workflow</description>
<location>global</location>
</skill>
</available_skills>
<!-- SKILLS_TABLE_END -->

Agent 通过读取此文件了解自己的能力边界,并在需要时通过命令加载具体技能内容。


How: 技术实现详解

整体架构

┌─────────────────────────────────────────────────────────┐
│                     cursor-skills CLI                    │
├─────────────────────────────────────────────────────────┤
│  index.mjs (入口)                                        │
│  ├── parseArgs()     参数解析                            │
│  └── 命令路由        list / sync / read / remove         │
├─────────────────────────────────────────────────────────┤
│  commands/                                               │
│  ├── list.mjs       列出所有技能                         │
│  ├── sync.mjs       交互式同步到 AGENTS.md               │
│  ├── read.mjs       输出技能完整内容                     │
│  └── remove.mjs     移除指定技能                         │
├─────────────────────────────────────────────────────────┤
│  lib/                                                    │
│  ├── skills.mjs     技能扫描、元数据解析                 │
│  ├── agents.mjs     AGENTS.md 文件操作                   │
│  └── terminal.mjs   终端 UI、交互式多选                  │
└─────────────────────────────────────────────────────────┘

核心流程:技能自动发现

1. 并行扫描双层目录

// lib/skills.mjs
async function getAllSkills(cwd) {
  const projectSkillsPath = getProjectSkillsPath(cwd);
  const globalSkillsPath = getGlobalSkillsPath();

  // 并行扫描,提升性能
  const [projectSkills, globalSkills] = await Promise.all([
    scanSkillsDirectory(projectSkillsPath, 'project'),
    scanSkillsDirectory(globalSkillsPath, 'global'),
  ]);

  // 合并并排序(项目级优先)
  return [...projectSkills, ...globalSkills].sort((a, b) =>
    a.name.localeCompare(b.name)
  );
}

2. 元数据提取

从 YAML Front Matter 中解析技能信息:

function parseFrontmatter(content) {
  const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
  if (!match) return {};

  const frontmatter = {};
  const lines = match[1].split('\n');

  for (const line of lines) {
    const colonIndex = line.indexOf(':');
    if (colonIndex > 0) {
      const key = line.slice(0, colonIndex).trim();
      const value = line.slice(colonIndex + 1).trim();
      frontmatter[key] = value;
    }
  }

  return frontmatter;
}

3. 容错设计

目录或文件不存在时优雅降级,不中断整体流程:

async function scanSkillsDirectory(skillsDir, location) {
  if (!await directoryExists(skillsDir)) {
    return [];  // 目录不存在,返回空数组
  }

  const skills = [];
  const entries = await fs.readdir(skillsDir, { withFileTypes: true });

  for (const entry of entries) {
    if (!entry.isDirectory()) continue;

    try {
      const skillFilePath = path.join(skillsDir, entry.name, 'SKILL.md');
      const content = await fs.readFile(skillFilePath, 'utf-8');
      const metadata = parseFrontmatter(content);

      skills.push({
        name: metadata.name || entry.name,
        description: metadata.description || '',
        location,
        path: path.join(skillsDir, entry.name),
        skillFilePath,
      });
    } catch {
      continue;  // 单个技能读取失败,跳过继续
    }
  }

  return skills;
}

核心流程:AGENTS.md 更新

标记分割策略

使用 HTML 注释作为标记,确保只更新技能区域,保留用户自定义内容:

// lib/agents.mjs
const SKILLS_TABLE_START = '<!-- SKILLS_TABLE_START -->';
const SKILLS_TABLE_END = '<!-- SKILLS_TABLE_END -->';

async function updateAgentsFile(skills, cwd) {
  const filePath = getAgentsFilePath(cwd);
  const skillsSection = generateSkillsSection(skills);

  // 检查文件是否存在
  if (!await fileExists(filePath)) {
    // 不存在:创建新文件
    await fs.writeFile(filePath, generateAgentsContent(skills), 'utf-8');
    return { created: true, path: filePath };
  }

  // 存在:读取并更新
  const existingContent = await fs.readFile(filePath, 'utf-8');
  const startIndex = existingContent.indexOf(SKILLS_TABLE_START);
  const endIndex = existingContent.indexOf(SKILLS_TABLE_END);

  let newContent;
  if (startIndex !== -1 && endIndex !== -1) {
    // 有标记:替换标记间的内容
    newContent =
      existingContent.substring(0, startIndex + SKILLS_TABLE_START.length) +
      '\n' + skillsSection + '\n' +
      existingContent.substring(endIndex);
  } else {
    // 无标记:生成完整内容
    newContent = generateAgentsContent(skills);
  }

  await fs.writeFile(filePath, newContent, 'utf-8');
  return { created: false, path: filePath };
}

核心流程:按需读取

Agent 需要执行某个技能时,通过 read 命令获取完整内容:

// commands/read.mjs
async function readCommand(args) {
  const [skillName] = args;

  if (!skillName) {
    printError('Skill name is required');
    process.exit(1);
  }

  const skill = await getSkillByName(skillName, process.cwd());

  if (!skill) {
    printError(`Skill '${skillName}' not found`);
    printInfo('Run "cursor-skills list" to see available skills');
    process.exit(1);
  }

  const content = await readSkillContent(skill);
  const output = formatSkillOutput(skill, content);

  // 直接输出到 stdout,供 Agent 消费
  process.stdout.write(output);
}

输出格式设计

输出包含基础目录路径,便于 Agent 解析技能中引用的相对资源:

# Skill: figma2code
Base directory: /Users/dev/project/.cursor/skills/figma2code
Location: project
---

[SKILL.md 完整内容]

交互式多选实现

sync 命令提供交互式界面,使用 Raw Mode 实现键盘控制:

// lib/terminal.mjs
async function multiSelect(items, options = {}) {
  return new Promise((resolve) => {
    const { stdin, stdout } = process;
    const selected = new Set(
      items.filter(item => item.checked).map((_, i) => i)
    );
    let cursor = 0;

    // 启用 Raw Mode
    stdin.setRawMode(true);
    stdin.resume();
    readline.emitKeypressEvents(stdin);

    const render = () => {
      // 清屏并移动光标到左上角
      stdout.write('\x1b[2J\x1b[H');

      // 渲染标题
      print(`\n  ${options.message || 'Select items:'}\n`);
      print(`  ${colorize('(Space to toggle, Enter to confirm, Ctrl+C to cancel)', colors.dim)}\n\n`);

      // 渲染选项
      items.forEach((item, index) => {
        const isSelected = selected.has(index);
        const isCursor = index === cursor;

        const checkbox = isSelected
          ? colorize('[✓]', colors.green)
          : colorize('[ ]', colors.dim);
        const pointer = isCursor ? colorize('❯', colors.cyan) : ' ';
        const label = isCursor
          ? colorize(item.label, colors.bold, colors.white)
          : item.label;

        print(`  ${pointer} ${checkbox} ${label}\n`);
      });
    };

    const handleKeypress = (str, key) => {
      if (key.ctrl && key.name === 'c') {
        cleanup();
        resolve(null);  // 取消
        return;
      }

      switch (key.name) {
        case 'up':
        case 'k':
          cursor = Math.max(0, cursor - 1);
          break;
        case 'down':
        case 'j':
          cursor = Math.min(items.length - 1, cursor + 1);
          break;
        case 'space':
          selected.has(cursor)
            ? selected.delete(cursor)
            : selected.add(cursor);
          break;
        case 'return':
          cleanup();
          resolve(Array.from(selected).map(i => items[i].value));
          return;
        case 'a':
          if (key.ctrl) {
            // Ctrl+A 全选/全不选
            if (selected.size === items.length) {
              selected.clear();
            } else {
              items.forEach((_, i) => selected.add(i));
            }
          }
          break;
      }
      render();
    };

    const cleanup = () => {
      stdin.removeListener('keypress', handleKeypress);
      stdin.setRawMode(false);
      stdin.pause();
    };

    stdin.on('keypress', handleKeypress);
    render();
  });
}

零依赖设计

整个 CLI 工具仅使用 Node.js 内置模块:

  • fs/promises - 异步文件操作
  • path - 路径处理
  • os - 获取用户目录
  • readline - 键盘事件
  • module - 动态导入

优势:

  • 安装体积极小
  • 无版本冲突风险
  • 启动速度快

实践指南

创建你的第一个 Skill

1. 创建目录结构

mkdir -p .cursor/skills/my-skill

2. 编写 SKILL.md

---
name: my-skill
description: My first custom skill
---

## 功能描述

这个技能用于...

## 使用方式

当用户请求...时,执行以下步骤:

1. 步骤一
2. 步骤二
3. 步骤三

## 参数说明

- `param1`: 参数说明
- `param2`: 参数说明

3. 同步到 AGENTS.md

cursor-skills sync
# 交互式选择需要启用的技能

Agent 如何消费 Skill

在 AGENTS.md 中,可以指导 Agent 如何使用技能:

<usage>
当需要使用某个技能时,执行:
Bash("cursor-skills read <skill-name>")

获取技能内容后,按照其中的指令执行任务。
</usage>

Agent 执行流程:

1. 用户请求:"帮我把这个 Figma 设计转成代码"
2. Agent 识别到 figma2code 技能可用
3. Agent 执行:cursor-skills read figma2code
4. 获取完整技能定义
5. 按照技能中的工作流执行任务

全局 vs 项目技能的选择

场景推荐位置
通用代码规范检查全局 ~/.cursor/skills/
项目特定的构建流程项目 .cursor/skills/
团队共享的工作流全局(或通过 Git 同步到项目)
实验性技能项目(便于迭代)

设计思考

为什么选择 XML 格式存储技能列表

  1. 结构化 - 易于程序解析
  2. 可读性 - 人类也能快速浏览
  3. 嵌套能力 - 未来可扩展更多元数据
  4. AI 友好 - LLM 对 XML 结构的理解能力强

为什么采用按需加载

完整的技能定义可能包含大量内容(工作流、示例、脚本说明等)。如果在 AGENTS.md 中存储所有技能的完整内容:

  • 浪费 Token 配额
  • 增加 Agent 初始化时间
  • 大部分技能可能本次用不到

按需加载策略:

  • AGENTS.md 只存储摘要(名称+描述)
  • Agent 需要时才读取完整内容
  • 有效利用上下文窗口

为什么使用标记分割更新

用户可能在 AGENTS.md 中添加其他自定义内容(如项目说明、特殊指令)。标记分割确保:

  • 只更新技能区域
  • 保留用户自定义内容
  • 支持重复执行 sync(幂等性)

总结

cursor-skills 通过简洁的设计实现了 AI Agent 的技能自动加载:

  1. 声明式技能定义 - 使用 SKILL.md 描述能力
  2. 双层管理 - 全局和项目级技能的优先级覆盖
  3. 自动发现 - 并行扫描目录,容错处理
  4. 按需加载 - AGENTS.md 存摘要,read 命令取全文
  5. 幂等更新 - 标记分割策略保护用户内容

这套机制让开发者能够:

  • 积累和复用 AI 工作流
  • 在团队间标准化最佳实践
  • 清晰声明 Agent 的能力边界

核心代码仅约 500 行,零外部依赖,体现了"恰到好处"的工程设计。


参考文档:

  1. https://code.claude.com/docs/en/skills
  2. https://github.com/numman-ali/openskills
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RI Code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值