Claude代码技能加载攻略:新手必看榜单
原址
s05_skill_loading.py 的核心使命很明确:给 AI Coding Agent 增加“按需加载技能”的能力。
换句话说,不要一股脑把所有专业知识都塞进 system prompt。取而代之的策略是:一开始只放技能的名称和简介,等到模型真需要的时候,再通过 load_skill 这个工具去加载完整的内容。文件开头的注释把这个“两层注入”设计讲得很清楚——第一层把技能名和简介放进 system prompt,第二层在模型调用 load_skill("pdf") 时返回完整技能正文。
这个文件要解决什么问题
回头想想,如果咱们的 Agent 把所有的规则、工具说明、PDF 处理方法、代码审查规范、MCP 构建指南……统统写进 system prompt,会怎样?
- prompt 变得巨长,白白浪费 token
- 模型的注意力被稀释,容易抓不住重点
- 那些与当前任务不相关的知识,反而会干扰判断
- 技能越多,系统提示词就越臃肿
所以,这个文件实现了一个更聪明的机制——类似于 Claude Code、Cursor 或 Agent Skill 的做法。注释里有一句话特别到位:“Don’t put everything in the system prompt. Load on demand.”
整体流程,六步走完
整个过程可以这样理解:
启动程序
↓
扫描 skills/ 目录下所有 SKILL.md
↓
解析每个 SKILL.md 的 YAML frontmatter
↓
把技能名称 + 简介塞进 system prompt
↓
用户提出任务
↓
模型判断是否需要某个技能,需要就调用 load_skill(name)
↓
load_skill 返回完整技能说明
↓
模型根据完整技能继续完成任务
目录结构怎么设计
文件注释里规定的技能目录大概长这样:
skills/
pdf/
SKILL.md
code-review/
SKILL.md
每个技能一个目录,每个目录里放一个 SKILL.md。从仓库的实际布局来看,确实存在 agent-builder、code-review、mcp-builder、pdf 这些技能目录。
而 SKILL.md 的典型结构是:
---
name: pdf
description: Process PDF files...
---
# PDF Processing Skill
具体操作说明……
拿 skills/pdf/SKILL.md 来说,里面就包含了 name: pdf 和 description: Process PDF files... 这样的元信息,以及后面完整的 PDF 处理说明。
核心代码逐段解析
1. 初始化环境
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
WORKDIR = Path.cwd()
client = Anthropic()
MODEL = os.environ["MODEL_ID"]
SKILLS_DIR = WORKDIR / "skills"
这段代码干了几件事:
| 代码 | 作用 |
|---|---|
load_dotenv(override=True) |
读取 .env 文件里的环境变量 |
ANTHROPIC_BASE_URL 判断 |
如果使用自定义 base url,就移除 ANTHROPIC_AUTH_TOKEN |
WORKDIR = Path.cwd() |
当前运行目录作为工作区 |
client = Anthropic() |
初始化 Anthropic 客户端 |
MODEL = os.environ["MODEL_ID"] |
从环境变量读取模型名 |
SKILLS_DIR = WORKDIR / "skills" |
默认从当前目录下的 skills/ 加载技能 |
这些初始化代码位于文件中部,负责准备模型客户端、模型 ID 和技能目录。
2. SkillLoader:技能加载器的核心
这是本文件最关键的类。
class SkillLoader:
def __init__(self, skills_dir: Path):
self.skills_dir = skills_dir
self.skills = {}
self._load_all()
它的作用简单来说就是:启动时扫描 skills/ 目录,把所有技能读进内存。 self.skills 是一个字典,用来保存所有技能。每个技能大概会被存成这样:
{
"pdf": {
"meta": {...},
"body": "...完整技能内容...",
"path": "skills/pdf/SKILL.md"
}
}
SkillLoader 会在初始化时调用 _load_all(),扫描 skills_dir 下面所有 SKILL.md 文件。
3. _load_all():扫描并加载所有技能文件
def _load_all(self):
if not self.skills_dir.exists():
return
for f in sorted(self.skills_dir.rglob("SKILL.md")):
text = f.read_text()
meta, body = self._parse_frontmatter(text)
name = meta.get("name", f.parent.name)
self.skills[name] = {"meta": meta, "body": body, "path": str(f)}
这段逻辑很关键:
| 步骤 | 含义 |
|---|---|
if not self.skills_dir.exists() |
如果没有 skills/ 目录,就直接返回 |
rglob("SKILL.md") |
递归查找所有叫 SKILL.md 的文件 |
f.read_text() |
读取技能文件内容 |
_parse_frontmatter(text) |
拆分 YAML 元信息和正文 |
meta.get("name", f.parent.name) |
优先用 frontmatter 里的 name,没有就用目录名 |
self.skills[name] = ... |
存入技能字典 |
也就是说,技能名可以来自两种地方:
name: pdf
或者直接取自目录名:
skills/pdf/SKILL.md
↑
pdf
相关的扫描和存储逻辑都在 SkillLoader._load_all() 里。
4. _parse_frontmatter():解析 YAML 头部
match = re.match(r"^---\n(.*?)\n---\n(.*)", text, re.DOTALL)
这行正则的意思是:
从文件开头开始匹配:
---
这里是 YAML 元信息
---
这里是正文
如果匹配不到 frontmatter:
return {}, text
那就说明没有元信息,整个文件都当成正文处理。
如果匹配到了:
meta = yaml.safe_load(match.group(1)) or {}
return meta, match.group(2).strip()
拆解一下:
| 部分 | 作用 |
|---|---|
match.group(1) |
YAML frontmatter |
match.group(2) |
技能正文 |
yaml.safe_load(...) |
把 YAML 字符串转成 Python 字典 |
.strip() |
去掉正文首尾空白 |
所以 SKILL.md 实际被拆成了两部分:
meta:name、description、tags 等信息息
body:完整技能说明
这就是后面“两层加载”的基础。
5. get_descriptions():生成第 1 层技能简介
def get_descriptions(self) -> str:
"""Layer 1: short descriptions for the system prompt."""
这个方法用来生成放进 system prompt 的内容。它不会返回完整的技能正文,而只返回类似这样的短描述:
- pdf: Process PDF files...
- code-review: Perform thorough code reviews...
代码里会读取每个技能的:
desc = skill["meta"].get("description", "No description")
tags = skill["meta"].get("tags", "")
然后拼成一行文本。这就是第 1 层:轻量索引,只告诉模型“我有这些技能”。
6. get_content():生成第 2 层完整技能内容
def get_content(self, name: str) -> str:
"""Layer 2: full skill body returned in tool_result."""
这个方法就是 load_skill 背后的真正逻辑。
如果技能不存在:
return f"Error: Unknown skill '{name}'. A vailable: ..."
如果技能存在:
return f""
也就是说,当模型调用:
load_skill("pdf")
工具会返回:
完整 PDF 处理说明……
这就是第 2 层:按需加载的完整知识。相关逻辑在 get_content() 中。
System Prompt 是怎么组装的?
SKILL_LOADER = SkillLoader(SKILLS_DIR)
SYSTEM = f"""You are a coding agent at {WORKDIR}.
Use load_skill to access specialized knowledge before tackling unfamiliar topics.
Skills a vailable:
{SKILL_LOADER.get_descriptions()}"""
这段就是把技能简介注入系统提示词。注意:注入的是 get_descriptions(),不是 get_content()。
所以 system prompt 里只会有:
你是一个 coding agent
遇到陌生任务时可以用 load_skill 加载专业知识
当前可用技能:
- pdf: ...
- code-review: ...
不会一开始就把 PDF 的详细命令、代码审查清单、MCP 构建流程全部塞进去。
工具系统:比前面章节多了 load_skill
这个文件仍然保留了普通 Agent 工具:
| 工具 | 作用 |
|---|---|
bash |
执行 shell 命令 |
read_file |
读取文件 |
write_file |
写文件 |
edit_file |
替换文件中的指定文本 |
load_skill |
按名称加载技能正文 |
工具处理函数在 TOOL_HANDLERS 里注册,其中 load_skill 对应的是:
"load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"])
也就是说,模型调用 load_skill,实际执行的就是 SKILL_LOADER.get_content(name)。
load_skill 和普通工具有什么区别?
这是这个文件最值得琢磨的点。普通工具是“做事”的:
bash → 执行命令
read_file → 读文件
write_file → 写文件
edit_file → 改文件
但 load_skill 不是直接去做某件事,而是“给模型补知识”的:
load_skill → 返回一段专业操作说明,让模型变得更会做某类任务
所以它更像是一个:
知识工具 / 上下文注入工具 / 按需说明书加载器
举个例子:用户说“帮我处理这个 PDF”。模型看到 system prompt 里有:
- pdf: Process PDF files...
于是模型可以先调用:
{
"name": "pdf"
}
然后 load_skill 返回完整的 PDF 技能,比如如何用 pdftotext、PyMuPDF、reportlab、pandoc 等处理 PDF。skills/pdf/SKILL.md 里确实包含了读取、创建、合并、拆分 PDF 的具体操作说明。
safe_path():限制文件路径不能逃出工作区
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
这个函数用于保护 read_file、write_file、edit_file 这些文件操作。它会把用户传入的路径拼到当前工作区,然后解析成绝对路径。如果最终路径不在 WORKDIR 里面,就抛出错误。
比如:
正常:src/main.py
危险:../../etc/passwd
../../etc/passwd 解析后会逃出工作区,所以会被拦截。相关路径保护逻辑在 safe_path() 中。
run_bash():执行命令,但做了简单危险命令拦截
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(d in command for d in dangerous):
return "Error: Dangerous command blocked"
它会拦截一些明显危险命令,比如:
rm -rf /
sudo
shutdown
reboot
> /dev/
然后用:
subprocess.run(..., shell=True, cwd=WORKDIR, timeout=120)
执行命令,最多跑 120 秒。输出会截断到 50000 字符。
不过这里得说清楚:这是教学版,不是生产级沙箱。原因有几个:
- 它用了
shell=True - 危险命令只靠字符串黑名单拦截
safe_path()只保护文件读写工具,并不保护bash命令本身bash理论上仍然能执行很多未被黑名单覆盖的危险操作
所以这个文件更适合用来学习 Agent 架构,不适合直接拿来跑不可信输入。
agent_loop():核心 Agent 循环
def agent_loop(messages: list):
while True:
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=TOOLS,
max_tokens=8000,
)
这里和前面的 Agent Loop 思路一致:
- 把历史消息
messages发给模型 - 带上
system - 带上
tools - 等模型回复
- 如果模型要调用工具,就执行工具
- 把工具结果塞回 messages
- 继续循环
- 直到模型不再调用工具
判断是否继续的关键是:
if response.stop_reason != "tool_use":
return
如果模型不是因为调用工具而停止,就说明它已经给出最终回答了,循环结束。
工具调用结果怎么回传给模型?
results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(output),
}
)
messages.append({"role": "user", "content": results})
这段会把工具执行结果包装成 Anthropic API 需要的 tool_result 格式,再作为一条新的 user message 追加到历史消息里。
所以完整交互像这样:
用户:帮我处理 PDF
模型:我要调用 load_skill("pdf")
程序:执行 load_skill,拿到 PDF 技能说明
程序:把技能说明作为 tool_result 返回给模型
模型:读完技能说明后,再继续完成 PDF 任务
命令行入口
if __name__ == "__main__":
history = []
while True:
query = input("\033[36ms05 >> \033[0m")
这部分让脚本变成一个命令行聊天程序。执行后会看到类似:
s05 >>
然后你输入问题,程序把你的输入追加到 history:
history.append({"role": "user", "content": query})
agent_loop(history)
最后从 history[-1]["content"] 里拿出模型回复并打印。
和前面几个文件的关系
前面看过的文件可以这样串起来理解:
| 文件 | 重点 |
|---|---|
s01_agent_loop.py |
最小 Agent Loop:用户输入 → 模型回复 |
s02_tool_use.py |
加入工具调用:模型可以调用 bash / read / write 等工具 |
s03_todo_write.py |
加入任务规划 / Todo 管理 |
s04_subagent.py |
加入子 Agent:主 Agent 可以把任务委托出去 |
s05_skill_loading.py |
加入技能加载:模型可以按需加载专业知识 |
s05 的本质升级是:
从“能调用工具”
升级到
“能根据任务加载专业操作手册”
换句话说:
Agent = LLM + Tools + Memory/Context + Planning + Skills
这里的 Skills 不是模型本身的能力,而是外部维护的一组专业说明书。
这个文件最核心的设计思想
可以总结成一句话:
System Prompt 只放索引,完整知识按需加载。
更具体一点:
skills/ 目录 = 技能库
SKILL.md frontmatter = 技能索引
SKILL.md body = 完整技能说明
get_descriptions() = 注入 system prompt 的轻量索引
load_skill() = 按需读取完整技能
tool_result = 把技能正文塞回模型上下文
这个设计很像:
书架目录 + 按需取书
不是一开始把所有书都摊在桌子上,而是先告诉模型:
我有 PDF 技能、代码审查技能、MCP 构建技能……
等模型真的要处理 PDF 时,再把 PDF 那本“说明书”拿出来。
最后用一句大白话理解
s05_skill_loading.py 就是在教你做一个更聪明的 AI Coding Agent:让它学会“用到什么学什么”,而不是“什么都先背下来”。
