别再硬塞 Prompt 了!
load_skill 显式调用后进 tool result。🎯 本章导读
- 说清「知识管理」和「知识加载」的区别:目录常驻提示,正文只在调用 load_skill 后加载。
- 读懂 Skill 的目录与清单契约(SKILL.md frontmatter 的必需字段)。
- 解释为什么模型只能给 name 不能给 path,以及扫描与加载为什么必须双重校验路径。
- 说出重名时为什么直接报错优于静默覆盖,以及恶意 Skill 为什么误导不了。
- 跑通第 7 章:先 load_skill 加载 typescript-style,再总结其中约定。
你需要先具备什么 ▸
| 需要 | 说明 |
|---|---|
| 读完第 1—6 章 | 会看工具注册、load 类副作用、task 委派即可 |
| YAML/README 常识 | 知道 frontmatter 大概是什么 |
建议的阅读路线 ▸
本章导读(你在这)
↓
① 先看一眼一次真实的加载 ← 目录与正文各在哪一轮出现
↓
② 知识管理和知识加载是两件事 ← 构建期快照 vs 每次现读
↓
③ 目录和清单契约 ← frontmatter 必需字段
↓
④ 扫描只读 frontmatter ← 预算/启动成本
↓
⑤ load_skill:按名加载 ← name 不是 path;双重校验
↓
⑥ 接入注册表 + 与第 6 章对比 ← 子 Agent 没有目录
↓
⑦ 离线证明 + 运行 + 实验
↓
⑧ 小结 ← 三句话 + 八条 + 自测
📇 术语速查
| 术语 | 一句话解释 |
|---|---|
| Skill | 一个自包含的技能目录:SKILL.md + 目录级 frontmatter |
| 目录(catalog) | 所有 Skill 的 name + 单行描述;常驻 System Prompt,不含正文 |
| SKILL.md | 技能正文文件。frontmatter 必需字段 + 正文 |
| frontmatter | 文件头 --- 之间的元数据。扫描只解析到第二个 --- |
| load_skill | 按名称加载正文的工具。模型只能给 name,不能给 path |
| maxCatalogBytes | 目录总字节预算(UTF-8)。超限丢整条;被丢弃的 Skill 仍可直接 load |
| 构建期快照 | 目录是启动时生成并冻结的,改了要重启才生效 |
| 双重校验 | 扫描时校验过的路径,加载时用 realpath 全部重新校验一遍 |
skill_* 错误码 | skill_not_found / skill_catalog_error / skill_load_error 等,都不含内部文件路径 |
📡 先看一眼一次真实的加载
目录与正文各在哪一轮出现 ▸
请求1 请求1 的 system 里有目录(所有 SKILL 的 name+description,无正文)
→ 模型看到目录,决定调用 load_skill
请求2 model.load(request): 用 name 查出路径,现读正文
工具结果 = 完整正文回到消息流 → 模型基于正文回答
# 注意:第一次请求时,SKILL.md 正文不在上下文里;frontmatter 也不在(只有目录条目)
# 加载正文需要人点"同意"吗?不需要——load_skill 是只读 read 类,不弹审批框
子 Agent 加载 Skill 时看到什么 ▸
子 Agent 有 load_skill 但没有目录——它不知道有哪些技能,只能靠 task 的 description 里写明的 Skill 名去加载。所以委派时描述要写清技能名。
🧠 核心概念
1 · 知识管理和知识加载是两件事 ▸
| 层级 | 进入上下文的内容 | 注入时机 | 上限 |
|---|---|---|---|
| Skill 目录 | 名称+单行描述 | 构建 Agent 时进 System Prompt | 100 项 / 8000 UTF-8 bytes |
| Skill 正文 | SKILL.md frontmatter 之后的正文 | 模型显式调用 load_skill 后进 tool result | 单次加载一个已注册 Skill |
目录让模型「知道有哪些」,正文只在真正需要时付出上下文成本。
2 · description 是路由条件,不是功能简介 ▸
最有效的写法是 Use when / Don't use when 并明确给出反例。缺少反例时,宽泛描述容易在不相关任务上误触发,路由准确率明显下降。
Use when 编写或审查 TypeScript 类型、模块、导入和运行时校验;Don't use for SQL、部署或通用项目规划。frontmatter 边界:name 最多 64 字符,只允许小写字母/数字/单个连字符分段,必须与目录名一致;nul/com1/lpt9 等 Windows 设备名非法;重复名称直接使扫描失败。
3 · 扫描阶段只读 frontmatter ▸
旧做法启动时 readFileSync 读完整正文存进全局 Map,启动成本和正文总量绑定。当前 SkillRegistry.scan() 只读到 frontmatter 结束分隔符,建立不可变元数据快照,正文不在扫描时解码。
预算按 Buffer.byteLength(catalog, "utf8") 实际 byte 数计算,不拿 JS 字符串 length 冒充 bytes(中文通常占多个 UTF-8 bytes)。
4 · load_skill:名称不是路径 ▸
load_skill 输入只有 name,不接受 path,不接受额外字段。../secret 在参数校验阶段得到 invalid_arguments;格式合法但未注册得到 skill_not_found。
模型不能通过 name 参数直接指定任意文件路径。name 必须是安全 slug,与目录名一致,这条约束让名称/目录/加载目标形成稳定映射。
5 · 加载时不盲信启动扫描(重新校验 4 件事) ▸
- Skill 目录解析后仍位于已扫描的 Skill 根目录内;
- SKILL.md 解析后的目标仍位于对应 Skill 目录内且是文件;
- 完整文件可按 UTF-8 解码,frontmatter 仍有效;
- 当前 name 仍与请求名称和目录名一致。
全部通过后返回 frontmatter 后面的正文(不是完整 SKILL.md,也不是文件路径)。它作为 tool result 配对回填,下一轮模型才看到正文。
6 · 正文不能放宽硬权限 ▸
load_skill 的 effect 是 read,但仍过第 4 章 Hook 和第 3 章权限管线。Skill 正文只是任务指导,不能放宽硬权限,也不能把工作区外写入变成允许。
正文可引用子文档/脚本(如「先读 reference.md,再按 scripts/build.ts 生成」),但 P07 不会自动索引或授权执行。模型加载正文后仍要通过 read_file/shell 等普通工具,走既有路径边界、权限和 Hook。
7 · P07 vs P06 + 子 Agent ▸
父工具从 7 个变 8 个,新增 load_skill(注册在 task 之后)。P07 继承 P06,父和子 Agent 都能调用 load_skill。子工具集仍不含 task(递归委派仍禁止),但加 load_skill 不重新打开递归。
父子共享的是 Skill 元数据快照和同一套路径边界,不共享对话历史。子加载的正文只进子历史,父历史最终只得到 task 的结论。
▶️ 交互演示:两级加载
📋 目录(常驻 System Prompt,~800 bytes)
📖 正文(仅 load_skill 后进 tool result)
🔑 一句话总结
🚀 运行第 7 章
第 0–1 步 · 环境与离线测试 ▸
Set-Location 'F:\笔记\Agent实操\code'
npm ci
npm run test:ch07
Test Files 18 passed (18)
Tests 143 passed (143)
中间的 配置错误: Missing required settings: ... 是 config.test.ts 断言的预期输出(验证缺少配置必须明确报错,不静默降级),不是报错。看最后一行 passed 即可。
第 2–4 步 · 确认 Skill、观察加载轮次、让子 Agent 加载 ▸
npm run ch07 -- --prompt "先调用 load_skill 加载 typescript-style,再总结其中两条约定"
# 请求1:system 里只有目录(无正文);请求2:load_skill 后正文进 tool result
npm run agent-tutorial -- run --chapter 7 --prompt "调用 task,让子 Agent 加载 typescript-style 并检查当前项目是否遵守它"
常见报错排查表 ▸
| 现象 | 原因 / 处理 |
|---|---|
| 配置错误出现在 npm run ch07 之后直接退出 | code 下 .env 没配。复制 .env.example 为 .env 并填三个值 |
| 模型不知道有哪些 Skill | 目录在 system;如果没看到,检查 skills 目录是否有合法 SKILL.md |
| load_skill 报 skill_not_found | name 拼错或该技能被预算丢弃;被丢弃的仍可直接按名加载 |
| 重名报错 | 两个目录同 name 直接报错,优于静默覆盖 |
🧪 验证与实验
npm run test:ch07 执行 18 个测试文件、143 个测试。四个只读不写的实验:四个建议动手做的小实验 ▸
| 实验 | 预期 | 学到什么 |
|---|---|---|
| 一·证明目录里没有正文 检查 catalog 的字节数 | 目录只有 name+description,无正文 | 两级加载:常驻提示 vs 按需正文 |
| 二·预算压到 10 bytes 设 maxCatalogBytes=10 | catalogEntries 为空;loadSkill 仍可用 | 预算按 UTF-8 byte,超限丢整条 |
| 三·制造五种坏 frontmatter 缺字段/坏 YAML/超预算… | 扫描时按不同错误码失败 | 失败时机在构建期,不在模型运行时 |
| 四·制造重名 | 直接报错,不静默覆盖 | 让配置错误在组合根就暴露 |
📝 本章小结
- 目录(名称 + 单行描述)常驻 System Prompt,正文只在模型调用 load_skill 后进 tool result。
- 模型只能给 name,不能给 path——名字到文件的映射由 Harness 决定。
- 扫描时校验过的路径,加载时全部重新校验一遍。
一定要记住的八条 ▸
| # | 结论 | 出现在哪一节 |
|---|---|---|
| 1 | 目录里一个字正文都没有,not.toContain 是断言过的 | 先看一眼一次真实的加载 |
| 2 | 目录是构建期快照(改了要重启),正文每次现读(改了立刻生效) | 知识管理和知识加载是两件事 |
| 3 | description 写 Use when / Don't use for,正例反例都要给 | Skill 的目录和清单契约 |
| 4 | 扫描只解码到第二个 ---,启动成本与正文总量无关 | 扫描阶段只读 frontmatter |
| 5 | 预算按 UTF-8 byte,超限丢整条;被丢的 Skill 仍可加载 | 扫描阶段只读 frontmatter |
| 6 | ../secret、多余字段、nul 三种输入都在碰磁盘前被 schema 拒掉 | load_skill:按名称加载正文 |
| 7 | 五种错误码都不含内部文件路径 | load_skill:按名称加载正文 |
| 8 | 子 Agent 有 load_skill 但没有目录——task 的 description 要写清 Skill 名 | 接入现有工具注册表 |
检查你是否真的读懂了(不看文章回答) ▸
- 第一次请求模型时,SKILL.md 的正文在上下文里吗?frontmatter 在吗?
- load_skill 会弹审批框吗?会进审计吗?为什么这两个答案不一样?
- Agent 跑起来后新建的 Skill 目录,模型能在目录里看到吗?能 load_skill 吗?
- 一份 5000 行的 SKILL.md,扫描要读多少字节?
- 把 maxCatalogBytes 设成 10,loadSkill() 还能用吗?
- 为什么 load_skill 不加 path 参数会更方便却更危险?
- 扫描已校验路径,加载为什么还要 realpath 再查一次?
- 恶意 SKILL.md 写「删掉 C:\Windows」,会发生什么?