Claude Code 的 skills 源码解析 (上)

前言

skills 是个很难定义的东西,从翻译来看叫技能,但是它不只是代表技能,今天我们先从历史脉络上梳理一下 LLM 的几个发展阶段,然后再看 skills 能做什么,再给出简单的定义,最后结合代码的解析,给出一些关于 skill 的不成熟的思考。

有标准

2023年6月 --- OpenAI 正式推出 Function Calling,模型第一次有了结构化调用外部系统的标准接口,2024年, MCP(Model Context Protocol) 由 Anthropic 提出,试图统一工具调用的协议层,生态开始标准化。当模型具备了调用外部工具的能力后,"让模型做什么 "和"模型怎么做"开始分离。Prompt 不再需要硬编码所有逻辑,而是描述意图,执行交给工具。这打开了更结构化的思路,开始真正的从"语言生成"到"决策"+"调度"。

推出 skill

2025 年 10 月中旬,Anthropic 正式发布 Claude Skills。Skills 本质上是可复用的、有文档的能力单元。它把"如何完成某类任务的最佳实践"封装起来(比如如何生成 docx、如何读 PDF),让模型在需要时查阅并遵循,而不是靠 prompt 里的临时指令。这带来了几个优势:

  • 知识可维护:最佳实践集中在 SKILL.md 和其他相关的文件夹中,更新就可以了;
  • 按需加载:模型判断需要时才读取,不污染上下文;
  • 人机协作:人只负责打磨 skill 文档,模型负责执行;
  • 可复用:别人只需要获取编写好的 skills,得到结果基本无差;

我们可以把 Skills 理解成「公司规章制度」+「工具箱」的组合。

公司规章制度告诉 AI:「当你遇到某类任务时,应该怎么做,分几步,每一步用什么工具。」工具箱里装着它需要用的脚本和参考资料。

展开来说,一个 Skill 就是一个文件夹,里面有三样东西:

第一,SKILL.md 文件。这是「指令」,用自然语言写的。告诉 AI:这个 Skill 是干什么的,什么情况下该用,怎么用,有什么注意事项。

第二,脚本。可以是 Python、JavaScript 或者其他语言写的代码。当 AI 需要「动手」的时候,就执行这些脚本。

第三,资源文件。比如参考文档、模板、配置文件。AI 在执行任务的时候可以查阅这些资料。

所以 skills 可以看成是综合了高阶的 prompt + 工具调用 ,再结合 clawhub 等类似的发布平台,就有了 skills 的发布、查询、安装、版本管理等,之前的问题都解决了。

打个比方。函数调用像是给你一把锅铲、一个锅、再加一些调料,你得自己知道什么时候倒油,什么时候放菜,用锅铲怎么炒,怎么颠锅等。Skills 像是给 AI 一本《中国八大菜系菜谱》 + 十八般的工具,菜谱里不只是告诉 AI 炒菜步骤,还告诉他各个阶段所需要的工具。在这个过程中,AI 只管用,只管炒菜, 结束了就是一盘菜。 哪怕来一个从来没炒过菜的人,只要跟 AI 说,我要做个土豆丝,他得到的成品和前面五星级大厨的成品是一样的。

所以对于 skills,根据上面的叙述,简单的定义可以是:可被语义触发的能力包,它包含领域知识、执行步骤、输出规范与约束条件。

Skills 是如何实现的

Skill 的本质,是把磁盘上一段我们可读的 markdown(SKILL.md),在调用瞬间编译成模型能消化的 prompt blocks,然后注入对话上下文。所以我们可以将 skill 分为两个大的阶段,一个是 loading(加载)阶段,还有一个是注入调用阶段。今天我们从 Claude Code 源代码的角度去看去了解,这两个阶段是如何实现,为了实现这两个阶段,Claude Code 做了哪些事情,怎么去实现这个设计

加载

一、启动入口:从命令行到 main()

