第5课:按需加载领域知识——Skill机制

系列导读

这是《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

问题不只是贵。

  1. 注意力稀释:模型处理 20000 token 的 system prompt 时,对每条规则的关注度都在下降。让模型同时记住 git 规范和安全清单和测试模式和代码风格,不如让它专注处理当前任务需要的那一两个。

  2. 大部分是浪费:你让 Agent 写一个单元测试,它需要测试规范,但完全不需要 git 工作流和安全审查清单。那 15000 token 里可能只有 2000 token 跟当前任务有关。

  3. 难以维护:所有知识糊在一个巨大的字符串里,更新一个领域要小心别碰坏别的领域。多人协作时更是噩梦。

这就像让一个工程师背下公司所有部门的操作手册,然后上班时只用到其中一本。合理的做法是——手册放书架上,用到哪本翻哪本。

知识臃肿:20000 token 的 system prompt


两层注入架构

解决方案是把知识注入拆成两层:

第一层: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 yaml

class SkillLoader:
    """扫描 skills/ 目录,提供目录描述和按需加载。"""

    def __init__(self, skills_dir: str = "skills"):
        self.skills_dir = skills_dir
        self.skills = {}  # name → {description, content, path}
        self._scan()

    def _scan(self):
        """扫描所有 skills/*/SKILL.md,解析 frontmatter。"""
        if not os.path.isdir(self.skills_dir):
            return
        for 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):
                continue
            with open(skill_path, "r", encoding="utf-8") as f:
                raw = f.read()

            # 解析 YAML frontmatter
            meta, 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,
            }

    @staticmethod
    def _parse_frontmatter(raw: str) -> tuple[dict, str]:
        """分离 YAML frontmatter 和 markdown 正文。"""
        if not raw.startswith("---"):
            return {}, raw
        parts = raw.split("---", 2)
        if len(parts) < 3:
            return {}, raw
        meta = yaml.safe_load(parts[1]) or {}
        body = parts[2]
        return meta, body

    def 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: OK

  tool_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_skilltool_result 注入的,而不是动态修改 system prompt。这是有意为之:

  1. 位置语义:system prompt 的内容模型会视为"全局指令",tool_result 的内容会视为"当前上下文中的参考资料"。后者更符合 Skill 的语义——它是任务相关的参考,不是永久规则。

  2. 可见性:tool_result 出现在对话流中,模型可以明确引用"根据刚才加载的测试规范"。如果塞在 system prompt 里,模型无法区分哪些知识是刚加载的、哪些是一直存在的。

  3. 生命周期: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 generator

What 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并行执行
posted @ 2026-04-19 10:26  Claw开发者  阅读(11)  评论(0)    收藏  举报