一、一个心智模型:Skill 是一张"会自动填好的任务纸条"
理解这两个机制,只需要抓住一句话:
Skill 的所有"动态能力",都发生在 Claude 读到它之前,由运行时(harness)完成填充。模型最终看到的,永远是一张已经填得满满当当的成品。
把 Skill 想象成一张写给 Claude 的任务纸条。平时纸条上的字是固定的;但它可以留两类"空":
- 一类空,等你调用时临时告诉它 ------比如"修第几号 bug"。这就是参数传递。
- 一类空,让纸条自己去查了填上 ------比如"当前 git 分支是什么"。这就是上下文预注入。
无论哪种空,填空动作都在交给模型之前完成。Claude 拿到手的是成品,它甚至不知道原文里曾有过占位符。记住这条主线,后面所有细节都能对号入座。
二、上下文预注入:让纸条"自带背景信息"
2.1 骨架:三级渐进式加载(Progressive Disclosure)
"预注入"首先是一套分层加载的设计,目的是在"让模型知道技能存在"和"不撑爆上下文"之间取得平衡:
| 层级 | 内容 | 何时进入上下文 | 体量 |
|---|---|---|---|
| L1 元数据 | name + description |
会话一开始就注入,始终在场 | ~100 词 |
| L2 正文 | SKILL.md 主体 | 技能被触发时才注入 | 建议 1.5k--2k 词 |
| L3 资源 | references/ scripts/ assets/ |
模型按需读取/执行 | 近乎无限 |
这三层揭示了"预注入"最本质的一层含义:L1 是真正的"预" 。你安装的每一个 Skill,它的 name + description 在对话还没开始时,就已经被写进系统提示。这就是模型判断"该不该自动调用某个技能"的唯一线索。
所以官方反复强调 description 要写成第三人称 + 具体触发短语:
yaml
# ✅ 好:模型能准确判断何时触发
description: This skill should be used when the user asks to "create a hook",
"add a PreToolUse hook", or mentions hook events.
# ❌ 差:太笼统,技能几乎不会被自动触发
description: Provides guidance for working with hooks.
描述写得含糊,技能就等于"存在但沉睡"------这是新手最常见的坑。
2.2 三种"动态源":把外部世界焊进纸条
当技能被触发、正文(L2)加载时,正文里可以嵌入三种会被实时替换的写法:
① ``!`命令``` ------ 注入命令的实时输出
markdown
## 当前状态
- 分支: !`git branch --show-current`
- 改动: !`git status --short`
运行时会先执行 这些命令,把标准输出内联 进正文。模型看到的不是那句 git branch,而是已经变成 - 分支: main 的成品。这正是"预注入"四个字最直白的体现:命令在模型接手前就跑完了,结果被物化进 prompt。
用
!时,通常要在 frontmatter 用allowed-tools: Bash(git:*)之类放行对应命令。
② @文件 ------ 注入文件内容
markdown
Review @src/api/users.ts for potential bugs.
@ 让运行时先把文件读进来 再交给模型。它还能和参数组合成 @$1,表示"读取用户传进来那条路径所指的文件"。
③ ${CLAUDE_PLUGIN_ROOT} ------ 插件内的可移植路径
插件型 Skill 专用,自动解析为插件的绝对路径,用来引用插件自带的脚本或模板,避免硬编码:
markdown
Run: !`node ${CLAUDE_PLUGIN_ROOT}/scripts/analyze.js`
三、运行时参数传递:让纸条"接受临时输入"
3.1 两条入口,一套语法
Skill 有两个调用入口,但共享同一套参数机制:
- 用户显式调用 :在对话里敲
/skill-name 参数 - 模型自动调用:模型根据 L1 描述判断相关,自行调用并传入参数
一个关键事实:传统的
.claude/commands/*.md和新的.claude/skills/<name>/SKILL.md,运行时是完全一样地加载的,只是文件布局不同。所以下面的参数写法对两者通用。
3.2 三种占位符
$ARGUMENTS ------ 全部参数当作一整个字符串
markdown
Fix issue #$ARGUMENTS following our coding standards.
/fix-issue 123 → Fix issue #123 following our coding standards.
$1 $2 $3 ------ 位置参数,分别对应第 1、2、3 个
markdown
Review PR #$1 with priority $2, then assign to $3.
/review-pr 123 high alice → Review PR #123 with priority high, then assign to alice.
混合 ------ 前几个用位置,剩下的打包
markdown
Deploy $1 to $2 with options: $3
/deploy api staging --force --skip-tests → Deploy api to staging with options: --force --skip-tests
3.3 一条几乎没人注意的兜底规则
如果 SKILL.md 里根本没写
$ARGUMENTS,运行时会把参数以ARGUMENTS: <值>的形式追加到内容末尾。
也就是说,参数永远不会丢。区别只在于:
- 写了占位符 → 参数被精确插到指定位置;
- 没写占位符 → 参数被兜底追加到结尾,交由模型自行理解。
3.4 argument-hint 只是"说明书",不参与传参
yaml
argument-hint: [pr-number] [priority] [assignee]
它只影响自动补全提示和 /help 里的文档展示,不做任何实际替换 。真正的传参靠 $ARGUMENTS / $N。
四、把两者串起来:一次调用的完整时序
参数替换、命令注入、文件读取------这些全部在模型之前 按序完成。以官方 pr-check 技能为例:
yaml
---
name: pr-check
description: Review PR against project checklist
disable-model-invocation: true
context: fork
---
## PR Context
- Diff: !`gh pr diff`
- Description: !`gh pr view`
Review against [checklist.md](checklist.md).
For each item, mark ✅ or ❌ with explanation.
调用 /pr-check 时,运行时的处理流水线是:
bash
用户输入 /pr-check
│
① 参数展开 替换 $ARGUMENTS / $1...(没有则末尾追加 ARGUMENTS:)
│
② 命令注入 执行 !`gh pr diff`、!`gh pr view`,把输出内联进正文
│
③ 文件/路径 解析 @文件 与 ${CLAUDE_PLUGIN_ROOT}
│
▼
一张"已物化"的纯文本 ──► 注入到上下文(本例因 context: fork,进入独立子代理)
│
▼
模型开始工作(只看到成品,看不到任何占位符)
另一种风格 值得对照:很多实用 Skill 几乎不用占位符,而是把一连串动作写成自然语言指令 ,让模型运行时自己去调工具收集上下文。比如一个典型的提交流程 Skill:它不预先传"要提交哪些文件",而是在正文里指挥模型"先跑 git status/git diff 分析改动,再分组生成 commit message"------参数(改了哪些文件)是模型在运行中动态产出的,不是调用时传入的。
这说明:参数传递不是必需品。"正文指令 + 模型自主收集"往往比硬塞参数更灵活。
五、Frontmatter:控制这两件事的开关
| 字段 | 作用 |
|---|---|
name / description |
技能身份与触发条件------L1 预注入的全部内容 |
argument-hint |
参数用法提示(仅文档,不参与替换) |
allowed-tools |
限定可用工具,如 Read, Bash(git:*);用 ! 注入命令时需放行 |
model |
覆盖执行模型(haiku/sonnet/opus) |
disable-model-invocation: true |
仅用户可调,模型不能自动触发------适合有副作用的操作(部署、发送) |
user-invocable: false |
仅模型可调,用户看不到------适合纯背景知识 |
context: fork |
在隔离子代理中运行,不污染主会话 |
agent: Explore |
fork 时用哪种代理类型 |
调用权限一览:
| 设置 | 用户 | 模型 | 用途 |
|---|---|---|---|
| 默认 | ✓ | ✓ | 通用技能 |
disable-model-invocation: true |
✓ | ✗ | 有副作用的操作 |
user-invocable: false |
✗ | ✓ | 后台知识 |
六、五条实战经验
- 描述质量 ≈ 自动触发概率。 模型对未触发的技能只看得见 L1 描述。用第三人称写清"用户说什么时该触发",而不是笼统一句"处理提交相关事务"。
- 占位符替换不可逆。 ``!`cmd``` 一旦跑完被内联,模型无法"重跑";要拿最新状态,只能靠下一次调用重新注入。
$ARGUMENTS是纯文本插值,不做校验。 需要校验就在正文里显式写(例如用 ``!`echo "$1" | grep -E ...``` 验证环境名)。- 别把所有东西堆进 SKILL.md。 那会破坏 L2 的精简性,每次触发都白灌一堆上下文。细节挪到
references/,靠指针按需加载(L3)。 - 新技能优先用
SKILL.md目录格式。.claude/commands/是 legacy;两者加载行为一致,但目录格式能捆绑references/scripts/assets,才能发挥完整的渐进式披露能力。
结语
Claude Code 的 Skill 之所以强大,不在于"写了一段提示词",而在于它把动态填充做成了模型无感的预处理层:
- 上下文预注入 让技能自带实时背景------三级加载控制"何时进上下文",
!/@/${CLAUDE_PLUGIN_ROOT}控制"注入什么内容"; - 运行时参数传递 让技能接受临时输入------
$ARGUMENTS/$N精确插值,外加"末尾追加"的兜底。
两者殊途同归:在模型读到之前,把一张模板纸条填成成品。想清楚"哪些空由用户填、哪些空由纸条自己查",你就能设计出真正好用的 Skill。