系列导读
这是《12课拆解Claude Code架构》系列的第 5 课。
前四课我们造了一个有工具链、规划能力、子任务隔离的 Agent:第 1 课建了 Agent Loop,第 2 课加了 Tool Use dispatch,第 3 课用 TodoWrite 引入了显式规划,第 4 课用 Subagent 做了上下文隔离。
但 Agent 的知识范围一直是固定的——system prompt 里写了什么,它就知道什么。
第 5 课的格言:
"用到什么知识,临时加载什么知识"
这一课,我们给 Agent 装上 Skill 机制,让它像工程师翻手册一样,按需加载领域知识。
知识臃肿问题
你希望 Agent 遵循特定的工作流——git 约定、测试模式、代码审查清单、安全检查规范。最直觉的做法是全塞进 system prompt:
system_prompt = """
你是一个 AI 编程助手。## Git 工作流
提交消息格式:<type>: <description>
类型包括 feat, fix, refactor, docs, test, chore...
分支命名规范...
PR 模板...
(800 tokens)## 测试规范
最低覆盖率 80%...
TDD 流程:红-绿-重构...
测试文件组织...
(1200 tokens)## 代码审查清单
函数不超过 50 行...
文件不超过 800 行...
安全检查触发条件...
(1500 tokens)## Python 最佳实践
...
(2000 tokens)## 安全指南
...
(1500 tokens)... 还有 5 个领域 ...
"""
10 个领域知识,每个 1500-2000 token,system prompt 直接膨胀到 15000-20000 token。
问题不只是贵。
-
注意力稀释:模型处理 20000 token 的 system prompt 时,对每条规则的关注度都在下降。让模型同时记住 git 规范和安全清单和测试模式和代码风格,不如让它专注处理当前任务需要的那一两个。
-
大部分是浪费:你让 Agent 写一个单元测试,它需要测试规范,但完全不需要 git 工作流和安全审查清单。那 15000 token 里可能只有 2000 token 跟当前任务有关。
-
难以维护:所有知识糊在一个巨大的字符串里,更新一个领域要小心别碰坏别的领域。多人协作时更是噩梦。
这就像让一个工程师背下公司所有部门的操作手册,然后上班时只用到其中一本。合理的做法是——手册放书架上,用到哪本翻哪本。

