refactor: streamline the course to 17 lessons

This commit is contained in:
Haoran
2026-08-12 03:02:42 +08:00
parent ab35e59672
commit 7e2f2fd99b
250 changed files with 12179 additions and 18653 deletions

View File

@@ -1,17 +1,18 @@
# s07: Skill Loading — 用到的时候才加载
# s07: Skill Loading — 用到时再加载
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s18 → s19
> *"用到时再加载, 别全塞 prompt 里"* — 通过 tool_result 注入, 不塞 system prompt。
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s16 → s17
> system prompt 保存技能目录;`load_skill` 返回完整的 `SKILL.md`。
>
> **Harness 层**: 知识 — 按需加载, 不堆满上下文
> **Harness 层**:知识加载 — 让模型先知道有哪些技能,再按名称读取内容
---
## 问题
假设某个项目有一套 React 组件规范、一份 SQL 风格指南和一份 API 设计文档。我们希望 Agent 在开发过程中遵守这些规范最直接的做法是把它们全部放进 system prompt
假设某个项目有一套 React 组件规范、一份 SQL 风格指南和一份 API 设计文档。我们希望 Agent 在开发过程中遵守这些规范最直接的做法是把它们全部放进 system prompt
```python
SYSTEM = (
@@ -22,7 +23,7 @@ SYSTEM = (
)
```
样,每次调用 LLM 都会携带三份完整文档。即使当前任务只涉及其中一份,另外两份仍会占用上下文
种做法能让 Agent 读到所有规范,但问题在于,三份文档被固定放进了 system prompt无法根据当前任务只选择需要的那一份。每次调用 LLM 时,三份文档的全文都会一起发送给模型。当前任务只修改 React 组件时,实际需要的只有 React 组件规范SQL 风格指南和 API 设计文档与任务无关,却仍然占用输入 token 和上下文窗口,留给代码、对话和工具结果的空间也会变少
---
@@ -30,24 +31,20 @@ SYSTEM = (
![Skill Overview](images/skill-overview.svg)
保留上一章的最小 hook 结构、`todo_write` 和子 Agent本章重点转向新增的 `load_skill` 工具。启动时把技能目录SYSTEM prompt,运行时多注册一个工具加载完整内容,用到才花 token
启动时,`SkillLoader` 扫描 `skills/*/SKILL.md`,读取 YAML frontmatter 中的 `name``description`,并把这份目录system prompt。模型需要完整说明时,调用 `load_skill(name)`;返回的 `SKILL.md` 作为 `tool_result` 追加到消息列表
两层设计:
| 层 | 位置 | 时机 | 代价 |
|---|------|------|------|
| 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]` 分发。
| 内容 | 进入模型的位置 | 何时加入 |
|------|----------------|----------|
| 技能名称和描述 | system prompt | 启动时 |
| 完整 `SKILL.md` | `tool_result` | 调用 `load_skill`|
---
## 工作原理
**skills/ 目录**每个技能一个子目录,包含 `SKILL.md` 文件
每个技能一个包含 `SKILL.md` 的目录
```
```text
skills/
agent-builder/SKILL.md
code-review/SKILL.md
@@ -55,64 +52,58 @@ skills/
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] = {}
class SkillLoader:
def scan(self):
self.skills.clear()
for manifest in sorted(self.skills_dir.glob("*/SKILL.md")):
content = manifest.read_text()
metadata, body = self.parse_frontmatter(content)
name = str(metadata.get("name") or manifest.parent.name).strip()
description = metadata.get("description") or body.splitlines()[0]
description = " ".join(str(description).lstrip("# ").split())
self.skills[name] = {
"name": name,
"description": description,
"content": content,
}
```
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}
`catalog()` 只输出名称和描述:
_scan_skills() # runs once at startup
```text
- code-review: Perform thorough code reviews...
- pdf: Process PDF files...
```
def list_skills() -> str:
return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
### 组装 system prompt
def build_system() -> str:
catalog = list_skills()
```python
def build_system_prompt() -> str:
return (
f"You are a coding agent at {WORKDIR}. "
f"Skills available:\n{catalog}\n"
"Use load_skill to get full details when needed."
f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. "
"Act, don't explain.\n\n"
f"Skills available:\n{SKILL_LOADER.catalog()}\n\n"
"Use load_skill to read the full instructions when a skill applies."
)
SYSTEM = build_system()
```
**第二级load_skill**Agent 决定"我需要 SQL 风格指南",调用 `load_skill("sql-style")`。通过注册表查找不走文件路径没有路径遍历风险。SKILL.md 内容通过 `tool_result` 注入,并可通过现有的 file 和 bash 工具进一步访问引用的 `references/``scripts/``assets/`
固定的 Agent 指令和扫描得到的技能目录在这里组成实际传给模型的 system prompt
### 加载完整内容
```python
def load_skill(name: str) -> str:
skill = SKILL_REGISTRY.get(name)
if not skill:
return f"Skill not found: {name}"
return skill["content"]
def load(self, name: str) -> str:
skill = self.skills.get(name)
if skill:
return skill["content"]
available = ", ".join(self.skills) or "none"
return f"Error: Unknown skill '{name}'. Available: {available}"
```
关键区别:技能内容不是 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 工具自动分发) |
`name` 用于查询启动时建立的注册表,不会被当作文件路径。工具返回后,原有 Agent Loop 会把内容作为新的 `tool_result` 消息追加
---
@@ -127,17 +118,17 @@ python s07_skill_loading/code.py
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`
3. `Review README.md and load the relevant skill first`
观察重点Agent 是否直接从 SYSTEM 里的目录知道有哪些技能?需要完整规范时是否出现 `[HOOK] load_skill`?加载后回答是否使用了对应 skill 的说明?
观察 system prompt 是否只有技能目录,以及调用 `load_skill` 后是否出现完整的 `SKILL.md` 内容。
---
## 接下来
按需加载解决了"不该带的不要带"。但另一个问题来了Agent 连续工作 30 分钟后messages 列表塞满了中间过程。旧的 tool_result、过时的文件内容占着上下文但不产生价值
随着工具调用增加,`messages[]` 会积累较早的文件内容和工具结果
s08 Context Compact → 四层压缩策略。便宜的先跑,贵的后跑
s08 Context Compact → 缩短较早的消息,为后续调用保留上下文空间
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
<!-- translation-sync: zh@v6, en@v6, ja@v6 -->