当初我刚开始接触 Claude Code 时,最让我困惑的并不是它的对话能力,而是“如何让它稳定地按我想要的流程办事”。每次临时在提示词里塞一堆规则,当时有效,换个场景又得重新说一遍;后来把规则写进系统提示词,倒是统一了,但全局生效又让无关任务被拖慢。直到我把技能(Skills)加载这一套吃透,才算真正把 Claude Code 从“聊天助手”用成了“团队实习生”。这篇第五课,我就把自己在项目里反复打磨技能加载的经验完整拆出来,从加载机制、目录规范,到实际编写、踩坑排查,一条龙讲清楚。
1. 定位与设计思路:Skills 到底解决了什么问题
1.1 技能是什么——把散落的提示词沉淀为可复用能力
很多刚上手 Claude Code 的同学,会把技能理解成“高级一点的提示词文件”。这个说法对了一半。技能确实以 Markdown 文件为主要载体,但它和普通提示词最大的区别在于:技能是一套完整的、可被按需加载的“能力包”。它不只是告诉模型“你要做什么”,还规定了“按什么顺序做、调用什么工具、参考什么资料、输出什么格式”。换句话说,普通提示词是你在现场口述需求,技能则是提前写好的一份岗位说明书,模型在接到对应任务时,自己会去翻这份说明书并按章执行。
我在某个跨平台系统项目里,需要频繁对多个服务的日志做异常分析。最初的做法是把分析规则复制粘贴到每次对话里,后来规则从 5 条涨到 20 多条,粘贴一次要占大量上下文窗口,还经常贴错版本。把规则沉淀成“日志分析技能”之后,我只需要在对话里说一句“用日志分析技能看下今天的报错”,Claude Code 就会自动加载对应的 SKILL.md,按里面的步骤让我提供日志路径、指定分析维度、调用脚本提取特征,最后按固定模板输出结论。整个过程不再依赖我每次记住规则细节,技能本身就是唯一的事实来源。
这套机制特别适合三类人:一是像我这样需要频繁做同类任务的开发者,二是团队里想统一编码规范、评审流程的技术负责人,三是想把个人经验固化下来、避免“人走经验流失”的长期项目维护者。技能加载解决的核心痛点很明确:让模型的行为可预期、可复用、可维护。
1.2 技能与 MCP、插件的边界怎么划
说到可扩展性,很多同学会立刻联想到 MCP(模型上下文协议)或者 IDE 插件。我最初也以为技能会被 MCP 取代,实际用下来发现,两者是互补关系,职责完全不同。
MCP 解决的是“模型能触达哪些外部世界”,比如连数据库、查工单、读对象存储,本质是给模型开了一组新的 API 通道。插件解决的是“编辑器/终端环境里怎么更方便地操作”,比如一键侧边栏、快捷键绑定。而技能解决的是“模型面对特定任务时该怎么思考和行动”,它既可以把 MCP 工具串联进流程,也可以单独使用内置的 Shell、文件读写等工具,去完成一个逻辑完整的任务。
我用一个项目里的真实配置说明。当时我做一个“代码变更影响面分析”技能,里面用到了 Git 命令来获取 diff,用文件搜索工具定位引用关系,最后还调用了内部的知识库 MCP 工具来补充背景信息。这个技能的 SKILL.md 里并没有发明任何新工具,它只是把已有能力组合成了一个固定的分析流程。如果只用 MCP,我依然需要每次手动告诉模型“先查 diff 再搜引用最后查知识库”,但如果只靠技能而没有 MCP 通道,知识库信息就拿不到。
所以我的建议是:技能是“流程编排层”,MCP/插件是“能力供给层”。先想清楚你要固化什么流程,再看流程里缺哪些外部能力,缺的用 MCP 补,流程本身用技能管。这三层叠在一起,才是完整的一套可扩展架构。
2. 加载机制拆解:技能包从哪里读取、如何被触发
2.1 技能目录规范:项目级与用户级的两种放置方式
技能加载的第一步,是让 Claude Code 知道技能包存在。默认情况下,它会在两个位置扫描技能目录:项目级目录和工作区根目录下的.claude/skills/,以及用户级目录~/.claude/skills/。项目级技能对当前仓库生效,适合存放与业务强相关的流程;用户级技能对你所有的项目生效,适合存放通用的编码规范、通用检查清单。
技能包本身是一个文件夹,文件夹名一般建议用短横线分隔的英文,比如code-review、log-analysis。文件夹内至少要有一个SKILL.md文件,这是技能的入口和说明书;还可以附带脚本、模板、参考文档等辅助文件。加载器只认SKILL.md,技能包里的其他文件不会被当作独立技能加载,它们只是执行过程中可以被读取或调用的资源。
下面是一个最小可用的技能包结构:
.claude/skills/ └── log-analysis/ ├── SKILL.md ├── scripts/ │ └── extract_errors.py └── templates/ └── report_template.md这里有个容易踩的坑:技能目录名称里最好不要有空格和中文。虽然某些版本可能兼容,但空格会导致加载器在解析路径时出现意外的匹配失败。我最早把技能文件夹命名为“日志分析 v2”,结果死活加载不出来,改成log-analysis-v2后一切正常。如果你在别人的项目里看到既有中文目录名也能用的案例,多半是因为文件系统的编码转换恰好掩盖了问题,不要拿自己的稳定性去赌这种兼容性。
2.2 SKILL.md 的书写要点:给模型看的“操作手册”
SKILL.md 是技能加载的核心,它由两部分组成:开头的 YAML 元信息和正文指令。元信息里最重要的字段是name和description,这两个字段直接决定了技能会不会被模型“想起来”。加载器会在会话初始化时扫描技能目录,把每个技能的name和description汇总成一份清单,之后模型会根据用户当前请求和这份清单做匹配,匹配到才加载对应 SKILL.md 正文。
我写 description 的经验是:不要写形容词,要写“场景 + 触发条件 + 效果”。比如“适用于代码审查场景,当用户要求检查代码质量、评审合并请求、分析改动风险时使用。执行后输出结构化评审报告。”这种描述既包含了触发词,也说明了技能能产出的结果,匹配成功率明显高于干巴巴的一句“代码审查技能”。描述长度控制在 100 到 200 字比较好,太短信息不足,太长会稀释关键词权重,影响匹配精度。
正文部分则要像给新同事写操作手册一样:分步骤、说清楚输入输出、给出判断标准。模型会按正文指令逐步执行,所以正文里每一步都要具体。比如“第一步:运行 git diff 获取当前分支的改动;第二步:仅筛选 .py、.ts 文件的变更;第三步:针对每个变更文件,按附录中的检查清单逐项评估”。如果你只在正文里写“请检查代码质量”,那等于没写,因为模型不知道你的“质量”标准是什么。
2.3 完整加载链路:五个阶段逐一说明
技能从“存在磁盘上”到“真正影响模型行为”,需要经过五个阶段,理解这条链路对排查问题非常重要。
第一阶段是启动扫描。Claude Code 在项目目录启动或会话初始化时,会扫描上述两个技能目录,识别出所有包含合法SKILL.md的技能包,并解析 YAML 元信息。第二阶段是清单注入。加载器会把所有技能的name和description整理成一份技能清单,注入到模型上下文中。第三阶段是触发匹配。模型在理解用户当前请求后,对照这份技能清单做判断,决定是否需要加载某个技能。第四阶段是按需加载。一旦匹配成功,加载器把对应的SKILL.md全文以及技能包内的关键文件路径注入上下文,模型开始按正文指令规划执行。第五阶段是执行与反馈。模型执行完技能流程后,会返回结果;一些实现里还会把执行记录写回日志,便于复盘。
从这个链路能看出一个关键点:技能正文并不会常驻上下文,只有匹配成功后才加载。这样设计是为了节省上下文窗口,但也意味着如果你的 description 写得不到位,技能就永远不会被唤醒。我见过有人辛辛苦苦写了 500 行 SKILL.md,结果因为 description 里描述过于泛化,模型始终没匹配上,技能形同虚设。所以调技能,第一步不是调正文,而是调 description。
3. 实操复现:5 步做出并加载一个代码评审技能
3.1 目标拆解与技能包骨架
纸上谈兵没意思,我拿实际项目里的“代码评审技能”作为例子,完整走一遍从创建到加载的流程。这个技能的目标很明确:当用户要求评审代码或检查合并请求时,自动提取 Git 改动、按团队规范逐项检查、输出带风险等级的评审结论。
先拆解任务步骤:第一步,获取改动范围,包括分支对比、暂存区状态;第二步,筛选需要评审的文件类型;第三步,按检查清单逐项扫描代码,包括错误处理、性能隐患、安全漏洞、命名规范、重复代码;第四步,对每个问题标注严重级别;第五步,输出评审报告,包含总体结论、问题列表、修改建议。
根据这个设计,技能包骨架如下:
.claude/skills/ └── code-review/ ├── SKILL.md ├── scripts/ │ ├── get_diff.sh │ └── check_todo.py └── docs/ └── review_checklist.md脚本是可选的,但强烈建议放。因为模型在终端环境里执行 Shell 命令时,复杂的管道命令容易出错,把逻辑封装成脚本能让技能更稳定。我在get_diff.sh里封装了分支差异获取逻辑,避免模型每次现场拼命令。
3.2 编写 SKILL.md:把评审标准转译成模型指令
SKILL.md 的 YAML 部分,我是这样写的:
--- name: code-review description: 适用于代码评审场景。当用户要求审查代码质量、评估合并请求风险、检查某个提交或分支的改动,或要求执行团队代码规范检查时使用。技能会提取 Git 差异、按检查清单逐项评估,并输出带风险等级的结构化评审报告。 ---正文部分按操作手册的风格组织,开头先交代技能目标和前置条件,然后分步骤列出执行流程。关键步骤我会写得非常细,比如“筛选文件”这一步,我会明确列出包含哪些扩展名、排除哪些目录:
# 代码评审技能 ## 目标 对当前分支或指定提交的代码变更执行一次结构化评审,输出风险等级明确、可直接执行的改进建议。 ## 前置条件 - 确认当前处于 Git 仓库内 - 明确评审范围:默认评审当前分支与主分支的差异 ## 执行步骤 1. 确定评审范围 - 若用户指定了提交 ID,则以该提交的变更集为准 - 否则运行 `scripts/get_diff.sh` 获取当前分支与主分支的差异 2. 筛选变更文件 - 仅评审以下类型:.py、.ts、.js、.tsx、.jsx、.go、.java - 排除 lock 文件、生成目录(dist、build、node_modules)、配置文件(.json、.yaml) 3. 逐项检查(对照 docs/review_checklist.md) - 错误处理:是否有裸异常、关键路径是否缺 try-catch、失败时是否有降级方案 - 安全风险:是否出现硬编码密钥、未过滤的用户输入拼接、危险反序列化 - 性能隐患:是否存在 N+1 查询、循环内发起 IO、未限制的集合拷贝 - 可维护性:命名是否体现意图、函数是否过长、是否存在重复逻辑 4. 输出格式 - 总体结论:通过 / 有条件通过 / 不通过 - 问题列表:按严重级别排序,每项标注文件、行号、问题描述、修改建议我在正文里尽量使用“若…则…”的条件句式,这样模型在遇到边界情况时有明确的决策依据,不会自己发挥。实际测试中,没有给足判断标准的正文,模型的评审结论经常出现“看情况”这种模糊表述,加上标准之后输出明显更果断。
3.3 添加辅助脚本:让技能拥有真实执行能力
脚本不是锦上添花,是技能稳定性的保障。我写get_diff.sh时最朴素的想法是:与其让模型记一串复杂的 git 命令,不如让它只记得“运行这个脚本”。这样即使模型对 Git 命令的某些参数记忆模糊,技能依然能用。
#!/usr/bin/env bash # 获取当前分支与主分支的差异,输出按文件分组的变更摘要 set -euo pipefail base_branch="${1:-origin/main}" target_branch="${2:-HEAD}" echo "=== 评审范围 ===" echo "基础分支: ${base_branch}" echo "目标分支: ${target_branch}" echo "" echo "=== 变更文件列表 ===" git diff --name-only "${base_branch}"..."${target_branch}" echo "" echo "=== 差异摘要 ===" git diff --stat "${base_branch}"..."${target_branch}" echo "" echo "=== 关键代码差异 ===" git diff --unified=5 "${base_branch}"..."${target_branch}" -- '*.py' '*.ts' '*.tsx' '*.js' '*.ts' ':!**/dist/**' ':!**/node_modules/**'脚本里用set -euo pipefail是我和模型一起踩过坑之后才加上的。如果没有这个,某条 git 命令在非 Git 目录执行失败时,脚本不会立即退出,后续逻辑照样往下跑,最后输出一份残缺的差异报告。另外我特意用origin/main作为默认基础分支,这个默认值根据团队习惯来定,有些团队用origin/master,有些用origin/develop,一定要在技能里跟用户确认或者允许用户传入,否则会拿错基线。
3.4 加载验证:从初次调用到跑通全流程
技能写好之后,验证加载是否成功是必不可少的一步。我在项目目录下启动 Claude Code,然后输入一句并不完全匹配的请求,测试描述里的触发词能不能生效:
我输入:“帮我看下当前分支的代码有没有问题。”
这句话里没有出现“评审”“审查”这些词,但包含了“代码有没有问题”这个意图。模型通过技能清单里的描述,匹配到了code-review技能,于是加载 SKILL.md,提示我确认评审范围,接着调用get_diff.sh获取差异,按清单逐项检查,最后输出了评审报告。从输出结果看,报告里包含了变更文件列表、三个问题项以及各自的风险等级,说明整个技能链路是通的。
这时候我要特别强调一个验证技巧:不要一上来就测试精确指令,先测模糊指令。因为精确指令比如“执行 code-review 技能”会让模型直接去找名为 code-review 的技能,这相当于开卷考试;而模糊指令才是日常使用的真实场景,能检验 description 的匹配质量。如果模糊指令没能触发技能,优先改 description 里的触发场景描述,而不是去改正文内容。
4. 调优三板斧:触发召回、执行权限、多技能协作
4.1 控制技能召回率:description 怎么写更“招之即来”
技能加载的体验,很大程度上取决于召回的准确率。召回率太高,稍微沾点边的请求就会加载技能,浪费上下文还拖慢响应;召回率太低,用户明确表达了需求,模型却当成普通对话处理。我在调优阶段总结出 description 书写的几个关键点。
第一,要写“技能不适用什么”的情形。很多人以为 description 只写适用场景就行,实际上把排除场景写进去同样重要。比如我有个“部署发布技能”,它只在用户要求发布上线时触发,但如果用户只是问“当前环境配了哪些变量”,就绝不该加载部署流程。我在 description 里加了“仅当需要执行发布相关操作时使用;若用户仅询问配置信息,不应匹配本技能”,这之后误触发的次数明显下降。
第二,把同义词和口语化表达写进去。模型理解能力很强,但它需要线索。如果你想覆盖“审查代码”这个场景,除了“代码审查”“代码评审”,还可以写上“帮我看看这段改得怎么样”“检查一下这次提交”。这些来自真实用户习惯的表达,比正式的术语更容易触发匹配。
第三,控制描述长度。我试过把 description 写到 300 字,结果不仅匹配不精准,还占用了不少上下文额度。后来压到 120 字左右,只保留场景、触发条件、输出效果三个要素,效果反而更好。这就好比给人发工作简介,写得太长反而抓不住重点。
4.2 执行边界与权限:让技能只做该做的事
技能一旦被加载,模型就拥有了执行整个流程的“指令依据”,但并不是所有技能都应该拥有全部工具权限。我在实践里踩过一次深刻的坑:某个技能脚本里包含清理临时文件的操作,结果一次误触发把项目构建目录给删了。从那以后,我做任何技能都会先明确“这个技能不应该做什么”。
在 SKILL.md 正文里,我会显式列出禁止事项。比如代码评审技能里,我写了“禁止修改任何源码文件,禁止运行构建命令,禁止推送分支”。这些禁止指令虽然不能做到系统级的强制约束,但在模型执行技能时的优先级很高,能有效阻止常见的越权操作。如果你的运行环境支持更细粒度的工具权限配置,我强烈建议把技能需要用的工具范围圈定出来,只给最小权限集合。
还有一类边界是数据边界。有些技能会读取敏感信息,比如日志分析技能可能要读取包含业务数据的日志文件。我在技能正文里要求模型在读取前先确认文件路径,并且在输出报告中移除可能的敏感字段,比如手机号、token、密钥。技能不是法外之地,数据安全约束写得越早,后面收拾烂摊子的成本越低。
4.3 多技能协作与冲突处理
当技能数量超过五个之后,一个新的问题浮现出来:多个技能的 description 可能会有重叠,导致同一个请求命中两三个技能,模型不知道该优先执行哪个。我遇到过“代码评审技能”和“重构建议技能”同时被触发的情况,两个技能给出的建议方向完全不同,模型纠结了很长时间,最后输出了一份混搭的结论。
解决重叠问题,我用的方法是给技能设置明确的分级与边界。在各自的 description 里声明优先级:如果用户请求包含重构诉求,应优先使用“重构建议技能”;如果仅要求对现有代码做质量评估,则使用“代码评审技能”。另外一种方案是干脆把两个技能合并成一个,在正文里按分支流程处理不同诉求。这里没有标准答案,取决于你希望技能粒度细还是粗。我个人的经验是,技能粒度宁粗勿细,一个技能如果能覆盖一类完整任务闭环,就别拆成多个半截技能。
还有一个容易忽略的点:技能之间的引用。一个技能可以显式调用另一个技能吗?在实际使用中是可以的,你只需要在正文里写明“若用户需要了解日志格式,请加载 log-format-reference 技能”。模型会像查资料一样,在需要的时候按名字搜索并加载对应技能。这种层级化设计适合更复杂的任务体系,比如一个项目级总控技能下面挂多个子技能。但层级不要超过三层,否则加载链路会变得难以追踪,排查问题时你会怀疑人生。
5. 踩坑实录:技能“不加载”“乱执行”“多冲突”的排查方法
5.1 技能未被识别:三个高频原因
我整理了实战里最常见的三个导致技能“失效”的原因,每一个都是我或者同事真实遇到过的。
第一个原因是技能目录名称不规范。前面提到过,目录名里的空格、特殊字符、中文都可能导致加载器解析异常。有个同事的项目里,技能目录叫analysis-tool-final-2,看起来没问题吧?但他在 YAML 里的 name 写的是Analysis Tool Final,空格在元信息里倒是合法,可是模型通过 name 去匹配技能时,带了空格的名字和用户请求的关键词之间匹配效果很差,造成技能时常不被触发。后来我把 name 统一改成小写短横线风格,问题就消失了。
第二个原因是 SKILL.md 的 YAML 元信息解析失败。最常见的是 description 里有特殊字符没有转义,比如引号冒号;或者 YAML 里的冒号后面没有空格。这些错误会导致整份 SKILL.md 被跳过,而且不会有明确报错。我的排查办法是先手动解析:把 SKILL.md 里的 YAML 复制到在线解析工具里看能否解析成功。只要 YAML 合法,加载器基本不会跳过。
第三个原因是技能目录位置放错。我一直以为技能放在项目根目录下的skills/就行,后来才发现 Claude Code 只认.claude/skills/这个约定目录。仔细看文档复盘时发现,不同版本甚至不同部署模式的默认扫描目录会有差异,有的会额外扫描.claude/skills之外的其他路径,但不能默认依赖这些差异。最稳妥的方式是启动后先问一下模型“现在可用哪些技能”,让它列出来看看你的技能在不在清单里,这一步是排查一切加载问题的起点。
5.2 执行途中报错:脚本、路径与上下文问题
技能加载成功不等于执行成功,真正的问题往往在执行阶段。我在测试日志分析技能时遇到过一个典型的报错:模型按照 SKILL.md 指令去运行脚本,但脚本里用了相对路径,而模型的工作目录和技能包目录并不是同一个位置,导致找不到文件。解决方案是在脚本开头显式切换到技能包所在的绝对路径,或者在技能正文里明确规定所有资源文件必须基于技能包目录进行定位。
另一个常发生在执行阶段的问题,是模型对长步骤的记忆偏差。当技能正文包含 8 个以上步骤时,模型可能在执行到第 5 步时忘记了第 2 步的约束条件,比如我在前面限制了“仅评审 .py 和 .ts 文件”,但模型到后面又去评审了 .json 文件。这种问题我到现在也没有完美解法,最有效的缓解措施是:让脚本承担更多过滤逻辑,把判断交给代码而不是只靠模型记忆。比如我在get_diff.sh里已经用-- '*.py' '*.ts'限制了 diff 输出范围,模型即使想跑偏,拿到的数据范围也是被过滤过的。
还有一类问题是上下文长度。技能包的辅助文件如果很大,比如参考文档有几十万字符,模型加载时会把全文塞进上下文,不仅拖慢响应,还可能导致后续对话直接挤爆上下文窗口。解决方案是把大文档拆成小模块,SKILL.md 里只写关键的摘要和索引,需要细节时再让模型按需读取对应的文档片段。
5.3 常见异常速查表
我把调试技能过程中碰到的常见异常整理成一张速查表,方便大家对照排查。
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 技能从未被触发 | description 缺少触发场景描述 | 用模糊口语指令测试,重写 description |
| 技能偶尔被触发 | 描述与用户请求匹配率低 | 补充同义词和排除场景,精简描述长度 |
| 启动时报错但无法定位技能 | SKILL.md YAML 解析失败 | 单独解析 frontmatter,检查特殊字符 |
| 技能不出现可用清单里 | 目录位置不对或名称不合规 | 确认.claude/skills/路径,检查目录名 |
| 脚本执行报“文件不存在” | 相对路径问题 | 在脚本内使用绝对路径或基于技能包目录定位 |
| 模型执行步骤遗漏 | 正文步骤太多,模型记忆偏差 | 减少步骤数,把逻辑放进脚本 |
| 技能间互相干扰 | description 重叠导致多匹配 | 增加优先级声明,合并同类技能 |
| 加载后响应太慢 | 辅助文档过大占用上下文 | 拆分文档,正文只留索引和摘要 |
这张表我建议直接贴在项目文档里,遇到问题先查表,能省下大量对着日志发呆的时间。
最后再分享一个我个人坚持的实践习惯:每次新写一个技能,我都会先建一个一个测试用的临时项目,在里面放一个最小样本文件,然后用至少三句不同风格的话去触发同一个技能,看它是否都能稳定加载、稳定执行。这个习惯帮我提前发现了很多“凑巧能用”的隐患。技能加载这件事,难的不是把文件写出来,而是让模型在正确的时机、用正确的方式、只执行正确的那部分流程。把这一课吃透之后,你再回头用 Claude Code 做重复性任务,会明显感觉到它真正开始懂你的流程习惯了。