Skip to content

Skills 技能系统

ℹ️

自定义命令已合并到 skills 中。 .claude/commands/deploy.md 中的文件和 .claude/skills/deploy/SKILL.md 中的 skill 都会创建 /deploy 并以相同的方式工作。现有的 .claude/commands/ 文件继续工作以兼容之前

内置Skills

内置 skills 随 Claude Code 一起提供,在每个会话中都可用。与内置命令不同,内置命令直接执行固定逻辑,内置 skills 是基于提示的:为 Claude 提供详细的步骤,让它使用其工具来安排工作。这意味着可以生成并行代理、读取文件并适应你的代码库。你调用的方式与调用任何其他 skill 相同:输入 / 后跟 skill 名称。

在下表中,<arg> 表示必需的参数,[arg] 表示可选参数

Skill目的
/batch <instruction>在代码库中并行编排大规模更改。研究代码库,将工作分解为 5 到 30 个独立单元,并呈现计划。获得批准后,在隔离的 git worktree 中为每个单元生成一个后台代理。每个代理实现其单元、运行测试并打开拉取请求。需要 git 存储库。示例:/batch migrate src/ from Solid to React
/claude-api为你的项目语言(Python、TypeScript、Java、Go、Ruby、C#、PHP 或 cURL)加载 Claude API 参考资料,以及 Python 和 TypeScript 的 Agent SDK 参考。涵盖工具使用、流式传输、批处理、结构化输出和常见陷阱。当你的代码导入 anthropic@anthropic-ai/sdkclaude_agent_sdk 时也会自动激活
/debug [description]为当前会话启用调试日志记录并通过读取会话调试日志来排查问题。默认情况下调试日志处于关闭状态,除非你使用 claude --debug 启动,因此在会话中期运行 /debug 会从该点开始捕获日志。可选地描述问题以集中分析
/loop [interval] <prompt>在会话保持打开状态时按间隔重复运行提示。适用于轮询部署、监督 PR 或定期重新运行另一个 skill。示例:/loop 5m check if the deploy finished。请参阅按计划运行提示
/simplify [focus]查看你最近更改的文件以查找代码重用、质量和效率问题,然后修复它们。并行生成三个审查代理,汇总其发现并应用修复。传递文本以专注于特定问题:/simplify focus on memory efficiency

目录结构与加载机制

之前所有的配置 CLAUDE.mdMCP 都是在告诉 Claude"这个项目是什么样的"。Skills 做的事情更进一步:告诉 Claude"在这个项目里,某类任务应该按照这套固定流程来做"。


Skills 是什么

一个 Skill 是放在 .claude/skills/ 目录下的一个文件夹,里面包含一组相关的指令、脚本和资源。Claude Code 在启动时会扫描这个目录,把所有 Skill 的描述加载进上下文,在执行任务时根据任务类型自动选择并激活对应的 Skill。

和 CLAUDE.md 的区别在于粒度和动态性。CLAUDE.md 是静态的全局上下文,每次都全量加载。Skills 是动态的专项能力,按需激活——处理推荐系统时激活推荐相关的 Skill,处理数据库迁移时激活迁移相关的 Skill,两者互不干扰,也不会同时占用上下文。


目录结构

Plain
.claude/
└── skills/
    ├── new-feature/
    │   ├── SKILL.md          # 技能描述和激活条件(必须)
    │   ├── steps.md          # 具体执行步骤
    │   ├── templates/        # 代码模板
    │   │   ├── controller.java.tmpl
    │   │   ├── service.java.tmpl
    │   │   └── req-resp.java.tmpl
    │   └── examples/         # 示例代码
    │       └── UserController.java
    ├── db-migration/
    │   ├── SKILL.md
    │   ├── checklist.md
    │   └── scripts/
    │       └── validate-migration.sh
    ├── write-test/
    │   ├── SKILL.md
    │   └── patterns.md
    └── code-review/
        ├── SKILL.md
        └── review-criteria.md

每个 Skill 是一个独立的目录,目录名就是 Skill 的标识符。目录里的文件结构没有强制要求,除了 SKILL.md 是必须的——它是 Claude Code 识别和加载 Skill 的入口。


SKILL.md 的结构

SKILL.md 是每个 Skill 最重要的文件,它决定了三件事:Claude 怎么识别这个 Skill、什么时候激活它、激活后做什么。

一个完整的 SKILL.md 示例:

Markdown
---
name: new-feature
description: 在项目中新建一个完整的业务功能,包含 Controller、Service、Mapper、Req/Resp 和单元测试
triggers:
  - 新建功能
  - 新增接口
  - 创建模块
  - new feature
  - add endpoint
version: 1.0.0
---

# 新建功能 Skill

## 适用场景
需要从零开始创建一个新的业务功能时使用。
覆盖从 Controller 到 Mapper 的完整垂直切片,包含单元测试。

## 执行前确认
在开始之前,先向用户确认:
1. 功能名称(用于生成类名)
2. 所属模块(user / trade / recommend / notify)
3. 主要操作类型(查询 / 创建 / 更新 / 删除)
4. 是否需要缓存
5. 是否需要发 MQ 消息

## 执行步骤
详见 steps.md

## 代码模板
详见 templates/ 目录,所有新代码必须基于模板生成,不要自由发挥结构

--- 包裹的部分是 YAML frontmatter,包含机器可读的元数据:name 是 Skill 的唯一标识,description 是 Claude 判断是否激活这个 Skill 的主要依据,triggers 是触发关键词列表。


加载机制

Claude Code 启动时对 Skills 的处理分三个阶段:

扫描阶段——遍历 .claude/skills/ 目录,找到所有包含 SKILL.md 的子目录,读取每个 SKILL.md 的 frontmatter,建立一个 Skill 索引:名称、描述、触发词。这个索引会占用少量上下文,但比把所有 Skill 的完整内容全部加载进来要轻量得多。

匹配阶段——当你发出一个任务请求时,Claude 会把任务描述和 Skill 索引里的 descriptiontriggers 做语义匹配。不是简单的关键词搜索,而是语义层面的相似度判断——"帮我加一个新的 REST 接口"和 new-feature Skill 的描述能匹配上,即使没有出现任何触发词。

激活阶段——匹配到合适的 Skill 之后,Claude 读取该 Skill 目录下的所有文件内容,加载进当前会话的上下文。此时 Skill 里定义的步骤、模板、示例才真正对 Claude 可见,它会按照 Skill 的指导来执行任务。

这套机制的核心优势是按需加载。你可以定义十几个 Skill,每次只激活和当前任务相关的一个或几个,避免所有 Skill 的内容同时占用上下文。


多级 Skills 目录

除了项目级的 .claude/skills/,Skills 支持多级目录结构,加载优先级从高到低:

用户级 ~/.claude/skills/——跨所有项目生效,适合放通用的技术 Skill,比如"写单元测试"、"生成 API 文档"。

项目级 .claude/skills/——只在当前项目生效,适合放项目专属的业务 Skill。

附加目录——通过 --add-dir 参数指定额外的 Skills 目录,适合在多个项目之间共享一套 Skills 而不想复制文件的场景:

Bash
claude --add-dir /shared/team-skills

指定了附加目录后,该目录下的 Skills 和项目级 Skills 一起被加载,团队级和项目级的 Skill 库可以分开维护。


Skills 和 CLAUDE.md 的分工

两者解决不同层次的问题,应该配合而不是替代:

CLAUDE.md 放项目认知——这是什么项目、用什么技术栈、有哪些全局规范。这些信息是所有任务的共同前提,需要始终在上下文里。

Skills 放任务流程——特定类型的任务应该按什么步骤执行、用什么模板、注意什么细节。这些信息只在执行对应类型的任务时才需要,不应该永久占用上下文。

实际使用中,CLAUDE.md 告诉 Claude 项目用 MyBatis Plus、禁止 BeanUtils,new-feature Skill 告诉 Claude 新建功能时应该生成哪几个文件、每个文件遵循什么模板。两层信息叠加,Claude 生成的代码既符合项目规范,又有标准化的结构。


手动激活 Skill

除了自动匹配,也可以在对话里手动指定使用某个 Skill:

Plain
用 new-feature skill 帮我创建一个账号举报功能
Plain
按照 db-migration skill 的流程,帮我新建一个给 game_account 表加索引的迁移脚本

显式指定在两种情况下特别有用:任务描述比较模糊,不确定自动匹配是否会选到正确的 Skill;或者想用某个特定的 Skill 处理一个看起来不那么典型的任务场景。

编写自定义 Skill

理解了 Skills 的结构和加载机制之后,下一步是真正动手写一个。一个写得好的 Skill 和一个写得差的 Skill,在实际使用中的效果差距可能比有 Skill 和没有 Skill 之间的差距还大。这一节从头到尾走完一个 Skill 的设计和编写过程。


从痛点出发,不要从功能出发

很多人写第一个 Skill 时的错误是:想到什么就封装什么,结果做出来一堆形式上的 Skill,实际用起来和直接描述任务差不多。

正确的起点是问自己:哪些任务我反复在做,而且每次都要费劲向 Claude 解释同样的背景和步骤?

答案往往集中在几类:新建一套完整的业务代码(每次都要解释项目结构和模板)、写单元测试(每次都要说明测试框架和风格约定)、数据库迁移(每次都要提醒检查同样的几个风险点)、排查线上问题(每次都要交代日志位置和排查流程)。

这些就是值得封装成 Skill 的候选。一个好的 Skill 封装的是重复性的领域知识,而不只是一个任务描述。


一个完整示例:新建业务功能

以"新建业务功能"为例,从头设计并编写一个 Skill。

第一步:确定 Skill 的边界

这个 Skill 应该覆盖什么,不覆盖什么。覆盖:从 Controller 到 Mapper 的完整垂直切片,含 Req/Resp、MapStruct Converter、单元测试。不覆盖:数据库表结构设计(那是另一个 Skill 的职责)、MQ 消费者(有专门的 MQ Skill)。

边界清晰,Skill 才不会变成一个什么都管但什么都管不好的大杂烩。

第二步:创建目录结构

Bash
mkdir -p .claude/skills/new-feature/{templates,examples}
Plain
.claude/skills/new-feature/
├── SKILL.md
├── steps.md
├── checklist.md
├── templates/
│   ├── Controller.java.tmpl
│   ├── Service.java.tmpl
│   ├── ServiceImpl.java.tmpl
│   ├── Mapper.java.tmpl
│   ├── Req.java.tmpl
│   ├── Resp.java.tmpl
│   ├── Converter.java.tmpl
│   └── ServiceTest.java.tmpl
└── examples/
    └── GameAccountFeature/
        ├── GameAccountController.java
        ├── GameAccountService.java
        └── GameAccountServiceImpl.java

第三步:编写 SKILL.md

Markdown
---
name: new-feature
description: 在交易平台中新建一个完整的业务功能,生成 Controller、Service 接口、ServiceImpl、Mapper、Req、Resp、Converter 和单元测试
triggers:
  - 新建功能
  - 新增接口
  - 创建业务模块
  - 添加 API
  - new feature
  - add endpoint
  - create module
version: 1.2.0
author: duoli
---

# 新建业务功能 Skill

## 适用场景

从零创建一个新的业务功能,需要完整的垂直切片代码。
如果只是在已有功能上增加方法,不需要使用这个 Skill,直接描述需求即可。

## 执行前必须确认的信息

在开始生成任何代码之前,先向用户确认以下信息。
不要跳过这个步骤,缺少任何一项都可能导致生成的代码需要大量修改。

1. **功能名称**:用于生成类名,如"账号举报"→ `AccountReport`
2. **所属模块**:user / trade / recommend / notify
3. **包路径**:如 `com.xxx.trade.accountreport`
4. **主要操作**:列出需要的接口(查询列表、查询详情、创建、更新、删除)
5. **是否需要缓存**:如需要,说明缓存策略
6. **是否发 MQ 消息**:如需要,说明 topic 和触发时机
7. **特殊约束**:任何不符合常规的地方

## 核心规范(必须遵守)

- 所有类的结构必须严格对照 templates/ 目录下的模板
- 参照 examples/ 目录下的示例理解模板的实际应用
- 不允许自行发明项目中未使用的模式
- 完整执行步骤见 steps.md
- 生成完毕后执行 checklist.md 中的自检项

第四步:编写 steps.md

Markdown
# 新建功能执行步骤

## 步骤 1:信息确认

完成信息确认后,整理成如下格式再开始:

```
功能名:AccountReport(账号举报)
模块:trade
包路径:com.xxx.trade.accountreport
接口:创建举报、查询举报列表(分页)、处理举报
缓存:无
MQ:举报创建后发送 TRADE_ACCOUNT_REPORTED topic
```

## 步骤 2:生成顺序

严格按以下顺序生成,后面的文件依赖前面的定义:

1. `AccountReportReq.java``AccountReportResp.java`
2. `AccountReport.java`(实体类,对照表结构)
3. `AccountReportMapper.java`
4. `AccountReportConverter.java`(MapStruct)
5. `AccountReportService.java`(接口)
6. `AccountReportServiceImpl.java`(实现)
7. `AccountReportController.java`
8. `AccountReportServiceTest.java`

## 步骤 3:每个文件生成后的检查

每生成一个文件,立即检查:
- 包名和导入是否正确
- 是否引用了不存在的类
- 命名是否符合约定(见 CLAUDE.md)

## 步骤 4:生成后整体检查

所有文件生成完毕后,运行 checklist.md 中的自检项。

## 步骤 5:提示用户

生成完毕后,告诉用户:
- 还需要手动创建的数据库表结构
- 需要在 ErrorCode 枚举里添加的错误码
- 如果有 MQ,需要在 MqConstants 里添加的 topic 常量

第五步:编写代码模板

模板是 Skill 里信息密度最高的部分,直接决定生成代码的质量。以 ServiceImpl 模板为例:

Java
// templates/ServiceImpl.java.tmpl
package {{packagePath}};

import com.xxx.common.exception.BizException;
import com.xxx.common.exception.ErrorCode;
import com.xxx.common.result.Result;
import com.xxx.common.result.PageResp;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * {{featureName}} Service 实现
 *
 * @author {{author}}
 * @since {{date}}
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class {{className}}ServiceImpl implements {{className}}Service {

    private final {{className}}Mapper {{instanceName}}Mapper;
    private final {{className}}Converter converter;
    // 如果有缓存,在此注入 RedissonClient
    // 如果有 MQ,在此注入对应的 EventPublisher

    @Override
    @Transactional(readOnly = true)
    public {{className}}Resp getById(Long id) {
        {{entityName}} entity = {{instanceName}}Mapper.selectById(id);
        if (entity == null) {
            throw new BizException(ErrorCode.{{ERROR_CODE_NOT_FOUND}});
        }
        return converter.toResp(entity);
    }

    @Override
    @Transactional(readOnly = true)
    public PageResp<{{className}}Resp> page({{className}}PageReq req) {
        Page<{{entityName}}> page = {{instanceName}}Mapper.selectPage(
            new Page<>(req.getPageNum(), req.getPageSize()),
            buildQueryWrapper(req)
        );
        return PageResp.of(page, converter::toResp);
    }

    @Override
    @Transactional
    public void save({{className}}SaveReq req) {
        {{entityName}} entity = converter.toEntity(req);
        {{instanceName}}Mapper.insert(entity);
        log.info("{{featureName}}创建成功, id={}", entity.getId());
        // 如果有 MQ:eventPublisher.publishXxxCreated(entity.getId());
    }

    @Override
    @Transactional
    public void update(Long id, {{className}}UpdateReq req) {
        {{entityName}} entity = {{instanceName}}Mapper.selectById(id);
        if (entity == null) {
            throw new BizException(ErrorCode.{{ERROR_CODE_NOT_FOUND}});
        }
        converter.updateEntity(req, entity);
        {{instanceName}}Mapper.updateById(entity);
        log.info("{{featureName}}更新成功, id={}", id);
    }

    @Override
    @Transactional
    public void remove(Long id) {
        {{entityName}} entity = {{instanceName}}Mapper.selectById(id);
        if (entity == null) {
            throw new BizException(ErrorCode.{{ERROR_CODE_NOT_FOUND}});
        }
        {{instanceName}}Mapper.deleteById(id);
        log.info("{{featureName}}删除成功, id={}", id);
    }

    private LambdaQueryWrapper<{{entityName}}> buildQueryWrapper({{className}}PageReq req) {
        return new LambdaQueryWrapper<{{entityName}}>()
            // 根据实际查询条件补充
            .orderByDesc({{entityName}}::getCreateTime);
    }
}

模板里用 标记需要替换的部分。Claude 在激活 Skill 后会读取模板,根据用户确认的信息(功能名、包路径等)把占位符替换成实际值。

第六步:编写 checklist.md

Markdown
# 生成完毕自检清单

生成所有文件后,逐项检查:

## 代码正确性
- [ ] 所有 import 是否都能在项目里找到对应的类
- [ ] 包路径是否和文件实际位置一致
- [ ] MapStruct Converter 的方法签名是否和 Req/Resp/Entity 的字段匹配
- [ ] Mapper 里的 LambdaQueryWrapper 泛型是否正确

## 规范遵守
- [ ] ServiceImpl 的写操作是否都有 @Transactional
- [ ] 查询方法是否都有 @Transactional(readOnly = true)
- [ ] Controller 是否只有参数处理,无业务逻辑
- [ ] 所有写操作是否都有 INFO 日志,包含业务 ID
- [ ] 是否有返回实体类而不是 Resp 对象的接口(不允许)

## 遗漏项提示
检查完毕后,告知用户还需要手动完成的事项:
- 数据库建表 SQL
- ErrorCode 枚举新增错误码
- MqConstants 新增 topic 常量(如果有 MQ)
- application.yml 新增配置(如果有特殊配置)

让 Skill 学会提问而不是乱猜

Skill 里最重要的设计决策之一:遇到不确定的信息,提问而不是假设

在 SKILL.md 或 steps.md 里明确写出"执行前必须确认的信息",并且告诉 Claude 不确认完这些信息就不要开始生成。这个约束很重要——Claude 的默认行为是尽量不打断用户直接完成任务,但在代码生成场景里,基于错误假设生成的一堆代码往往比没有代码更让人头疼。

Markdown
## 执行前必须确认

以下信息缺失时,停止生成并向用户提问:
- 功能名称(直接影响所有类名)
- 所属模块(影响包路径和依赖关系)

以下信息可以有合理默认值,但告知用户你的假设:
- 缓存策略(默认:无缓存)
- MQ(默认:无 MQ 消息)

迭代改进 Skill 的节奏

第一版 Skill 不会是最好的。实际使用几次之后,你会发现生成的代码还是有一些固定的错误或遗漏——这些就是 Skill 需要改进的地方。

建立一个简单的习惯:每次发现 Claude 用这个 Skill 生成的代码有问题,不只是在对话里纠正它,同时更新 Skill 的模板或 checklist,把这个问题的修复固化进去。几轮迭代下来,Skill 生成的代码质量会越来越接近你的标准,需要手动修改的地方越来越少。

这个过程本质上是把你的领域知识和质量标准,逐步沉淀进 Skill 的定义里。Skill 越成熟,它替你承担的认知负担就越多。

通过 Skills API 管理和分发组织级 Skill

从个人习惯到团队规范

当你独自开发时,自定义 Skill 放在 ~/.claude/skills/ 就够了——只要自己能用到就行。但当团队规模扩大,问题随之而来:你写了一个封装公司 API 规范的 Skill,同事怎么获取最新版本?新人入职第一天,谁来告诉他有哪些 Skill 可用?某个 Skill 里的安全策略更新了,怎么同步到所有人的本地环境?

靠"口口相传"或者群里发压缩包,是管不住这件事的。Skills API 解决的正是这个问题:通过 /v1/skills 端点,把 Skill 提升为工作区(Workspace)级别的共享资源,所有成员通过 API 统一获取,由管理员集中版本控制。

Skills API 的基本模型

通过 /v1/skills 端点上传的自定义 Skill 在整个工作区内共享,所有成员都可以访问。这与 Claude Code 的文件系统模式截然不同——后者是每个人自己维护本地目录,前者是统一的中心化分发。

Skills API 提供工作区范围的分发能力,支持上传、版本管理和权限控制。每个 Skill 目录(包含 SKILL.md 及其捆绑文件)与 Git 追踪的文件夹自然对应。

理解这个模型之后,我们来看一个完整的管理流程。假设你在一个交易平台的后端团队工作,需要把「Spring Boot 代码审查规范」这个 Skill 统一下发给所有后端工程师。

上传一个组织级 Skill

Skill 的结构本身没有变化,仍然是一个目录加一个 SKILL.md。以 Spring Boot 代码审查 Skill 为例:

Plain
springboot-review/
├── SKILL.md
└── checkstyle-rules.xml

SKILL.md 内容如下:

Markdown
---
name: springboot-review
description: 对 Spring Boot 项目进行代码审查,包括 API 设计、异常处理、事务边界和安全规范检查
---

# Spring Boot 代码审查规范

## 核心检查项

审查时必须验证以下几个关键领域:

### API 层
- Controller 方法必须使用 `@Valid` 注解校验入参
- 统一返回 `Result<T>` 包装对象,禁止直接返回裸实体
- 异常信息不得透传到响应体,使用错误码替代

### 事务管理
- `@Transactional` 只加在 Service 层,Controller 层不允许开事务
- 涉及多表写操作必须显式声明 `rollbackFor = Exception.class`
- 禁止在事务方法内调用外部 HTTP 接口

### 安全规范
- 敏感字段(手机号、身份证)必须脱敏后返回
- 账号交易金额字段必须使用 `BigDecimal`,禁止 `double`

## 示例:标准 Controller 结构

参考 checkstyle-rules.xml 执行自动化格式检查。

准备好目录之后,通过 API 上传:

Bash
# 将 Skill 目录打包为 zip
zip -r springboot-review.zip springboot-review/

# 上传到工作区
curl -X POST "https://api.anthropic.com/v1/skills" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: skills-2025-10-02" \
  -F "files[]=@springboot-review/SKILL.md;filename=springboot-review/SKILL.md" \
  -F "files[]=@springboot-review/checkstyle-rules.xml;filename=springboot-review/checkstyle-rules.xml"

上传成功后,API 返回一个 skill_id,格式类似 skill_01AbCdEfGhIjKlMnOpQrStUv。这个 ID 是后续版本管理和调用的锚点,需要存入你的内部注册表。

版本管理:让更新可控

在生产环境中,建议将 Skill 固定到特定版本,并在每次发布新版本前运行完整的评估套件,将每次更新视为需要完整审查的新部署。

版本管理的操作通过对已有 Skill 创建新版本来完成:

Bash
# 修改 SKILL.md 后,创建新版本
NEW_VERSION=$(curl -X POST \
  "https://api.anthropic.com/v1/skills/skill_01AbCdEfGhIjKlMnOpQrStUv/versions" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: skills-2025-10-02" \
  -F "files[]=@springboot-review/SKILL.md;filename=springboot-review/SKILL.md" \
  | jq -r '.version')

echo "新版本号: $NEW_VERSION"

在 API 调用时指定版本,可以确保灰度发布期间不同环境使用不同版本:

Bash
# 调用时固定版本
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-4-6",
    "max_tokens": 4096,
    "container": {
      "skills": [{
        "type": "custom",
        "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv",
        "version": "2"
      }]
    },
    "messages": [{"role": "user", "content": "审查这段 OrderService 代码"}],
    "tools": [{"type": "code_execution_20250825", "name": "code_execution"}]
  }'

在 Spring Boot 项目中集成管理脚本

实际工程中,与其手动执行 curl 命令,不如把 Skill 的生命周期管理封装成项目脚本。下面是一个用 Java 编写的 Skill 管理客户端,适合集成到内部运维工具或 CI/CD 流水线:

Java
@Service
public class SkillManagementService {

    private static final String API_BASE = "https://api.anthropic.com/v1";
    private static final String BETA_HEADER = "skills-2025-10-02";

    @Value("${anthropic.api-key}")
    private String apiKey;

    private final RestTemplate restTemplate;

    /**
     * 上传或更新一个组织级 Skill
     * @param skillDir Skill 目录路径
     * @param existingSkillId 若为 null 则创建新 Skill,否则创建新版本
     */
    public SkillUploadResult uploadSkill(Path skillDir, String existingSkillId) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("x-api-key", apiKey);
        headers.set("anthropic-version", "2023-06-01");
        headers.set("anthropic-beta", BETA_HEADER);
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);

        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();

        // 遍历目录,将所有文件加入请求体
        try (Stream<Path> files = Files.walk(skillDir)) {
            files.filter(Files::isRegularFile).forEach(file -> {
                String relativePath = skillDir.getParent()
                    .relativize(file).toString();
                body.add("files[]", new FileSystemResource(file) {
                    @Override
                    public String getFilename() {
                        return relativePath;
                    }
                });
            });
        } catch (IOException e) {
            throw new SkillUploadException("读取 Skill 目录失败", e);
        }

        String url = existingSkillId == null
            ? API_BASE + "/skills"
            : API_BASE + "/skills/" + existingSkillId + "/versions";

        HttpMethod method = existingSkillId == null
            ? HttpMethod.POST : HttpMethod.POST;

        ResponseEntity<Map> response = restTemplate.exchange(
            url, method,
            new HttpEntity<>(body, headers),
            Map.class
        );

        return SkillUploadResult.from(response.getBody());
    }

    /**
     * 查询工作区内所有已上传的 Skill
     */
    public List<SkillInfo> listWorkspaceSkills() {
        HttpHeaders headers = new HttpHeaders();
        headers.set("x-api-key", apiKey);
        headers.set("anthropic-version", "2023-06-01");
        headers.set("anthropic-beta", BETA_HEADER);

        ResponseEntity<Map> response = restTemplate.exchange(
            API_BASE + "/skills",
            HttpMethod.GET,
            new HttpEntity<>(headers),
            Map.class
        );

        List<Map<String, Object>> skills =
            (List<Map<String, Object>>) response.getBody().get("data");

        return skills.stream()
            .map(SkillInfo::from)
            .collect(Collectors.toList());
    }
}