两层注入架构
解决方案是把知识注入拆成两层:
第一层:system prompt(始终存在)
┌──────────────────────────────────────────────┐
│ 你是一个 AI 编程助手。 │
│ │
│ 你可以使用以下技能(用 load_skill 加载): │
│ • git-workflow — Git 提交与分支规范 │
│ • testing — 测试规范与 TDD 流程 │
│ • code-review — 代码审查标准 │
│ • python-patterns — Python 最佳实践 │
│ • security — 安全检查清单 │
│ │
│ 总计: ~500 tokens(名称+描述) │
└──────────────────────────────────────────────┘││ 模型判断:当前任务需要 "testing" 知识▼
第二层:tool_result(按需注入)
┌──────────────────────────────────────────────┐
│ load_skill("testing") │
│ │
│ → tool_result: │
│ <skill name="testing"> │
│ ## 测试规范 │
│ 最低覆盖率 80%... │
│ TDD 流程:红-绿-重构... │
│ 测试文件组织... │
│ (完整内容 ~1200 tokens) │
│ </skill> │
└──────────────────────────────────────────────┘
第一层:目录(便宜)
system prompt 里只放技能名称和一句描述,每个技能 ~100 tokens。10 个技能只要 ~1000 tokens,而不是 20000。模型看到目录后知道有哪些知识可用,但不会被全文淹没。
第二层:全文(按需)
当模型判断当前任务需要某个领域知识时,调用 load_skill 工具。完整内容通过 tool_result 注入到对话上下文中。只有用到的知识才占 token。
成本对比:写单元测试的任务,旧方案吃 20000 token system prompt,新方案吃 1000 token 目录 + 1200 token 测试规范 = 2200 token。节省 89%。
核心拆解
SKILL.md 文件结构
每个 Skill 是一个独立目录,包含一个 SKILL.md 文件:
skills/
├── git-workflow/
│ └── SKILL.md
├── testing/
│ └── SKILL.md
├── code-review/
│ └── SKILL.md
├── python-patterns/
│ └── SKILL.md
└── security/└── SKILL.md
SKILL.md 的格式——YAML frontmatter 加 markdown 正文:
---
name: testing
description: 测试规范与 TDD 流程
---## 最低测试覆盖率:80%测试类型(全部必需):
1. **单元测试** — 单个函数、工具、组件
2. **集成测试** — API 端点、数据库操作
3. **E2E 测试** — 关键用户流程## 测试驱动开发强制工作流:
1. 写测试(RED)
2. 运行测试 — 应该失败
3. 写最小实现(GREEN)
4. 运行测试 — 应该通过
5. 重构(IMPROVE)
6. 验证覆盖率(80%+)...
frontmatter 提供元数据(名称和描述),正文是真正的知识内容。这个结构让"目录生成"和"全文加载"可以分别读取不同的部分。
SkillLoader 类
import os
import yamlclass SkillLoader:"""扫描 skills/ 目录,提供目录描述和按需加载。"""def __init__(self, skills_dir: str = "skills"):self.skills_dir = skills_dirself.skills = {} # name → {description, content, path}self._scan()def _scan(self):"""扫描所有 skills/*/SKILL.md,解析 frontmatter。"""if not os.path.isdir(self.skills_dir):returnfor entry in sorted(os.listdir(self.skills_dir)):skill_path = os.path.join(self.skills_dir, entry, "SKILL.md")if not os.path.isfile(skill_path):continuewith open(skill_path, "r", encoding="utf-8") as f:raw = f.read()# 解析 YAML frontmattermeta, body = self._parse_frontmatter(raw)name = meta.get("name", entry)description = meta.get("description", "")self.skills[name] = {"description": description,"content": body.strip(),"path": skill_path,}@staticmethoddef _parse_frontmatter(raw: str) -> tuple[dict, str]:"""分离 YAML frontmatter 和 markdown 正文。"""if not raw.startswith("---"):return {}, rawparts = raw.split("---", 2)if len(parts) < 3:return {}, rawmeta = yaml.safe_load(parts[1]) or {}body = parts[2]return meta, bodydef get_descriptions(self) -> str:"""生成 system prompt 中的技能目录(第一层)。"""if not self.skills:return ""lines = ["Available skills (use load_skill to access):"]for name, info in self.skills.items():lines.append(f" - {name}: {info['description']}")return "\n".join(lines)def get_content(self, skill_name: str) -> str:"""返回完整技能内容(第二层),用 XML 标签包裹。"""if skill_name not in self.skills:available = ", ".join(self.skills.keys())return f"Error: Skill '{skill_name}' not found. Available: {available}"content = self.skills[skill_name]["content"]return f'<skill name="{skill_name}">\n{content}\n</skill>'
四个方法,职责清晰:
| 方法 | 作用 | 调用时机 |
|---|---|---|
_scan() |
遍历 skills 目录,解析所有 SKILL.md | 初始化时(一次) |
_parse_frontmatter() |
分离 YAML 元数据和正文 | _scan() 内部 |
get_descriptions() |
生成技能目录列表 | 构建 system prompt 时 |
get_content() |
返回完整技能内容 | load_skill 工具被调用时 |
两层注入的组装
第一层注入——system prompt 拼接:
skill_loader = SkillLoader("skills")SYSTEM = f"""You are an AI coding assistant with access to tools.{skill_loader.get_descriptions()}When a task involves a specific domain (testing, git, security, etc.),
use the load_skill tool to load the relevant guidelines before proceeding.
"""
模型看到的 system prompt 是这样的:
You are an AI coding assistant with access to tools.Available skills (use load_skill to access):- git-workflow: Git 提交与分支规范- testing: 测试规范与 TDD 流程- code-review: 代码审查标准- python-patterns: Python 最佳实践- security: 安全检查清单When a task involves a specific domain (testing, git, security, etc.),
use the load_skill tool to load the relevant guidelines before proceeding.
~500 tokens。模型知道有哪些知识可用,但不会被全文淹没。
第二层注入——load_skill 工具:
LOAD_SKILL_TOOL = {"name": "load_skill","description": ("Load domain-specific guidelines and best practices. ""Use this when the current task involves a specific domain ""like testing, git workflow, code review, etc."),"input_schema": {"type": "object","properties": {"skill_name": {"type": "string","description": "Name of the skill to load",}},"required": ["skill_name"],},
}
注册到 dispatch map:
TOOL_HANDLERS = {"bash": run_bash,"read": read_file,"write": write_file,"edit": edit_file,"todo": todo_write,"task": lambda prompt: run_subagent(prompt),"load_skill": lambda skill_name: skill_loader.get_content(skill_name), # 新增
}
和前面每一课一样——加新能力只加注册,循环不碰。
完整运行流程
用户让 Agent 写一个单元测试:
用户: "给 utils/parser.py 写单元测试"模型思考: 这是测试相关任务,我先加载测试规范。tool_use: load_skill(skill_name="testing")tool_result: <skill name="testing">## 最低测试覆盖率:80%测试类型(全部必需):1. 单元测试 — 单个函数...2. 集成测试 — API 端点...## 测试驱动开发强制工作流:1. 写测试(RED)...</skill>模型思考: 好的,按照 TDD 流程,先看目标代码再写测试。tool_use: read(path="utils/parser.py")tool_result: (文件内容)tool_use: write(path="tests/test_parser.py", content="...")tool_result: OKtool_use: bash(command="pytest tests/test_parser.py -v")tool_result: 3 passed, 1 failed模型思考: 有一个测试失败了,需要检查实现......(继续修复直到通过)最终回复: "已为 utils/parser.py 创建单元测试,覆盖率 85%,遵循 TDD 流程:先写测试 → 验证失败 → 修复 → 全部通过。"
注意:模型自己判断需要加载 testing skill。你不需要告诉它"先加载测试规范"——它看到任务是写测试,就知道应该先翻一下测试手册。

