Claude Code Skill开发避坑指南:从30个失败案例到高效实践

发布时间:2026/10/31 18:48:27
Claude Code Skill开发避坑指南:从30个失败案例到高效实践

1. 从“写了50个Skill”到“前30个都白写”的真相

我大概是在去年年底开始密集写 Claude Code Skill 的。那会儿刚把 Claude Code 装到 Ubuntu 和 Windows 两套环境里跑通,觉得这东西太顺手了——终端里直接对话,能读文件、能跑命令、能改代码,比在 IDE 里来回切窗口舒服太多。于是我开始琢磨:既然它支持自定义 Skill,那我是不是可以把日常重复的活儿全封装成 Skill?

这个念头一开,就收不住了。头一个月我写了差不多 30 个 Skill,覆盖代码审查、接口文档生成、Spring Boot 监控配置、数据库迁移脚本、日志分析、单元测试补全……每个 Skill 我都认真写了SKILL.md,定义了触发条件、输入输出、执行步骤。当时自我感觉良好,觉得这套东西以后就是我的“自动化军火库”。

结果用了两个月,我发现自己真正高频调用的 Skill 不超过 5 个。剩下那 25 个,要么触发条件写得太宽泛导致误触发,要么步骤设计得太理想化、实际执行时到处报错,要么就是我把一个本来用一条命令就能搞定的事,硬生生拆成了七八步让模型去“理解”。更讽刺的是,我后来重新写的第 31 到第 50 个 Skill,数量少了,但每一个的调用频率和稳定性都远超前面那 30 个。

所以这篇东西不是教你“怎么写 Skill”的入门教程——那种内容网上已经很多了。我想聊的是:为什么前 30 个会白写,以及第 31 个之后我到底改了什么思路。如果你也在用 Claude Code、Codex、或者任何支持 Skill/MCP 的 Agent 工具,正在兴冲冲地批量生产 Skill,那这篇大概率能帮你省下至少一个月的无效劳动。

先给个结论性的判断:Skill 的价值不在于“能做什么”,而在于“什么时候该被触发”以及“触发后能不能稳定收敛”。前 30 个我全在解决“能做什么”,后 20 个我才开始认真对待“触发”和“收敛”。这个认知转变,是整件事的分水岭。

2. 前30个Skill到底错在哪:四个反复踩中的坑

2.1 把Skill当函数写,忽略了它是“意图识别器”

我最早写 Skill 的思路,完全是后端开发的惯性:定义一个输入,定义一段处理逻辑,定义输出。比如我写过一个“生成 Spring Boot Actuator 监控配置”的 Skill,SKILL.md里列了十几个参数:端口、暴露端点、健康检查路径、指标前缀……写得像一份 API 文档。

问题在于,Claude Code 调用 Skill 不是“传参调用”,而是“根据你的自然语言描述,判断该不该用这个 Skill,然后从上下文里抽取参数”。我那个监控配置 Skill,触发描述写的是“当用户需要配置 Spring Boot 监控时使用”。听起来没问题,但实际上用户说“帮我看看这个服务的健康检查怎么加”“actuator 端点能不能只暴露 health”的时候,模型不一定能稳定命中这个 Skill——因为我的触发描述太“正式”了,跟真实对话里的口语表达对不上。

后来我改成把触发描述写成“用户提到 actuator、健康检查、监控端点、metrics、health 等关键词,或要求为 Spring Boot 服务添加运行时状态查看能力时”,命中率立刻上来了。Skill 的第一性原理不是“功能封装”,而是“意图匹配”。你得站在“用户会怎么随口说”的角度去写触发条件,而不是站在“这个功能叫什么”的角度。

2.2 步骤写得越细,执行时崩得越惨

第二个坑更隐蔽。我写过一个“数据库迁移脚本生成”的 Skill,步骤列了 9 步:读现有 schema、对比目标 schema、生成 diff、检查索引冲突、生成 up/down 脚本、加事务包裹、加回滚注释、输出到指定目录、提示人工复核。每一步我都写得清清楚楚。

实际跑的时候,第 3 步“生成 diff”就经常出问题——因为模型在不同上下文里对“diff”的理解不一致,有时候它去调git diff,有时候它自己脑补一个差异,有时候它直接跳过。第 5 步“生成 up/down”又依赖第 3 步的结果,一旦第 3 步跑偏,后面全歪。

我后来才明白:Skill 的步骤不是给“执行器”看的,是给“模型”看的。模型不是确定性的函数,它每一步都在做概率判断。你步骤写得越细,它需要做的判断越多,累积误差越大。正确的做法是把步骤收敛到 3 到 5 步,每步都是一个“高确定性的原子动作”,中间不要留太多需要模型自由发挥的空间。

