feat: refresh course through workflow and goal loops

This commit is contained in:
Haoran
2026-07-30 19:14:04 +08:00
parent 2dd1852d9e
commit cb8fae1bdd
125 changed files with 10882 additions and 7661 deletions

View File

@@ -1,182 +0,0 @@
# s07: Skill Loading — Load Only When Needed
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20
> *"Load when needed, don't stuff the prompt"* — Inject via tool_result, not system prompt.
>
> **Harness Layer**: Knowledge — load on demand, don't fill the context.
---
## The Problem
Your project has a React component spec, a SQL style guide, and an API design doc. You want the Agent to follow these specs automatically. The most straightforward idea — stuff them all into the system prompt:
```python
SYSTEM = (
f"You are a coding agent. "
+ open("docs/react-style.md").read() # 2000 lines
+ open("docs/sql-style.md").read() # 1500 lines
+ open("docs/api-design.md").read() # 3000 lines
)
```
6500 lines of system prompt. The Agent carries these docs on every LLM call — whether it's changing a CSS color or fixing a SQL query. 99% of the content is irrelevant to the current task, burning tokens for nothing.
---
## The Solution
![Skill Overview](images/skill-overview.en.svg)
The minimal hook structure, `todo_write`, and sub-Agent from the previous chapter are preserved. This chapter focuses on the new `load_skill` tool. At startup, inject the skill catalog into the SYSTEM prompt; at runtime, register one more tool to load full content, spending tokens only when used.
Two-level design:
| Level | Location | Timing | Cost |
|-------|----------|--------|------|
| 1. Catalog | system prompt | Injected at startup (harness scans skills/) | ~100 tokens/skill, carried every turn |
| 2. Content | tool_result | When Agent calls load_skill; SKILL.md can guide later read_file/bash access to extra resources | ~2000 tokens/skill, on demand |
The dispatch mechanism is unchanged, `load_skill` auto-dispatches via `TOOL_HANDLERS[block.name]`.
---
## How It Works
**skills/ directory**, one subdirectory per skill, each containing a `SKILL.md` file:
```
skills/
agent-builder/SKILL.md
code-review/SKILL.md
mcp-builder/SKILL.md
pdf/SKILL.md
```
**Level 1: Inject catalog at startup**: the harness calls `_scan_skills()` at startup to scan the skills/ directory, parsing each SKILL.md's YAML frontmatter (`name`, `description`) into a `SKILL_REGISTRY` dictionary. `list_skills()` generates the catalog from the registry, injected into the SYSTEM prompt. The Agent sees "which skills I have available" every turn, with no extra API calls:
```python
SKILL_REGISTRY: dict[str, dict] = {}
def _scan_skills():
if not SKILLS_DIR.exists():
return
for d in sorted(SKILLS_DIR.iterdir()):
if not d.is_dir():
continue
manifest = d / "SKILL.md"
if manifest.exists():
raw = manifest.read_text()
meta, body = _parse_frontmatter(raw)
name = meta.get("name", d.name)
desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": raw}
_scan_skills() # runs once at startup
def list_skills() -> str:
return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
def build_system() -> str:
catalog = list_skills()
return (
f"You are a coding agent at {WORKDIR}. "
f"Skills available:\n{catalog}\n"
"Use load_skill to get full details when needed."
)
SYSTEM = build_system()
```
**Level 2: load_skill**: the Agent decides "I need the SQL style guide" and calls `load_skill("sql-style")`. Lookup goes through the registry, not file paths, eliminating path traversal risk. The SKILL.md content is injected via `tool_result`, and can include later access to referenced `references/`, `scripts/`, or `assets/` through the existing file and bash tools.
```python
def load_skill(name: str) -> str:
skill = SKILL_REGISTRY.get(name)
if not skill:
return f"Skill not found: {name}"
return skill["content"]
```
The key distinction: skill content is not part of the system prompt. It enters the current messages as a tool result. Subsequent calls carry it along with the history until context compaction, truncation, or session end. This naturally connects to s08's compact: on-demand loading solves "don't carry what you shouldn't", compact solves "how to drop what you should."
---
## Changes from s06
| Component | Before (s06) | After (s07) |
|-----------|-------------|-------------|
| Tool count | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |
| Knowledge loading | None | Two-level: startup catalog in SYSTEM + runtime load_skill; SKILL.md may guide later resource access |
| SYSTEM prompt | Static string | Startup scan of skills/ injects catalog |
| Skill registry | None | SKILL_REGISTRY (populated at startup, prevents path traversal) |
| Loop | Unchanged | Unchanged (skill tool auto-dispatches) |
---
## Try It
```sh
cd learn-claude-code
python s07_skill_loading/code.py
```
Try these prompts:
1. `What skills are available?`
2. `Load the code-review skill and follow its instructions`
3. `I need to do a code review -- load the relevant skill first`
What to watch for: Does the Agent know available skills from the SYSTEM catalog? Does `[HOOK] load_skill` appear when full instructions are needed? Does the answer use the loaded skill's instructions?
---
## What's Next
On-demand loading solved "don't carry what you shouldn't." But another problem looms: after the Agent works for 30 minutes, the messages list fills up with intermediate process. Old tool_results, stale file contents, occupying context but adding no value.
→ s08 Context Compact: A four-layer compaction strategy. Cheap layers run first, expensive layers run last.
<details>
<summary>Dive into CC Source Code</summary>
> The following is based on analysis of CC source code `loadSkillsDir.ts`, `SkillTool.ts`, `bundledSkills.ts`, `commands.ts`.
### 1. Skill Sources: Not Just One skills/ Directory
The teaching version assumes all skills live in a `skills/` directory. CC loads from multiple sources spread across multiple files: `loadSkillsDir.ts` handles user/project/`--add-dir` directories and legacy commands (`.claude/commands/`); `bundledSkills.ts` handles built-in skills; `SkillTool.ts` handles MCP remote skills; `commands.ts` handles command aggregation. Types include managed/policy skills, user skills (`~/.claude/skills/`), project skills (`.claude/skills/`), `--add-dir` skills, legacy commands, dynamic skills, conditional skills (with `paths` frontmatter, activated by file path), bundled skills, plugin skills, MCP skills.
### 2. SKILL.md Frontmatter — Common Fields
CC's SKILL.md YAML frontmatter is parsed by `parseSkillFrontmatterFields()` in `loadSkillsDir.ts`. Common fields include:
| Field | Purpose |
|-------|---------|
| `name` / `description` | Display name and description |
| `when_to_use` | Guides the model on when to invoke |
| `allowed-tools` | Auto-allow list of tools available to the skill |
| `context` | `inline` (default) or `fork` (run as sub-Agent) |
| `model` | Model override (haiku/sonnet/opus/inherit) |
| `hooks` | Skill-level hook configuration |
| `paths` | Glob patterns for conditional activation |
| `user-invocable` | Users can invoke via `/name` |
The complete field list changes across versions; above are the core fields relevant to the teaching version.
### 3. Precise Implementation of Two-Level Loading
1. **Catalog (at startup)**: `getSkillDirCommands()` scans directory → registers as `Command` objects containing only metadata. `getSkillListingAttachments()` formats the skill list as attachments, budgeted at ~1% of the context window (cap 8000 characters).
2. **Load (on invocation)**: Model calls `Skill` tool (input fields are `skill` + optional `args`; teaching version uses `name`) → `getPromptForCommand()` expands full SKILL.md content → `SkillTool` returns a tool_result with display text `"Launching skill: {name}"`, while the actual skill content is injected via `newMessages`. The teaching version merges both into "injected via tool_result" as a simplification; the loaded SKILL.md can still guide later access to referenced resources through existing file/bash tools.
### The Teaching Version's Simplification Is Intentional
- Multiple files and sources → 1 `skills/` directory: sufficient to demonstrate the core concept of two-level loading
- Multiple frontmatter fields → only parse name/description: reduces parsing complexity
- Forked skills (`context: 'fork'`) → omitted: the teaching version only expands inline skill loading
- `Skill` tool input `skill`+`args` → teaching version uses `name`: avoids extra argument parsing complexity
</details>
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->