配合 Spring Boot 的配置管理,可以把已发布的 Skill ID 和版本号维护在 application.yml 中:

YAML
anthropic:
  api-key: ${ANTHROPIC_API_KEY}
  skills:
    springboot-review:
      skill-id: skill_01AbCdEfGhIjKlMnOpQrStUv
      version: "2"   # 固定版本,避免自动升级引发行为变化
    db-migration-helper:
      skill-id: skill_02XyZwVuTsRqPoNmLkJiHgFe
      version: "latest"  # 内部工具可跟最新版

企业管控的关键:安全审查流程

部署企业级 Skill 需要回答两个独立问题:Skills 平台层面是否安全?以及如何评估某个具体 Skill 的风险?

在批准任何来自第三方或内部贡献者的 Skill 之前,需要完成以下步骤:阅读 Skill 目录的全部内容,确认跳转目标的合法性(若 Skill 引用外部 URL,验证其指向预期域名),并检查是否存在数据泄露模式。同时要求 Skill 评估人与作者分离,避免自审。

对于交易这类涉及资金的业务场景,这一点尤为重要。一个 Skill 如果在执行代码时能访问数据库连接字符串或支付密钥,其审查标准应该等同于审查一段生产代码。

分发方式的选择

Team 和 Enterprise 计划的管理员可以通过管理员设置集中下发 Skill,管理员下发的 Skill 默认对所有用户启用,用户也可以根据自己的偏好将单个 Skill 关闭。