洞见:三个关键设计决策
为什么不全塞 system prompt
表面原因是 token 成本。但更深层的原因是认知负荷。
研究表明,大模型处理 system prompt 中的指令时,指令越多,每条指令的遵循率越低。这和人类一样——你给一个工程师一份 50 页的规范手册,他不可能同时严格执行每一条规则。但如果你告诉他"今天做测试任务,请翻到第 3 章",他对那一章的执行质量会高得多。
两层注入本质上就是注意力管理:用少量 token 让模型知道有哪些知识可用(目录),用按需加载让模型在需要时获得完整细节(全文)。模型在任何时刻只需要深度处理 1-2 个 Skill,而不是同时消化 10 个。
这就是 Claude Code 的 Skill 加载机制
如果你用过 Claude Code,你会发现它的 /skill 命令和 skills/ 目录就是这个架构的生产实现:
skills/xxx/SKILL.md→ 知识文件,YAML frontmatter + markdown 正文- 启动时扫描所有技能 → 名称和描述注入系统提示
- 模型调用时 → 完整内容通过 tool_result 注入对话
我们这 40 行 Python 就是这个机制的最小实现。生产版本多了缓存、权限控制、嵌套引用,但核心的两层架构完全一致。
为什么用 tool_result 而不是追加 system prompt
技能内容是通过 load_skill 的 tool_result 注入的,而不是动态修改 system prompt。这是有意为之:
-
位置语义:system prompt 的内容模型会视为"全局指令",tool_result 的内容会视为"当前上下文中的参考资料"。后者更符合 Skill 的语义——它是任务相关的参考,不是永久规则。
-
可见性:tool_result 出现在对话流中,模型可以明确引用"根据刚才加载的测试规范"。如果塞在 system prompt 里,模型无法区分哪些知识是刚加载的、哪些是一直存在的。
-
生命周期:tool_result 在对话上下文中有明确的位置,后续的 context compact(第 6 课内容)可以对其做智能压缩。system prompt 的内容没法按需压缩。
五分钟跑起来
# 进入项目目录
cd learn-claude-code# 启动第五课
python agents/s05_skill_loading.py
启动后你会看到 s05 >> 提示符。以下是真实的运行记录:
任务 1:查看可用技能
s05 >> What skills are available?Based on my configuration, here are the skills currently available:| # | Skill Name | Description |
|---|------------|-------------|
| 1 | agent-builder | Design and build AI agents for any domain |
| 2 | code-review | Perform thorough code reviews |
| 3 | mcp-builder | Build MCP servers that give Claude new capabilities |
| 4 | pdf | Process PDF files — extract text, create PDFs |Just ask me about any of these topics and I'll load the relevant skill.
注意:Agent 没有调用任何工具,直接从 system prompt 里的技能目录回答了。这就是第一层注入的效果——目录始终在 system prompt 中,模型可以随时查阅。
任务 2:加载并使用技能
s05 >> Load the agent-builder skill and follow its instructions
> load_skill:
<skill name="agent-builder">
# Agent Builder
Build AI agents for any domain...The **Agent Builder** skill is loaded. Here's what I've learned:### The Core Philosophy
> "The model already knows how to be an agent.Your job is to get out of the way."### The Three Elements
| Element | Purpose | Principle |
|---------|---------|-----------|
| Capabilities | What the agent can do | Start with 3–5 |
| Knowledge | What the agent knows | Load on-demand |
| Context | What has happened | Protect clarity |### Available Resources
- references/minimal-agent.py — Complete working agent (~80 lines)
- references/tool-templates.py — Capability definitions
- references/subagent-pattern.py — Context isolation patterns
- scripts/init_agent.py — Project scaffolding generatorWhat would you like to build?
模型调用 load_skill("agent-builder"),完整知识注入后,Agent 立刻具备了 Agent 设计领域的专业能力——包括哲学理念、架构模式、参考代码路径。加载前它只知道名字,加载后它拥有全部知识。
任务 3:技能驱动的实际构建
s05 >> Build an MCP server using the mcp-builder skill
> load_skill:
<skill name="mcp-builder">
# MCP Server Building Skill ...> bash: (创建项目目录)
> write_file: Wrote 10338 bytes (server.py)
> write_file: Wrote 1971 bytes (README.md)
> bash: (安装依赖 mcp>=1.0.0)
> bash: (运行出错 — AttributeError: 'Server' object has no attribute...)
> bash: (检查 MCP SDK 版本和 API)
> write_file: Wrote 10202 bytes (修复后的 server.py)
> bash:
Running tests...
✅ word_count
✅ search_files
✅ compare_files
✅ file_stats
✅ find_duplicates## ✅ MCP File Utilities Server — Complete!| Tool | What it does |
|---|---|
| word_count | Counts lines, words, and characters |
| search_files | Regex search across files in a directory |
| compare_files | Unified diff between two files |
| file_stats | Full metadata: size, permissions, SHA-256 |
| find_duplicates | Finds duplicate files by content hash |
这个任务展示了 Skill 机制的真正威力:Agent 加载 mcp-builder 技能后,获得了 MCP 协议的专业知识,从零构建了一个完整的 MCP Server。中间遇到 API 不兼容问题时,它用加载的知识自主排查并修复。没有 Skill,Agent 可能需要反复试错;有了 Skill,它知道正确的 API 用法。
变更表
| 组件 | 第 4 课 (Subagent) | 第 5 课 (Skill Loading) |
|---|---|---|
| 系统提示词 | 静态,所有知识写死 | +Skill 描述列表(目录) |
| 知识库 | 无 | skills/*/SKILL.md 文件 |
| 知识注入 | 无 | 两层:system prompt 目录 + tool_result 全文 |
| 新增工具 | task | +load_skill |
| 新增组件 | run_subagent() |
+SkillLoader 类 |
| Token 效率 | 所有知识塞 system prompt | 按需加载,节省 ~80% |
| 新增代码 | ~50 行 | ~40 行(SkillLoader + 工具注册) |
下一课预告
前五课解决了工具、规划、隔离、知识加载,但有一个问题始终没有正面处理——对话越来越长怎么办?
Subagent 隔离了子任务的上下文膨胀,但父 Agent 本身的消息列表还是在不断增长。对话进行到 50 轮,messages 可能有几万 token。离上下文窗口上限越来越近,每轮 API 调用的成本也越来越高。
第 6 课:Context Compact —— 上下文压缩。当对话长度逼近阈值时,自动把旧的消息摘要化,释放空间给新内容。就像笔记本快写满时,把前面的内容浓缩成要点,腾出页面继续写。
# 预告:s06 的上下文压缩
def compact_context(messages: list, max_tokens: int) -> list:if count_tokens(messages) < max_tokens * 0.8:return messages # 还没到阈值,不压缩# 保留最近的消息,压缩旧消息recent = messages[-KEEP_RECENT:]old = messages[:-KEEP_RECENT]summary = summarize(old) # 用模型生成摘要return [{"role": "user", "content": summary}] + recent
从"上下文不够用"到"自动压缩续航",Agent 的对话能力第一次变得可持续。
这是《12课拆解Claude Code架构:从零掌握Agent Harness工程》系列的第 5 课。关注Claw开发者,不错过后续更新。
完整代码和交互式学习平台:github.com/shareAI-lab/learn-claude-code
如果这篇文章对你有帮助,欢迎转发给你的技术团队。
系列目录
- 第1课:用20行Python造出你的第一个AI Agent
- 第2课:给Agent加工具 —— dispatch map模式详解
- 第3课:TodoWrite —— 让Agent先想后做:规划系统
- 第4课:Subagent —— 拆解大任务,上下文隔离
- 第5课:按需加载领域知识——Skill机制(本文)
- 第6课:无限对话——上下文压缩三层策略
- 第7课:任务持久化——文件级DAG任务图
- 第8课:后台执行——异步任务与通知队列
- 第9课:Agent Teams——多Agent协作:团队与邮箱系统
- 第10课:团队协议——状态机驱动的协商
- 第11课:自治Agent——自组织任务认领
- 第12课:终极隔离——Worktree并行执行
