Skip to content

Skill 系统(Skills)

讲清 Skill 的本质、四种来源、内联 vs 分叉两种执行方式、如何暴露给模型(发现与延迟加载),以及它与 Agent / Plugin / 命令的关系。

原则:源码为准。 机制均从 claude-code-cli 求证;无法确证者标注「推断」。文末附涉及模块。


1. Skill 的本质:一段可复用的"提示流程"

Skill 不是一种新工具,而是一个 prompt 类型的命令(源码类型 PromptCommand):把"一段专门的做事流程"封装成提示模板 + 元数据,可由模型自主调用,也可由用户以 /名字 调用

关键在于它把三样东西打包:

  • 一段提示(正文模板,支持变量/参数替换);
  • 运行配置(允许的工具子集、模型、effort、钩子等);
  • 执行方式(内联展开,或分叉成子 Agent)。

2. 两种执行方式:内联 vs 分叉

一个 Skill 由其 context 字段决定怎么跑(tools/SkillTool/SkillTool.ts):

  • 内联(inline):把 Skill 的提示展开进当前对话(做变量替换、参数替换、必要时权限拦截),并可通过"上下文修改"影响后续的允许工具集 / 模型 / effort。适合"给当前 Agent 追加一段指导/流程"。
  • 分叉(fork):为 Skill 另起一个子 Agent(隔离上下文),跑完把输出的文本抽取为结果返回。适合"独立完成一段有边界的子任务、不污染主上下文"——本质上是《Agent 系统》里子 Agent 的一种封装用法。

默认与实际分布inline默认(源码 types/command.ts"'inline' (default)"loadSkillsDircontext === 'fork' ? 'fork' : undefined只有显式写 context: fork 才分叉,否则一律 inline)。内置 17 个 bundled skill 全是 inline(debug/verify/remember/simplify/stuck…无一 fork);fork 是 opt-in 少数派,仅用于"自成一体、中途不需要用户介入"的封闭子任务。

2.1 inline 的实现机制:newMessages + contextModifier(以及"何时恢复")

inline 不只是"注入消息"——SkillTool 的 inline 分支返回两样东西

何时恢复——不持久化,换提交轮自动失效(这是关键):

  • 作用域 = 本提交轮(query())剩余的 API 交互轮:override 顺 ctx 链往下传,影响接下来的调模型/工具。
  • 边界 = 下一个提交轮:contextModifier 从不 setAppState 写回全局,只活在这条被线程式传递的 ctx 对象里;新 query() 重建 ctx 时 override 自然消失——不需要显式"恢复"动作,是"不落地 → 换轮即失效"
  • allowedTools 是叠加式:只往 alwaysAllowRules 加放行不删工具池——inline 不会缩小工具集,只是给 skill 声明的工具自动放行。
  • 对比 fork:fork 的注入与 override 全在隔离子 Agent 内,不进父对话、跑完只回一段结果文本,父上下文毫发无损;inline 则"注入父对话 + 改父的后续 ctx(但不持久化)"。

3. 四种来源,统一成同一种定义

Skill 从四处汇聚,最终都归一成 PromptCommand

  • ① 内置skills/bundled/):随程序发布,启动时注册。典型有 verifydebugremembersimplifybatchstuckskillifyupdate-configkeybindings 等,另有按功能开关条件注册的(如 loopclaude-api 等)。
  • ② 文件系统skills/loadSkillsDir.ts):从 .claude/skills/<名字>/SKILL.md 加载;亦兼容旧的 .claude/commands/ 目录。
  • ③ 插件:插件清单里的 skillsPaths 指向的技能目录(见《Plugin 系统》)。
  • ④ MCP:当 MCP 服务器暴露 prompt 类型命令时,由专门的构建器(skills/mcpSkillBuilders.ts)把它转成 Skill。

统一的定义结构types/command.tsPromptCommand):name / description / allowedTools / model / context(inline|fork)/ agent(fork 时的子 Agent 类型)/ effort / hooks / paths / source / loadedFrom,以及生成实际提示的 getPromptForCommand。文件式 Skill 的 frontmatter 字段(如 allowed-toolsargument-hintargumentswhen_to_usedisable-model-invocationuser-invocablecontextagentpathsshellhooks 等)会被解析映射到这些字段。


4. 如何暴露给模型:发现 + 延迟加载