对于 Claude Code 用户,除了 API 上传方式,还有两条分发路径值得了解:

第一是 Git 仓库。将 Skill 目录存入 Git 作为唯一事实来源,通过 Pull Request 进行代码审查和版本回滚。团队成员克隆仓库后,将 Skill 目录软链接到 ~/.claude/skills/ 即可本地使用,更新时只需 git pull。这个方式最轻量,适合规模较小且技术背景一致的团队。

第二是 Plugin 机制。通过将 Skill 提交到版本控制,项目成员可以直接使用;也可以通过创建含 skills/ 目录的 Plugin,在 Claude Code 中集中安装。这种方式可以把多个相关 Skill 打包成一个 Plugin 分发,安装命令简洁明了:

Bash
/plugin install springboot-tools@your-org

Skills API 把 Skill 从「个人工具」升级为「组织资产」。它的价值不在于技术复杂度,而在于把团队知识沉淀下来并让它可流动:一位资深工程师提炼出的代码审查经验,通过几个 API 调用,就能成为所有人都能调用的能力。对于正在构建内部工程工具链的团队而言,这是值得投入的基础设施。

设计 Skill 的 token budget 与上下文策略

一个容易被忽视的约束

大多数人在写完第一批 Skill 之后,不会立刻遇到问题。三个、五个 Skill,运行得很顺畅,Claude 总能找到正确的那一个。但当你认真对待 Skill 体系、开始为团队沉淀知识时,数量慢慢增加到二三十个,然后某天你忽然发现 Claude 对某个 Skill "视而不见"——你明确描述的场景,它就是没有触发。

