本教程面向已经写过第一个 SKILL.md、却卡在"技能死活不触发"或"触发了但行为跑偏"的开发者。你将掌握一套可复用的 Skill 排查方法论:从理解渐进式披露(progressive disclosure)的触发机制,到用 /skills 与 --debug 建立可观测性,再到重写 description、收敛 allowed-tools 修复行为偏差。教程以一个"函数转测试用例"技能为案例,先植入两类真实缺陷,再一步步诊断修复,全程可复现。
claude --version 能输出版本号)~/.claude/skills/<name>/SKILL.md 的目录结构)90% 的"不触发"问题,根因是没理解 Claude Code 的渐进式披露机制。它的核心只有一句话:
会话启动时,Claude 只把每个 Skill 的
name+description(即 frontmatter 元数据)加载进上下文;SKILL.md的正文(真正的指令、脚本、模板)只有在被判定相关时才加载。
这意味着:
flowchart LR
A[用户输入] --> B{扫描所有 Skill 的<br/>name + description}
B -->|匹配度高| C[加载完整 SKILL.md 正文]
B -->|匹配度低| D[技能被忽略<br/>正文永不加载]
C --> E[按正文指令执行]
D --> F[用户以为'坏了']
由此推出三条铁律:
一个常见误解是"我把关键词堆进 description 就行"。堆砌反而会稀释语义、降低匹配精度。正确做法见第四步。
排查的前提是"看得见"。Claude Code 提供三个观测手段,缺一不可。
手段一:/skills 命令确认被发现
在交互式会话里输入 /skills,会列出当前会话可用的所有技能及其状态。第一步永远是确认你的技能是否出现在列表里。
name 不一致、frontmatter 解析失败。disable-model-invocation: true 导致它只能显式调用。手段二:显式调用做隔离实验
把"触发判断"这个变量先消掉。在 /skills 里手动选中你的技能,或在对话里直接说"请使用 xxx 技能"。如果显式调用能正常工作,说明正文没问题,问题在 description 触发;如果显式调用也跑偏,说明问题在正文逻辑或工具配置。这一步能帮你把问题域一刀切成两半。
手段三:--debug 查看决策日志
启动时加上调试标志:
claude --debug
# 或针对更细的工具调用日志
claude --debug --verbose
--debug 会打印工具选择、技能加载、上下文注入等内部决策。当技能没触发时,你能在日志里看到模型是否"考虑过"这个技能、为何放弃。这是定位"触发失败"最硬的证据。
常见问题:日志太多看不过来? 解决:用管道过滤关键词,例如
claude --debug 2>&1 | tee debug.log,再用编辑器搜索你的技能name,只看相关片段。
我们造一个真实有用的技能:fn-to-test——给定一个函数,生成对应单元测试。下面是"带病"的 v1 版本。
目录结构:
~/.claude/skills/fn-to-test/
└── SKILL.md
SKILL.md v1(带病版本):
---
name: fn-to-test
description: test helper
allowed-tools: Read, Write, Bash
---
# 函数转测试
你是一个测试专家。请帮用户写测试。
读取用户给的代码,然后生成测试用例,写到一个文件里。
这个 v1 看似能跑,实际埋了两个典型缺陷:
description: test helper 太短、太泛、第一人称缺失触发语境。模型看到"test helper"完全无法判断何时该用它——它和"写个测试脚本""帮我测一下"等无数意图竞争,匹配度极低。allowed-tools: Read, Write, Bash 范围过宽,等于没设防。接下来两步分别诊断 A 和 B。
复现:新建一个会话,贴一段函数代码,说"帮我补测试"。预期它会自动用 fn-to-test,实际——大概率什么都没发生,或用了别的技能。
用 /skills + --debug 验证:
/skills 里能看到 fn-to-test → 发现正常,问题在 description。claude --debug 日志里搜 fn-to-test,会看到模型"未匹配"或根本没把它列为候选。修复:重写 description。好的 description 要满足四个要素:第三人称、动词开头、说清"做什么"、点明"何时触发"。对比:
# ❌ v1 病变
description: test helper
# ✅ v2 修复
description: |
Generates unit tests for a given function. Use when the user provides a
function (any language) and asks for tests, test cases, or coverage for it.
Produces table-driven tests with happy-path, edge-case, and error-path
cases, and writes them next to the source file.
关键改进点:
"Generates unit tests" —— 动词开头,动作明确。"Use when the user provides a function and asks for tests" —— 直接告诉模型触发条件,覆盖"补测试/加测试用例/提高覆盖率/写单测"等多种自然语言变体。table-driven、三类用例、写回源文件旁),让模型知道"匹配后能得到什么",提升相关度评分。验证修复:重开会话,贴函数说"帮我补单测",技能应自动触发。若仍偶尔不触发,检查是否有更强的竞争技能抢了匹配——比如项目里另有一个 description 更泛的 test-writer。多个技能 description 重叠时,模型会选语义最贴近的那个;解决办法是让每个技能的 description 在"领域"或"语言"上差异化。
flowchart TD
Q[技能不触发] --> R1{出现在 /skills 列表?}
R1 -->|否| P1[发现层问题: 路径/目录名/frontmatter]
R1 -->|是| R2{显式调用能工作?}
R2 -->|否| P2[正文/工具层问题: 见第五步]
R2 -->|是| R3{description 是否动词开头+含触发条件?}
R3 -->|否| P3[重写 description]
R3 -->|是| P4[检查竞争技能描述重叠]
触发修好后,运行发现新问题:测试写出来了,但风格混乱、路径乱放、甚至误删了源文件里的内容。这是正文约束不足 + 工具权限过宽的复合症。
修复 B-1:用结构化正文约束行为。把模糊的"写测试"变成有边界的工程指令:
---
name: fn-to-test
description: |
Generates unit tests for a given function. Use when the user provides a
function (any language) and asks for tests, test cases, or coverage for it.
Produces table-driven tests with happy-path, edge-case, and error-path
cases, and writes them next to the source file.
allowed-tools: Read, Write, Edit
---
# 函数转测试
## 何时使用
当用户提供一段函数并希望为其生成单元测试时。
## 执行步骤
1. 用 Read 读取目标函数源码,识别函数签名、参数、返回值、可观测副作用。
2. 至少设计三类用例:正常路径、边界值(空值/极值)、错误路径(异常/非法输入)。
3. 使用与源文件相同的语言与测试框架(无明确框架时,TS 用 Vitest,Python 用 pytest)。
4. 用 Write 把测试写到源文件同级目录的 `<name>.test.<ext>`。
5. 用 Edit 只追加测试文件,禁止修改原始源码。
## 禁止事项
- 不要修改、重命名、删除用户提供的原始源文件。
- 不要引入未在项目中声明的依赖。
- 测试文件超过 300 行时,先与用户确认拆分。
注意正文从"自由发挥"变成了"步骤 + 禁止事项"双约束,行为立刻可控。
修复 B-2:收敛 allowed-tools。v1 给了 Bash,意味着技能能在执行期间跑任意命令——这对一个"生成测试"的技能既无必要又有风险。v2 收敛到 Read, Write, Edit:读源码、写测试、追加测试,恰好够用。
重要坑提示:
allowed-tools在 frontmatter 里声明的"限制"并不总是被严格执行。社区已记录一个已知问题(anthropics/claude-code #37683):声明了allowed-tools后,模型在技能执行期间仍可能调用未列出的工具。因此不要把 allowed-tools 当作唯一的安全防线——它更像"建议而非强制"。真正要做的是:(1) 正文用"禁止事项"明确约束;(2) 对涉及删除/网络的操作,配合 settings.json 的 hooks 做硬性拦截;(3) 在受限环境运行高危技能前,用只读沙箱先验证。
验证修复 B:再次运行,确认:测试只写不删、路径正确、用例覆盖三类、无多余依赖。若仍偶发越界,回到正文继续收紧"禁止事项"。
主观判断"现在好多了"不可靠。标准做法是建一个 eval 集:5~10 条会触发的 prompt + 3~5 条不该触发的负样本,分别在 v1、v2 上跑,统计触发准确率与行为合规率。v2 应在两者上都显著优于 v1。这套"从祈祷到工程化"的评测方法在专门的 Skill 评测教程中有完整展开,本文不重复,只强调一点:没有评测,所有修复都是猜。
把上述 v2 整理成一个可直接落地的技能。三个文件,复制即用。
文件 1:~/.claude/skills/fn-to-test/SKILL.md(即上文 v2 全文)
文件 2:一份测试输入样本 examples/auth.ts
export function canAccess(user: { role: string; active: boolean }, resource: string): boolean {
if (!user || !user.active) return false;
if (resource.startsWith('admin')) return user.role === 'admin';
return true;
}
文件 3:排查命令清单(保存为 debug.sh 备查)
#!/usr/bin/env bash
# 1. 确认技能被发现
echo "==> /skills 列表(交互式会话内执行)"
# 2. 开启调试,捕获决策日志
claude --debug 2>&1 | tee debug.log
# 在日志中搜索: fn-to-test ,观察是否被纳入候选
# 3. 隔离实验:显式调用,绕过触发判断
# 在会话中输入: 请使用 fn-to-test 技能,为 examples/auth.ts 生成测试
# 4. 对比 v1/v2:保留两个目录,逐个测同样 prompt
# ~/.claude/skills/fn-to-test (当前版本)
# ~/.claude/skills/_archive/fn-to-test-v1
预期结果:
auth.test.ts,含正常/边界/错误三类用例,源文件未被动。flowchart TD
Start[Skill 表现异常] --> Q1{出现在 /skills?}
Q1 -->|否| F1[发现层: 检查路径/目录名=name/frontmatter YAML]
Q1 -->|是| Q2{disable-model-invocation?}
Q2 -->|是 且想自动触发| F2[改为 false 或接受仅显式调用]
Q2 -->|否| Q3{显式调用正常?}
Q3 -->|否| Q4{--debug 显示工具被拒?}
Q4 -->|是| F3[修正 allowed-tools / 正文工具引用]
Q4 -->|否| F4[正文逻辑缺陷: 补步骤+禁止事项]
Q3 -->|是| Q5{自动触发失败?}
Q5 -->|是| F5[重写 description: 动词+触发条件]
Q5 -->|否| Q6{行为跑偏?}
Q6 -->|是| F6[正文约束 + 收敛 allowed-tools + hooks 硬拦截]
Q6 -->|否| Done[稳定可用: 建 eval 持续监控]
F1 --> Re[重测]
F2 --> Re
F3 --> Re
F4 --> Re
F5 --> Re
F6 --> Re
Re --> Start
Q1:description 写多长合适? 官方上限是 1024 字符,但实操中 80~200 字符最佳。过长会稀释语义、挤占"加载所有 description"的预算。把最关键的"做什么 + 何时用"放最前面。
Q2:我堆了很多关键词,为什么反而不触发? 关键词堆砌让 description 语义模糊,模型难以判断"现在到底该不该用"。改用一两句通顺的第三人称陈述,明确触发场景,精度反而更高。
Q3:/skills 里技能名是灰的/带禁用标记怎么办?
检查 frontmatter 里 disable-model-invocation 是否被设为 true——这会让技能只能显式调用。需要自动触发就删掉这一行或设为 false。
Q4:改了 SKILL.md 为什么没生效? Skill 元数据在会话启动时加载。改完要新开会话(或按工具提示重载),旧会话仍用旧 description。
Q5:allowed-tools 设了 Read 却还想让它跑命令?
要么在 allowed-tools 加 Bash(npm test:*) 这类带限制的形式(如支持),要么在正文用"禁止事项"约束,并把高危操作交给 hooks 硬拦截——因为 allowed-tools 并非总是被强制执行(见第五步坑提示)。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
