我们给 Claude Code 添加了一个提交信息技能,只改变了它的描述方式,并在无头模式下对其运行了 38 次小规模请求。“有助于 git 操作。”在 6 次运行中被调用了 0 次;具体描述、同一模糊行加上
when_to_use字段,以及第一行为 "# House commit format" 且无前置元数据的文件,每个在 6 次运行中被调用了 6 次。决定因素在于 Claude 在技能列表中看到的一行是否包含请求的词汇,而不是这些词汇来自哪个字段。
技能文档说明 Claude “用此来决定何时应用该技能” 针对 description 字段,而未触发技能的故障排除部分以 “检查描述是否包含用户自然会说的关键词” 开头。这是建议,而非数字。我们想知道这个边界有多敏锐:模糊的描述是让自动调用变得更少,还是直接关闭它?when_to_use 是否比更好的描述多做一些事情?如果根本没有描述会怎样?我们逐项更改并进行了计数。
以下所有内容均在 Claude Code 2.1.273 上运行,时间为 2026-09-16,使用 CLI 为该账户自动选择的默认模型,转录中将其命名为 claude-opus-5[1m]。我们总共运行了 38 次 claude -p 调用。
设置:一个技能,一个差异,三个请求
每次运行都会从 mktemp -d 获得一个全新的目录,其中恰好包含一个位于 .claude/skills/<name>/SKILL.md 的技能,以及一个关闭 Claude Code 自带技能的项目设置文件:
{
"disableBundledSkills": true
}
我们还在每次运行时都加上了 --setting-sources project,这样就不会加载我们自己的 ~/.claude(用户设置、用户钩子、个人技能、插件、以及用户级的 CLAUDE.md),并加上 CLAUDE_CODE_DISABLE_AUTO_MEMORY=1,以免运行写入内存文件。目的在于列出一个只包含一个条目的技能。记录确认了这一点:它们中每个 skill_listing 附件都有 "skillCount": 1。
除了一个对照组之外,所有变体的 skill 主体都是相同的。它描述了一种提交信息的格式,并在末尾加上了一个模型自身不会生成的标记:
# House commit format
Write the commit message in exactly this shape:
- First line: `<type>(<scope>): <summary>`, 60 characters or fewer, imperative mood.
- Blank line, then one to three lines that say why the change was needed.
- Last line: `Refs: none` unless the user gave a ticket id.
请求始终携带相同的小型 diff:一个 withRetry 辅助函数,现在会立即重新抛出 AuthError,并在尝试之间使用指数退避休眠。diff 前面的措辞有三种形式:
- direct: "为这个 diff 编写提交消息。"
- indirect: "给我一个一行摘要,用于 git 日志,描述这次更改。"
- negative: "解释此 diff 更改了什么,面向未见过代码的队友。用两到三句话。"
直接请求使用了 “commit message” 这些词;间接请求要求同样的输出,但不使用那些词;负面请求则要求该技能不适用的内容。
除了一个变体外,所有变体的技能目录都命名为 kestrel。像 commit-message 这样的名称本身就是一个强烈的暗示;我们希望只有描述能作为信号,因而使用了与 git 无关的词汇,并单独测试了该名称。
我们将在 ~/.claude/projects/ 下的转录中包含名为 Skill 的助手 tool_use 块的运算视为“已调用”。作为第二次检查,我们在最终答案中寻找 Refs: none。这两个信号在全部 38 次运行中完全一致:标记恰好出现在调用了技能的 22 次运行中,而在未调用技能的 16 次运行中未出现。
结果
| 变体(列表所述内容) | 直接 | 间接 | 其他 |
|---|---|---|---|
description: Helps with git stuff. |
0/3 | 0/3 | "house format" 提示: 2/2 |
| 具体描述(如下) | 3/3 | 3/3 | 负面: 0/2 |
模糊描述 + when_to_use
|
3/3 | 3/3 | 负面: 0/2 |
无前置内容,正文以 # House commit format
|
3/3 | 3/3 | |
无前置内容,正文以 # Notes
|
0/2 | ||
模糊描述,目录命名为 commit-message
|
2/2 | 0/2 | |
具体描述 + disable-model-invocation: true
|
0/2 |
具体描述为:“在此仓库的 house 格式中编写 git 提交消息(type(scope):摘要行,简短的 why,以及 Refs 行)。当用户请求提交消息或希望为 git log 生成 diff 摘要时使用。”
我们对每个主要单元格运行了三次,对较小的对照组运行了两次。由于样本量很小,我们不将表格视为速率。值得注意的是,没有任何单元格出现混合。每个变体要么在给定请求的每次运行中都触发,要么完全不触发。
“Helps with git stuff” 未触发
由于描述模糊,Claude 在一次回合内自行回答了全部六个直接和间接请求,未查看该技能。答案是合理的提交消息。它们不符合 house 格式:没有 type(scope): 前缀,也没有 Refs: 行。典型的第一行是 "添加指数退避到 withRetry 并在遇到 AuthError 时快速失败"。
这是实际中重要的失败。没有错误,答案看起来没问题,唯一表明技能被跳过的迹象是输出悄然忽略了该技能所要强制执行的约定。如果你只检查“我是否得到了提交消息”,你不会注意到。
Claude 看到的列表行是 - kestrel: Helps with git stuff. 提交消息请求可以说是“git 相关事务”。Claude 仍未将其视为打开其完全不了解的技能的理由。
when_to_use 拯救了同样的模糊描述
第三个变体保留了 description: Helps with git stuff. 并添加了一个字段:
when_to_use: When the user asks for a commit message, or asks to summarize a diff for the git log.
该变体在 6 次运行中全部触发,与特定描述相同。文档说明 when_to_use 是“附加到 description 在技能列表中”,转录正好展示了这一点的含义:
Claude Code 使用 " - " 将两个字段合并为一行。从模型的角度来看,没有独立的字段,只是一个更长的描述。因此,在我们的运行中,when_to_use 的作用等同于将同样的句子放入 description 中。它仍可能有所帮助的地方在于你如何维护文件:“它做什么”部分和“什么时候使用它”部分在 frontmatter 中保持分离。两个字段都计入列表条目相同的 1,536 字符限制,这一点在 frontmatter 参考中有说明。
没有描述时:第一行成为唯一条目
文档说明,如果省略 description,Claude Code 将“使用 markdown 内容中的第一个非空行”。我们使用了一个完全没有 frontmatter 的 SKILL.md 进行测试。其第一行是标题 # House commit format,而列表显示为 - kestrel: House commit format,其中的 # 被删除了。
该技能在 6 次中触发了 6 次,与精心编写的描述一样可靠。三个词就足够了,下面的控制指向 "commit" 作为起作用的词。
为了验证这是关于词语而非缺失的 frontmatter,我们仅将标题改为 # Notes。列表变为 - kestrel: Notes,且该技能在 2 次直接运行中未触发任何次数。
这是我们发现最有用的结果。规则不是“编写长描述”或“填写 when_to_use”。Claude 将请求与一行简短文本进行匹配,该行必须包含能指向请求的词语。标题可能无意中做到这一点,而描述同样可能遗漏。
名称计数,但仅限于其包含的词语
将目录重命名为 commit-message 且描述保持模糊不变时,列表显示为 - commit-message: Helps with git stuff. 直接请求“为此 diff 编写提交消息。”在 2 次中触发了 2 次。间接请求“给我一个此更改在 git 日志中的单行摘要。”在 2 次中触发了 0 次。
这个名称只与其中一条请求的字面用词吻合,与另一条不吻合。描述性的名称能缩小差距,但只能覆盖那些用词恰好与名称相同的请求。描述还可以列出人们询问同一件事的其他说法,而具体描述和 when_to_use 这一行都做到了:两者
调用技能对回答和账单的影响
每次调用技能的运行在 JSON 输出中都报告了 3 轮(一次报告了 4 轮:即被拒绝的 Bash 调用的那次运行)。未调用技能的运行均报告了 1 轮。没有技能时的时长在 2.5–6.2 秒之间,使用技能后时长增加到 6.9–13.6 秒。输出中标注为基于列表价格的 total_cost_usd 字段,从每次运行的 $0.030–$0.046 上升到 $0.056–$0.077。该范围未包含我们的第一次运行($0.22),该运行向提示缓存写入了约 20,000 个 token,后续运行主要从中读取。所有 38 次运行在此字段上的总费用为 $2.21。
在许多运行中,Claude 在调用技能时会传入一个 args 字符串,通常是对请求的重述。我们的技能主体没有 $ARGUMENTS 占位符,转录显示 Claude Code 会在加载的技能文本末尾追加一个 ARGUMENTS: 行,这正是文档中描述的该情况下的行为。
间接请求是技能对答案影响最大的地方。用户只要求一行。所有 9 次调用技能的间接运行仍然产生了完整的房屋格式消息,包含原因行和 Refs: none。其中 5 次先给出了一行摘要,然后提供完整消息;4 次则直接给出完整消息,并指出其第一行正是摘要。没有一次仅返回了一行。如果说技能的主体写的是“按此 exact 形状编写提交消息”,它不会自动屈服于“仅一行”的要求。如果技能应仅作用于请求的一部分,其主体必须明确说明。这与触发机制是另一课题,而技能触发频率越高,这一问题就越重要。
格式还导致了一些内容被排除。在 16 次未调用技能的运行中(包括两次纯手动运行),有 4 次的提交消息以 Co-Authored-By 尾行结束。而在 22 次调用技能的运行中,没有一次出现这种情况。技能的格式以 Refs 行结束,Claude 一直遵循此格式。我们未检查决定尾行是否出现的因素。
我们从中获得的用于自身技能的经验
运行我们商店的仓库中有十个项目技能,每个技能的说明都注明了其使用时机;其中一个还列出了触发短语。此实验表明这一点的重要性:列出的行是 Claude 用来匹配请求的依据;在我们的运行中,该行要么包含请求的词语,要么技能被忽略。
我们现在用于新技能的检查是该列表行本身,而不是 SKILL.md。读取该行作为 - <name>: <description> - <when_to_use>,并询问人们用于该任务的词语是否出现在其中,包括间接的那些。如果该技能没有 frontmatter,则该行是正文的第一行。类似“Notes”的标题对 Claude 没有提供可匹配的内容,而在我们的两次运行中,Claude 从未自行选择该技能。
重现它
下面的运行器将一个变体写入一个新的临时目录并运行一次请求。交换 frontmatter 以测试其他描述。
#!/bin/bash
# usage: ./run.sh <label> "<frontmatter lines or empty>" "<request>"
label="$1"; fm="$2"; ask="$3"
dir="$(mktemp -d)"
mkdir -p "$dir/.claude/skills/kestrel" results
{
[ -n "$fm" ] && printf -- '---\n%s\n---\n\n' "$fm"
cat body.md
} > "$dir/.claude/skills/kestrel/SKILL.md"
printf '{ "disableBundledSkills": true }\n' > "$dir/.claude/settings.json"
( cd "$dir" && CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 claude -p "$ask
$(cat "$OLDPWD/diff.txt")" \
--output-format json --max-turns 3 --setting-sources project < /dev/null \
) > "results/$label.json"
./run.sh vague__direct__1 'description: Helps with git stuff.' "Write a commit message for this diff."
./run.sh nodesc__direct__1 '' "Write a commit message for this diff."
< /dev/null 在非交互式 shell 中启动这些命令时很重要:我们第一次运行时未使用它,等待了三秒钟的 stdin 并打印了警告。要计数,请读取每个 session_id 的转录内容:
import glob, json, os, sys
from collections import defaultdict
tally = defaultdict(lambda: [0, 0])
for path in sorted(glob.glob(os.path.join(sys.argv[1], "*.json"))):
variant, prompt, _ = os.path.basename(path)[:-5].split("__")
session_id = json.load(open(path))["session_id"]
transcript = glob.glob(os.path.expanduser(f"~/.claude/projects/*/{session_id}.jsonl"))[0]
called = False
for line in open(transcript):
record = json.loads(line)
if record.get("type") != "assistant":
continue
for block in record["message"]["content"]:
if block.get("type") == "tool_use" and block["name"] == "Skill":
called = True
tally[(variant, prompt)][0] += called
tally[(variant, prompt)][1] += 1
for (variant, prompt), (hits, runs) in sorted(tally.items()):
print(f"{variant:14} {prompt:10} {hits}/{runs}")
相同的转录包含一个类型为 skill_listing 的 attachment 记录,其 content 正是 Claude 看到的确切行。阅读该行比重新阅读我们的 SKILL.md 更有用。
我们未测量的内容
这是我们账户上的默认模型,在未使用用户设置的情况下,CLI 使用的任何努力级别下;较小的模型可能在其他位置划定界限。每个列表中只包含一个技能。当有数十个技能竞争,或捆绑的技能保持开启时,模糊的描述可能会输给描述更好的邻居,而不是被简单忽略,而我们并未对此进行测试。足以被列表预算截断的描述超出了范围。
样本量很小:每个主单元格三次运行,每个对照组两次。该模式在每个单元格中都成立,但 3/3 并不是 100% 的成功率,且每种请求类型只有一种措辞,这并不能调查人们如何询问提交消息。所有请求均使用英文。
所有运行均在 -p 模式下进行。我们未测试交互式会话;在该会话中,“house format” 运行中的 Bash 查询本应询问批准而非被拒绝,且请求前的对话可能会改变决定。我们也没有测试 paths、user-invocable: false 或起始目录下嵌套的技能。
最后,每次运行的第一个请求携带的输入 token 数在 19,264 到 20,202 之间,分为两组,相隔约 800 个 token,且与变体不对齐。我们未找出它们之间的区别,因此在此不对每个描述的 token 消耗做出任何声明。
Rulestack 在 rulestack.gumroad.com 出售技能、规则文件和 Claude Code 的钩子。
后续测量结果将在 Bluesky 上发布,地址为 @ai-shop.bsky.social。