比如那个迁移脚本 Skill,我后来砍成 3 步:第一步,让模型读取当前 schema 文件并原样输出;第二步,让模型读取目标 schema 文件并原样输出;第三步,把两份内容一起交给一个专门的 diff 提示词处理。前两步是纯读取,几乎不会错;第三步虽然复杂,但它是独立的一次模型调用,失败了可以单独重试,不会污染前面。

2.3 没有“退出条件”,Skill 会自己绕圈

这是最要命的一个坑。我写过一个“日志分析”Skill,目标是让模型读日志文件、找出异常模式、给出可能原因。触发描述没问题,步骤也还算合理。但它经常陷入一种状态:读一段日志,说“这里可能有异常”,然后去读更多日志,又说“需要更多上下文”,再读……最后要么超时,要么输出一堆模棱两可的猜测。

根因是我没定义退出条件。Skill 的SKILL.md里我只写了“做什么”,没写“做到什么程度算完”。模型没有“够了”的概念,它会一直尝试“更全面”。后来我在每个 Skill 里都强制加一段“终止条件”,比如“当已识别出至少 3 类异常模式,或已读取日志文件的前 500 行,或连续两次读取未发现新异常时,停止分析并输出结论”。

这个改动看起来很小,但效果立竿见影。没有退出条件的 Skill,就像一个没有 base case 的递归函数,迟早栈溢出。

2.4 把MCP能做的事,硬塞给Skill

我早期还写过一个“查询本地数据库表结构”的 Skill,让模型去执行mysql命令、解析输出、整理成表格。跑是能跑,但极不稳定——数据库连接串变了要改 Skill,输出格式变了要改 Skill,权限不够了还要改 Skill。

后来我把它换成了 MCP。MCP 协议本身就是为“工具调用”设计的,数据库查询这种确定性操作,交给 MCP Server 去做,Skill 只负责“决定什么时候调用这个 MCP 工具”。职责一分开,两边都稳了。

这里有个判断标准我后来总结出来了:

任务性质该用 Skill 还是 MCP
需要理解自然语言意图、做判断Skill
确定性数据读写、命令执行MCP
多步骤流程编排Skill 调 MCP
单一原子操作MCP
需要跨会话保持状态都不适合,用外部存储

前 30 个 Skill 里,至少有 8 个是“本该是 MCP 却写成了 Skill”,这是白写的重灾区。

3. 第31个Skill开始的转向:先想“触发”,再想“功能”

3.1 触发描述要写成“用户会说的话”,不是“功能说明书”

我现在的习惯是:写任何 Skill 之前,先不写功能,先写 10 句“用户可能会怎么说”。比如我要写一个“接口文档生成”Skill,我会先列:

  • “帮我把这个 Controller 的接口整理成文档”
  • “这些 API 能不能生成一份 markdown”
  • “我要给前端一份接口说明”
  • “把这个类的请求参数和返回值列出来”
  • “生成 swagger 那种文档”
  • ……

然后我把这些句子里的共同关键词和表达模式提炼出来,写进触发描述。这样写出来的触发条件,命中率比“当用户需要生成接口文档时”高出一个量级。

核心心法:触发描述是“召回策略”,不是“定义”。你要的是尽可能召回真实意图,而不是精确描述功能边界。宁可稍微宽一点,后面再用“排除条件”收窄,也不要写得太窄导致该触发时不触发。

3.2 一个Skill只解决一个“意图”,不要贪多

我早期特别喜欢写“全能型” Skill,一个 Skill 里塞进“读代码、分析、生成文档、顺便检查规范”。结果就是触发条件模糊、步骤冗长、退出条件难定义。

后 20 个我严格遵循“一个 Skill 一个意图”。比如“接口文档生成”和“接口规范检查”是两个 Skill,哪怕它们都要读同一个 Controller。分开之后,每个 Skill 的触发描述可以写得很精准,步骤可以很短,退出条件也很清晰。

代价是有时候用户一句话会同时命中两个 Skill,但那没关系——Claude Code 会依次调用,或者你可以用一个“编排型” Skill 去串联它们。编排是编排,执行是执行,不要混在一个 Skill 里。

3.3 用“反例”来校准触发边界

这是我后来学到的一个很实用的技巧:在SKILL.md里显式写出“什么情况下不要用这个 Skill”。

比如“接口文档生成”Skill 里我会写:“当用户只是询问某个接口的参数含义,而不是要求生成完整文档时,不要触发本 Skill。”这条反例一加,误触发率明显下降。