这不是 Claude 变笨了,而是你踩到了 Skill 的 token budget 上限。

Skill 的描述字段会被加载进上下文,用于让 Claude 判断哪个 Skill 与当前任务相关。当 Skill 数量增多时,可能超出字符预算。这个预算随上下文窗口动态调整,基准值是上下文窗口的 2%,并有一个 16,000 字符的兜底上限。可以运行 /context 命令检查是否出现 Skill 被排除的警告。

16,000 字符,听起来不少。但一旦认真量化,你会发现它比想象中更紧张。

预算的真实消耗量

社区对这个预算做了实证测量。每个 Skill 在 available_skills 区域中消耗的字符量由两部分组成:固定开销(XML 标签、Skill 名称、位置字段等)约 109 个字符,加上 description 字段本身的长度。预算大约在 15,700 字符时填满。

换算下来,容量与 description 长度的关系大致是这样:

description 长度能容纳的 Skill 数量
263 字符(典型值)~42 个
200 字符~52 个
150 字符~60 个
130 字符~67 个

这个数字有一个关键含义:当 63 个 Skill 安装时,系统提示中出现了 <!-- Showing 42 of 63 skills due to token limits -->,有 21 个 Skill(33%)被完全隐藏,Claude 既无法发现也无法调用它们。截断是按累积总量计算的,而非单个 description 的长度——被隐藏的 Skill 和被显示的 Skill,平均 description 长度几乎完全相同,这证明顺序靠后的 Skill 会被整体丢弃。

