learn claude code S05 Skill 加载详解笔记
S05 Skill 加载详解笔记
基于
s05_skill_loading.py源码逐行分析,配合s05-skill-loading.md设计思路。
一、问题:所有知识塞进 system prompt 太浪费
s04 的 agent 已经能派子 agent 出去干活了,但还有一个问题没解决:领域知识放哪里?
假设你希望 agent 遵循特定的工作流——git commit 规范、代码审查清单、PDF 处理流程、MCP server 构建指南。最直接的做法是把这些全写进 system prompt:
你是一个编程 agent。
Git 规范:第一条...第二条...(500 tokens)
测试模式:AAA 模式...mock 策略...(800 tokens)
代码审查:检查项一...检查项二...(600 tokens)
PDF 处理:先用 pdftotext...再提取表格...(1200 tokens)
...
10 个 skill,每个 500-2000 token,加起来就是 上万 token 常驻在 system prompt 里。问题是:用户这次只想处理一个 PDF,其他 9 个 skill 的 token 全浪费了。更糟的是,大部分 API 对 system prompt 也计费——你为没用的知识付了全价。
二、解决方案:两层架构,按需加载
s05 的方案很优雅:
第一层(便宜):system prompt 里只放 skill 的名字和一句话描述
第二层(按需):模型调用 load_skill("xxx") 时,完整内容才以 tool_result 注入
System prompt:
+--------------------------------------+
| You are a coding agent. |
| Skills available: |
| - pdf: Process PDF files... | ← Layer 1: 只有名字和描述,~100 tokens/skill
| - code-review: Review code... |
+--------------------------------------+
当模型调用 load_skill("pdf"):
+--------------------------------------+
| tool_result: |
| <skill name="pdf"> |
| 完整的 PDF 处理流程... | ← Layer 2: 完整 body,~2000 tokens
| 步骤一:... | 但只在实际用到时才加载
| 步骤二:... |
| </skill> |
+--------------------------------------+
核心思想:“Don’t put everything in the system prompt. Load on demand.” — 别把所有东西塞进系统提示词,用到什么加载什么。
三、和 s04 相比,多了什么?
| 组件 | s04 | s05 |
|---|---|---|
| 知识管理 | 无 | SkillLoader:从 skills/*/SKILL.md 加载 |
| system prompt | 静态字符串 | + 动态注入 skill 名称列表 |
| 新工具 | task |
load_skill(按需加载完整内容) |
| 分层设计 | 无 | 两层注入(prompt 层 + tool_result 层) |
s04 引入了子 agent 来隔离上下文,s05 则是在不隔离的情况下减少上下文浪费——两个互补策略。
四、Skill 文件长什么样?
4.1 目录结构
skills/
pdf/
SKILL.md ← YAML frontmatter + Markdown body
code-review/
SKILL.md
agent-builder/
SKILL.md
每个 skill 是一个目录,目录名是 skill 的标识。目录下必须有一个 SKILL.md 文件。
4.2 SKILL.md 格式
---
name: pdf
description: Process PDF files - extract text, fill forms, merge documents
tags: [document, conversion]
---
# PDF Processing
## Step 1: Extract text
Use `pdftotext input.pdf -` to extract text content.
## Step 2: Extract tables
Use `pdfplumber` or `tabula-py` for table extraction.
...
上半部分是 YAML frontmatter(两行 --- 之间的内容)——包含元数据:name(skill 名)、description(一句话描述,会出现在 system prompt 里)、tags(可选标签)。
下半部分是 Markdown body——skill 的完整指令,模型调用 load_skill 后才会看到。
初学者注意:YAML 是一种人类可读的数据格式,用缩进表示层级关系。--- 是 YAML 文档的分隔符。Python 读取时用 yaml.safe_load() 把文本解析成 dict。
五、SkillLoader 类:skill 的扫描、解析、分发
5.1 初始化与扫描
class SkillLoader:
def __init__(self, skills_dir: Path):
self.skills_dir = skills_dir
self.skills = {} # {name: {meta: {...}, body: "...", path: "..."}}
self._load_all() # 初始化时自动扫描所有 skill 文件
创建实例时立刻调用 _load_all() 扫描整个 skills/ 目录。所有 skill 在程序启动时一次性加载到内存,后续 get_content() 只是读字典——不需要每次访问硬盘。
5.2 _load_all() — 递归发现所有 SKILL.md
def _load_all(self):
if not self.skills_dir.exists():
return # skills/ 目录不存在就静默跳过,不报错
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) # 优先用 frontmatter 里的 name,
self.skills[name] = { # 没有就用目录名
"meta": meta,
"body": body,
"path": str(f)
}
几个关键细节:
rglob("SKILL.md"):递归搜索所有子目录。rglob是Path的方法,类似**/*.md通配。sorted(...):按文件名排序,确保每次加载顺序一致——避免 skill 列表顺序随机变化干扰模型。name的取值优先级:frontmatter 里写了name: xxx→ 用它;没写 → 用父目录名(如skills/pdf/SKILL.md→name = "pdf")。- 不存在不报错:
skills/目录没有时不崩溃,agent 照样跑,只是get_descriptions()返回"(no skills available)"。这种"可选的增强"设计贯穿整个项目。
5.3 _parse_frontmatter() — 分离元数据和正文
def _parse_frontmatter(self, text: str) -> tuple:
match = re.match(r"^---\n(.*?)\n---\n(.*)", text, re.DOTALL)
if not match:
return {}, text # 没有 frontmatter → 整个文本当 body
try:
meta = yaml.safe_load(match.group(1)) or {}
except yaml.YAMLError:
meta = {} # YAML 格式坏了也不崩溃,降级为空字典
return meta, match.group(2).strip()
正则 ^---\n(.*?)\n---\n(.*) 把文件切成两段:group(1) 是 frontmatter(YAML),group(2) 是正文(Markdown)。re.DOTALL 让 . 匹配换行符——否则 .*? 只能匹配一行。
容错设计值得注意:YAML 解析失败返回空字典,正则不匹配返回原文本作为 body。无论输入多差,函数都不会崩。对 agent 来说,这意味着用户可以随意写 SKILL.md——格式不对最多导致 skill 不可用,不会让整个程序挂掉。
5.4 get_descriptions() — 第一层:给 system prompt 的简介
def get_descriptions(self) -> str:
if not self.skills:
return "(no skills available)"
lines = []
for name, skill in self.skills.items():
desc = skill["meta"].get("description", "No description")
tags = skill["meta"].get("tags", "")
line = f" - {name}: {desc}"
if tags:
line += f" [{tags}]"
lines.append(line)
return "\n".join(lines)
输出效果:
- pdf: Process PDF files - extract text, fill forms, merge documents [document, conversion]
- code-review: Review code for bugs, style, and security issues [quality, security]
这段文本被嵌入 system prompt,模型看到的只是"有哪些 skill 以及一句话描述"。每个 skill 大约占 100 tokens——10 个 skill 才 1000 tokens,比全部塞进去省了 10-20 倍。
5.5 get_content() — 第二层:返回完整内容
def get_content(self, name: str) -> str:
skill = self.skills.get(name)
if not skill:
return f"Error: Unknown skill '{name}'. Available: {', '.join(self.skills.keys())}"
return f"<skill name=\"{name}\">\n{skill['body']}\n</skill>"
模型调 load_skill("pdf") → 返回用 <skill> 标签包裹的完整 body。<skill> 标签不是 HTML,是一个语义标记——帮助模型区分"这是系统注入的 skill 指令"和"对话中的其他文本"。和 s03 的 <reminder> 是同一思路。
六、工具分发与 system prompt 注入
6.1 load_skill 工具定义
核心字段:
name:skill 名称,必填。模型从 system prompt 里看到了可用列表,调用时指定要加载哪个。
6.2 分发
TOOL_HANDLERS = {
# ...s04 的 5 个工具...
"load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"]),
}
load_skill 和其他工具完全平权。模型的视角是:我看到了一个叫 “pdf” 的 skill → 我需要处理 PDF → 我先调 load_skill("pdf") 获取完整指令 → 然后按指令执行。skill 的 body 在 tool_result 里出现在 messages 最新位置,模型注意力最强。
6.3 System prompt 的动态注入
SYSTEM = f"""You are a coding agent at {WORKDIR}.
Use load_skill to access specialized knowledge before tackling unfamiliar topics.
Skills available:
{SKILL_LOADER.get_descriptions()}"""
注意这行:“Use load_skill…before tackling unfamiliar topics” — 告诉模型:遇到不熟悉的领域,先加载 skill 再看。这就是那个"使用规范":工具 schema 告诉你 怎么调(传 name 参数),system prompt 告诉你 什么时候调(遇到陌生领域先加载)。
七、完整流程走读
假设 skills/ 下有一个 pdf skill,用户说:“帮我把 report.pdf 转成文本”。
第 1 轮
- 模型收到 system prompt,里面写着:
Skills available: - pdf: Process PDF files - extract text... - 模型看到 “pdf”,任务又涉及 PDF,判断需要加载 skill。
- 模型调用
load_skill("pdf")。
第 2 轮
SKILL_LOADER.get_content("pdf")返回:<skill name="pdf"> # PDF Processing Step 1: Use pdftotext input.pdf - to extract text. Step 2: For tables, use pdfplumber. </skill>- 这段内容作为 tool_result 追加到 messages——出现在上下文最新位置,模型注意力最强。
- 模型读到完整指令,知道第一步该用
pdftotext。
第 3 轮
- 模型调用
bash: pdftotext report.pdf -,得到文本输出。 - 任务完成。
关键洞察:上 1 万 token 的 skill body 只在第 2 轮存在于上下文中。第 1 轮和第 3 轮里就没有。如果把它塞进 system prompt,它会占用每一轮的 token 预算——不管用不用。
八、设计洞察
8.1 懒加载:让昂贵的东西可选
skill 的完整内容很贵(2000+ tokens),但不是每次对话都需要。s05 的方案等价于编程中的"懒加载"(lazy loading)——只在实际访问时把完整数据装入内存(上下文)。system prompt 里放名字,相当于放了一个"指针",解引用由模型决定。
8.2 文本即接口
skill 文件没有任何自定义格式——YAML frontmatter + Markdown body。任何人可以用任何编辑器创建 skill,不需要懂 Python。降低贡献门槛就是提高系统可扩展性。
8.3 容错贯穿始终
_parse_frontmatter 的三层降级:正则不匹配 → ({}, text);YAML 解析失败 → {};name 没有 → 用目录名。无论输入多差,函数不崩。这个模式在 skill 系统里尤其重要——skill 是用户写的,用户会犯错。
8.4 信号与内容的分离
system prompt 里放的是信号(“这个 skill 存在,它大概干什么”),tool_result 里放的是内容(“具体怎么做”)。信号帮助模型做路由决策(“我需要这个 skill 吗?”),内容帮助模型做执行决策(“怎么操作 pdftotext?”)。把两者分开是对 token 预算的最优分配。
8.5 能力扩展不侵入循环
加一个 skill 不需要改 agent_loop。SkillLoader 扫描文件系统,load_skill 只是 dispatch map 里又一个条目。这延续了 s02 以来的一贯哲学:扩展能力 = 加工具 + 注册 handler,循环永远不变。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)