模型对“否定条件”的敏感度其实很高,你给它一个明确的反例,它就能更好地理解边界。这比你把触发描述写得更窄要有效得多,因为写窄了会漏召回,写反例则是在保持召回的同时提高精度。

4. 让Skill真正跑起来的三个工程化细节

4.1 SKILL.md 的结构:我现在的固定模板

踩了那么多坑之后,我现在的SKILL.md基本固定成这么几块:

# Skill 名称 ## 触发条件 - 用户表达意图 A 时触发 - 用户提到关键词 B、C、D 时触发 - 反例:当用户只是……时,不要触发 ## 前置检查 - 确认当前工作目录存在 xxx 文件 - 确认 xxx 命令可用 - 若不满足,提示用户并终止 ## 执行步骤 1. 原子动作一(高确定性) 2. 原子动作二(高确定性) 3. 复杂处理(可独立重试) ## 终止条件 - 满足条件 X 时停止 - 连续 N 次无新发现时停止 - 超过 M 步未收敛时停止并输出当前结果 ## 输出格式 - 固定输出为 markdown 表格 / JSON / 纯文本 - 必须包含字段:xxx、yyy ## 失败处理 - 步骤 1 失败:提示用户检查 xxx - 步骤 3 失败:回退到步骤 2 的结果并输出

这个模板不是拍脑袋来的,每一块都对应我之前踩过的一个坑。触发条件对应“意图匹配”,前置检查对应“环境假设”,终止条件对应“绕圈”,失败处理对应“崩了之后没法恢复”。

4.2 参数不要“传”,要“抽”

前面提过,Skill 不是函数调用。但很多人(包括早期的我)还是习惯在SKILL.md里定义一堆参数让用户填。这是错的。

正确做法是:在步骤里写清楚“从上下文抽取什么信息”。比如:

步骤 1:从用户当前对话或最近打开的文件中,抽取目标 Controller 类名。若无法确定,询问用户。

这样模型会自己去上下文里找,而不是傻等用户传参。实测下来,这种“抽取式”写法比“参数式”写法的成功率高出很多,因为用户根本不会按你定义的参数格式说话。

4.3 测试Skill的正确方式:构造“脏输入”

我早期测试 Skill,都是拿“标准输入”测——比如规规矩矩地说“请为 UserController 生成接口文档”。测出来都通过,一上真实场景就崩。

后来我改成专门构造“脏输入”来测:

  • 口语化输入:“那个用户接口的文档帮我搞一下”
  • 带错别字输入:“生成 user controler 的接口文档”
  • 混合意图输入:“帮我看看 UserController 有没有问题,顺便生成个文档”
  • 缺上下文输入:“生成接口文档”(不说哪个类)

只有这些“脏输入”能稳定通过,Skill 才算真正可用。标准输入通过只说明“理想情况下能用”,脏输入通过才说明“真实场景下能用”。

5. 那些“白写”的Skill,后来怎么处理的

5.1 直接删掉的:约12个

前 30 个里,有大约 12 个我最后直接删了。删掉的标准很简单:连续一个月没有被自然触发过,或者触发后我每次都手动中断。

这些 Skill 的共同特征是:解决的是一个“我臆想出来的需求”,而不是“我真实高频遇到的需求”。比如我写过一个“自动生成 CHANGELOG”的 Skill,但实际上我项目的 CHANGELOG 是手动维护的,根本不需要自动化。写它纯粹是因为“觉得这个功能很酷”。

这件事给我的教训是:Skill 的需求应该来自“我重复做了三次以上的事”,而不是“我觉得这件事可以自动化”。前者是真实需求,后者是自我感动。

5.2 拆成MCP的:约8个

前面提过,有 8 个左右是“确定性操作”被硬写成了 Skill。这些我后来都改成了 MCP Server,Skill 只保留一个“什么时候调用”的薄壳。

拆完之后最大的感受是:稳定性提升了一个数量级。因为 MCP 是真正的代码执行,不受模型概率判断影响;而 Skill 只负责“决定调不调”,这个判断即使偶尔出错,代价也很小。

5.3 合并重写的:约10个

还有 10 个左右是“意图太碎”或者“步骤重叠”的,我把它们合并成了 4 个新 Skill。比如原来有“读 Controller”“分析参数”“生成文档”“检查规范”四个 Skill,后来合并成“接口文档生成”和“接口规范检查”两个。

合并的原则是:如果两个 Skill 的触发场景高度重叠,且执行步骤有 50% 以上重合,就该合并。分开写只会增加维护成本和误触发概率。