这意味着你在 ~/.claude/skills/ 里排列目录的顺序,实际上决定了哪些 Skill 有机会被 Claude 看到。

Description 的写法:从散文到精准触发器

理解了预算机制之后,description 的写法就不再是「描述清楚就好」,而是一个需要刻意设计的字段。

一个常见的反面写法是这样的:

YAML
---
name: springboot-review
description: 这个 Skill 用于对 Spring Boot 项目进行全面的代码审查,
  包括检查 API 设计规范、异常处理方式、事务边界划分、
  安全配置以及代码风格等各个方面的问题,帮助团队保持
  代码质量和技术一致性。
---

这段 description 约 110 个汉字,换算成字符超过 110 个,且触发条件模糊。模糊的 description 会导致误触发——Claude 加载了一个并不匹配当前任务的 Skill,白白消耗上下文。description 中应该点名具体场景。

改写后的版本:

YAML
---
name: springboot-review
description: Spring Boot 代码审查:Controller/Service 分层、
  事务边界、BigDecimal 金额、敏感字段脱敏。
  用于:review 代码、检查规范、发现潜在问题。
---

这个版本做了三件事:说明了 Skill 处理的技术域(Spring Boot 分层、事务、金融字段),列出了具体的触发关键词(review、检查规范、发现潜在问题),以及把字符数压缩到 80 字符以内。

