1. 当 CLAUDE.md 越来越长,AI 的表现却一路下滑
1.1 规则的本质:贴在墙上的规章制度
先聊一个反直觉的现象。很多刚开始接触 Claude Code、Codex 这类 AI 编程工具的人,会把“配置规则”当成第一优先级:把编码规范、提交规范、命名约束、禁止事项全部写进项目里的 CLAUDE.md,或者塞进 IDE 的 Rules 配置。这看起来非常合理——既然 AI 不熟悉我们的项目,那就用规则把话说清楚。但我自己的实测结果是:规则文件从十几行涨到两三百行之后,模型的整体表现反而变差了。
这不是玄学,而是规则这种形态本身的问题。规则本质上是“被动声明”,它在每次对话开始的时候被加载进上下文,模型会像读工程文档一样把它读完,然后在这个约束集下面执行任务。这个机制对短规则非常有效,但对长规则完全不是线性叠加的关系。打个比方,规则就像贴在工位上的一整面墙的规章制度:如果只有三五条,每个人扫一眼就记住了;如果密密麻麻贴满三面墙,员工反而会选择性失明,只会盯着自己眼前那条看,甚至为了不违反某一条,做出一个违反另外三条的操作。
我见过最典型的场景:CLAUDE.md 里同时写了“优先使用已有工具函数,不要重复造轮子”和“保持代码自包含,减少模块间耦合”。这两条单独拿出来都正确,但放到一起时,AI 在处理某一个具体需求时经常会被两条规则同时拉扯,最后给出的代码既没有复用工具函数,也不自包含,而是东拼西凑。要命的是,你很难责怪它——因为两条规则都对,它只是不知道哪条优先级更高。
1.2 上下文税:规则越多,有效注意力越少
说道底,规则的本质是“上下文税”。AI 编程工具在每一轮对话里都要维护一个上下文窗口,窗口里的内容决定了它这一次能看到的代码范围、能记住的修改历史、能携带的任务信息。规则文件被加载之后,就永久占用了这部分空间。你每写十条规则,相当于从模型的工作记忆里划走一块区域;规则冗余和矛盾越多,模型真正用来理解需求、阅读代码的空间就越小。
我自己做过一个很粗暴的测试:同一个中等体量的前端项目,分别在 5 条规则和 120 条规则两套配置下要求 AI 增加一个表单校验功能。前者一次就给出了可运行的实现,后者花了三轮对话还在反复确认“应该优先遵循提交规范还是项目结构规范”。120 条规则的配置里包含了很多我认为非常重要的领域知识,但实际上它们根本没有机会被同时激活——模型只会抽取其中一部分来遵循,而且是每次抽取的都不一样。这个测试让我意识到:Rules 擅长的是“禁止”和“不做什么”,它完全不擅长“引导”和“怎么做”。而 AI 编程工作流恰恰需要的是后者。
这里顺便提一个容易踩的误区:很多人把 Rules 写成了巨大的百科全书。常见的错误包括把整个项目的架构说明塞进去、把第三方库的 API 文档复制进去、把历史上踩过的所有坑都以警告形式堆进去。这些内容写成 Markdown 文档没问题,但写成规则就有问题——因为规则是每轮对话都要被重新阅读一遍的,不是按需调用的。你给我的不是规则,是包袱。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Skills 的开放标准为什么能解决这个问题:目录协议与按需加载
2.1 SKILL.md + 脚本 + reference:一个标准包的解剖
Anthropic 在 Agent Skills 上给出的思路和 Rules 完全不同。Rules 是一条一条的“禁令”,Skills 是一个一个的“工具包”。一个标准的 Skill 是一个文件夹,里面可以装三种东西:说明文件、脚本和参考文档。它的目录长得像这样:
code复制my-skill/
├── SKILL.md # 唯一必须存在的文件
├── scripts/ # 可选的执行脚本(Python、Shell、Node)
├── reference/ # 可选的参考文档,按需读取
└── requirements.txt # 脚本依赖,会被自动安装
最关键的是 SKILL.md,它用 YAML frontmatter 声明这个技能的元信息,然后用 Markdown 正文写完整的操作流程。一个最基本的例子:
code复制---
name: frontend-review
description: 当用户希望对前端代码进行审查,检查 TypeScript 类型、ESLint 规范、Sass @import 弃用以及构建产物时使用。
---
# 前端代码审查
当你需要审查前端代码时,按照以下步骤操作:
1. 运行 scripts/preflight.sh,收集变更文件列表。
2. 运行 scripts/check_sass.sh,检查 SCSS 文件中是否还有已废弃的 @import。
3. 运行 npx tsc --noEmit,收集 TypeScript 类型错误。
4. 运行 npx eslint --format json .,收集规范问题。
5. 将所有结果汇总为一份审查报告。
这里的关键差异在于:Rules 是“无论你在做什么,都要遵守这些条款”,Skills 是“只有在做某类任务时,才把这个能力包加载进来”。也就是说,Skill 里的内容不会常驻上下文窗口,只有在模型判断当前任务和这个 Skill 的 description 匹配时,它才会把 SKILL.md 和必要的脚本加载进来。这种“按需加载”机制直接解决了我在第一部分说的上下文税问题。
2.2 description 才是真正的“开关”
很多人第一次自己写 Skill 时会忽略 description 的写法,随手写一句“这是一个前端审查工具”。这句话对 AI 来说几乎等于什么都没说。Skill 的加载机制不是一个按钮,而是模型基于当前任务目标和各个 Skill 的 description 做语义匹配。description 写得好不好,直接决定这个 Skill 会不会在正确的时候被加载。
我建议的写法是:包含触发场景、任务对象和预期输出。比如“当用户需要对 React 项目进行代码审查,检查组件性能、Hook 依赖和 TypeScript 类型时,使用此技能”,就比“前端审查工具”强得多。还有一个技巧:在 description 里写清楚“什么时候不要用”,比如“注意:仅适用于业务代码审查,不适用于基础设施代码”,这能有效避免模型在无关任务上误用技能。
另外,Skills 本身还是一等公民的文件包。你可以把它放进 ~/.claude/skills 作为个人全局技能,也可以放进项目下的 .claude/skills 作为项目级技能;在 Claude Code 里执行 claude add skill <文件夹路径> 就能安装,执行 /skill 可以查看当前可用的技能列表。这种组织方式天然适合团队共享——把技能文件夹放进 Git 仓库,团队成员拉下来就能用,不需要每个人复制粘贴同一份规则文档。
2.3 领域技能库正在出现
社区里已经出现了大量高质量 Skill 库,覆盖了各种常见场景:测试用例自动生成、前端项目代码审查、数学建模、学术文献整理、代码安全审计、数据库 Schema 分析、Git 提交信息规范化等等。这些 Skills 的意义不只是“开箱即用”,更在于它们示范了如何把某一个领域的经验结构化:把隐性的知识迁移过程,变成了显式的文件结构和脚本。
比如测试用例生成类 Skill 往往包含一个 SKILL.md 说明“如何根据变更代码生成测试用例”,一个 scripts/collect_changes.py 负责读取 Git diff,一个 reference/ 目录存放测试框架规范。这些组件加在一起,AI 在面对“给这个新函数补测试”的任务时,不再靠猜,而是会被引导到一个稳定的、可重复的流程里。这一点是 Rules 根本做不到的——Rules 能告诉你“要写测试”,但无法告诉你“针对这次变更,具体该怎么写”。
3. 一次完整迁移:把“前端代码检查规则”改造成“前端审查 Skill”
3.1 迁移前的规则现状
我拿自己项目里的一段真实规则来看迁移过程。这部分规则最开始写在 CLAUDE.md 里,内容大概是:
code复制- 审查前端代码时,必须检查 TypeScript 类型错误,运行 tsc --noEmit。
- 检查 ESLint 规范,运行 eslint。
- 检查 Sass 中是否使用了 @import,如果使用了要提示改为 @use。
- 检查 package.json 中是否存在未使用的依赖。
- 输出问题清单,标注文件路径和严重程度。
这段规则看起来没毛病,而且非常具体。但实际使用中它的效果很差:第一,它是在所有对话里被加载的,哪怕我只是让它改一行文案,它也会在背后“惦记”着这几条规则,浪费上下文;第二,它只告诉模型要检查什么,却没有告诉模型按什么顺序、产出什么格式的报告;第三,这部分规则散落在几十条其他规则中间,模型经常只抓住其中一两条,其他几条被忽略。
3.2 设计 Skill 的目录和边界
迁移的第一步不是写文件,而是划边界。我花了十几分钟想清楚一个问题:哪些东西是真正的“前端审查技能”,哪些东西是应该保留为规则的“硬性纪律”。
我的划分标准很简单:如果一个要求是跨所有任务的“底线”,比如“不要提交敏感信息”“回复使用中文”,那它适合留在规则里;如果一个要求是“完成某类任务时需要执行的一整套流程”,那它适合做成 Skill。前端代码审查明显属于后者。
最终我把这个 Skill 设计成四个部分:入口说明、变更收集脚本、Sass 检查脚本、报告汇总逻辑。目录结构如下:
code复制frontend-review/
├── SKILL.md
├── scripts/
│ ├── collect_changes.sh
│ ├── check_sass.sh
│ └── summarize_report.py
└── requirements.txt
这样设计的好处是:每个脚本只做一件简单的事,组合起来构成一个完整的审查流程。模型不需要在运行时动态决定“要不要跑 tsc”——SKILL.md 里的流程已经规定好了步骤 1、步骤 2、步骤 3。
3.3 把“检查清单”改写为“工作流”
技能的核心在 SKILL.md 里。我在正文里明确规定了执行的顺序和每步的输出格式:
code复制---
name: frontend-review
description: 当用户需要对前端代码进行审查,检查 TypeScript 类型、ESLint 规范、Sass @import 弃用、未使用依赖以及构建产物时使用。适合在 Pull Request 合并前执行。
license: MIT
---
# 前端代码审查工作流
执行以下步骤,顺序不能调换:
1. 先运行 `scripts/collect_changes.sh`,获取本次变更涉及的文件列表。
2. 按变更文件列表,运行 `npx tsc --noEmit`,记录 TypeScript 类型错误。
3. 运行 `scripts/check_sass.sh`,扫描所有 `.scss`/`.sass` 文件中的 `@import` 用法。
4. 对变更文件逐个运行 `npx eslint --format json`,仅保留涉及文件的规范问题。
5. 检查 `package.json`,对照变更文件确认是否存在新增但未使用的依赖。
6. 用 `scripts/summarize_report.py` 把以上结果合并为一份带文件路径的 Markdown 报告。
注意:本 Skill 只做检查,不做修改。如果用户明确要求修复,才可以在报告基础上提出修改建议。
其中 check_sass.sh 的脚本很简单,但非常实用,专门用来抓 Sass 的弃用写法:
bash复制#!/usr/bin/env bash
# 扫描 SCSS 文件中的 @import,Dart Sass 已弃用该语法
if grep -rn "@import" src --include='*.scss' --include='*.sass' 2>/dev/null; then
echo "发现过时的 Sass @import,建议改为 @use"
else
echo "未发现 Sass @import"
fi
3.4 迁移前后效果对比
迁移后的第一个变化是上下文窗口明显释放了。以前那段规则在每一轮对话里占据的篇幅被移除,模型能看到的有效代码更多了。第二个变化是行为变得更稳定:过去模型有时候会跳过 Sass 检查,有时候会先跑 ESLint 后跑 tsc,顺序不稳定;现在整个流程被固定下来,每次输出的报告结构都是一样的,我甚至可以写一段自动解析脚本来处理它的输出。
这里要说一个容易被忽略的点:写 Skill 不是简单地把规则文本换个格式。规则是一堆“条款”,Skill 是一个“流程”。我要刻意在 SKILL.md 里写“顺序不能调换”,因为很多检查项之间有依赖关系——比如 Surmmarize 报告之前必须先收集变更文件,否则报告最后列出的文件列表和前面检查的文件列表对不上。你把它变成一个流程,AI 就会按你设定的方式去执行,而不是靠它临场发挥。
4. 给 Skill 配上“手脚”:与 MCP 工具的协作模式
4.1 两者不是竞争关系,而是互补关系
社区里经常有人问:Skills 和 MCP 工具到底有什么关系?是不是有了 Skills 就不需要 MCP 了?答案恰恰相反。MCP(Model Context Protocol)解决的是“能力接入”问题,它让 AI 能调用外部服务和工具,比如读取 GitHub 仓库、操作浏览器、查询数据库;Skills 解决的是“流程编排”问题,它把有经验的做事方法固化下来。一个 Skill 可以明确声明自己依赖哪些 MCP 工具,然后在流程里按顺序调用它们。
我自己的理解是:SKILL.md 提供的是“知道怎么做”的剧本,MCP 提供的是“真的能动手做”的手脚。两者配合起来,AI 才能从“给你意见”进化到“替你干活”。
4.2 在 SKILL.md 里声明工具依赖
具体怎么在 Skill 里用 MCP 工具?方法不复杂。你只要在 SKILL.md 的流程描述里写出工具的名称和调用意图,AI 就会尝试调用对应的 MCP 服务器。比如我在测试用例生成这个 Skill 里这样写:
code复制# 测试用例生成流程
1. 使用 github_mcp.get_pull_request_files 获取本次变更涉及的文件列表。
2. 对每个变更文件,使用 file_mcp.read_file 读取完整代码。
3. 根据变更内容,确定测试边界。
4. 使用 test_runner_mcp.run_tests 运行现有测试,确认没有回归。
5. 生成新测试用例,输出到 tests/ 目录。
这里的关键是,MCP 工具的命名要和实际配置的服务器一致。如果你并没有配置 github_mcp,这个 Skill 在执行时会卡住,模型会告诉你“无法调用该工具”。所以我在 Skill 的正文里还会加一个“前置条件”区块,明确写清楚“需要配置以下 MCP 服务器:xxx”。这样模型在最开始就能判断自己有没有能力执行这个技能,如果没有,它会提前告诉你,而不是做到一半才失败。
4.3 一个真实场景:测试用例生成
我实际用得最多的一个组合是“代码变更分析 + 测试运行”两个 MCP 工具配合一个测试生成 Skill。以前让 AI 补测试,它只能看着最终代码猜,经常猜错测试边界;现在有了 Skill 之后,它会主动去拉取 Git diff、读取变更前后的代码、运行现有测试,把整个变更上下文串起来,再决定新测试要覆盖哪条分支。
这个变化的效果非常惊人。原来“给新接口补单测”这种任务基本要靠我手动列出函数名和输入输出,AI 才能动工;现在模型可以在 Skill 的引导下自己完成数据收集,我只需要在关键时刻确认一下测试方向。而且因为 Skill 里的流程是固定的,每一次生成的测试用例风格都很统一,不会出现这次用 pytest.fixture、下次全写在测试函数里的混乱。
我建议每个深度使用 AI 编程的人都可以尝试把自己最常用的一个“人工工作流”改写成 Skill + MCP 的组合。不用一开始就做很复杂的,哪怕只是一个“运行测试并分析失败原因”的小技能,都能在后续无数次重复劳动中省下大量时间。
5. 同一套 Skills 在不同 AI 编程工具里的体验差异
5.1 三个主流工具的加载方式对比
Skills 标准的核心是“文件夹 + SKILL.md + 可选脚本”。理论上有 AI 编程工具都该能读,但现实中不同工具对它的支持深度是不一样的。我同时在 Claude Code、Codex 和 opencode 里测试过同一套 Skill,结论是:越接近 Anthropic 官方生态的工具,兼容性越好;其他工具则需要一些配置上的调整。
这里有一个常见的误解值得专门澄清:所谓的“开放标准”只规定了 Skill 文件夹内部怎么写,并没有规定工具该从哪里加载它。所以不同的工具各自定义了读取路径和优先级,我把目前实际体验到的差异汇总如下:
| 工具 | 个人/全局技能目录 | 项目级技能目录 | 触发方式 |
|---|---|---|---|
| Claude Code | ~/.claude/skills |
.claude/skills |
按照 description 自动匹配,/skill 命令列表 |
| Codex | 以官方文档为准,常用 ~/.codex/skills |
项目内配置 | 在提示词中显式引用 |
| opencode | 项目内 .opencode/skills 或插件机制 |
同左 | 根据配置声明加载 |
需要注意的是,以上目录路径在不同版本中可能有所变化,不要把它当成永恒真理。我实际踩过的一个坑是:当时把一个 Skill 放在项目级的 .claude/skills 下,在 Claude Code 里正常运行;换到另一个工具后,它却不读这个目录,而是要放在自己的配置路径底下。后来我养成了一个习惯:在每个工具的文档确认技能路径,而不是想当然。
5.2 团队级同步的实操方案
既然不同工具各自有路径,那团队共享怎么办?我目前采用的做法是:把 Skills 统一放在一个 Git 仓库的 skills/ 目录下管理,然后在各个工具里建立一个符号链接或复制脚本。这样既能享受版本管理的便利,又能让不同工具的 AI 助手都能读取。
同步脚本的思路很简单:
bash复制#!/usr/bin/env bash
# 把仓库内的 skills 同步到各工具的技能目录
ln -sfn "$(pwd)/skills/frontend-review" ~/.claude/skills/frontend-review
ln -sfn "$(pwd)/skills/test-generator" ~/.claude/skills/test-generator
# 其他工具按需添加
这样团队成员拉完仓库后只需要跑一次脚本,就能在 Claude Code 里用上同一套技能。而且我建议在仓库里同时维护一个 README.md,写明每个技能的 description、依赖的 MCP 服务、适用场景,这比让每个成员自己读 SKILL.md 高效得多。
还有一个经验:写 Skill 时尽量少依赖特定工具的系统提示词特性,比如某些工具支持的表情符号或特殊标记。保持 SKILL.md 是纯粹的 Markdown,脚本是独立的可执行文件,这样你就最大程度保证了它可以被不同工具复用。
6. 迁移路上踩过的坑与保留规则的原则
6.1 description 写得太空,技能变成摆设
把 Rules 改造成 Skills 之后,最容易出现的问题不是语法错误,而是这个技能根本不会被触发。原因我在前面说过:description 是加载开关。写“前端审查工具”和“当用户需要审查前端代码,检查 TypeScript、ESLint、Sass 弃用问题时使用”,触发频率天差地别。我见过不少人把自己精心写的 Skill 放在那里一整周都没被调用过一次,最后得出结论“Skills 没用”。说实话,这时候先回去审一审自己的 description 比较靠谱。
6.2 沉没成本错误:把规则尽数导入技能
另一个极端是把所有规则全都改写成 Skill。这样做的问题在于,某些规则执行频率太高、跨任务太广,一旦变成按需加载的 Skill,反而会导致模型在面对普通任务时丢失了关键约束。比如“回复使用中文”这种约束,如果只存在于一个“文档生成 Skill”里,那让它写提交信息时它可能就切回英文了。
我的取舍原则是:与具体任务强绑定的流程性知识,做成 Skill;跨任务的安全底线和风格偏好,保留为规则。Rules 应该瘦身成一个很小的清单,只放那些不遵守就会出事的硬约束,而不是一个臃肿的百科全书。
6.3 脚本幂等性问题:跑一次和跑两次结果不一样
写 Skill 里的脚本时,我踩过最多次的坑是脚本有副作用。比如某个检查脚本第一次运行时自动安装了依赖,或者在最后一步自动改了一个配置文件。这在首次运行时看起来很正常,但第二次、第三次运行时,因为环境已经变了,脚本的行为就不可控了。Skill 里最有价值的脚本应该是“读操作优先”:只收集信息、只做检查、只输出报告;写操作要单独拆出来,并且设置显式开关。
bash复制# 坏例子:检查完顺手改了代码
grep -rl "TODO" src/ | xargs sed -i 's/TODO/FIXME/g'
# 好例子:只输出清单,不修改文件
grep -rn "TODO" src/ || true
这个原则能让你的 Skill 变得非常安全,模型随时可以调用它,不会在用户没要求的情况下改了代码。
6.4 依赖与环境不一致:requirements 的坑
带 Python 脚本的 Skill 会在安装时读取 requirements.txt。这个机制很方便,但也会带来“环境漂移”问题:你在自己的机器上装的是 pandas 1.5,别人执行技能时默认装的是 pandas 2.x,结果行为对不上。解决方法是把关键依赖锁定版本号,同时不要在 SKILL.md 里依赖某个 Python 包的“最新特性”,否则跨环境复现时会莫名其妙失败。
6.5 多版本同名技能互相打架
还有一个我需要特别提醒的坑:~/.claude/skills 和项目下 .claude/skills 如果存在同名 Skill,具体会加载哪个,不同工具的处理逻辑不一样。我自己就被坑过一次:全局放了一个旧版 test-generator,项目里放了一个新版,结果模型有时候用的是全局旧版,导致输出格式和项目风格不一致。解决方法是给 Skill 目录起名字时带上版本号,或者在 README 里记录“本项目应使用项目级技能”,不要依赖工具内部的优先级逻辑。
6.6 保留为规则的核心清单
经过这轮迁移,我现在留给规则文件的内容非常克制,大概就是这几类东西:
- 安全底线:不输出密钥、不上传敏感日志。
- 输出偏好:默认回复语言、代码注释风格。
- 最高优先级的项目约束:比如“不允许修改
src/generated目录”。 - 显式的技术选型:比如“TypeScript 项目不得使用
any类型规避错误”。
除此之外,所有流程性的、任务绑定型的知识,我都尽量迁到 Skill 里。这样 AI 在普通任务里的上下文负担最小,在特定任务里又能发挥出完整工作流的能力。
最后分享一个我现在的习惯:每个 Skill 建好之后,我不会急着推广,而是先在真实任务里用一周,记录触发次数和失败原因。如果一个 Skill 一周内从没有被触发过,基本说明 description 有问题,或者这个场景根本不需要技能化;如果一个 Skill 被触发但经常中途失败,那大概率是流程里的某个步骤依赖了不存在的 MCP 工具。迭代稳定之后,再把 Skill 提交到团队仓库里共享。
Skills 这套东西最有价值的地方不在于它是 Anthropic 推的,而在于它给“可复用的 AI 工作流”提供了一个足够简单的标准:一个文件夹加一个 Markdown 文件。你不需要理解复杂的框架,只需把一个重复劳动流程拆成步骤、写成脚本、配好描述,就能让 AI 按照你的经验去工作。这条路我从 Rules 迁移过来之后,再也不想走回去了。