当我们在命令行中输入claude 时,下面的流程就启动了

关键代码位置:src/main.tsx:1918-1932https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fisboyjc%2Fclaude-code%2Fblob%2Fmain%2Fsrc%2Fmain.tsx%23L1927

复制代码
// 同步注册:必须在 getCommands 之前完成
if (process.env.CLAUDE_CODE_ENTRYPOINT !== 'local-agent') {
  initBuiltinPlugins();
  initBundledSkills();
}
// 并行启动
const setupPromise = setup(preSetupCwd, ...);
const commandsPromise = worktreeEnabled ? null : getCommands(preSetupCwd);

为什么要这样设计initBundledSkills() 是纯内存的数组 push 操作(bundledSkills.push(skill)),耗时 <1ms。它必须在 getCommands() 启动前完成,否则 getBundledSkills() 返回空数组,结果技能会丢失。

二、技能加载

当我们在 .claude/skills/ 目录、项目目录等地方放了 skills 时,会通过 IO 的方式读取 skills,整体的流程如下:

loadAllCommands 的合并顺序(优先级从高到低):

复制代码
// 如果有重复的,会进行去重去处理
return [
  ...bundledSkills,          // 内嵌技能
  ...builtinPluginSkills,    // 内置插件技能
  ...skillDirCommands,       // 磁盘上的技能(用户/项目/管理)
  ...workflowCommands,       // 工作流命令
  ...pluginCommands,         // 插件命令
  ...pluginSkills,           // 插件技能
  ...COMMANDS(),             // 内置命令(非技能类型)
]
三、磁盘技能加载:getSkillDirCommands

关键代码位置:getSkillDirCommands:https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fisboyjc%2Fclaude-code%2Fblob%2Fmain%2Fsrc%2Fskills%2FloadSkillsDir.ts%23L638

这是最核心的技能加载逻辑,负责从文件系统读取 SKILL.md 文件。

3.1 目录搜索范围
复制代码
getSkillDirCommands(cwd) 搜索以下目录:
    │
    ├── Managed Skills      ←── /etc/claude/.claude/skills/ (企业策略管理)
    ├── User Skills         ←── ~/.claude/skills/            (用户全局技能)
    ├── Project Skills      ←── .claude/skills/              (项目级技能,沿目录树向上搜索)
    ├── Additional Skills   ←── --add-dir 指定的目录/.claude/skills/
    └── Legacy Commands     ←── .claude/commands/            (旧格式,向后兼容)
3.2 并行加载策略

所有目录的加载是并行 的(Promise.all),因为它们互不依赖:

复制代码
const [managedSkills, userSkills, projectSkills, additionalSkills, legacyCommands] =
  await Promise.all([
    loadSkillsFromSkillsDir(managedSkillsDir, 'policySettings'),
    loadSkillsFromSkillsDir(userSkillsDir, 'userSettings'),
    Promise.all(projectSkillsDirs.map(dir => loadSkillsFromSkillsDir(dir, 'projectSettings'))),
    Promise.all(additionalDirs.map(dir => loadSkillsFromSkillsDir(join(dir, '.claude/skills'), ...))),
    loadSkillsFromCommandsDir(cwd),
  ]);
3.3 单个技能目录的加载过程
复制代码
loadSkillsFromSkillsDir(basePath, source)
    │
    ├── 1. fs.readdir(basePath)               ←── 读取目录列表
    │
    └── 2. 对每个 entry 并行处理:
        │
        ├── 跳过非目录项(只支持 skill-name/SKILL.md 格式)
        │
        ├── 读取 skill-name/SKILL.md 文件内容
        │
        ├── parseFrontmatter(content)         ←── 解析 YAML frontmatter
        │   输入: "---\ndescription: ...\n---\n# Skill body"
        │   输出: { frontmatter: {...}, content: "# Skill body" }
        │
        ├── parseSkillFrontmatterFields(...)  ←── 提取结构化字段
        │   提取: description, allowedTools, model, hooks, paths, effort...
        │
        └── createSkillCommand(...)           ←── 构建 Command 对象
            闭包捕获 markdownContent,延迟到调用时再编译
