第 12 章:Skill 与扩展体系

前面几章我们学了工具(MCP)、钩子、权限、多 Agent……这些都是在 Python 代码里 定义的扩展。 但 Claude Code 还有一套 文件驱动 的扩展体系——Slash Commands、Skills、Plugins—— 用 Markdown 文件就能给 Claude 加功能,不用写一行 Python。 本章教你怎么通过 SDK 把这些扩展集成进你的项目。


1. 扩展体系概览

Claude Code 的扩展体系分四种,先看全景图:

类型 定义位置 用途 SDK 加载方式
Slash Commands .claude/commands/*.md 可重用的提示词模板,类似快捷指令 setting_sources
Skills .claude/skills/*/SKILL.md 专业知识 + 脚本,给 Claude 特定领域的能力 setting_sources
Plugins plugin-dir/.claude-plugin/ 把 Commands + Skills 打包分发 plugins
Agents 代码中 AgentDefinition.claude/agents/*.md 专业子代理,处理特定任务 agents 参数

一句话总结:Commands 是"快捷指令",Skills 是"专业技能",Plugins 是"技能包",Agents 是"专家团队"。


2. Slash Commands — 可重用的指令模板

2.1 什么是 Slash Commands?

Slash Commands 就是你在终端里用 / 开头触发的快捷指令。比如 /commit 自动生成提交信息,/review 自动审查代码。

它们的本质是 Markdown 模板 ——你把一段精心设计的提示词存成 .md 文件,放到 .claude/commands/ 目录下,Claude 就会自动识别并注册为可用命令。

2.2 创建你的第一个 Command

在项目根目录下创建 .claude/commands/review.md

---
description: 审查代码变更,找出潜在问题
allowed-tools: Bash(git diff:*), Read, Glob, Grep
argument-hint: "<文件路径或留空审查全部>"
---

## 任务

审查以下代码变更,找出:
1. 潜在的 Bug
2. 安全隐患
3. 性能问题
4. 代码风格问题

## 当前变更

!`git diff HEAD`

## 目标文件

$ARGUMENTS

## 要求

- 用中文回答
- 每个问题给出具体的改进建议
- 严重程度分级:🔴 严重 / 🟡 警告 / 🟢 建议

文件格式说明

2.3 再来一个:翻译命令

创建 .claude/commands/translate.md

---
description: 将文本翻译成指定语言
argument-hint: "<目标语言> <文本或文件路径>"
---

## 任务

将以下内容翻译成用户指定的语言。

## 翻译内容

$ARGUMENTS

## 要求

- 保持原文的格式和结构
- 技术术语保留英文,括号内标注翻译
- 代码块不翻译
- 如果输入是文件路径,先读取文件内容再翻译

2.4 在 SDK 中加载 Commands

关键配置:setting_sources。它告诉 SDK 从哪里加载扩展:

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, SystemMessage

options = ClaudeAgentOptions(
    # 从项目目录加载 commands 和 skills
    setting_sources=["project"],
    # 工作目录——SDK 会在这里找 .claude/ 目录
    cwd="/path/to/your-project",
)
来源 加载路径 说明
"user" ~/.claude/ 你的全局设置,所有项目共享
"project" 项目目录/.claude/ 项目级设置,团队共享
"local" 项目目录/.claude-local/ 本地设置,不提交到 Git

可以组合使用:

# 同时加载全局和项目设置
setting_sources=["user", "project"]

# 加载所有来源
setting_sources=["user", "project", "local"]

# 不设置 = 不加载任何预定义命令/技能(默认行为)
setting_sources=None

2.5 检测已加载的 Commands

SDK 启动时会通过 SystemMessage 告诉你加载了哪些命令:

import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, SystemMessage

async def list_commands():
    options = ClaudeAgentOptions(
        setting_sources=["project"],
        cwd="/path/to/your-project",
    )

    async with ClaudeSDKClient(options) as client:
        # 连接时会收到 init 消息
        async for msg in client.receive_response():
            if isinstance(msg, SystemMessage) and msg.subtype == "init":
                # 从 init 数据中提取已加载的命令
                commands = msg.data.get("slash_commands", [])
                print(f"已加载 {len(commands)} 个命令:")
                for cmd in commands:
                    print(f"  /{cmd}")
                break

asyncio.run(list_commands())

3. Skills — 给 Claude 专业技能

3.1 什么是 Skills?

如果说 Commands 是"快捷指令",那 Skills 就是"专业知识库"。

Skills 让 Claude 拥有特定领域的专业能力。比如一个"代码分析"Skill 可以包含: - 代码分析的方法论和最佳实践 - 辅助脚本(Python/Bash) - 参考文档

Skills 存放在 .claude/skills/ 目录下,每个 Skill 一个子目录。

3.2 Skill 的目录结构

.claude/skills/
└── code-analyzer/
    ├── SKILL.md          # 技能描述文件(必需)
    └── scripts/          # 辅助脚本(可选)
        └── analyze.py

3.3 创建一个代码分析 Skill

创建 .claude/skills/code-analyzer/SKILL.md

---
name: code-analyzer
description: "分析 Python 代码质量,给出优化建议"
---

# 代码分析技能

## 激活触发器

当对话中提到以下关键词时,自动使用这个技能:
- "分析代码"、"代码质量"
- "代码审查"、"code review"
- "优化建议"、"重构"

## 分析维度

1. **代码复杂度**
   - 函数长度(超过 50 行标记为过长)
   - 嵌套深度(超过 3 层标记为过深)
   - 圈复杂度(超过 10 标记为过高)

2. **命名规范**
   - 变量名是否有意义(避免 a, b, tmp)
   - 函数名是否表达行为(动词开头)
   - 类名是否表达概念(名词)

3. **错误处理**
   - 是否有裸 except
   - 是否忽略了异常
   - 关键操作是否有 try/except

4. **性能**
   - 是否在循环中做了不必要的操作
   - 是否可以用生成器替代列表
   - 是否有 N+1 查询问题

## 输出格式

分析完成后,按以下格式输出:

### 代码质量评分:X/10

#### 发现的问题

| 严重程度 | 位置 | 问题描述 | 建议 |
|---------|------|---------|------|

#### 亮点

列出代码中做得好的地方。

3.4 Skill vs Command 的区别

对比项 Slash Command Skill
触发方式 用户手动输入 /命令名 Claude 自动识别并激活
本质 提示词模板 领域知识库
参数 支持 $ARGUMENTS 无参数,靠上下文触发
适合场景 重复性操作(提交、审查) 专业领域知识(分析、诊断)

4. Plugins — 打包与分发

4.1 什么是 Plugin?

当你积累了一堆好用的 Commands 和 Skills,想分享给团队或其他项目用?Plugin 就是 Commands + Skills 的打包格式。

4.2 Plugin 的目录结构

my-plugin/
├── .claude-plugin/
│   └── plugin.json         # 插件元信息(必需)
├── commands/               # 斜杠命令(可选)
│   ├── lint.md
│   └── format.md
├── skills/                 # 技能(可选)
│   └── best-practices/
│       └── SKILL.md
└── agents/                 # 代理(可选)
    └── reviewer.md

4.3 创建 plugin.json

{
    "name": "code-quality",
    "description": "代码质量工具包:lint、format、最佳实践",
    "version": "1.0.0",
    "author": {
        "name": "Your Name"
    }
}

4.4 在 SDK 中加载插件

通过 plugins 配置加载:

from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions

options = ClaudeAgentOptions(
    plugins=[
        {
            "type": "local",
            "path": str(Path(__file__).parent / "my-plugin"),
        }
    ],
)

可以同时加载多个插件:

options = ClaudeAgentOptions(
    plugins=[
        {"type": "local", "path": "/path/to/code-quality-plugin"},
        {"type": "local", "path": "/path/to/doc-helper-plugin"},
    ],
    # 同时加载项目自己的 commands 和 skills
    setting_sources=["project"],
)

4.5 检测已加载的插件

async for msg in client.receive_response():
    if isinstance(msg, SystemMessage) and msg.subtype == "init":
        plugins = msg.data.get("plugins", [])
        for p in plugins:
            print(f"  插件: {p.get('name')} (路径: {p.get('path')})")

5. 实战:为 MiniClaw 集成 Skill 系统

我们来给前几章构建的 MiniClaw 项目加上 Skill 扩展能力。

5.1 创建项目目录结构

在 MiniClaw 项目中创建:

miniclaw/
├── .claude/
│   ├── commands/
│   │   ├── review.md           # /review 命令
│   │   └── explain.md          # /explain 命令
│   └── skills/
│       └── python-expert/
│           └── SKILL.md        # Python 专家技能
├── engine.py
├── cli.py
└── ...

5.2 修改引擎配置

engine.py 中启用 setting_sources:

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient

class ChatEngine:
    def __init__(self, project_dir: str = "."):
        self.options = ClaudeAgentOptions(
            # 加载项目中定义的 commands 和 skills
            setting_sources=["project"],
            # 设置工作目录为项目根目录
            cwd=project_dir,
            # 其他配置...
            max_turns=10,
        )

5.3 在 CLI 中展示可用命令

async def show_available_commands(client: ClaudeSDKClient):
    """显示所有可用的斜杠命令(包括自定义的)"""
    info = await client.get_server_info()
    if info:
        commands = info.get("slash_commands", [])
        if commands:
            print("可用的斜杠命令:")
            for cmd in commands:
                print(f"  /{cmd}")

5.4 完整集成示例

import asyncio
from pathlib import Path

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ClaudeSDKClient,
    ResultMessage,
    SystemMessage,
    TextBlock,
)


async def main():
    project_dir = Path(__file__).parent

    options = ClaudeAgentOptions(
        setting_sources=["project"],
        cwd=str(project_dir),
        max_turns=10,
    )

    print("MiniClaw + Skill 系统")
    print("=" * 50)

    async with ClaudeSDKClient(options) as client:
        # 打印已加载的扩展信息
        async for msg in client.receive_response():
            if isinstance(msg, SystemMessage) and msg.subtype == "init":
                commands = msg.data.get("slash_commands", [])
                if commands:
                    print(f"\n已加载 {len(commands)} 个斜杠命令:")
                    for cmd in commands:
                        print(f"  /{cmd}")
                break

        # 交互式聊天
        while True:
            user_input = input("\n你: ").strip()
            if user_input in ("/quit", "/exit", ""):
                print("再见!")
                break

            await client.query(user_input)

            print("Claude: ", end="", flush=True)
            async for msg in client.receive_response():
                if isinstance(msg, AssistantMessage):
                    for block in msg.content:
                        if isinstance(block, TextBlock):
                            print(block.text, end="", flush=True)
                elif isinstance(msg, ResultMessage):
                    print()
                    print(f"  [{msg.duration_ms}ms | ${msg.total_cost_usd or 0:.4f}]")


asyncio.run(main())

6. 最佳实践

Commands 设计原则

  1. 单一职责:每个命令只做一件事
  2. 参数灵活:用 $ARGUMENTS 支持可选参数
  3. 动态上下文:用 !`command` 注入当前状态
  4. 限制工具:用 allowed-tools 约束命令的能力范围

Skills 设计原则

  1. 明确的触发条件:在 SKILL.md 中列出激活关键词
  2. 结构化知识:用 Markdown 标题和列表组织知识
  3. 带示例:给 Claude 看具体的分析示例
  4. 附带脚本:复杂操作用 Python 脚本辅助

setting_sources 使用建议

场景 推荐配置
个人学习 ["user"]
团队项目 ["user", "project"]
CI/CD ["project"](只加载项目级配置)
完全隔离 None(默认,不加载任何预定义扩展)

本章完整示例代码

以下是本章涉及的完整示例文件,可以直接复制运行。

skill_loader.py — 加载并展示可用扩展

import asyncio
from pathlib import Path

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ClaudeSDKClient,
    ResultMessage,
    SystemMessage,
    TextBlock,
)


async def main():
    """演示如何通过 setting_sources 加载并展示可用的 Commands 和 Skills。"""

    # 指向包含 .claude/ 目录的示例项目
    sample_project = Path(__file__).parent / "sample-project"

    options = ClaudeAgentOptions(
        # 加载项目级设置(commands + skills)
        setting_sources=["project"],
        # 工作目录指向示例项目
        cwd=str(sample_project),
        max_turns=1,
    )

    print("=" * 50)
    print("Skill 系统加载器")
    print("=" * 50)

    async with ClaudeSDKClient(options) as client:
        # 读取初始化消息,获取已加载的扩展信息
        async for msg in client.receive_response():
            if isinstance(msg, SystemMessage):
                if msg.subtype == "init":
                    print(f"\n初始化完成!")

                    # 打印已加载的斜杠命令
                    commands = msg.data.get("slash_commands", [])
                    print(f"\n斜杠命令 ({len(commands)} 个):")
                    if commands:
                        for cmd in commands:
                            print(f"  /{cmd}")
                    else:
                        print("  (无)")

                    # 打印已加载的插件
                    plugins = msg.data.get("plugins", [])
                    print(f"\n插件 ({len(plugins)} 个):")
                    if plugins:
                        for p in plugins:
                            print(f"  - {p.get('name', '未知')}")
                    else:
                        print("  (无)")

                    break

        # 测试:让 Claude 使用已加载的技能
        print("\n" + "-" * 50)
        print("测试:让 Claude 分析一段代码")
        print("-" * 50)

        await client.query("请分析这段代码的质量:\n\ndef f(x):\n    if x>0:\n        if x>10:\n            if x>100:\n                return 'big'\n            return 'medium'\n        return 'small'\n    return 'negative'\n")

        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(f"\nClaude: {block.text}")
            elif isinstance(msg, ResultMessage):
                print(f"\n[耗时: {msg.duration_ms}ms]")


if __name__ == "__main__":
    asyncio.run(main())

运行方式:

python examples/skill_loader.py

plugin_demo.py — 创建和加载插件

import asyncio
import json
import tempfile
from pathlib import Path

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ClaudeSDKClient,
    ResultMessage,
    SystemMessage,
    TextBlock,
)


def create_demo_plugin(plugin_dir: Path) -> None:
    """在指定目录创建一个演示插件。"""
    # 1. 创建插件元信息
    plugin_meta_dir = plugin_dir / ".claude-plugin"
    plugin_meta_dir.mkdir(parents=True, exist_ok=True)

    plugin_json = {
        "name": "demo-helper",
        "description": "演示插件:包含一个 /greet 命令",
        "version": "1.0.0",
        "author": {"name": "Tutorial"},
    }
    (plugin_meta_dir / "plugin.json").write_text(
        json.dumps(plugin_json, indent=2, ensure_ascii=False),
        encoding="utf-8",
    )

    # 2. 创建一个斜杠命令
    commands_dir = plugin_dir / "commands"
    commands_dir.mkdir(exist_ok=True)

    greet_md = """\
---
description: 用指定的风格打招呼
argument-hint: "<风格:正式/搞笑/诗意>"
---

## 任务

请用 $ARGUMENTS 的风格,和用户打个招呼。

如果没有指定风格,就用友好的方式打招呼。

要求用中文回答。
"""
    (commands_dir / "greet.md").write_text(greet_md, encoding="utf-8")

    print(f"已创建演示插件: {plugin_dir}")
    print(f"  - 插件名: {plugin_json['name']}")
    print(f"  - 命令: /greet")


async def main():
    """演示如何创建和加载一个本地插件。"""

    print("=" * 50)
    print("Plugin 插件演示")
    print("=" * 50)

    # 在临时目录创建插件
    with tempfile.TemporaryDirectory() as tmpdir:
        plugin_dir = Path(tmpdir) / "demo-helper"
        create_demo_plugin(plugin_dir)

        # 通过 plugins 配置加载
        options = ClaudeAgentOptions(
            plugins=[
                {
                    "type": "local",
                    "path": str(plugin_dir),
                }
            ],
            max_turns=1,
        )

        print()

        async with ClaudeSDKClient(options) as client:
            # 检查初始化信息
            async for msg in client.receive_response():
                if isinstance(msg, SystemMessage) and msg.subtype == "init":
                    plugins = msg.data.get("plugins", [])
                    commands = msg.data.get("slash_commands", [])
                    print(f"已加载 {len(plugins)} 个插件, {len(commands)} 个命令")
                    break

            # 使用插件提供的 /greet 命令
            print("\n调用 /greet 命令(搞笑风格)...")
            await client.query("/greet 搞笑")

            async for msg in client.receive_response():
                if isinstance(msg, AssistantMessage):
                    for block in msg.content:
                        if isinstance(block, TextBlock):
                            print(f"\nClaude: {block.text}")
                elif isinstance(msg, ResultMessage):
                    print(f"\n[耗时: {msg.duration_ms}ms]")


if __name__ == "__main__":
    asyncio.run(main())

运行方式:

python examples/plugin_demo.py

小结

本章我们学习了 Claude Code 的文件驱动扩展体系:

和前面章节学到的代码扩展(MCP 工具、钩子、AgentDefinition)的关系

扩展方式 定义方式 适合场景
MCP 工具(第 5 章) Python 代码 需要执行逻辑、访问数据
钩子(第 6 章) Python 代码 拦截、审计、修改行为
AgentDefinition(第 11 章) Python 代码 专业子代理
Slash Commands Markdown 文件 可重用提示词
Skills Markdown + 脚本 领域知识
Plugins 目录打包 跨项目分享

它们不是互斥的,而是互补的。 一个完整的项目通常会同时使用代码扩展和文件扩展:代码扩展处理"做什么",文件扩展处理"怎么想"。


完整代码

以下是本章所有示例文件的完整可运行代码。

examples/skill_loader.py

"""Skill 系统加载器 — 展示如何加载并查看可用的 Commands 和 Skills。

通过 setting_sources 配置加载项目中定义的扩展,
然后打印可用的斜杠命令和插件信息。

运行方式:
    python examples/skill_loader.py
"""

import asyncio
from pathlib import Path

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ClaudeSDKClient,
    ResultMessage,
    SystemMessage,
    TextBlock,
)


async def main():
    """演示如何通过 setting_sources 加载并展示可用的 Commands 和 Skills。"""

    # 指向包含 .claude/ 目录的示例项目
    sample_project = Path(__file__).parent / "sample-project"

    options = ClaudeAgentOptions(
        # 加载项目级设置(commands + skills)
        setting_sources=["project"],
        # 工作目录指向示例项目
        cwd=str(sample_project),
        max_turns=1,
    )

    print("=" * 50)
    print("Skill 系统加载器")
    print("=" * 50)

    async with ClaudeSDKClient(options) as client:
        # 读取初始化消息,获取已加载的扩展信息
        async for msg in client.receive_response():
            if isinstance(msg, SystemMessage):
                if msg.subtype == "init":
                    print(f"\n初始化完成!")

                    # 打印已加载的斜杠命令
                    commands = msg.data.get("slash_commands", [])
                    print(f"\n斜杠命令 ({len(commands)} 个):")
                    if commands:
                        for cmd in commands:
                            print(f"  /{cmd}")
                    else:
                        print("  (无)")

                    # 打印已加载的插件
                    plugins = msg.data.get("plugins", [])
                    print(f"\n插件 ({len(plugins)} 个):")
                    if plugins:
                        for p in plugins:
                            print(f"  - {p.get('name', '未知')}")
                    else:
                        print("  (无)")

                    break

        # 测试:让 Claude 使用已加载的技能
        print("\n" + "-" * 50)
        print("测试:让 Claude 分析一段代码")
        print("-" * 50)

        await client.query(
            "请分析这段代码的质量:\n\n"
            "def f(x):\n"
            "    if x>0:\n"
            "        if x>10:\n"
            "            if x>100:\n"
            "                return 'big'\n"
            "            return 'medium'\n"
            "        return 'small'\n"
            "    return 'negative'\n"
        )

        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(f"\nClaude: {block.text}")
            elif isinstance(msg, ResultMessage):
                print(f"\n[耗时: {msg.duration_ms}ms]")


if __name__ == "__main__":
    asyncio.run(main())

examples/plugin_demo.py

"""Plugin 插件演示 — 创建和加载一个本地插件。

在临时目录中动态创建一个包含 /greet 命令的插件,
然后通过 SDK 的 plugins 配置加载并使用它。

运行方式:
    python examples/plugin_demo.py
"""

import asyncio
import json
import tempfile
from pathlib import Path

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ClaudeSDKClient,
    ResultMessage,
    SystemMessage,
    TextBlock,
)


def create_demo_plugin(plugin_dir: Path) -> None:
    """在指定目录创建一个演示插件。"""
    # 1. 创建插件元信息
    plugin_meta_dir = plugin_dir / ".claude-plugin"
    plugin_meta_dir.mkdir(parents=True, exist_ok=True)

    plugin_json = {
        "name": "demo-helper",
        "description": "演示插件:包含一个 /greet 命令",
        "version": "1.0.0",
        "author": {"name": "Tutorial"},
    }
    (plugin_meta_dir / "plugin.json").write_text(
        json.dumps(plugin_json, indent=2, ensure_ascii=False),
        encoding="utf-8",
    )

    # 2. 创建一个斜杠命令
    commands_dir = plugin_dir / "commands"
    commands_dir.mkdir(exist_ok=True)

    greet_md = """\
---
description: 用指定的风格打招呼
argument-hint: "<风格:正式/搞笑/诗意>"
---

## 任务

请用 $ARGUMENTS 的风格,和用户打个招呼。

如果没有指定风格,就用友好的方式打招呼。

要求用中文回答。
"""
    (commands_dir / "greet.md").write_text(greet_md, encoding="utf-8")

    print(f"已创建演示插件: {plugin_dir}")
    print(f"  - 插件名: {plugin_json['name']}")
    print(f"  - 命令: /greet")


async def main():
    """演示如何创建和加载一个本地插件。"""

    print("=" * 50)
    print("Plugin 插件演示")
    print("=" * 50)

    # 在临时目录创建插件
    with tempfile.TemporaryDirectory() as tmpdir:
        plugin_dir = Path(tmpdir) / "demo-helper"
        create_demo_plugin(plugin_dir)

        # 通过 plugins 配置加载
        options = ClaudeAgentOptions(
            plugins=[
                {
                    "type": "local",
                    "path": str(plugin_dir),
                }
            ],
            max_turns=1,
        )

        print()

        async with ClaudeSDKClient(options) as client:
            # 检查初始化信息
            async for msg in client.receive_response():
                if isinstance(msg, SystemMessage) and msg.subtype == "init":
                    plugins = msg.data.get("plugins", [])
                    commands = msg.data.get("slash_commands", [])
                    print(f"已加载 {len(plugins)} 个插件, {len(commands)} 个命令")
                    break

            # 使用插件提供的 /greet 命令
            print("\n调用 /greet 命令(搞笑风格)...")
            await client.query("/greet 搞笑")

            async for msg in client.receive_response():
                if isinstance(msg, AssistantMessage):
                    for block in msg.content:
                        if isinstance(block, TextBlock):
                            print(f"\nClaude: {block.text}")
                elif isinstance(msg, ResultMessage):
                    print(f"\n[耗时: {msg.duration_ms}ms]")


if __name__ == "__main__":
    asyncio.run(main())