View File

@@ -1,8 +1,8 @@
# s07: Skill Loading — 必要なときにだけ読み込む
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20 → s21 → s22
> *"Load when needed, don't stuff the prompt"* — tool_result で注入、system prompt には詰め込まない。
>
> **Harness レイヤー**: 知識 — 必要に応じて読み込み、コンテキストに詰め込まない。

View File

@@ -1,51 +1,51 @@
# s07: Skill Loading — 用到的时候才加载
# s07: Skill Loading — Load Only When Needed
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20
> *"用到时再加载, 别全塞 prompt"* — 通过 tool_result 注入, 不塞 system prompt
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20 → s21 → s22
> *"Load when needed, don't stuff the prompt"* — Inject via tool_result, not system prompt.
>
> **Harness 层**: 知识 — 按需加载, 不堆满上下文。
> **Harness Layer**: Knowledge — load on demand, don't fill the context.
---
## 问题
## The Problem
你的项目有一套 React 组件规范、一份 SQL 风格指南、一份 API 设计文档。你希望 Agent 自动遵守这些规范。最直接的想法,全塞进 system prompt
Your project has a React component spec, a SQL style guide, and an API design doc. You want the Agent to follow these specs automatically. The most straightforward idea — stuff them all into the system prompt:
```python
SYSTEM = (
f"You are a coding agent. "
+ open("docs/react-style.md").read() # 2000
+ open("docs/sql-style.md").read() # 1500
+ open("docs/api-design.md").read() # 3000
+ open("docs/react-style.md").read() # 2000 lines
+ open("docs/sql-style.md").read() # 1500 lines
+ open("docs/api-design.md").read() # 3000 lines
)
```
6500 system prompt。Agent 每次调用 LLM 都带着这些文档——不管是在改 CSS 颜色还是修 SQL 查询。99% 的内容和当前任务无关,白白消耗 token。
6500 lines of system prompt. The Agent carries these docs on every LLM call — whether it's changing a CSS color or fixing a SQL query. 99% of the content is irrelevant to the current task, burning tokens for nothing.
---
## 解决方案
## The Solution
![Skill Overview](images/skill-overview.svg)
![Skill Overview](images/skill-overview.en.svg)
保留上一章的最小 hook 结构、`todo_write` 和子 Agent本章重点转向新增的 `load_skill` 工具。启动时把技能目录注入 SYSTEM prompt运行时多注册一个工具加载完整内容用到才花 token。
The minimal hook structure, `todo_write`, and sub-Agent from the previous chapter are preserved. This chapter focuses on the new `load_skill` tool. At startup, inject the skill catalog into the SYSTEM prompt; at runtime, register one more tool to load full content, spending tokens only when used.
两层设计:
Two-level design:
| 层 | 位置 | 时机 | 代价 |
|---|------|------|------|
| 1. 目录 | system prompt | 启动时注入(harness 扫描 skills/ | ~100 tokens/skill,每轮都带 |
| 2. 内容 | tool_result | Agent 调用 load_skill 时;SKILL.md 可指引后续的 read_file/bash 调用,用于按需访问额外资源 | ~2000 tokens/skill,按需 |
| Level | Location | Timing | Cost |
|-------|----------|--------|------|
| 1. Catalog | system prompt | Injected at startup (harness scans skills/) | ~100 tokens/skill, carried every turn |
| 2. Content | tool_result | When Agent calls load_skill; SKILL.md can guide later read_file/bash access to extra resources | ~2000 tokens/skill, on demand |
dispatch 机制不变load_skill 通过 `TOOL_HANDLERS[block.name]` 分发。
The dispatch mechanism is unchanged, `load_skill` auto-dispatches via `TOOL_HANDLERS[block.name]`.
---
## 工作原理
## How It Works
**skills/ 目录**,每个技能一个子目录,包含 `SKILL.md` 文件:
**skills/ directory**, one subdirectory per skill, each containing a `SKILL.md` file:
```
skills/
@@ -55,7 +55,7 @@ skills/
pdf/SKILL.md
```
**第一级:启动时注入目录**harness 启动时调用 `_scan_skills()` 扫描 skills/ 目录,解析每个 SKILL.md YAML frontmatter`name``description`),存入 `SKILL_REGISTRY` 字典。`list_skills()` 从注册表生成目录,注入 SYSTEM prompt。Agent 每轮都能看到"我有哪些技能可用",不花额外 API 调用:
**Level 1: Inject catalog at startup**: the harness calls `_scan_skills()` at startup to scan the skills/ directory, parsing each SKILL.md's YAML frontmatter (`name`, `description`) into a `SKILL_REGISTRY` dictionary. `list_skills()` generates the catalog from the registry, injected into the SYSTEM prompt. The Agent sees "which skills I have available" every turn, with no extra API calls:
```python
SKILL_REGISTRY: dict[str, dict] = {}
@@ -90,7 +90,7 @@ def build_system() -> str:
SYSTEM = build_system()
```
**第二级:load_skill**Agent 决定"我需要 SQL 风格指南",调用 `load_skill("sql-style")`。通过注册表查找不走文件路径没有路径遍历风险。SKILL.md 内容通过 `tool_result` 注入,并可通过现有的 file 和 bash 工具进一步访问引用的 `references/``scripts/` `assets/`
**Level 2: load_skill**: the Agent decides "I need the SQL style guide" and calls `load_skill("sql-style")`. Lookup goes through the registry, not file paths, eliminating path traversal risk. The SKILL.md content is injected via `tool_result`, and can include later access to referenced `references/`, `scripts/`, or `assets/` through the existing file and bash tools.
```python
def load_skill(name: str) -> str:
@@ -100,82 +100,82 @@ def load_skill(name: str) -> str:
return skill["content"]
```
关键区别:技能内容不是 system prompt 的一部分,它作为一次工具结果进入当前 messages。后续调用会随历史一起携带直到上下文压缩、截断或会话结束。这和 s08 compact 自然衔接:按需加载解决了"不该提前带的不要带"compact 解决"该丢的怎么丢"。
The key distinction: skill content is not part of the system prompt. It enters the current messages as a tool result. Subsequent calls carry it along with the history until context compaction, truncation, or session end. This naturally connects to s08's compact: on-demand loading solves "don't carry what you shouldn't", compact solves "how to drop what you should."
---
## 相对 s06 的变更
## Changes from s06
| 组件 | 之前 (s06) | 之后 (s07) |
|------|-----------|-----------|
| 工具数量 | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |
| 知识加载 | 无 | 两级:启动时目录注入 SYSTEM + 运行时 load_skillSKILL.md 可指引后续资源访问 |
| SYSTEM 提示 | 静态字符串 | 启动时扫描 skills/ 注入目录 |
| 技能注册表 | 无 | SKILL_REGISTRY启动时填充防路径遍历 |
| 循环 | 不变 | 不变skill 工具自动分发) |
| Component | Before (s06) | After (s07) |
|-----------|-------------|-------------|
| Tool count | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |
| Knowledge loading | None | Two-level: startup catalog in SYSTEM + runtime load_skill; SKILL.md may guide later resource access |
| SYSTEM prompt | Static string | Startup scan of skills/ injects catalog |
| Skill registry | None | SKILL_REGISTRY (populated at startup, prevents path traversal) |
| Loop | Unchanged | Unchanged (skill tool auto-dispatches) |
---
## 试一下
## Try It
```sh
cd learn-claude-code
python s07_skill_loading/code.py
```
试试这些 prompt
Try these prompts:
1. `What skills are available?`
2. `Load the code-review skill and follow its instructions`
3. `I need to do a code review -- load the relevant skill first`
观察重点Agent 是否直接从 SYSTEM 里的目录知道有哪些技能?需要完整规范时是否出现 `[HOOK] load_skill`?加载后回答是否使用了对应 skill 的说明?
What to watch for: Does the Agent know available skills from the SYSTEM catalog? Does `[HOOK] load_skill` appear when full instructions are needed? Does the answer use the loaded skill's instructions?
---
## 接下来
## What's Next
按需加载解决了"不该带的不要带"。但另一个问题来了Agent 连续工作 30 分钟后messages 列表塞满了中间过程。旧的 tool_result、过时的文件内容占着上下文但不产生价值。
On-demand loading solved "don't carry what you shouldn't." But another problem looms: after the Agent works for 30 minutes, the messages list fills up with intermediate process. Old tool_results, stale file contents, occupying context but adding no value.
s08 Context Compact → 四层压缩策略。便宜的先跑,贵的后跑。
s08 Context Compact: A four-layer compaction strategy. Cheap layers run first, expensive layers run last.
<details>
<summary>深入 CC 源码</summary>
<summary>Dive into CC Source Code</summary>
> 以下基于 CC 源码 `loadSkillsDir.ts``SkillTool.ts``bundledSkills.ts``commands.ts` 的分析。
> The following is based on analysis of CC source code `loadSkillsDir.ts`, `SkillTool.ts`, `bundledSkills.ts`, `commands.ts`.
### 一、技能来源:不是只有一个 skills/ 目录
### 1. Skill Sources: Not Just One skills/ Directory
教学版假设所有技能在 `skills/` 目录下。CC 实际从多个来源加载,分布在多个文件中:`loadSkillsDir.ts` 负责从 user/project/`--add-dir` 目录和 legacy commands`.claude/commands/`)加载;`bundledSkills.ts` 负责内置技能;`SkillTool.ts` 处理 MCP 远程技能;`commands.ts` 负责命令聚合。类型包括 managed/policy skillsuser skills`~/.claude/skills/`)、project skills`.claude/skills/`)、`--add-dir` skillslegacy commandsdynamic skillsconditional skills(带 `paths` frontmatter,按文件路径激活)、bundled skillsplugin skillsMCP skills
The teaching version assumes all skills live in a `skills/` directory. CC loads from multiple sources spread across multiple files: `loadSkillsDir.ts` handles user/project/`--add-dir` directories and legacy commands (`.claude/commands/`); `bundledSkills.ts` handles built-in skills; `SkillTool.ts` handles MCP remote skills; `commands.ts` handles command aggregation. Types include managed/policy skills, user skills (`~/.claude/skills/`), project skills (`.claude/skills/`), `--add-dir` skills, legacy commands, dynamic skills, conditional skills (with `paths` frontmatter, activated by file path), bundled skills, plugin skills, MCP skills.
### 二、SKILL.md Frontmatter 常见字段
### 2. SKILL.md Frontmatter — Common Fields
CC SKILL.md YAML frontmatter `parseSkillFrontmatterFields()` 解析(`loadSkillsDir.ts`),常见字段包括:
CC's SKILL.md YAML frontmatter is parsed by `parseSkillFrontmatterFields()` in `loadSkillsDir.ts`. Common fields include:
| 字段 | 用途 |
|------|------|
| `name` / `description` | 显示名称和描述 |
| `when_to_use` | 指导模型何时调用 |
| `allowed-tools` | 技能可用工具的自动允许列表 |
| `context` | `inline`(默认)或 `fork`(作为子 Agent 运行) |
| `model` | 模型覆盖(haiku/sonnet/opus/inherit |
| `hooks` | 技能级别的 hook 配置 |
| `paths` | 条件激活的 glob 模式 |
| `user-invocable` | 用户可以通过 `/name` 调用 |
| Field | Purpose |
|-------|---------|
| `name` / `description` | Display name and description |
| `when_to_use` | Guides the model on when to invoke |
| `allowed-tools` | Auto-allow list of tools available to the skill |
| `context` | `inline` (default) or `fork` (run as sub-Agent) |
| `model` | Model override (haiku/sonnet/opus/inherit) |
| `hooks` | Skill-level hook configuration |
| `paths` | Glob patterns for conditional activation |
| `user-invocable` | Users can invoke via `/name` |
完整字段列表随版本迭代会变化,以上仅列出教学版涉及的核心字段。
The complete field list changes across versions; above are the core fields relevant to the teaching version.
### 三、两级加载的精确实现
### 3. Precise Implementation of Two-Level Loading
1. **Catalog(启动时)**`getSkillDirCommands()` 扫描目录 → 注册为 `Command` 对象,只包含元数据。`getSkillListingAttachments()` 把技能列表格式化为附件,预算为上下文窗口的 ~1%(上限 8000 字符)。
2. **Load(调用时)**:模型调 `Skill` 工具(输入字段是 `skill` + 可选 `args`,教学版用 `name``getPromptForCommand()` 展开完整 SKILL.md 内容`SkillTool` 返回的 tool_result 展示文本只是 `"Launching skill: {name}"`,真正的技能内容通过 `newMessages` 注入对话。教学版把两者合并为"通过 tool_result 注入"是一种简化;加载后的 SKILL.md 仍可作为指引,帮助模型后续通过现有 file/bash 工具访问相关资源。
1. **Catalog (at startup)**: `getSkillDirCommands()` scans directory → registers as `Command` objects containing only metadata. `getSkillListingAttachments()` formats the skill list as attachments, budgeted at ~1% of the context window (cap 8000 characters).
2. **Load (on invocation)**: Model calls `Skill` tool (input fields are `skill` + optional `args`; teaching version uses `name`) `getPromptForCommand()` expands full SKILL.md content`SkillTool` returns a tool_result with display text `"Launching skill: {name}"`, while the actual skill content is injected via `newMessages`. The teaching version merges both into "injected via tool_result" as a simplification; the loaded SKILL.md can still guide later access to referenced resources through existing file/bash tools.
### 教学版的简化是刻意的
### The Teaching Version's Simplification Is Intentional
- 多文件多来源 → 1 `skills/` 目录:足以展示两级加载的核心概念
- 多个 frontmatter 字段 → 只解析 name/description减少解析复杂度
- forked skills`context: 'fork'`)→ 省略:教学版只展开 inline 技能加载
- `Skill` 工具输入 `skill`+`args`教学版用 `name`:避免参数解析的额外复杂度
- Multiple files and sources → 1 `skills/` directory: sufficient to demonstrate the core concept of two-level loading
- Multiple frontmatter fields → only parse name/description: reduces parsing complexity
- Forked skills (`context: 'fork'`) → omitted: the teaching version only expands inline skill loading
- `Skill` tool input `skill`+`args`teaching version uses `name`: avoids extra argument parsing complexity
</details>

View File

@@ -0,0 +1,182 @@
# s07: Skill Loading — 用到的时候才加载
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20 → s21 → s22
> *"用到时再加载, 别全塞 prompt 里"* — 通过 tool_result 注入, 不塞 system prompt。
>
> **Harness 层**: 知识 — 按需加载, 不堆满上下文。
---
## 问题
你的项目有一套 React 组件规范、一份 SQL 风格指南、一份 API 设计文档。你希望 Agent 自动遵守这些规范。最直接的想法,全塞进 system prompt
```python
SYSTEM = (
f"You are a coding agent. "
+ open("docs/react-style.md").read() # 2000 行
+ open("docs/sql-style.md").read() # 1500 行
+ open("docs/api-design.md").read() # 3000 行
)
```
6500 行 system prompt。Agent 每次调用 LLM 都带着这些文档——不管是在改 CSS 颜色还是修 SQL 查询。99% 的内容和当前任务无关,白白消耗 token。
---
## 解决方案
![Skill Overview](images/skill-overview.svg)
保留上一章的最小 hook 结构、`todo_write` 和子 Agent本章重点转向新增的 `load_skill` 工具。启动时把技能目录注入 SYSTEM prompt运行时多注册一个工具加载完整内容用到才花 token。
两层设计:
| 层 | 位置 | 时机 | 代价 |
|---|------|------|------|
| 1. 目录 | system prompt | 启动时注入harness 扫描 skills/ | ~100 tokens/skill每轮都带 |
| 2. 内容 | tool_result | Agent 调用 load_skill 时SKILL.md 可指引后续的 read_file/bash 调用,用于按需访问额外资源 | ~2000 tokens/skill按需 |
dispatch 机制不变load_skill 通过 `TOOL_HANDLERS[block.name]` 分发。
---
## 工作原理
**skills/ 目录**,每个技能一个子目录,包含 `SKILL.md` 文件:
```
skills/
agent-builder/SKILL.md
code-review/SKILL.md
mcp-builder/SKILL.md
pdf/SKILL.md
```
**第一级:启动时注入目录**harness 启动时调用 `_scan_skills()` 扫描 skills/ 目录,解析每个 SKILL.md 的 YAML frontmatter`name``description`),存入 `SKILL_REGISTRY` 字典。`list_skills()` 从注册表生成目录,注入 SYSTEM prompt。Agent 每轮都能看到"我有哪些技能可用",不花额外 API 调用:
```python
SKILL_REGISTRY: dict[str, dict] = {}
def _scan_skills():
if not SKILLS_DIR.exists():
return
for d in sorted(SKILLS_DIR.iterdir()):
if not d.is_dir():
continue
manifest = d / "SKILL.md"
if manifest.exists():
raw = manifest.read_text()
meta, body = _parse_frontmatter(raw)
name = meta.get("name", d.name)
desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": raw}
_scan_skills() # runs once at startup
def list_skills() -> str:
return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
def build_system() -> str:
catalog = list_skills()
return (
f"You are a coding agent at {WORKDIR}. "
f"Skills available:\n{catalog}\n"
"Use load_skill to get full details when needed."
)
SYSTEM = build_system()
```
**第二级load_skill**Agent 决定"我需要 SQL 风格指南",调用 `load_skill("sql-style")`。通过注册表查找不走文件路径没有路径遍历风险。SKILL.md 内容通过 `tool_result` 注入,并可通过现有的 file 和 bash 工具进一步访问引用的 `references/``scripts/``assets/`
```python
def load_skill(name: str) -> str:
skill = SKILL_REGISTRY.get(name)
if not skill:
return f"Skill not found: {name}"
return skill["content"]
```
关键区别:技能内容不是 system prompt 的一部分,它作为一次工具结果进入当前 messages。后续调用会随历史一起携带直到上下文压缩、截断或会话结束。这和 s08 的 compact 自然衔接:按需加载解决了"不该提前带的不要带"compact 解决"该丢的怎么丢"。
---
## 相对 s06 的变更
| 组件 | 之前 (s06) | 之后 (s07) |
|------|-----------|-----------|
| 工具数量 | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |
| 知识加载 | 无 | 两级:启动时目录注入 SYSTEM + 运行时 load_skillSKILL.md 可指引后续资源访问 |
| SYSTEM 提示 | 静态字符串 | 启动时扫描 skills/ 注入目录 |
| 技能注册表 | 无 | SKILL_REGISTRY启动时填充防路径遍历 |
| 循环 | 不变 | 不变skill 工具自动分发) |
---
## 试一下
```sh
cd learn-claude-code
python s07_skill_loading/code.py
```
试试这些 prompt
1. `What skills are available?`
2. `Load the code-review skill and follow its instructions`
3. `I need to do a code review -- load the relevant skill first`
观察重点Agent 是否直接从 SYSTEM 里的目录知道有哪些技能?需要完整规范时是否出现 `[HOOK] load_skill`?加载后回答是否使用了对应 skill 的说明?
---
## 接下来
按需加载解决了"不该带的不要带"。但另一个问题来了Agent 连续工作 30 分钟后messages 列表塞满了中间过程。旧的 tool_result、过时的文件内容占着上下文但不产生价值。
s08 Context Compact → 四层压缩策略。便宜的先跑,贵的后跑。
<details>
<summary>深入 CC 源码</summary>
> 以下基于 CC 源码 `loadSkillsDir.ts`、`SkillTool.ts`、`bundledSkills.ts`、`commands.ts` 的分析。
### 一、技能来源:不是只有一个 skills/ 目录
教学版假设所有技能在 `skills/` 目录下。CC 实际从多个来源加载,分布在多个文件中:`loadSkillsDir.ts` 负责从 user/project/`--add-dir` 目录和 legacy commands`.claude/commands/`)加载;`bundledSkills.ts` 负责内置技能;`SkillTool.ts` 处理 MCP 远程技能;`commands.ts` 负责命令聚合。类型包括 managed/policy skills、user skills`~/.claude/skills/`、project skills`.claude/skills/`)、`--add-dir` skills、legacy commands、dynamic skills、conditional skills`paths` frontmatter按文件路径激活、bundled skills、plugin skills、MCP skills。
### 二、SKILL.md Frontmatter 常见字段
CC 的 SKILL.md YAML frontmatter 由 `parseSkillFrontmatterFields()` 解析(`loadSkillsDir.ts`),常见字段包括:
| 字段 | 用途 |
|------|------|
| `name` / `description` | 显示名称和描述 |
| `when_to_use` | 指导模型何时调用 |
| `allowed-tools` | 技能可用工具的自动允许列表 |
| `context` | `inline`(默认)或 `fork`(作为子 Agent 运行) |
| `model` | 模型覆盖haiku/sonnet/opus/inherit |
| `hooks` | 技能级别的 hook 配置 |
| `paths` | 条件激活的 glob 模式 |
| `user-invocable` | 用户可以通过 `/name` 调用 |
完整字段列表随版本迭代会变化,以上仅列出教学版涉及的核心字段。
### 三、两级加载的精确实现
1. **Catalog启动时**`getSkillDirCommands()` 扫描目录 → 注册为 `Command` 对象,只包含元数据。`getSkillListingAttachments()` 把技能列表格式化为附件,预算为上下文窗口的 ~1%(上限 8000 字符)。
2. **Load调用时**:模型调 `Skill` 工具(输入字段是 `skill` + 可选 `args`,教学版用 `name`)→ `getPromptForCommand()` 展开完整 SKILL.md 内容 → `SkillTool` 返回的 tool_result 展示文本只是 `"Launching skill: {name}"`,真正的技能内容通过 `newMessages` 注入对话。教学版把两者合并为"通过 tool_result 注入"是一种简化;加载后的 SKILL.md 仍可作为指引,帮助模型后续通过现有 file/bash 工具访问相关资源。
### 教学版的简化是刻意的
- 多文件多来源 → 1 个 `skills/` 目录:足以展示两级加载的核心概念
- 多个 frontmatter 字段 → 只解析 name/description减少解析复杂度
- forked skills`context: 'fork'`)→ 省略:教学版只展开 inline 技能加载
- `Skill` 工具输入 `skill`+`args` → 教学版用 `name`:避免参数解析的额外复杂度
</details>
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->