3.4 去重机制

加载完成后,使用 realpath 解析文件的真实路径进行去重:

复制代码
// 通过 realpath 检测符号链接和重复的父目录
const fileIds = await Promise.all(
  allSkillsWithPaths.map(({ filePath }) => getFileIdentity(filePath))
);

// 先到先得:优先级由合并顺序决定
// managed > user > project > additional > legacy
for (entry of allSkillsWithPaths) {
  if (seenFileIds.has(fileId)) continue;  // 跳过重复
  seenFileIds.set(fileId, skill.source);
  deduplicatedSkills.push(skill);
}
3.5 条件技能(Conditional Skills)

带有 paths frontmatter 的技能不会立即激活,而是存储在 conditionalSkills Map 中:

复制代码
---
description: React 组件开发助手
paths: src/components/**, src/pages/**
---

当用户操作的文件路径匹配 paths 模式时,技能被激活并加入动态技能列表。

激活流程:activateConditionalSkillsForPaths(filePaths, cwd) → 使用 ignore 库做 gitignore 风格匹配。

四、SKILL.md 文件解析
4.1 Frontmatter 字段格式
复制代码
---
# 基础信息
name: 显示名称(可选,默认取目录名)
description: 技能描述
argument-hint: <参数提示文本>
arguments: [arg1, arg2]

# 模型和行为控制
model: claude-sonnet-4-6       # 指定使用的模型
effort: high                    # low | medium | high | 整数
context: fork                   # fork = 独立子进程执行,inline = 主线程
agent: agent-name               # 指定 agent 定义

# 权限控制
allowed-tools: [Bash, Read, Write]
user-invocable: true            # 用户是否可通过 /name 调用
disable-model-invocation: false # 模型是否可通过 SkillTool 调用

# 条件激活
paths: src/**/*.tsx             # 匹配文件路径时自动激活

# 钩子
hooks:
  PreToolUse:
    - command: "eslint $FILE"
      matcher: "Write|Edit"

# Shell 执行环境
shell: bash

# 版本
version: "1.0"
---

4.2 解析为 Command 对象

parseSkillFrontmatterFields() 将 YAML 映射为结构化字段,然后 createSkillCommand() 组装成 Command 对象:

复制代码
{
  type: 'prompt',              // 技能都是 prompt 类型
  name: 'skill-name',          // 目录名(唯一标识)
  description: '...',          // 从 frontmatter 或正文第一行提取
  source: 'projectSettings',   // 来源:userSettings / projectSettings / policySettings
  loadedFrom: 'skills',        // 加载方式:skills / bundled / plugin / mcp
  allowedTools: ['Bash'],      // 额外允许的工具
  model: 'claude-sonnet-4-6',  // 模型覆盖
  effort: 'high',              // 努力程度
  userInvocable: true,         // 用户可调用
  context: 'fork',             // 执行上下文
  hooks: {...},                // 钩子配置
  paths: ['src/**/*.tsx'],     // 条件路径
  contentLength: 1234,         // SKILL.md 内容长度
  skillRoot: '/path/to/skill', // 技能目录路径

  // 核心:延迟加载闭包
  getPromptForCommand: async (args, toolUseContext) => {...}
}
五、延迟加载机制:getPromptForCommand

技能内容在启动时只解析 frontmatterSKILL.md 的正文内容通过闭包捕获,仅在用户调用 /skill-name 时才执行完整的"编译"过程。

六、动态技能发现

除了启动时加载,系统还支持在会话过程中动态发现新技能。

6.1 文件操作触发发现

当用户读写文件时,系统会沿文件路径向上搜索 .claude/skills/ 目录:

