Claude Code Agent Skills 完全指南(下):实战与进阶 ------ 从零实现一个完整的 Skill 系统
摘要
上篇我们理解了 Skills 的原理与规范 ,本篇将动手实现------用核心代码实现一个完整的 SkillsManager,让你在自己的 Agent 中也能使用 Skills 机制。
引言
上篇回顾
Skills 通过渐进式披露解决上下文窗口浪费:
- 启动时只加载 name + description(~100 tokens)
- 激活时才加载完整 SKILL.md(~3000 tokens)
- 详细内容按需加载(references/)
本篇目标
从零实现一个完整的 Skill 系统,理解 Skills 的工作原理。
你将学会:
- SkillsManager 的核心实现(~200 行)
- 如何将 Skills 集成到 Agent 中(~150 行)
- 完整的代码结构和运行效果
一、工作原理总览
在写代码之前,先理解 Skills 系统由哪些组件组成,以及它们如何协作。
1.0 本质理解:Skill 就是"动态内容注入"的工具调用
很多人会问:Skill 和普通工具调用有什么区别?
答案是:本质上没有区别。从代码角度看,Skill 只是一个做了额外封装的工具:
python
# 普通工具:固定内容
{
"name": "get_weather",
"description": "获取天气信息",
"parameters": {...}
}
# 调用后返回:天气数据
# Skill 工具:动态内容
{
"name": "Skill",
"description": "调用 Skill...\n- code-reviewer: 审查代码...\n- pdf-analyzer: 分析 PDF...",
"parameters": {"skill": "string", "args": "string"}
}
# 调用后返回:对应 SKILL.md 的完整内容
核心区别只有一个:
| 普通工具 | Skill 工具 |
|---|---|
| 调用时返回固定内容 | 调用时返回动态内容(根据 skill_name 加载不同 SKILL.md) |
为什么这样做?
为了管理 LLM 的上下文:
ini
┌─────────────────────────────────────────────────────────┐
│ 如果把所有 Skill 内容写进系统提示词: │
│ 20 个 Skill × 3000 tokens = 60,000 tokens ❌ 爆上下文 │
├─────────────────────────────────────────────────────────┤
│ 用 Skill 工具动态加载: │
│ 启动时:20 × 100 tokens = 2,000 tokens ✅ │
│ 激活时:只加载用户需要的那个(~3000 tokens) │
└─────────────────────────────────────────────────────────┘
一句话总结:
Skill 就是"按需加载提示词"的工具调用,本质是用代码管理 LLM 上下文。
1.1 三个核心组件
scss
┌─────────────────────────────────────────────────────────────┐
│ Skills 系统 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ SKILL.md │ │ SkillsManager │ │ Agent │ │
│ │ (定义层) │ │ (管理层) │ │ (执行层) │ │
│ └─────────────┘ └──────────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ ① 扫描解析 │ │ │
│ └──────────────────►│ │ │
│ │ │ │
│ │ ② 提供元工具 Schema │ │
│ ├────────────────────►│ │
│ │ │ │
│ │ ③ 加载完整内容 │ │
│ │◄────────────────────┤ │
│ │ │ │
│ │ ④ 返回 Skill 提示词 │ │
│ ├────────────────────►│ │
│ │ │ │
│ │ │ ⑤ 注入对话│
│ │ │ ↓ │
│ │ │ LLM 执行 │
│ │
└─────────────────────────────────────────────────────────────┘
| 组件 | 职责 | 对应代码 |
|---|---|---|
| SKILL.md | 定义 Skill 的元数据和指令 | Markdown 文件 |
| SkillsManager | 扫描、解析、加载 Skills | skills_manager.py |
| Agent | 调用 LLM、处理 Skill 激活、注入提示词 | agent.py |
1.2 完整消息流程
当用户说"帮我审查代码"时,发生了什么?
scss
用户 Agent SkillsManager LLM
│ │ │ │
│ "帮我审查代码" │ │ │
│───────────────────────►│ │ │
│ │ │ │
│ │ ① 发送请求(含 Skill 工具描述) │
│ │─────────────────────────────────────────────────►│
│ │ │ │
│ │ ② LLM 决定调用 Skill("code-reviewer") │
│ │◄────────────────────────────────────────────────│
│ │ │ │
│ │ ③ generate_skill_messages("code-reviewer") │
│ │────────────────────────►│ │
│ │ │ │
│ │ ④ 返回 (visible_msg, skill_prompt) │
│ │◄────────────────────────│ │
│ │ │ │
│ │ ⑤ 向用户显示 visible_msg │
│◄───────────────────────│ │ │
│ "code-reviewer 正在加载" │ │
│ │ │ │
│ │ ⑥ 发送 tool_result(含 skill_prompt) │
│ │─────────────────────────────────────────────────►│
│ │ │ │
│ │ ⑦ LLM 根据 Skill 指令执行代码审查 │
│ │◄────────────────────────────────────────────────│
│ │ │ │
│ ⑧ 返回审查结果 │ │ │
│◄───────────────────────│ │ │
│ │ │ │
关键点:
- LLM 选择 Skill:不是算法匹配,而是 LLM 根据用户意图和 Skill 描述推理决定
- 两消息模式:一条给用户看(状态通知),一条给 LLM 看(执行指令)
- 提示词注入:Skill 内容通过 tool_result 注入到对话中
1.3 渐进式披露的三层
objectivec
┌─────────────────────────────────────────────────────────────┐
│ 第一层:元数据(启动时加载) │
│ ───────────────────────────── │
│ • 内容:name + description │
│ • 大小:~100 tokens / Skill │
│ • 用途:LLM 据此选择要激活哪个 Skill │
│ │
│ SkillsManager.get_skill_tool_schema() │
│ → 生成 Skill 元工具的 JSON Schema │
│ → 放入 API 请求的 tools 数组 │
├─────────────────────────────────────────────────────────────┤
│ 第二层:完整指令(激活时加载) │
│ ───────────────────────────── │
│ • 内容:完整 SKILL.md 文件 │
│ • 大小:~500-5000 tokens │
│ • 用途:告诉 LLM 如何执行任务 │
│ │
│ SkillsManager.generate_skill_messages() │
│ → 读取 SKILL.md 文件 │
│ → 作为 tool_result 返回给 LLM │
├─────────────────────────────────────────────────────────────┤
│ 第三层:资源(按需加载) │
│ ───────────────────────────── │
│ • 内容:references/、scripts/、assets/ │
│ • 大小:不定 │
│ • 用途:详细的参考资料、检查清单等 │
│ │
│ SKILL.md 中写:参见 [references/checklist.md] │
│ → LLM 需要时会调用 Read 工具自行读取 │
└─────────────────────────────────────────────────────────────┘
第三层的本质区别:
| 层级 | 谁决定加载? | 何时加载? |
|---|---|---|
| 第一层(元数据) | Agent 代码 | 启动时 |
| 第二层(完整指令) | Agent 代码 | Skill 被激活时 |
| 第三层(资源) | LLM 自己 | LLM 认为需要时 |
第三层的工作流程:
objectivec
┌─────────────────────────────────────────────────────────┐
│ 用户: 帮我审查这段复杂的微服务代码 │
├─────────────────────────────────────────────────────────┤
│ 1. LLM 激活 Skill,收到 SKILL.md │
│ → 看到:"对于详细审查,请按需读取: │
│ - 完整检查清单: references/checklist.md" │
├─────────────────────────────────────────────────────────┤
│ 2. LLM 自主判断:代码复杂,需要详细清单 │
│ → 决定调用 Read("references/checklist.md") │
├─────────────────────────────────────────────────────────┤
│ 3. Agent 的 Read 工具返回 checklist 内容 │
│ → LLM 按照详细清单执行审查 │
└─────────────────────────────────────────────────────────┘
对比:如果用户只是"帮我看看这个简单函数"
→ LLM 判断不需要详细清单
→ 不调用 Read,直接用 SKILL.md 中的精简版检查
这意味着你的 Agent 需要额外提供 Read 工具 ,LLM 才能读取 references/ 中的内容:
python
# Agent 需要提供 Read 工具
tools: [
skill_schema, # Skill 元工具
read_tool, # ← 读文件工具(第三层需要)
write_tool,
bash_tool
]
为什么这样设计?
假设你有 20 个 Skills:
- 一次性加载:20 × 3000 = 60,000 tokens(直接爆上下文)
- 渐进式披露:20 × 100 = 2,000 tokens(节省 97%)
二、深度案例:code-reviewer Skill
理解原理后,来看一个完整的 Skill 案例。
2.1 目录结构
bash
skills/
└── code-reviewer/
├── SKILL.md # 核心指令(~50 行)
└── references/
└── checklist.md # 详细检查清单(按需加载)
2.2 SKILL.md 完整内容
yaml
---
name: code-reviewer
description: 审查代码的质量、安全性和最佳实践。当用户要求审查代码、检查 bug、建议改进或审计安全时使用。
allowed-tools: "Read Grep Glob"
---
# 代码审查器
## 审查检查清单
复制此清单并跟踪进度:
~~~
代码审查进度:
- [ ] 检查代码结构和组织
- [ ] 识别潜在的 bug 或逻辑错误
- [ ] 审查安全漏洞
- [ ] 提出性能优化建议
- [ ] 验证错误处理
- [ ] 检查命名规范
~~~
## 审查流程
1. **阅读代码** - 理解目的和上下文
2. **识别问题** - 使用上面的检查清单
3. **优先级排序** - 严重 > 重要 > 轻微 > 建议
4. **提供修复** - 展示修正后的代码片段
## 输出格式
~~~
## 审查摘要
**严重问题:** X
**重要问题:** X
**轻微问题:** X
### 严重
- [第X行] 问题描述
# 当前代码
# 建议修复
~~~
## 安全关注点
- SQL 注入(查询中的用户输入)
- XSS(未转义的输出)
- 硬编码凭证
- 路径遍历
- 命令注入
## 高级资源
对于详细的代码审查,请按需读取:
- **完整检查清单**: 参见 [references/checklist.md](references/checklist.md)
2.3 设计要点
| 设计决策 | 原因 |
|---|---|
| SKILL.md 保持简洁(~50行) | 核心指令每次都需要,避免占用过多上下文 |
| 检查清单放在 references/ | 详细内容按需加载,不用的不占空间 |
| 使用"复制此清单"指令 | 模拟 for 循环,让 Agent 逐步执行 |
| allowed-tools 限制为只读 | 代码审查不应该修改文件 |
三、SkillsManager 核心实现
SkillsManager 的核心职责是三层渐进式披露的实现。
3.1 两个数据结构
python
@dataclass
class SkillMetadata:
"""第一层:元数据(启动时加载,~100 tokens)"""
name: str # Skill 名称
description: str # 描述(LLM 选择的依据)
path: Path # SKILL.md 路径
@dataclass
class SkillContent:
"""第二层:完整内容(激活时加载,~500-5000 tokens)"""
metadata: SkillMetadata
full_content: str # 完整 SKILL.md
3.2 三个核心方法
python
# config.py - 客户端配置
from openai import OpenAI
import os
MODEL = "qwen-plus" # 或 "deepseek-chat", "gpt-4o" 等
BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1" # 各厂商的兼容端点
def create_client() -> OpenAI:
return OpenAI(api_key=os.environ.get("DASHSCOPE_API_KEY"), base_url=BASE_URL)
python
class SkillsManager:
def __init__(self, skills_dir: str = "./skills"):
self.skills: dict[str, SkillMetadata] = {}
# 方法1:启动时扫描(第一层)
def discover_skills(self):
"""扫描 skills 目录,只解析 frontmatter"""
for skill_folder in Path(self.skills_dir).iterdir():
skill_md = skill_folder / "SKILL.md"
if skill_md.exists():
# 解析 YAML frontmatter,提取 name 和 description
# _parse_metadata() 实现:读取文件 → 找到 --- 之间的内容 → yaml.safe_load()
metadata = self._parse_metadata(skill_md)
self.skills[metadata.name] = metadata
# 方法2:生成元工具 Schema(LLM 选择 Skill 的依据)
def get_skill_tool_schema(self) -> dict:
"""生成 Skill 元工具的 JSON Schema"""
skill_list = "\n".join([
f"- **{name}**: {meta.description}"
for name, meta in self.skills.items()
])
return {
"type": "function",
"function": {
"name": "Skill",
"description": f"调用 Skill 以扩展能力。\n\n可用的 Skills:\n{skill_list}",
"parameters": {
"type": "object",
"properties": {
"skill": {"type": "string", "enum": list(self.skills.keys())},
"args": {"type": "string"}
},
"required": ["skill"]
}
}
}
# 方法3:激活时加载(第二层 + 两消息模式)
def generate_skill_messages(self, skill_name: str, args: str = "") -> tuple[str, str]:
"""生成两条消息"""
meta = self.skills[skill_name]
# 消息1:用户可见
visible_msg = f'<command-message>"{skill_name}" 正在加载</command-message>'
# 消息2:LLM 可见(完整 SKILL.md)
skill_prompt = meta.path.read_text(encoding='utf-8')
return visible_msg, skill_prompt
核心要点:
discover_skills()只解析 frontmatter,不读取完整文件get_skill_tool_schema()的 description 是 LLM 选择 Skill 的唯一依据generate_skill_messages()返回两条消息,分别给用户和 LLM 看
3.3 LLM 如何选择 Skill?
当调用 chat.completions.create() 时,tools 参数会发送给 LLM:
python
# LLM 实际收到的 tools 参数
tools: [{
"name": "Skill",
"description": "调用 Skill 以扩展能力。\n\n可用的 Skills:\n- **code-reviewer**: 审查代码质量、安全...\n- **pdf-analyzer**: 分析 PDF 文档...",
"parameters": {"skill": {"enum": ["code-reviewer", "pdf-analyzer"]}}
}]
LLM 会根据:
- 用户的输入("帮我审查代码")
- tools 中的 description("- code-reviewer: 审查代码...")
自行推理决定调用哪个 Skill。
💡 你不需要写任何匹配算法,只需要写好 description。LLM 的理解能力会自动完成"意图 → Skill"的映射。
四、Agent 集成
💡 说明:本文示例使用 OpenAI 兼容接口风格,可对接阿里千问、DeepSeek 等模型。
4.1 核心流程
Agent 的核心是处理 LLM 决定调用 Skill 的情况:
python
class SkillEnabledAgent:
def __init__(self):
# Phase 1: 加载 Skills 元数据
self.skills_manager = SkillsManager("./skills")
self.skills_manager.discover_skills()
self.client = create_client()
self.messages = []
def chat(self, user_message: str):
self.messages.append({"role": "user", "content": user_message})
# 调用 LLM,传入 Skill 元工具
response = self.client.chat.completions.create(
model=MODEL,
messages=self.messages,
tools=[self.skills_manager.get_skill_tool_schema()],
tool_choice="auto"
)
return self._handle_response(response)
def _handle_response(self, response):
message = response.choices[0].message
# LLM 决定调用 Skill
if message.tool_calls:
# ⚠️ 关键:先添加 assistant 消息(包含 tool_calls)
# 这是 OpenAI Tool Calling 的规范要求
self.messages.append({
"role": "assistant",
"content": message.content,
"tool_calls": [{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
} for tc in message.tool_calls]
})
for tool_call in message.tool_calls:
if tool_call.function.name == "Skill":
return self._activate_skill(tool_call)
return message.content
def _activate_skill(self, tool_call):
"""两消息模式的核心"""
args = json.loads(tool_call.function.arguments)
skill_name = args["skill"]
# 生成两消息
visible_msg, skill_prompt = self.skills_manager.generate_skill_messages(skill_name)
# 消息1:向用户显示(通过 stdout)
print(visible_msg)
# 消息2:作为 tool_result 发送给 LLM
# ⚠️ 关键:tool 角色的消息会被 LLM 看到
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": f"Skill 已加载。请按以下指令执行:\n\n{skill_prompt}"
})
# 继续调用 LLM(此时 LLM 已获得 Skill 指令)
# 传入空字符串是因为不需要新的用户输入
# 循环会在 LLM 返回文本响应(而非 tool_calls)时自动结束
return self.chat("")
4.2 运行效果
text
用户: 帮我审查这段代码: def get_user(id):
query = f"SELECT * FROM users WHERE id = {id}"
return db.execute(query)
<command-message>"code-reviewer" 正在加载</command-message>
助手: ## 审查摘要
**严重问题:** 1
### 严重
- [第2行] 🔴 SQL 注入漏洞
# 修复后: cursor.execute("SELECT * FROM users WHERE id = ?", (id,))
五、思考:从"自己写好代码"到"让 AI 写好代码"
在结束之前,我想分享一个最近的感悟。
5.1 一个内置 Skill 带来的启发
Claude Code 最新版内置了一个 simplify Skill,用于代码审查和清理。它的设计非常精妙:
yaml
┌─────────────────────────────────────────────────────────┐
│ Simplify Skill │
├─────────────────────────────────────────────────────────┤
│ Phase 1: git diff → 获取变更内容 │
├─────────────────────────────────────────────────────────┤
│ Phase 2: 并行启动 3 个 Agent(同时进行) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Agent 1 │ │ Agent 2 │ │ Agent 3 │ │
│ │ 代码复用 │ │ 代码质量 │ │ 效率审查 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Phase 3: 汇总发现 → 直接修复 │
└─────────────────────────────────────────────────────────┘
三个维度覆盖了代码审查的核心关注点:
| 维度 | 核心问题 | 典型检查项 |
|---|---|---|
| 代码复用 | 是否重复造轮子? | 现有工具函数、重复功能、内联逻辑 |
| 代码质量 | 是否有反模式? | 冗余状态、参数膨胀、复制粘贴变体、抽象泄露 |
| 效率审查 | 性能是否达标? | 不必要的工作、错失并发、热路径膨胀、内存泄漏 |
5.2 一个感悟
看完 simplify Skill 的设计,我突然意识到:
过去我用 AI 写代码时,经常要反复拉扯。
makefile
我: 帮我重构这段代码
AI: 好的,重构完成
我: 这里有个重复的函数,应该用现有的工具类
AI: 好的,我改一下
我: 这里的状态管理有问题
AI: 好的,再改
我: 这里性能不太好,应该加缓存
AI: 好的...
(重复 N 次)
然后我看到 simplify Skill 把这些维度都写进去了:
- 代码复用:是否重复造轮子?
- 代码质量:是否有反模式?
- 效率审查:性能是否达标?
我突然明白------
这些维度,其实就是我每次都会说的话。
如果把它们写进 Skill,AI 调用时就会自动按这个标准执行。
这就是 Skill 的本质:把"你反复说的那些话",写成一份文档,让 AI 每次都按这个标准来。
5.3 写在最后
当你发现自己和 AI 反复说同一类话时,就是该创建 Skill 的时机:
- 记录你每次的反馈
- 提取可复用的模式
- 写成 SKILL.md
- 下次直接调用
把"反复说的话"变成"一次编码,永久复用"。
