Skill的实现方式:让 Agent 学会“开挂“
Skill的实现方式:让 Agent 学会"开挂"
用 Claude Code 写代码时间长了,你会发现一个现象:有时候它表现得像一个经验丰富的老手,对你的项目结构、编码习惯、甚至某些专业领域的知识了如指掌;有时候又像个新手,什么都不懂,需要你从头解释。
区别在哪?在于它有没有加载对应的Skill。
Skill 其实就是一份提前写好的提示词,在特定时机注入到 Claude 的上下文中,告诉它"你现在要扮演一个擅长做某某领域的专家"。就像给一个聪明但什么都不知道的新员工一份详细的岗位手册,他看完就知道该怎么干活了。
Skill 长什么样
一个 Skill 本质上就是一个 Markdown 文件,放在特定目录下。Claude Code 在启动时会扫描这些目录,把找到的 Skill 注册到系统中。
最简单的 Skill 结构是这样的:
my-skill/ SKILL.mdSKILL.md就是 Skill 的全部内容。来看一个最基础的例子:
--- name: code-review description: 审查代码变更,检查潜在问题 --- 你是一个严格的代码审查员。审查代码时重点关注: 1. 安全漏洞(SQL 注入、XSS、硬编码密钥) 2. 性能问题(N+1 查询、未优化的循环) 3. 代码风格(命名规范、函数长度) 输出格式: - 用中文回复 - 每个问题标注严重程度: 高 / 中 / 低 - 给出具体的修改建议开头用---包裹的部分是frontmatter,定义了 Skill 的元信息:名字和描述。描述很重要,Claude Code 靠它来判断什么时候该加载这个 Skill。
下面的内容就是注入到上下文中的指令。你写什么,Claude 就会遵循什么。
Skill 放在哪里
Claude Code 会从多个位置扫描 Skill:
~/.claude/skills/ # 用户级,全局生效 <project>/.claude/skills/ # 项目级,仅当前项目生效用户级的 Skill 适合放通用能力,比如代码审查、文档生成;项目级的 Skill 适合放项目专属的知识,比如"我们项目用的是 Vue 3 + TypeScript,组件放 src/components 下"。
每个 Skill 就是一个文件夹,里面放SKILL.md:
~/.claude/skills/ code-review/ SKILL.md doc-generator/ SKILL.md api-design/ SKILL.mdSkill 是怎么被加载的
这是最关键的部分。回到开头那个问题:如果把所有规范文档全塞进 system prompt,6500 行的 prompt 每次调用都带着,99% 的内容和当前任务无关,白白烧 token。
Skill 的解决方案是两层设计——目录层和内容层分开加载:
| 层 | 注入位置 | 时机 | 代价 |
|---|---|---|---|
| 目录 | system prompt | 启动时扫描 skills/ | ~100 tokens/skill,每轮都带 |
| 内容 | tool_result | Agent 调用 load_skill 时 | ~2000 tokens/skill,按需 |
第一层:启动时注入目录
Claude Code 启动时,harness 会扫描skills/目录,解析每个SKILL.md的 YAML frontmatter(name、description),存入一个注册表SKILL_REGISTRY。然后从注册表生成目录,注入 system prompt。
Agent 每轮都能看到"我有哪些技能可用",但只看到名字和描述,不花额外 API 调用。
SKILL_REGISTRY:dict[str,dict]={}def_scan_skills():fordinsorted(SKILLS_DIR.iterdir()):ifnotd.is_dir():continuemanifest=d/"SKILL.md"ifmanifest.exists():raw=manifest.read_text()meta,body=_parse_frontmatter(raw)name=meta.get("name",d.name)desc=meta.get("description",raw.split("\n")[0].lstrip("#").strip())SKILL_REGISTRY[name]={"name":name,"description":desc,"content":raw}defbuild_system()->str:catalog="\n".join(f"- **{s['name']}**:{s['description']}"forsinSKILL_REGISTRY.values())returnf"You are a coding agent. Skills available:\n{catalog}\nUse load_skill to get full details when needed."这里有个安全细节:注册表是启动时填充的,后续通过名字查找,不走文件路径,没有路径遍历风险。
第二层:load_skill 按需加载内容
Agent 看到目录后,决定"我需要 code-review 这个 Skill",于是调用load_skill("code-review")工具。这时候才把SKILL.md的完整内容读出来,通过 tool_result 注入当前 messages。
TOOLS.append({"name":"load_skill","description":"Load a skill by name to get full instructions","input_schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}})TOOL_HANDLERS["load_skill"]=load_skilldefload_skill(name:str)->str:skill=SKILL_REGISTRY.get(name)ifnotskill:returnf"Skill not found:{name}"returnskill["content"]注意一个关键区别:Skill 内容不是 system prompt 的一部分,它作为一次工具调用的结果进入 messages。后续轮次会随对话历史一起携带,直到上下文被压缩或会话结束。
用伪代码来描述整个流程:
启动时: 扫描 skills/ → 解析 frontmatter → 存入 SKILL_REGISTRY → 生成目录 → 注入 system prompt 运行时: 用户: "帮我 review 代码" ↓ Agent 看到目录里有 code-review Skill ↓ Agent 调用 load_skill("code-review") ↓ tool_result 返回 SKILL.md 完整内容 ↓ Agent 基于 Skill 指令执行任务这个设计和上下文压缩(compact)自然衔接:按需加载解决了"不该提前带的不要带",compact 解决"该丢的怎么丢"。两层配合,让 Agent 的上下文始终保持在最精简的状态。
Skill 和普通 Prompt 有什么区别
你可能会想:这不就是把一段 prompt 塞进上下文吗?我自己复制粘贴一段指令进去不也一样?
从效果上看确实类似,但 Skill 有几个关键优势:
1. 按需加载,不浪费 token
你有一百个 Skill,但每次对话只用到一两个。两层设计确保目录层始终轻量(~100 tokens/skill),内容层只在需要时才通过工具调用加载(~2000 tokens/skill)。如果全塞进 system prompt,上下文窗口直接就被吃掉了。
2. 声明式的,不需要写代码
Skill 不是插件系统,不需要实现接口、注册回调。你只需要写 Markdown,描述你想要的行为就行。门槛低到任何会写字的人都能创建 Skill。
3. 可以调用工具
Skill 里可以声明它需要使用哪些工具,以及如何使用。比如一个"数据库迁移" Skill 可以指定使用bash工具执行prisma migrate命令。
4. 版本管理和共享
Skill 就是文件,可以放在 Git 仓库里做版本管理。团队共享一套 Skill,每个人的行为就统一了。
来看一个真实场景
假设你团队有一套代码规范,每次 review 代码都要重复说一遍:
我们用 TypeScript strict 模式 函数命名用 camelCase,组件命名用 PascalCase API 返回值统一用 { code, data, message } 格式 错误处理统一用 try-catch,不用 .catch()每次都复制粘贴这些规则,烦不烦?
写成 Skill 就一劳永逸了:
--- name: team-code-review description: 按照团队规范审查代码变更 --- 你是一个严格的代码审查员,审查时遵循以下团队规范: ## TypeScript 规范 - 使用 strict 模式 - 禁止使用 any,必须定义具体类型 - 接口以 I 开头,如 IUserData ## 命名规范 - 函数/变量:camelCase - 组件/类:PascalCase - 常量:UPPER_SNAKE_CASE - 文件名:kebab-case ## API 规范 - 返回值统一格式:{ code: number, data: T, message: string } - 错误处理统一使用 try-catch - 不使用 .catch() 链式调用 ## 审查输出格式 1. 列出所有不符合规范的地方 2. 每条标注:文件名 + 行号 + 问题描述 + 修改建议 3. 最后给一个总体评价把这个文件放到~/.claude/skills/team-code-review/SKILL.md,以后说"帮我 review 代码",Claude 就会自动按照你们团队的规范来审查,不用你再重复那些规则了。
Skill 的触发方式
Skill 的触发有两种方式:
自动触发:Agent 在推理过程中看到 system prompt 里的 Skill 目录,根据用户消息和 Skill 描述自行判断是否需要加载。如果匹配上了,就调用load_skill工具获取完整内容。比如用户说"帮我 review 一下这个 PR",Agent 看到目录里有一个 description 是"审查代码变更"的 Skill,就会主动调用load_skill("code-review")。
手动触发:在 Claude Code 中输入/skill-name可以强制加载某个 Skill。比如/code-review会直接加载代码审查 Skill,不管当前消息是什么。
手动触发适合那些不容易被自动匹配的场景。比如你有一个 Skill 是"按照公司模板生成周报",自动匹配很难判断什么时候该触发,但手动/weekly-report就很明确。
一个更复杂的例子
Skill 不只能写审查规则,还能定义完整的工作流程。来看一个生成 API 文档的 Skill:
--- name: api-doc-generator description: 根据代码自动生成 API 接口文档 --- 你是一个 API 文档生成专家。当用户要求生成 API 文档时,按以下流程执行: ## 第一步:扫描项目 - 使用 Glob 工具查找所有路由文件(routes/、api/、controllers/) - 使用 Read 工具读取每个路由文件的内容 ## 第二步:提取接口信息 对每个接口提取: - HTTP 方法和路径 - 请求参数(query、body、path params) - 响应格式 - 认证要求(是否需要 token) ## 第三步:生成文档 使用 Markdown 格式输出,每个接口包含: - 接口名称和描述 - 请求示例(curl 格式) - 响应示例(JSON 格式) - 错误码说明 ## 第四步:保存文件 将生成的文档保存到 docs/api.md这个 Skill 不只定义了"输出什么",还定义了"怎么做"——先扫描、再提取、再生成、再保存。Claude 会严格按照这个流程执行,就像一个有标准作业流程的员工。
Skill 和 CLAUDE.md 的关系
你可能还听说过CLAUDE.md,它和 Skill 有什么区别?
CLAUDE.md是项目级的全局指令,每次对话都会加载。适合放项目的基本信息:技术栈、目录结构、构建命令这些。
Skill是按需加载的能力包,只在需要的时候才注入。适合放专业领域的工作流和规则。
两者可以配合使用。CLAUDE.md告诉 Claude “这个项目是什么”,Skill 告诉 Claude “在特定场景下该怎么做”。
CLAUDE.md(始终加载) "这是一个 Vue 3 + TypeScript 项目,使用 pnpm 管理依赖" Skill(按需加载) "当用户要求 review 代码时,按照团队规范审查" "当用户要求生成文档时,按照标准模板输出"小结
Skill 的核心思想就一句话:用到的时候才加载,别全塞 prompt 里。
传统的做法是把所有规范文档塞进 system prompt,每次调用都带着,不管有没有用。Skill 的两层设计解决了这个问题:目录层始终轻量,内容层按需通过工具调用注入。从软件工程的角度看,这就是懒加载思想——和前端的路由懒加载、后端的依赖注入是同一个思路。
Claude Code 的 Skill 机制没有用什么复杂的技术,没有编译器、没有运行时、没有沙箱。它就是一个 Markdown 文件加一套两层加载策略。但正是这种简洁,让它变得极其灵活——你不需要会写代码,只需要会写字,就能给 Agent 扩展能力。
注:本文参考了 GitHub 开源学习项目:learn-claude-code。这是一个非常优秀的学习资源,推荐读者结合项目内容一同学习。