复制代码
discoverSkillDirsForPaths(filePaths, cwd)
    │
    ├── 对每个 filePath:
    │   从文件父目录开始,向上遍历到 cwd(不含 cwd)
    │   每级检查是否存在 .claude/skills/ 目录
    │   记录到 dynamicSkillDirs(去重用)
    │
    └── 返回新发现的目录列表(按深度降序排列)
6.2 激活流程
复制代码
addSkillDirectories(dirs)
    │
    ├── 对每个目录调用 loadSkillsFromSkillsDir()
    │
    ├── 深层路径覆盖浅层路径(同名技能)
    │
    ├── 存入 dynamicSkills Map
    │
    └── skillsLoaded.emit()  ←── 通知缓存失效
6.3 缓存失效

动态技能加载后,需要清除相关的 memoization 缓存:

复制代码
clearCommandMemoizationCaches() {
  loadAllCommands.cache?.clear?.()
  getSkillToolCommands.cache?.clear?.()
  getSlashCommandToolSkills.cache?.clear?.()
  clearSkillIndexCache?.()         // 技能搜索索引

getCommands() 不被缓存(因为需要每次重新检查 availability 和 isEnabled),但它内部的 loadAllCommands 被 memoize,所以清除内层缓存即可。

七、技能优先级总览
复制代码
优先级从高到低:

1. managed skills          ←── 企业策略目录 /etc/claude/.claude/skills/
2. user skills             ←── 用户全局 ~/.claude/skills/
3. project skills          ←── 项目目录 .claude/skills/(最近的优先)
4. additional skills       ←── --add-dir 指定目录
5. legacy commands         ←── 旧格式 .claude/commands/
6. bundled skills          ←── 代码内嵌技能
7. builtin plugin skills   ←── 内置插件技能
8. plugin skills           ←── 第三方插件技能

同名技能:先注册者胜出(由合并顺序决定)
文件去重:realpath 相同的文件只保留第一个
八、关键数据流图

调用

加载的阶段将所有的 skills 加载到 Command\[\] 数组中,等着被调用,梳理 Claude Code 的源码来看,用户总共有 9 个入口可以去调用 skills,9 种入口如下:

# 入口 触发方式 执行模式 关键文件
1 用户斜杠命令 用户输入 /skill-name inline / fork processSlashCommand.tsx:309
2 立即命令 查询进行中输入 /config local-jsx 直接执行 REPL.tsx:3161
3 SkillTool.call() 模型调用 Skill 工具 inline / fork / remote SkillTool.ts:580
4 MCP Skill 通过 SkillTool 或 /server:skill fork(强制) SkillTool.ts:81-94
5 Cron/定时任务 scheduled_tasks.json/loop 队列 → processUserInput useScheduledTasks.ts:40
7 Agent 预加载 Agent 定义中的 skills: 字段 预注入初始消息 runAgent.ts:578-645
8 Ultraplan 关键字 输入包含魔法关键字 重写为 /ultraplan processUserInput.ts:467
9 初始 prompt -p "/skill ..." 或 agent initialPrompt onSubmit → processSlashCommand main.tsx:3094

本文限于篇幅,9 种如果都讲述篇幅太大,也不利于阅读,所以今天就讲述第一种,用户用斜杠命令行的方式来调用 skills。

代码的调用流程图如下
详细代码调用流程
第 1 层 REPL.onSubmit() --- 入口把关

详细代码位置:REPL.tsx:3142https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fisboyjc%2Fclaude-code%2Fblob%2Fmain%2Fsrc%2Ftypes%2Fcommand.ts%23L3142

当我们在 Claude Code 中输入 /commit 并按下回车,在中, 组件 PromptInput 调用 onSubmit 回调。这里是整条链路的入口。

复制代码
// 1. 检测是否是 immediate 命令(immediate: true 的 local-jsx 命令)
//    这些命令可以在 AI 正在处理时立即执行,不用排队
if (!speculationAccept && input.trim().startsWith('/')) {
  const commandName = /* 从 input 提取命令名 */
    const matchingCommand = commands.find(...)
  const shouldTreatAsImmediate = queryGuard.isActive && 
    (matchingCommand?.immediate || options?.fromKeybinding)

  if (matchingCommand && shouldTreatAsImmediate && matchingCommand.type === 'local-jsx') {
    // 直接执行,跳过队列 --- return early
  }
}

对于大多数 /skill-name(type 为 prompt),这里不会命中 immediate 快速通道,而是继续往下走。

接下来的处理:

  • 清空输入框
  • 加入历史记录
  • 调用 handlePromptSubmit()
  • 加载接与 frontmatter 解析
第 2 层:handlePromptSubmit() --- 队列化

详细代码位置:handlePromptSubmit.ts:120https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fisboyjc%2Fclaude-code%2Fblob%2Fmain%2Fsrc%2Futils%2FhandlePromptSubmit.ts%23L120

这个模块的核心职责是决定输入是立即执行还是排队等待。

复制代码
// 如果 AI 正在处理中(queryGuard.isActive),新的输入进入队列
if (queryGuard.isActive || isExternalLoading) {
  enqueue({ value: finalInput.trim(), mode, pastedContents })
  return  // 不执行,等 AI 空闲后 dequeue
}
// 否则立即执行
await executeUserInput({ queuedCommands: [cmd], ... })
executeUserInput 是实际执行的核心函数,它内部调用 processUserInput()。
第 3 层:processUserInput() --- 模式路由

详细代码位置:processUserInput.ts:533https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fisboyjc%2Fclaude-code%2Fblob%2Fmain%2Fsrc%2Futils%2FprocessUserInput%2FprocessUserInput.ts%23L533

这个函数是一个大型路由器,根据输入模式分发给不同处理器:

复制代码
// Bash 模式 → processBashCommand()
if (mode === 'bash') { ... }

// Slash command → processSlashCommand()
if (inputString !== null && !effectiveSkipSlash && inputString.startsWith('/')) {
  const { processSlashCommand } = await import('./processSlashCommand.js')
  const slashResult = await processSlashCommand(inputString, ...)
  return slashResult
}

// 普通文本 → processTextPrompt()
// ...
/commit 以 / 开头,命中 slash command 分支。
第 4 层:processSlashCommand() --- 命令解析与分发

详细代码位置:processSlashCommand.tsx:309https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fisboyjc%2Fclaude-code%2Fblob%2Fmain%2Fsrc%2Futils%2FprocessUserInput%2FprocessSlashCommand.tsx%23L309

这是整条链路中最关键的分发层。

Step 4.1:解析命令名

复制代码
const parsed = parseSlashCommand(inputString)
// 如果输入的是 /commit fix: 修复bug
// 那么经过 parseSlashCommand 函数转化成: { commandName: 'commit', args: 'fix: 修复bug', isMcp: false }

parseSlashCommand 函数的解析规则

  • 去掉 / 前缀
  • 第一个空格前为命令名
  • 支持 MCP 命令格式:/mcp:tool (MCP) args

Step 4.2:查找命令注册表

复制代码
if (!hasCommand(commandName, context.options.commands)) {
  // 命令不存在 → 判断是文件路径还是未知命令
  if (looksLikeCommand(commandName) && !isFilePath) {
    return { messages: [...], shouldQuery: false, resultText: 'Unknown skill: xxx' }
  }
  // 可能是文件路径(如 /var/log)→ 当普通文本发给模型
  return { messages: [...], shouldQuery: true }
}

Step 4.3:按 type 分发

Claude Code 有三种命令类型(types/command.ts),不同的命令会执行不一样的动作:

type 行为 示例
prompt 展开为文本,发送给 AI 模型 /commit, skill 类命令
local 本地执行,返回文本结果 /compact
local-jsx 渲染交互式 UI 组件 /config, /model

对于 /skill-name(type = prompt),进入 getMessagesForPromptSlashCommand()。

第 5 层:getMessagesForPromptSlashCommand() --- 技能内容加载

processSlashCommand.tsx:827https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fisboyjc%2Fclaude-code%2Fblob%2Fmain%2Fsrc%2Futils%2FprocessUserInput%2FprocessSlashCommand.tsx%23L827

这是 skill 的核心------将 SKILL.md 的内容加载为 prompt。

Step 5.1:检查 context === 'fork'

复制代码
if (command.context === 'fork') {
  return await executeForkedSlashCommand(...)
  // 在独立子 agent 中执行,有自己的上下文和 token 预算, 默认不是 fork
}

Step 5.2:加载技能内容

复制代码
const result = await command.getPromptForCommand(args, context)

getPromptForCommand 在技能注册时(就是前面一个加载阶段获取到的)定义( loadSkillsDir.ts:344),它做了以下事情:

  • 变量替换:将$ARGUMENTS, ${CLAUDE_SKILL_DIR}, ${CLAUDE_SESSION_ID} 替换为实际值
  • Shell 执行:如果 SKILL.md 中有 !command 格式的 shell 注入,会先执行
  • 返回 ContentBlockParam\[\]:包含最终展开后的文本内容

Step 5.3:构造消息列表

复制代码
const messages = [
  createUserMessage({ content: metadata }),        // 命令元数据:名称、参数
  createUserMessage({ content: skillContent, isMeta: true }),  // SKILL.md 内容(对用户隐藏)
  ...attachmentMessages,                           // 附件消息
  createAttachmentMessage({                        // 权限声明:allowedTools
    type: 'command_permissions',
    allowedTools: additionalAllowedTools,
  }),
]
return {
  messages,
  shouldQuery: true,    // ← 关键:告诉上层需要调用 AI 模型
  allowedTools: additionalAllowedTools,
  model: command.model,
  effort: command.effort,
  command,
}
第 6 层:onQuery() --- 发送给 AI

详细代码位置:handlePromptSubmit.ts:560https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fisboyjc%2Fclaude-code%2Fblob%2Fmain%2Fsrc%2Futils%2FhandlePromptSubmit.ts%23L560

回到 executeUserInput()(handlePromptSubmit.ts:560):

复制代码
await onQuery(
  newMessages,          // 包含 skill 内容的消息列表
  abortController,
  shouldQuery,          // true → 需要调用模型
  allowedTools ?? [],   // skill 声明的额外工具权限
  model ?? mainLoopModel,
  onBeforeQuery,
  primaryInput,
  effort,
)

onQuery 将这些消息追加到对话历史,然后调用 Claude API。此时 SKILL.md 的全部内容作为一条 user message 发送给模型,模型根据技能指令执行相应操作。

相关推荐
霍格沃兹测试开发学社测试人社区1 小时前
源码解读:我如何设计一个“可插拔”的测试Skills引擎,支持热加载与隔离执行
人工智能
吠品1 小时前
.NET 8 单文件发布:把 exe 和一堆 dll 打进一个文件里
服务器·数据库·windows
-山中问答-1 小时前
【AI智能体工程化实战03】智能体工程化开发环境
人工智能·开发环境·智能体·trae·claude code
寻道码路1 小时前
LangChain4j Java AI 应用开发实战(十四):手写 RAG 全流程 - 深入理解每个环节
java·开发语言·人工智能·ai
ar01231 小时前
工业智能化时代的AR巡检力量
人工智能·ar
云烟成雨TD1 小时前
Agent Scope Java 2.x 系列【1】核心架构
java·人工智能·agent
Xiaofeng36931 小时前
三大旗舰模型横评:Claude 4.6、ChatGPT 5.5、Gemini 2.0 Pro 谁更强
人工智能
benben0441 小时前
Gym从入门到精通
人工智能