对于一个 Spring Boot 后端项目,你可能同时维护多个 Skill,每个都需要这种精简写法:

YAML
# db-migration-helper/SKILL.md
---
name: db-migration-helper
description: MyBatis Plus + Flyway 数据库迁移:
  生成 migration SQL、检查索引、处理字段变更。
  用于:添加表字段、创建索引、数据迁移任务。
---
YAML
# api-doc-generator/SKILL.md
---
name: api-doc-generator
description: 从 Spring Boot Controller 生成 OpenAPI 文档。
  用于:写接口文档、生成 Swagger、补充接口注释。
---

SKILL.md 内容本身的分层策略

description 只是冰山一角。真正决定上下文效率的,是 SKILL.md 正文的组织方式。

核心原则是:让 Skill 的正文只包含 Claude 在执行这个任务时真正需要的信息

一个常见的错误是把所有背景知识都塞进 SKILL.md,比如在一个代码生成 Skill 里附上完整的公司技术规范文档。这些内容在 Skill 被触发时会全部注入上下文,但实际上大部分内容对当前这次调用毫无意义。

更好的做法是按「参考型」和「任务型」两种内容分开设计。

参考型内容适合写轻量的原则和约束,让 Claude 把它当成背景知识:

Markdown
---
name: order-service-conventions
description: 订单服务编码约定,写 OrderService 相关代码时自动加载。
---

