Skip to content
概述
当模型需要完成一个任务时,它面临的实际问题不是“能不能理解指令”,而是上下文窗口里能容纳多少指令。传统做法是把所有工具定义一次性注入系统提示,工具数量一多,context 先爆。Skills 把这个逻辑反过来——指令和代码留在文件系统里,模型按需加载,上下文里只保留当前任务需要的那一小块。
以下内容涵盖 Skills、Tools、Plugins、Commands 和 Agents 的定义边界、协作关系,以及在 Agent 循环中 Function Calling 与 Skill 加载的实际执行路径。
基本概念
Skill
Skill 是一组告诉模型如何完成特定任务的说明,以及配套的脚本和资源文件。它不只是一段 prompt——它是标准化的目录,入口是 SKILL.md,旁边可以放可执行脚本、参考文档、模板文件。
一个典型的 Skill 目录:
my-skill/
├── SKILL.md
├── scripts/
│ └── convert.sh
├── references/
│ └── api-docs.md
└── assets/
└── template.jsonSKILL.md 顶部是 YAML frontmatter,至少包含 name 和 description:
yaml
---
name: pdf-to-obsidian
description: >
Convert PDF files into clean Obsidian Markdown notes.
Extracts text, preserves headings, and links embedded images.
---正文是模型在任务匹配时实际加载的指令,用祈使句写。正文控制在 5000 字以内,超出部分放到 references/ 下按需引用。
启动阶段,Agent 扫描技能目录,只把 name 和 description 预加载到系统提示里,正文不加载。等到用户的请求和某个 Skill 的 description 匹配上,正文才被拉进上下文。这种渐进式披露的加载策略是控制 context 开销的关键。
Tool
Tool 是传统 Function Calling 意义上的工具——一个带名称、描述和 JSON Schema 参数的函数定义,模型通过 tool call 来调用。Tool 的定义需要在每次请求时放进上下文,或者至少保持在系统提示里。工具一多,定义本身就成为上下文开销。
Skill 和 Tool 的边界:Skill 可以引用 Tool,但它本身不是 Tool。Skill 提供的是“怎么用这些 Tool 来完成一件事”的知识,而 Tool 是原子操作。
Command
Command 是用户显式触发的操作入口。与 Skill 的自动识别加载不同,Command 需要用户主动调用。它类似于显式的按钮,触发后执行对应的操作。
下面这条命令安装一个包含 Skills、Commands、Agents、Hooks、MCP 连接的完整插件包:
/plugin install my-team-config@team-marketplace执行后,包内所有组件一次性注册到位,不需要逐个手动配置。
Plugin
Plugin 不是一种新的功能组件,而是打包和分发机制。Skill、Command、Agent 是功能单元,Plugin 负责将它们打包在一起进行分发。Skill 可以类比为 recipe(操作说明集),Plugin 则是 cookbook(包含多个 Skill 及其他组件的合集),一个 cookbook 可以附带 MCP 连接、hooks 等配置。
Agent
Agent 是具备自主决策能力的运行实体。它有自己的循环——接收输入、匹配 Skill 或调用 Tool、观察结果、决定下一步。无状态的 LLM 加上编排循环、工具、记忆和上下文管理,才构成一个能执行多步任务的 Agent。
概念对比
这几个概念不是平级的。从功能层级看:
- Skill / Command / Agent 是功能组件
- Plugin 是分发层,把这些组件打包
- Tool 是底层原子能力,被 Skill 和 Agent 调用
从触发方式看:
- Skill:模型自动识别、按需加载,用户不直接触发
- Command:用户显式调用
- Agent:自主循环决策,触发点不在单次调用上
从职责范围看:
AGENTS.md(或类似的项目级配置)定义项目里所有任务都要遵守的全局规则- Skill 定义某一类重复任务的具体做法
- Plugin 解决能力的安装、分发和组合
工作原理
Agent 的一次典型循环大致是这样:系统提示里已经预加载了所有可用 Skill 的 name 和 description(只占很少的 token)。用户请求进来,模型根据 description 匹配到相关 Skill,系统把对应 SKILL.md 的正文注入上下文。模型获得完整的任务知识后,开始规划和执行。执行过程中如果需要调用外部工具,走 Function Calling 路径,工具返回结果后模型继续推理。
关键点在于 context window 的使用策略变了。传统模式下,工具定义占用上下文空间,且不管这次任务用不用得到,定义都在那里。Skills 机制下,定义留在文件系统里,正文在匹配后才进入上下文,元数据常驻但极轻量。带来的直接效果是,在工具数量增长时 context 压力不会线性增加。
渐进式披露还有一层隐含的好处:description 写得越具体,匹配越精准,加载的内容越贴合实际任务。模糊的 description 会导致误匹配,把不相关的指令塞进上下文,既浪费 token 又干扰推理。
基本用法
下面用 Node.js 走一遍 Skill 的声明和注册过程。
声明 Skill
在项目目录下创建 skills/pdf-to-obsidian/SKILL.md:
markdown
---
name: pdf-to-obsidian
description: >
Convert PDF files into clean Obsidian Markdown notes.
Extracts text, preserves heading hierarchy, handles embedded images.
---
# PDF to Obsidian Conversion
## Steps
1. Use the `extract_pdf_text` tool to get raw text and structure.
2. For each extracted page:
- Map headings to Obsidian heading levels (##, ###, ####).
- Wrap code blocks in triple backticks with language hints.
3. If images are embedded, use `extract_images` tool and reference them
with `![[image-name.png]]` syntax.
4. Write the result to `{outputDir}/{filename}.md`.
## References
For detailed API parameters, see `references/api-docs.md`.正文用的是祈使句指令,没有解释性文字。路径用 {outputDir} 这种相对占位符,避免硬编码绝对路径。
注册 Skill 到 Agent
Agent 启动时扫描 skills/ 目录,解析每个子目录中的 SKILL.md,提取 frontmatter 并构建元数据索引:
javascript
// skills/registry.js
import { readdir, readFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import matter from 'gray-matter';
async function scanSkills(skillsDir) {
const entries = await readdir(skillsDir, { withFileTypes: true });
const skills = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillPath = join(skillsDir, entry.name, 'SKILL.md');
const raw = await readFile(skillPath, 'utf-8');
const { data: frontmatter, content } = matter(raw);
skills.push({
name: frontmatter.name,
description: frontmatter.description,
sourcePath: skillPath,
body: content
});
}
return skills;
}gray-matter 负责解析 YAML frontmatter,得到 name、description 和正文三部分。扫描完成后,skills 数组包含所有可用 Skill 的元数据和完整指令。
构建系统提示
注册后的 Skill 列表在构建系统提示时只注入元数据:
javascript
function buildSystemPrompt(skills) {
const skillEntries = skills.map(s =>
`- **${s.name}**: ${s.description}`
).join('\n');
return `
You have access to the following skills:
${skillEntries}
When a task matches a skill's description, the full skill instructions
will be loaded automatically.
`;
}这里只把 name 和 description 写进系统提示,正文完全留在外部。因此,即使注册了数十个 Skill,上下文开销也仅由这些简短描述决定。
命令
安装 Plugin
/plugin install 命令从指定的 marketplace 拉取插件包并安装:
/plugin install my-team-config@team-marketplace执行后,包内的 Skills、Commands、Agents、Hooks 和 MCP 连接配置一次性注册到位。
手动注册单个 Skill
如果不想走 Plugin 分发,也可以在代码里直接注册:
javascript
import { Agent } from 'some-agent-framework';
const agent = new Agent({
skillsDir: './skills',
systemPrompt: 'You are a document processing assistant.'
});
await agent.init();
// init() 内部扫描 skillsDir,加载所有 SKILL.md 的元数据查看已注册的 Skill
/skills list返回当前 Agent 已加载的 Skill 名称和描述摘要。这个命令在排查“为什么模型没有触发某个 Skill”时有用——先确认 Skill 有没有被正确扫描和注册。
示例
下面展示运行时匹配 Skill 并执行任务的实际流程。假设 Agent 已完成上一节的注册,系统提示中已包含 pdf-to-obsidian 的描述。
javascript
async function handleUserRequest(userMessage, skills, llm) {
// 第一次调用:模型判断需要哪个 Skill
const planningResponse = await llm.chat({
system: buildSystemPrompt(skills),
messages: [{ role: 'user', content: userMessage }],
tools: [{ name: 'load_skill', /* ... */ }]
});
// 如果模型决定加载某个 Skill
const skillName = planningResponse.toolCalls?.[0]?.arguments?.skillName;
if (skillName) {
const skill = skills.find(s => s.name === skillName);
if (skill) {
// 把 Skill 正文追加到系统提示,进行第二次调用
const executionResponse = await llm.chat({
system: buildSystemPrompt(skills) + '\n\n' + skill.body,
messages: [{ role: 'user', content: userMessage }],
tools: [/* 实际工具列表 */]
});
return executionResponse;
}
}
return planningResponse;
}当用户发送 “Convert this PDF to markdown” 时,模型在第一轮调用中发现 pdf-to-obsidian 的描述与该请求高度匹配,于是返回 load_skill 调用并指定 skillName。系统根据名称找到对应的 Skill,将其正文注入系统提示后发起第二轮调用。第二轮中,模型已经拥有完整的转换指令和可用工具,可以逐步执行文本提取、格式转换和图片引用等操作。
实际实现中,匹配逻辑可能由 Agent 框架内部完成,不一定要暴露为 tool call。但核心流程不变:元数据常驻,正文按需注入。
应用
Skills 适合封装那些重复出现、流程相对固定的工作流。模型自己能识别什么时候该用它,用户不需要额外指示。
典型场景:
- 文档格式转换(PDF → Markdown、HTML → Markdown)
- 代码审查流程(检查 commit diff、生成 review 意见)
- 数据清洗流水线(读 CSV → 校验字段 → 写入数据库)
- 项目脚手架(根据模板生成目录结构和配置文件)
Command 适合偶尔需要、但希望精确控制的操作,比如一次性迁移配置、触发部署脚本。
Agent 适合多步骤、需要模型自己做决策的任务链。每一步的结果影响下一步的走向,中间可能穿插多个 Skill 和 Tool 调用。
Plugin 的使用场景是分发。团队内部沉淀了一组 Skills 和 Commands,想分享给其他项目或成员,打成 Plugin 一键安装,比手动拷贝目录和配置可靠得多。
当工作流自包含、指令能塞进一个 SKILL.md 时就做成 Skill;当一个能力包需要捆绑 Agents、Hooks、MCP 连接等多个组件时才需要 Plugin。
注意点
- description 质量直接影响匹配准确度。
"Handles documents"这种描述基本没用,模型没法判断什么时候应该加载。需要写清楚输入类型、输出格式和适用条件。 - 正文控制在 5000 字以内。超出部分拆分到
references/目录,通过相对路径引用。正文里不要塞 API 文档全文。 - 路径用相对路径和占位符。
{baseDir}或{outputDir}这类占位符由 Agent 运行时替换,不要硬编码/home/user/projects/xxx。 - 祈使句指令效果更好。SKILL.md 正文是给模型看的操作手册,不是给人看的教程。用“Extract the text using...”而不是“You should extract the text...”。
- Skill 不是 Workflow 引擎。它有固定的指令模板和执行路径,但不提供条件分支、循环、错误重试这些工作流编排能力。需要复杂流程控制时搭配 Agent 循环或其他编排层。
- MCP 和 Skill 是不同的扩展机制。MCP 提供工具和数据源的标准化连接,Skill 提供操作知识。一个 Skill 可以引用 MCP 提供的工具,但它们解决的问题不同。
限制
传统工具定义的 context 膨胀问题,Skill 用渐进式披露解决了一部分,但不是全部。匹配机制本身有开销——description 写得太宽泛会导致多个 Skill 同时匹配,反而增加 context;写得太窄又可能漏掉应该触发的场景。
当前 Skill 机制还不具备动态参数化能力。SKILL.md 正文是静态的,不能在运行时根据输入动态调整指令分支。需要变体时只能创建多个 Skill 或在 Agent 循环里做二次处理。
Skill 的执行依赖 Agent 框架的支持,不同框架对目录结构、frontmatter 字段、加载策略的实现有差异。跨平台迁移时可能需要调整 SKILL.md 的元数据格式。
