06 -- 技能系统:把经验装进书架
每天从零推理同一件事
子代理隔离了中间产物,上下文干净了。但隔离解决不了另一个浪费------重复推理。
让模型执行一次代码提交,它从头推理:检查变更、逐文件暂存、写提交信息。这套流程你每天用十次,模型每次从零走一遍------耗 token、路径不同、质量不稳定。人会把反复执行的流程内化为习惯,agent 需要同样的机制:把反复使用的推理路径预编译成指令,用到时直接加载。 一个技能就是一份写给模型看的操作手册。
知识,不是代码
工具和技能容易混淆。工具 = 能力 ------执行命令、读写文件,硬编码在程序里。技能 = 知识------提交规范、审查标准、测试策略,写在纯文本里。
两者的变更节奏完全不同。改一行提交规范不应该需要重新部署;任何人都能写纯文本,不需要编程能力;纯文本天然适配版本控制和团队共享。代码定义机制,文本定义内容,各自以自己的速率演进。
SKILL.md 的结构
技能以目录为单位,每个技能的存放路径遵循固定约定:
.agents/skills/<skill-name>/SKILL.md
<skill-name> 就是技能名------小写字母加连字符,1-64 字符,全局唯一。目录名即技能名,SKILL.md 是入口文件。一个项目可以有多个技能,并排放在 .agents/skills/ 下:
.agents/skills/
├── commit-workflow/
│ ├── SKILL.md # 必须:指令 + 元数据
│ ├── scripts/ # 可选:可执行脚本
│ ├── references/ # 可选:参考文档
│ └── assets/ # 可选:模板、资源
├── code-review/
│ └── SKILL.md
└── security-guidance/
└── SKILL.md
扫描器启动时遍历 .agents/skills/ 下的每个子目录,解析其中的 SKILL.md。目录结构保持一层深度------扁平意味着模型不需要递归查找,人也不需要翻找。
SKILL.md 由 YAML frontmatter 和 Markdown body 两部分组成:
yaml
---
name: commit-workflow # 必需,1-64 字符,小写 + 连字符
description: | # 必需,1-1024 字符
代码提交工作流。用户要求提交代码时激活。
license: MIT # 可选
allowed-tools: Bash Read # 可选,空格分隔,限制可用工具
---
## 提交规范
1. 运行 git status 检查变更
2. 逐文件暂存,不要 git add -A
3. 提交信息使用祈使语气,首行不超过 72 字符
两个必需字段各有职责。name 是唯一标识,扫描和覆盖靠它定位。description 不只描述"做什么",更要说"什么时候用"------模型靠这行描述判断当前任务是否匹配,描述越准确,激活越精准。
Body 部分无格式限制,推荐包含分步指令、输入输出示例和边界情况处理。引用资源文件用相对路径(scripts/lint.sh),与目录结构的扁平原则一致。
三层渐进式披露
20 个技能全量注入约 40K tokens,占窗口 20%,大部分与当前任务无关。解法是按需逐层展开------先看书脊,感兴趣翻目录,确定相关了才读正文:
任务匹配描述
指令引用文件
Metadata
name + description
≈100 tokens/技能
Instructions
完整 SKILL.md body
推荐 <5000 tokens
Resources
scripts/ references/ assets/
按需加载
Metadata。 启动时从所有 SKILL.md 的 frontmatter 提取 name 和 description,注入上下文。20 个技能约 2000 tokens------模型扫一眼就知道书架上有什么,几乎不占空间。不相关的技能永远停留在这一层。
Instructions。 模型判断某个技能与当前任务相关时,调用 load_skill 工具,加载完整 body。内容作为工具返回值追加到消息历史,一次性成本。Body 推荐控制在 5000 tokens 以内------太长会稀释上下文,违背"预编译"的初衷。
Resources。 执行 body 中的指令时,才读取 scripts/、references/、assets/ 下的文件。这些资源从不主动加载,只在指令明确引用时按需获取。
成本曲线很陡:大部分技能停留在第一层(约 100 tokens),少数进入第二层(几千 tokens),资源文件只在执行时才产生开销。
注入位置:追加,不改前缀
技能内容追加到最后一条用户消息的末尾,不修改系统提示词。原因是缓存:大模型 API 普遍支持前缀缓存------请求前缀相同就复用已缓存的 KV 向量。改系统提示词一个字节,缓存全部失效。追加到消息末尾,前缀始终不变,缓存始终命中。
beforeModel(state):
// autoload 技能:每轮直接注入正文,跳过 Metadata 直入 Instructions
for skill in registry:
if skill.autoload:
inject(skill.body)
// 其余技能:只注入 Metadata 层
summary = registry
.filter(s => !s.autoload)
.map(s => "- {s.name}: {s.description}")
append_to_last_user_message(summary)
autoload 是一个特殊标记。标记了的技能(如输出风格规范)每轮自动注入完整正文,跳过 Metadata 直接进入 Instructions。适合每次对话都必定用到的基础规范------数量少、体积小,省去模型每次主动加载的开销。
小结
技能把运行时的重复推理转移到编写时。SKILL.md 用 frontmatter 声明身份,用 body 承载指令,用子目录挂载资源。三层渐进式披露------Metadata、Instructions、Resources------确保模型只为真正用到的知识付费。追加注入保护缓存命中率。
但不管书架管理得多精细,对话本身一直在增长------每轮工具调用、每次文件读取都在往窗口里堆东西。窗口终会满,届时需要的不是更聪明的加载,而是压缩。