033、Skill 系统入门:定义、触发与编写第一个 Skill
上周五凌晨两点,我在调试一个诡异的 CI 失败——Claude Code 在生成 Kubernetes 部署清单时,突然开始用 YAML 写起了莎士比亚十四行诗。service 字段变成了“thou shalt expose port 8080”,replicas 被写成“three score and ten”。排查了半天,发现是某个同事在 .claude/skills 里塞了一个“poetic-mode”的 Skill,触发条件写的是“当用户提到 deploy 时”。这玩意儿没被 review 就合并了,结果整个团队被坑了一整晚。
Skill 系统是 Claude Code 最容易被低估的能力,也是翻车率最高的功能。它本质上是一种“行为插件”——不是改模型权重,而是通过定义触发条件和响应逻辑,让 Claude 在特定场景下执行你预设的操作。今天我们就从踩坑开始,把 Skill 的机制、触发规则和编写规范彻底讲透。
Skill 到底是什么
别被名字唬住。Skill 就是一个 JSON 或 YAML 文件,放在项目根目录的 .claude/skills/ 下。Claude Code 启动时会扫描这个目录,把每个 Skill 注册到它的“行为库”里。当用户输入匹配了某个 Skill 的触发条件,Claude 就会执行该 Skill 定义的动作——可能是执行一段 shell 命令、调用一个 API、或者修改对话上下文。
我习惯把 Skill 理解为“AI 的肌肉记忆”。你不需要每次告诉它“用 pytest 跑测试”,只要写一个 Skill,以后提到“跑测试”它就自动执行 pytest -v --tb=short。但问题在于,触发条件写得太宽泛,就会像我们那个“poetic-mode”一样,把整个对话带偏。
触发机制:别让 Skill 变成幽灵
Skill 的触发分两种:显式触发和隐式触发。显式触发是用户主动调用,比如输入 /run-tests;隐式触发是 Claude 根据对话上下文自动匹配。新手最容易在隐式触发上翻车。
隐式触发的核心是 trigger 字段,它支持三种模式:
- 关键词匹配:
trigger: "deploy"会匹配任何包含“deploy”的输入。这太危险了——用户说“别 deploy”,它照样触发。 - 正则表达式:
trigger: "/deploy\s+(staging|production)/i"更精确,但写不好会漏匹配。 - 语义匹配:
trigger: { type: "semantic", prompt: "用户想要部署应用" }最灵活,但需要调阈值,默认 0.7 的相似度经常误触。
我的经验是:优先用显式触发,除非你非常确定隐式触发的边界。显式触发就是在 Skill 文件名里加 @ 符号,比如 run-tests@v1.yaml,用户输入 /run-tests 才会激活。这能避免 90% 的误触发问题。
编写第一个 Skill:从需求到文件
假设我们要写一个 Skill:当用户说“检查代码风格”时,自动运行 flake8 并格式化输出结果。
在 .claude/skills/ 下创建 lint-checker.yaml:
# 别这样写:trigger: "检查代码风格" # 太宽泛,用户说"帮我检查一下代码风格"就匹配不上
name: "lint-checker"
description: "运行 flake8 检查 Python 代码风格,输出带行号的问题列表"
trigger:
type: "semantic"
prompt: "用户想要检查 Python 代码的 lint 问题"
threshold: 0.8 # 这里踩过坑:默认 0.7 会匹配到"检查代码质量",调高到 0.8 更安全
actions:
- type: "command"
command: "flake8 . --max-line-length=100 --statistics"
# 别这样写:command: "flake8 ." # 没有参数,输出太啰嗦,用户会骂
parse_output: true # 让 Claude 解析输出结果,而不是直接扔给用户
- type: "respond"
template: |
## Lint 检查结果
{{#if output.exit_code == 0}}
✅ 代码风格通过,没有发现问题。
{{else}}
⚠️ 发现 {{output.line_count}} 个问题:
```
{{output.parsed}}
```
建议修复后再提交。
{{/if}}
这个 Skill 的关键在于 parse_output: true。如果不加,Claude 会把 flake8 的原始输出直接贴出来——一堆文件名和行号,用户根本看不懂。加了之后,Claude 会解析输出,提取关键信息,按模板格式化。我见过有人在这里写 parse_output: false,结果每次跑完都要手动解释输出,Skill 的价值直接减半。
高级技巧:Skill 链与上下文传递
单个 Skill 能做的事有限。真正有用的是 Skill 链——一个 Skill 触发后,修改上下文,让另一个 Skill 接着触发。
比如我们想做一个“部署流水线”Skill:先跑测试,再构建镜像,最后部署。可以拆成三个 Skill:
# test-runner.yaml
name: "test-runner"
trigger: "/run-pipeline"
actions:
- type: "command"
command: "pytest -v --tb=short"
set_context:
tests_passed: "{{output.exit_code == 0}}"
# 这里踩过坑:set_context 的变量名不要用特殊字符,否则后续 Skill 解析会炸
# docker-builder.yaml
name: "docker-builder"
trigger:
type: "context"
key: "tests_passed"
value: "true"
actions:
- type: "command"
command: "docker build -t myapp:latest ."
set_context:
image_built: "{{output.exit_code == 0}}"
# deployer.yaml
name: "deployer"
trigger:
type: "context"
key: "image_built"
value: "true"
actions:
- type: "command"
command: "kubectl apply -f k8s/deployment.yaml"
这样,用户只需要输入 /run-pipeline,三个 Skill 就会按顺序执行。但有个坑:如果中间某个 Skill 失败,后面的不会自动停止。你需要手动在 set_context 里判断退出码,或者用 condition 字段控制执行条件。
调试 Skill:日志是你的朋友
Skill 出问题最难排查,因为它是隐式触发的。我一般会在 Skill 里加一个调试模式:
# 调试用,别提交到生产
name: "debug-skill"
trigger: "/debug-skill"
actions:
- type: "command"
command: "echo 'Skill triggered at $(date)' >> /tmp/claude-skill.log"
# 别这样写:command: "echo 'debug'" # 信息太少,根本看不出触发上下文
- type: "respond"
template: |
当前上下文变量:
- tests_passed: {{context.tests_passed}}
- image_built: {{context.image_built}}
- 用户输入: {{user_input}}
把这个 Skill 放在 .claude/skills/debug/ 目录下,Claude Code 会递归扫描。调试完直接删掉整个目录,不会影响生产 Skill。
个人经验:Skill 系统的三个铁律
-
触发条件宁可严格,不要宽松。一个 Skill 少触发几次,比误触发十次要好。误触发会污染对话上下文,让 Claude 产生幻觉。我们团队现在规定:所有隐式触发必须经过至少两个人 review,阈值不低于 0.85。
-
Skill 文件必须版本控制。
.claude/skills/目录要提交到 Git,但.claude/skills/debug/要加到.gitignore。我见过有人把调试 Skill 提交上去,结果 CI 里每次构建都触发调试输出,日志文件涨到几个 GB。 -
每个 Skill 只做一件事。如果你发现一个 Skill 里写了三个 command,或者 template 超过 50 行,就该拆分了。Skill 链比大 Skill 更容易调试和复用。我们有个同事写了一个 200 行的 Skill,后来没人敢改,最后重写了六个小 Skill,维护成本直接降了 80%。
Skill 系统用好了,Claude Code 能从“对话助手”变成“自动化引擎”。但用不好,就是那个写十四行诗的 deploy 脚本——看起来有趣,实际上让人想砸键盘。下次我们聊聊 Skill 的权限控制和安全边界,那又是一个能写三篇的坑。
1010

被折叠的 条评论
为什么被折叠?