# 订单服务约定

金额字段统一用 BigDecimal,精度 scale=2。
状态流转顺序:PENDING → PAID → SHIPPED → COMPLETED。
订单号生成规则:`ORD-{yyyyMMdd}-{6位序号}`,由 OrderIdGenerator 统一生成。
所有数据库操作必须经过 OrderRepository,禁止在 Service 中直接调用 Mapper。

这种 Skill 全文不超过 200 字,却精准传递了新人需要一周才能摸清楚的隐性规范。

任务型内容则需要包含完整的步骤,但要避免用大段文字解释「为什么」——原因留给 CLAUDE.md,步骤才属于 Skill:

Markdown
---
name: add-api-endpoint
description: 在 Spring Boot 项目中新增 REST 接口的完整流程。
  用于:加接口、新增 API、实现新功能端点。
disable-model-invocation: true
---

# 新增 REST 接口

1.`dto/request/` 下创建请求 DTO,加 `@Valid` 注解
2.`dto/response/` 下创建响应 DTO,继承 `BaseResponse<T>`
3. 在 Controller 中添加方法,统一用 `Result<T>` 包装返回值
4. 在 Service 接口和实现类中添加业务方法
5.`src/test/` 下创建对应的单元测试
6. 更新 Swagger 注解

参考现有示例:`src/main/java/com/example/controller/AccountController.java`