Skill 不是作为一个个独立工具塞给模型的,而是通过单一入口 SkillTool + 一份可发现的技能清单

  • 延迟加载:启动阶段只读取每个 Skill 的摘要(名字/描述/何时使用),正文在被调用时才加载——省启动时间与上下文体积。
  • 每轮动态清单:技能清单每轮重建并按上下文预算裁剪(formatCommandsWithinBudget),确保不喧宾夺主。
  • 可见性控制
    • disable-model-invocation 的 Skill 对模型隐藏(只供用户 / 调用);
    • user-invocable=false 则反之;
    • paths 声明的 Skill 只有在触及匹配文件后才浮现(按需出现)。
  • 权限SkillTool 也走权限校验——可配 Skill(名字:*) 的 allow/deny/ask 规则,"安全属性"的 Skill 可自动放行。

4.1 两条通道要分清:SkillTool(怎么调)vs 技能清单(有哪些)

上面 §4 图里"清单随 SkillTool 说明呈现给模型"要拆成两条独立通道——它们进 API 的方式、是否每轮重发、压缩后的命运都不同:

载体内容每轮重发压缩后
① 怎么调SkillTool(API tools 参数 → <functions>"能调 skill" + 调用协议(skill:"name", args:…✅ 随 schema永在
② 有哪些skill_listing<system-reminder> 附件)全部 skill 的名字 + 描述❌ 首次 + 增量不重列(会丢)

关键事实SkillTool 的描述里明写 "Available skills are listed in system-reminder messages",且入参 skillz.string()非 enum)。所以工具 schema 本身不枚举名单——"怎么调"永在,"有哪些"全靠通道②那条 reminder。("名单不进工具描述"是为保最前端 tools 缓存,机制见《Prompt 缓存机制》§5。)下面把两条通道真实的字贴出来。

通道① 的真实内容——SkillTool<functions> 里渲染成一个 <function>tools/SkillTool/prompt.tsgetPrompt + inputSchema 原文拼出):

text
<function>{
  "name": "Skill",
  "description": "Execute a skill within the main conversation

When users ask you to perform tasks, check if any of the available skills match. ...
When users reference a \"slash command\" or \"/<something>\" ... Use this tool to invoke it.
How to invoke:
- Use this tool with the skill name and optional arguments
- Examples: skill: \"pdf\"  /  skill: \"commit\", args: \"-m 'Fix bug'\"  ...
Important:
- Available skills are listed in system-reminder messages in the conversation   ← 把'有哪些'甩给通道②
- When a skill matches ... this is a BLOCKING REQUIREMENT: invoke ... BEFORE ...
- NEVER mention a skill without actually calling this tool ...",
  "parameters": {
    "type": "object",
    "properties": {
      "skill": { "type": "string", "description": "The skill name. E.g., \"commit\", \"review-pr\", or \"pdf\"" },
      "args":  { "type": "string", "description": "Optional arguments for the skill" }
    },
    "required": ["skill"]
  }
}</function>

注意:里面 "commit"/"review-pr"/"pdf" 只是 schema 描述里的举例占位不是你实际装了哪些 skill。这块全是"怎么用"的规矩,没有一个真实 skill 名。每轮随 tools 参数重发,压缩碰不到。

通道② 的真实内容——skill_listing 附件渲染成 <system-reminder>utils/messages.ts 原文模板,每行 formatCommandDescription):

text
<system-reminder>
The following skills are available for use with the Skill tool:

- commit: Create a git commit - Use when the user asks to commit changes
- review-pr: Review a pull request - Use when reviewing a PR by number
- pdf: Extract and analyze PDF content - Use when working with PDFs
(每个实际安装的 skill 一行:`- 名字: 描述 - 何时用`,每条 ≤250 字符,整份 ≤1% 上下文窗口)
</system-reminder>

4.1.1 压缩后模型手里到底还剩什么(逐条·源码级)

这是本篇最尖锐的一点,务必如实讲:通道① 说"去 reminder 找名单",但通道② 压缩后不重发——于是那句指引指向一个空信箱。所以:

压缩后模型确实拿不到完整名单了。 这不是"有兜底所以无损",而是设计者明知并接受的有损取舍