6. 如果你现在要写第一个Skill,我的建议顺序

6.1 先别写,先记录一周

我的建议是:在写任何 Skill 之前,先花一周时间记录你每天在 Claude Code 里重复输入的命令或提示词。不用很正式,就记在备忘录里。一周后你回看,哪些模式出现了 3 次以上,那些才是值得写成 Skill 的。

我后来第 31 到 50 个 Skill,全部来自这种“真实重复记录”,没有一个是我拍脑袋想出来的。这也是为什么它们的使用率远高于前 30 个。

6.2 从“只读型”Skill 开始

如果你刚开始写,我强烈建议从“只读型” Skill 入手——就是那些只读取信息、不修改任何东西的 Skill。比如“分析日志”“读取配置并解释”“列出接口清单”。

原因很简单:只读型 Skill 即使触发错了、执行歪了,也不会造成破坏。你可以放心地观察它的行为,慢慢调触发条件和步骤。等你对 Skill 的行为模式有感觉了,再去写“写入型”的。

我前 30 个里有一半是写入型,结果就是每次出错都要手动回滚,调试成本极高。后 20 个我从只读型重新起步,学习曲线平缓了很多。

6.3 给每个Skill设一个“观察期”

新写的 Skill 不要立刻当成正式工具用,先给它两周观察期。这两周里,每次它被触发,你都记录一下:触发得对不对、步骤跑得顺不顺、输出有没有用。

两周后如果触发准确率低于 80%,或者你手动中断超过 3 次,就回去改触发描述或步骤。改完再观察两周。一个 Skill 从写完到稳定,通常需要 2 到 3 轮这样的迭代。前 30 个我都是写完就用,从不回看,所以一直不稳定。

7. 关于MCP和Skill的边界,再说几句实在的

现在网上关于 MCP 和 Skill 的讨论很多,但很多都停留在概念层面。我说几个我实际用下来的判断标准:

第一,看操作是否“可枚举”。如果一个操作的输入输出可以完全枚举清楚,比如“查询某张表的某几列”,那它适合 MCP。如果输入输出依赖自然语言理解,比如“分析这段代码有没有坏味道”,那它适合 Skill。

第二,看失败代价。MCP 失败通常是明确的报错,容易定位;Skill 失败往往是“输出了一段看起来对但实际不对的内容”,隐蔽性强。所以高风险操作尽量走 MCP,让失败显性化。

第三,看复用范围。MCP Server 可以被多个 Skill、多个工具复用;Skill 通常是绑定在特定 Agent 上的。如果你发现某个能力会被反复用到,把它下沉成 MCP 更划算。

我现在的项目里,MCP 负责“手脚”,Skill 负责“大脑”。手脚要稳,大脑要灵活。两者职责分清之后,整个系统的可靠性比早期“全塞进 Skill”高太多了。

8. 最后分享几个我压箱底的小技巧

第一个技巧:在 Skill 的触发描述里加入“时间/频率”维度。比如“当用户在同一会话中第三次提到某个接口时触发”。这个技巧对“整理类” Skill 特别有用,能避免用户只是随口一提就被触发。

第二个技巧:给 Skill 加一个“干跑模式”。在SKILL.md里写“若用户说‘先看看’‘模拟一下’,则只输出将要执行的步骤,不实际执行”。这个模式在调试新 Skill 时极其好用,也方便你向别人演示。

第三个技巧:把常用的 Skill 组合成一个“元 Skill”。比如我有“接口文档生成”“接口规范检查”“接口测试用例生成”三个 Skill,我又写了一个“接口全流程处理”的元 Skill,依次调用它们。这样用户一句话就能跑完整个流程,不用分别触发。

第四个技巧:定期清理。我现在的习惯是每个月回看一次所有 Skill 的调用记录,把连续一个月没被触发的标记出来,连续两个月没被触发的直接删。Skill 库跟代码库一样,不清理就会腐化。

写 Skill 这件事,说到底是在跟“模型的概率性”打交道。你越想让它精确,它越容易在你没预料到的地方跑偏。前 30 个我一直在对抗这种概率性,后 20 个我学会了顺着它设计——把不确定性收拢到少数几个可控的环节,其余部分尽量做成确定性的原子动作。这个思路转变之后,Skill 才真正从“玩具”变成了“工具”。

如果你现在正写到第 10 个、第 20 个,感觉越写越累、越写越不好用,别急着怀疑自己。很可能你只是还没到那个“该转向”的点。我是在第 30 个之后才想明白的,希望你能比我早一点。