注意这里使用了 disable-model-invocation: true。任务型内容适合通过 /skill-name 直接调用,而不是让 Claude 自主判断何时运行。加上 disable-model-invocation: true 可以防止 Claude 在你没有明确意图时自动触发它。

Subagent 分叉:把上下文消耗隔离到子空间

对于计算量大、会产生大量中间结果的任务,还有另一种上下文策略:把任务分叉给 Subagent 执行,让主会话保持干净。

Markdown
---
name: codebase-audit
description: 对整个代码库做架构合规性审查,扫描禁用模式和潜在风险。
context: fork
agent: Explore
---

对当前项目的 `src/` 目录执行以下检查:

1. 扫描所有 Controller,确认返回值是否统一使用 Result<T> 包装
2. 检查 @Transactional 是否只出现在 Service 层
3. 找出所有直接使用 double/float 存储金额的字段
4. 输出问题列表,格式:文件路径 + 行号 + 问题描述

context: fork 让任务在一个分叉的 Explore agent 中运行,Skill 内容成为该 agent 的任务,agent 只返回最终结论,主会话的上下文不会被大量的文件读取结果污染。代价是真实的:子 agent 对主 agent 的完整上下文不可见,无法进行整体性推理。在上下文隔离真正有价值的场景才使用它——平行探索、沙箱工具调用、或需要保持主会话干净的长任务。

环境变量与诊断

当你需要调整默认预算,或者排查某个 Skill 为什么没有触发,有几个实用的工具:

Bash
# 检查当前上下文状态,包括 Skill 加载情况
/context

# 临时扩大 Skill 字符预算(适合本地开发调试)
export SLASH_COMMAND_TOOL_CHAR_BUDGET=32000

# 查看当前会话的 token 消耗
/cost

相比把所有内容放进 CLAUDE.md 一次性加载,按需触发的 Skill 架构在实践中每次会话能节省约 15,000 token,效率提升约 82%。 这个差距在单次对话里看不出来,但对于一个每天都在运行的团队,积累下来是显著的成本和速度收益。


Token budget 并不是一个需要绕开的限制,而是迫使你把 Skill 设计得更精准的约束。description 是触发信号,不是说明书;SKILL.md 正文是执行指令,不是知识库。把握住这两点区别,你的 Skill 体系才能在数量增长的同时保持有效。