还剩 / 丢了具体源码验证
怎么调Skill 工具 + 调用协议 + "匹配到必须先调"的硬规矩每轮随 tools schema 重发
用过的 skill(全文)本会话 invoke 过的,连正文补回invoked_skills 附件(createSkillAttachmentIfNeeded
🟡 摘要里蹭到的名字压缩摘要若在散文里提到某 skill,名字就还在概率性,不保证
磁盘 skill 集真变插件 reload / 新增 skill 文件 → 重新播报skillChangeDetectorresetSentSkillNames
skill-search 构建按需重新发现EXPERIMENTAL_SKILL_SEARCH 的 discovery / ToolSearch
没用过 + 摘要没提 + 磁盘没变 + 非 skill-search这类 skill 模型就是不知道了getSkillListingAttachmentssentSkillNames 全命中 → 返回 []

为什么设计者敢这么丢(注释原话 "pure cache_creation with marginal benefit"):① 压缩后通常在继续同一件事,相关 skill 多半已用过(→invoked_skills 保住)或在摘要里;② 重列整份 ~4K tokens 是纯 cache_creation 开销,而"压缩后恰好第一次要用一个从没碰过的 skill"是低频事件;③ 代价不对称——宁可"忘了没用过的",需要时靠磁盘变化 / skill-search 再补。一句话:用极低概率的一次遗忘,换每次压缩省 4K token。

  • 频率与压缩细节(sentSkillNamespostCompactCleanup 故意不重置、invoked_skills 补回、stripReinjectedAttachments)另见 《08 上下文装配》§5.3B/C

5. Skill vs Agent vs Plugin vs 命令

四个概念常被混,区别如下:

概念是什么关系
命令(Command)通用容器(prompt / 本地命令 / JSX 命令等)Skill 是其中 type='prompt' 的那一类
Skillprompt 命令:提示模板 + 配置 + 执行方式SkillTool 给模型;context:fork内部起一个子 Agent
Agent一次隔离上下文的完整主循环Skill 的 fork 执行会用到它;两者是"封装 vs 底座"的关系
Plugin打包分发的扩展(可含多个 skill/agent/命令/MCP/钩子)是 Skill 的来源之一

一句话:Skill 是"打包好的一段提示流程",命令是它的通用外壳,Agent 是它 fork 执行时的底座,Plugin 是它的分发容器之一。

5.1 命令(Command)到底是什么——不是 Tool,是斜杠命令单元

Command ≠ Tool,两个不同概念,最容易混:

Tool(工具)Command(命令)
谁调模型经 API tools 参数调(→ <functions>用户/名字 调(prompt 型也能被模型经 SkillTool 调)
是什么Read/Bash/Edit/Agent/Skill… 的能力斜杠命令单元
例子ReadBashEdit/help/commit/login

唯一交叉点:prompt 型命令(= Skill)既能用户 / 调、也能模型经 SkillTool——这就是 skill 同时出现在"用户命令"和"模型工具"两边的原因。

Command 有三型(源码 types/command.tsCommand = CommandBase & (PromptCommand | LocalCommand | LocalJSXCommand)):

type是什么调模型吗例子能自定义吗
prompt提示模板(= Skill),展开成提示喂模型 / 或 fork 子 Agent✅ 会/commit/debug、自定义 skill.claude/skills/.claude/commands/、插件)
local跑一段本地代码、直接返回结果文本❌ 不调/help/cost❌ 内置代码(仅插件可带代码贡献)
local-jsx弹一个交互式 UI 组件(Ink/JSX)❌ 不调/login/model 选择器❌ 内置代码
  • Skill = Command 里 type='prompt' 的子集:命令是外壳,skill 是"提示型"那一类。
  • "自定义命令"实际就是写一个 prompt 命令(skill):markdown(frontmatter + 正文)即可;而 local/local-jsx(如 /help/login,要跑代码/渲染 UI)是随程序发布的 TS 模块,普通用户写不了。

6. 关键设计取舍

取舍做法为什么
Skill = 提示命令统一成 PromptCommand多来源归一,单一入口暴露
内联 vs 分叉context: inline / fork前者轻量追加流程,后者隔离做子任务
单工具入口 + 清单SkillTool + 每轮技能清单不用给每个 skill 占一个工具位
延迟加载启动只读摘要、调用才加载正文省启动与上下文成本
可见性/触发控制disable-model-invocation / user-invocable / paths精确控制"谁能调、何时出现"
四源统一内置/文件/插件/MCP开箱即用 + 可扩展

附录 · 涉及模块

  • Skill 工具(内联/分叉执行、权限、调用协议描述):tools/SkillTool/(SkillTool、prompt——描述里指明"名单在 system-reminder 中"、入参 skill 为自由字符串)
  • 技能清单注入与去重:utils/attachments.tsgetSkillListingAttachmentssentSkillNamesresetSentSkillNames/suppressNextSkillListing
  • 变更侦测与压缩补回:utils/skills/skillChangeDetector.tsservices/compact/compact.tsstripReinjectedAttachments 剔除清单、createSkillAttachmentIfNeeded 补回 invoked_skills)、services/compact/postCompactCleanup.ts(故意不重置技能清单)
  • 内置 Skill:skills/bundledSkills.tsskills/bundled/index.ts
  • 文件系统加载与 frontmatter 解析:skills/loadSkillsDir.ts
  • MCP 来源构建器:skills/mcpSkillBuilders.ts
  • 插件来源:utils/plugins/loadPluginCommands.tsplugins/builtinPlugins.ts
  • 统一命令类型:types/command.ts;命令收集:commands.ts