Cursor 中自动加载 Skill 的技术实现解析
本文深入剖析
cursor-skillsCLI 工具的设计与实现,探讨如何构建一个面向 AI Agent 的技能管理系统。

Why: 为什么需要自动加载 Skill
问题背景
在 AI 辅助编程的时代,Cursor 等智能 IDE 依赖上下文来理解项目并执行任务。然而,我们面临几个核心问题:
1. 上下文碎片化
开发者积累了大量可复用的工作流、代码模板和领域知识,但这些内容散落在各处:
- 项目文档
- 个人笔记
- 历史对话记录
- 团队 Wiki
AI Agent 每次启动都是"失忆"状态,无法有效利用这些积累。
2. 提示词管理困境
优秀的提示词是宝贵资产,但管理成本高:
- 手动复制粘贴效率低下
- 版本控制困难
- 跨项目共享不便
- 难以在团队间标准化
3. Agent 能力边界模糊
AI Agent 不知道自己"能做什么"。没有一个清晰的能力声明机制,导致:
- 用户不知道可以请求什么
- Agent 无法主动提供帮助
- 复杂工作流难以触发
解决思路
我们需要一个系统来:
- 声明式定义技能 - 用结构化文件描述 Agent 的能力
- 自动发现与加载 - Agent 启动时自动获取可用技能列表
- 按需读取内容 - 只在需要时加载完整技能定义,节省 Token
- 分层管理 - 支持全局技能和项目级技能的优先级覆盖
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 格式存储技能列表
- 结构化 - 易于程序解析
- 可读性 - 人类也能快速浏览
- 嵌套能力 - 未来可扩展更多元数据
- AI 友好 - LLM 对 XML 结构的理解能力强
为什么采用按需加载
完整的技能定义可能包含大量内容(工作流、示例、脚本说明等)。如果在 AGENTS.md 中存储所有技能的完整内容:
- 浪费 Token 配额
- 增加 Agent 初始化时间
- 大部分技能可能本次用不到
按需加载策略:
- AGENTS.md 只存储摘要(名称+描述)
- Agent 需要时才读取完整内容
- 有效利用上下文窗口
为什么使用标记分割更新
用户可能在 AGENTS.md 中添加其他自定义内容(如项目说明、特殊指令)。标记分割确保:
- 只更新技能区域
- 保留用户自定义内容
- 支持重复执行 sync(幂等性)
总结
cursor-skills 通过简洁的设计实现了 AI Agent 的技能自动加载:
- 声明式技能定义 - 使用 SKILL.md 描述能力
- 双层管理 - 全局和项目级技能的优先级覆盖
- 自动发现 - 并行扫描目录,容错处理
- 按需加载 - AGENTS.md 存摘要,read 命令取全文
- 幂等更新 - 标记分割策略保护用户内容
这套机制让开发者能够:
- 积累和复用 AI 工作流
- 在团队间标准化最佳实践
- 清晰声明 Agent 的能力边界
核心代码仅约 500 行,零外部依赖,体现了"恰到好处"的工程设计。
参考文档:
- https://code.claude.com/docs/en/skills
- https://github.com/numman-ali/openskills
1818

被折叠的 条评论
为什么被折叠?



