代码语言
选择后全站课程跟随

Skills:渐进式加载的三层

4,183 字 · 预计阅读 12 分钟

前情提要

  • Skill 是一个目录,核心是一份 SKILL.md:开头的 frontmatter 写 name 和 description,正文写流程
  • Skill 最核心的设计是渐进式加载,分三层:
层级 加载什么 什么时候
第一层 name + description 会话启动时
第二层 SKILL.md 正文 判断需要这个 Skill 时
第三层 模板、脚本、参考文档 真正用到时
  • 常听到的一句话:「装 500 个 Skill,也只会把 name + description 加载到上下文」
  • 斜杠指令和 Skill 已经合并成同一种机制;disable-model-invocation 可以让 Skill 只能手动 /名字 触发

这一节去源码里看三层分别是怎么实现的,顺便回答一个问题:装 500 个 Skill,真的没问题吗?

主要看两个地方:src/skills/loadSkillsDir.ts(从磁盘加载)和 SkillTool/(调用 Skill 的那个工具)。


一、Skill 和斜杠指令是一回事

loadSkillsDir.ts 会同时从这几个地方找 Skill:

来源 路径
组织级 IT 统一下发目录下的 .claude/skills/
用户级 ~/.claude/skills/
项目级 从当前目录往上找,每一层的 .claude/skills/
额外目录 --add-dir 指定的目录下的 .claude/skills/
旧的指令目录 .claude/commands/

注意最后一行:旧的 commands/ 目录,用的是同一个加载器。 从那里加载进来的东西,来源被标成了这个名字:

typescript
cmd.loadedFrom === 'commands_DEPRECATED'

直接写着 DEPRECATED(已弃用)。

斜杠指令已经是 Skill 的一种,新写的流程放 skills/ 就对了。


二、frontmatter 里能写什么

loadSkillsDir.ts 会从 SKILL.md 开头的 frontmatter 里读这些字段:

字段 作用 在哪讲
name / description 出现在 Skill 目录里 本节第三、四部分
when_to_use 拼在 description 后面:描述 - 什么时候用
disable-model-invocation 模型不能自己调用,只能你手动 /名字 本节第五部分
user-invocable 设为 false 时,你不能用 /名字 调用
allowed-tools 这个 Skill 运行时,这些工具自动放行 本节第五部分
argument-hint / arguments 参数提示,正文里的 $ARGUMENTS 会被替换
model / effort 运行这个 Skill 时换模型、换思考强度
context: fork 放到子代理里运行,只带回结果 本节第七部分
paths 碰到匹配的文件才激活,和规则文件的写法一样
hooks 这个 Skill 运行期间生效的 Hook 第9节

description 如果没写,从 skills/ 目录加载的 Skill 会拿正文第一行凑一个;但插件和 MCP 提供的 Skill 没写 description 的话,不会出现在目录里。


三、第一层:目录是一条附件

Skill 目录先用简短说明和预算进入上下文

你可能以为 Skill 目录写在系统提示词里。其实不是。

它是一个叫 skill_listing 的附件,以一条 <system-reminder> 消息的形式插进对话:

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

- weekly-report: 根据本周 git 提交生成周报。 - 当用户要求写周报、总结本周工作时使用。
- release: 发版流程……
- ……
</system-reminder>

而且只发新增的。源码里用一个集合记着「哪些 Skill 已经告诉过模型了」:会话开始时发一次全量,之后你中途又装了新 Skill,只补发新的那几个。

为什么这么麻烦?回想第5节:系统提示词不能中途改,改了缓存就作废;会话中的变化要以附件形式追加在末尾。Skill 目录就是这么做的。

Skill 目录不在系统提示词里,而是一条只补发新增项的附件。


四、目录有预算

关键在 SkillTool/prompt.ts 开头这几行:

typescript
// Skill listing gets 1% of the context window (in characters)
// Skill 目录只能占上下文窗口的 1%(按字符算)
export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01
export const CHARS_PER_TOKEN = 4

// v2.1.117: raised from 250 → 1536 to allow richer skill descriptions.
// v2.1.117:单条上限从 250 提高到 1536,允许写更丰富的描述
export const MAX_LISTING_DESC_CHARS = 1536

两个限制:

限制 数值 说明
单条上限 1536 字符 description 和 when_to_use 加起来,超出的截断
总预算 上下文窗口的 1% 按 1 token ≈ 4 字符折算。200K 窗口 → 8,000 字符;1M 窗口 → 40,000 字符

超预算了怎么办

formatCommandsWithinBudget() 分三步处理:

1. 先试着放完整描述 → 放得下就全放
2. 放不下 → 内置的 Skill 保留完整描述;其余的平分剩下的预算,每条截到一样长
3. 平分之后每条连 20 个字符都不到 → 其余的 Skill 只留名字,描述全部删掉

算一笔账

200K 窗口,预算 8,000 字符。你装了 100 个 Skill,名字平均 15 个字符:

名字和格式本身就要占:100 × (15 + 4) + 99 ≈ 2,000 字符
剩给描述的:         8,000 − 2,000 = 6,000 字符
每条描述能分到:      6,000 ÷ 100 = 60 字符

这还没算内置 Skill 占掉的部分。也就是说,装到 100 个左右,每个 Skill 的描述就只剩一句话了;再多装,就只剩名字。

还有一个中文用户要注意的细节:在这份源码里,预算是按终端显示宽度算的,一个汉字算 2 个宽度。中文 description 吃预算的速度,是同样字数英文的两倍。

本文是会员内容

全文 4,183 字,已试读约 30%。 开通专业版可阅读全文,并解锁全部课程与后续更新。

开通专业版

支持 7 天无理由退款 · 早鸟期前 500 名自动升级永久卡