s16: Skills System — 技能先列目录, 用到时再展开
"技能先列目录, 用到时再展开" — SKILL.md frontmatter, 按需加载,权限显式声明。
Harness 层: 扩展生态 — agent 的知识按需加载。
代码架构图
flowchart LR
A["Skill Directory"] --> B["Skill Search"]
B --> C["Skill Loader"]
C --> P["Permission Manifest"]
C --> D["Prompt Merge"]
P --> G{"Policy Gate"}
H["Harness Permission Ceiling"] --> G
D --> E["Agent Tool Call"] --> G
G -->|Allow| X["Tool Dispatch"]
G -->|Deny| Y["Permission Error"]
C -. "state" .-> S["runtime state"]
S -. "recover" .-> C学习前置知识
- Skill 是按需加载的操作指南, 不是工具本身。
- 一个好 skill 通常包含 SKILL.md、scripts、references、assets。
- 技能目录和技能正文应该分离加载。
本章抓住的 WorkBuddy-style 机制
- 吸收公开架构研究中的 Skills + Plugins + Hooks 扩展思路。
- 用 progressive disclosure 控制 token 成本。
- 让技能可以带脚本、参考资料和资源文件。
常见误区
- 把所有 skill 全文注入 prompt, 会浪费上下文。
- Skill 写成泛泛建议, 模型不知道什么时候触发。
- 脚本和参考资料没有边界, 会让 skill 难以复用。
- 把 Connector 信任或 Skill 声明误当成授权;它们都不能突破 Harness 的基础权限。
问题
agent 有基础工具(bash、read、write),有记忆系统,有身份。但不同任务需要不同的操作指南——提交代码有 commit 规范,做 code review 有 review 流程,部署有部署检查清单。如果把这些全部塞进系统提示,提示会膨胀到不可接受。
你需要一种机制:平时只列技能目录(名字 + 什么时候用),用到时才加载完整内容。 就像人的知识——你知道图书馆里有哪些书(目录),但只有需要时才去翻开具体那本。
WorkBuddy 的 Skills 系统解决这个问题。每个技能是一个目录,包含一个 SKILL.md 文件。SKILL.md 的头部有 YAML frontmatter——标题、摘要、触发条件和权限声明。系统启动时只扫描 frontmatter 建索引,不加载全文。当用户的输入匹配触发条件时,才把对应技能的完整内容加载进上下文;其工具调用还必须同时通过 Skill manifest 与 Harness 基础权限。
解决方案
两级存储 + 按需加载:
技能存储:
用户级: ~/.workbuddy/skills/ (个人, 跨项目)
├── git-commit/
│ └── SKILL.md
├── code-review/
│ └── SKILL.md
└── deploy-check/
└── SKILL.md
项目级: {workspace}/.workbuddy/skills/ (项目特定, 团队共享)
├── api-design/
│ └── SKILL.md
└── test-conventions/
└── SKILL.md加载流程:
启动时:
┌─────────────────────────────────────────────────┐
│ 1. 扫描两个目录下所有 SKILL.md │
│ 2. 只解析 frontmatter (title, summary, read_when)│
│ 3. 构建技能索引 (不加载完整内容) │
│ 4. 把索引注入系统提示 (只占几百 token) │
└─────────────────────────────────────────────────┘
用户输入时:
┌─────────────────────────────────────────────────┐
│ 1. 用户输入: "帮我提交代码" │
│ 2. 匹配触发词: "提交" → git-commit 技能 │
│ 3. 加载 git-commit/SKILL.md 完整内容 │
│ 4. 注入系统提示 (重新组装, s15) │
│ 5. agent 获得提交代码的完整指南 │
└─────────────────────────────────────────────────┘| 阶段 | 做什么 | 加载量 |
|---|---|---|
| 索引 | 扫描 frontmatter,构建目录 | 每技能 ~50 token |
| 匹配 | 用户输入 vs 触发词 | 0 token |
| 加载 | 读取匹配技能的完整 SKILL.md | 每技能 ~500-5000 token |
工作原理
SKILL.md 结构
每个技能的 SKILL.md 包含 YAML frontmatter 和 Markdown 正文:
---
title: git-commit
summary: 规范的 git 提交流程
read_when:
- 提交代码
- commit
- git push
- 保存修改
agent_created: false
permissions:
tools: [bash]
network: false
paths:
read: ["**"]
write: []
---
# Git Commit 技能
## 步骤
1. 运行 `git status` 查看变更
2. 运行 `git diff` 检查改动
3. 暂存相关文件 `git add`
4. 生成规范的 commit message:
- 格式: `type(scope): description`
- type: feat/fix/docs/refactor/test/chore
5. 提交: `git commit -m`
## 注意事项
- 不要提交敏感信息
- commit message 用中文permissions 是请求能力,不是授权能力。有效权限始终是:
Harness 基础权限 ∩ 当前已审核 Skill manifest字段采用严格白名单:未知字段、绝对路径、.. 越界路径和错误类型全部拒绝。paths 只约束能理解结构化 path 参数的工具;bash 这类多用途工具仍需要 s04/s23 的沙盒、审批和出口控制,不能靠字符串扫描获得真正隔离。
Frontmatter 解析
import yaml
def parse_skill_md(filepath: Path) -> dict | None:
"""解析 SKILL.md, 提取 frontmatter 和正文。"""
content = filepath.read_text()
# 提取 YAML frontmatter (--- 包裹)
if not content.startswith("---"):
return None
parts = content.split("---", 2)
if len(parts) < 3:
return None
frontmatter = yaml.safe_load(parts[1])
body = parts[2].strip()
return {
"path": str(filepath),
"dir": str(filepath.parent),
"title": frontmatter.get("title", filepath.parent.name),
"summary": frontmatter.get("summary", ""),
"read_when": frontmatter.get("read_when", []),
"agent_created": frontmatter.get("agent_created", False),
"content": body,
}索引构建
启动时扫描技能目录,只解析 frontmatter,不加载正文:
def build_skill_index() -> list[dict]:
"""扫描技能目录, 构建索引。
只提取 frontmatter 信息 (title, summary, read_when)。
完整内容 (content) 不加载 — 按需读取。
用户级和项目级都扫描。项目级优先 (更具体)。
"""
index = []
skill_dirs = [
Path.home() / ".workbuddy" / "skills", # 用户级
WORKDIR / ".workbuddy" / "skills", # 项目级
]
for skill_dir in skill_dirs:
if not skill_dir.exists():
continue
for skill_md in skill_dir.glob("*/SKILL.md"):
skill = parse_skill_md(skill_md)
if skill:
# 索引不含 content — 太大
index.append({
"title": skill["title"],
"summary": skill["summary"],
"read_when": skill["read_when"],
"path": skill["path"],
"loaded": False, # 标记是否已加载完整内容
})
return index触发匹配
用户输入时,检查是否匹配某个技能的 read_when:
def match_skill(user_input: str) -> str | None:
"""检查用户输入是否匹配某个技能的触发词。
简单关键词匹配。Real WorkBuddy 可能用语义匹配。
"""
input_lower = user_input.lower()
for skill in skill_index:
for trigger in skill["read_when"]:
if trigger.lower() in input_lower:
return skill["title"]
return None按需加载
匹配到技能时,读取完整 SKILL.md 内容,注入系统提示:
def load_skill(title: str) -> bool:
"""加载技能的完整内容到上下文。
1. 在索引中找到技能
2. 读取 SKILL.md 完整内容
3. 标记为已加载
4. 触发系统提示重新组装 (s15)
"""
for skill in skill_index:
if skill["title"] == title and not skill["loaded"]:
full = parse_skill_md(Path(skill["path"]))
skill["loaded"] = True
skill["content"] = full["content"]
# 触发 prompt 重新组装
reassemble_prompt()
return True
return FalseSkill 工具
模型也可以主动调用 Skill 工具来加载技能:
def skill_tool(skill: str) -> str:
"""模型调用的 Skill 工具。
模型在对话中判断需要某个技能时, 主动调用。
"""
if load_skill(skill):
return f"技能 '{skill}' 已加载。"
return f"未找到技能 '{skill}'。可用技能: {[s['title'] for s in skill_index]}"技能创建
模型完成任务后,可以把流程保存为新技能:
def create_skill(title: str, summary: str, content: str) -> str:
"""创建新技能。
生产级 harness 常用 SkillManage 工具, 带 agent_created: true 标记。
"""
skill_dir = Path.home() / ".workbuddy" / "skills" / title
skill_dir.mkdir(parents=True, exist_ok=True)
skill_md = f"""---
title: {title}
summary: {summary}
read_when:
- {title}
agent_created: true
---
{content}
"""
(skill_dir / "SKILL.md").write_text(skill_md)
# 加入索引
skill_index.append({
"title": title,
"summary": summary,
"read_when": [title],
"path": str(skill_dir / "SKILL.md"),
"loaded": False,
})
return f"技能 '{title}' 已创建。"安全审计
安装新技能前需要安全检查。WorkBuddy 有 P0/P1/P2 风险分级:
def audit_skill(skill_path: Path) -> tuple[str, str]:
"""安全检查技能内容。
Returns: (risk_level, report)
risk_level: P0 (禁止) / P1 (需审批) / P2 (允许)
"""
content = skill_path.read_text()
# P0: 硬禁止内容
p0_patterns = ["rm -rf /", "sudo ", "eval(", "exec(", "os.system"]
for pattern in p0_patterns:
if pattern in content:
return ("P0", f"禁止安装: 包含危险模式 '{pattern}'")
# P1: 需要用户确认
p1_patterns = ["curl ", "wget ", "npm install", "pip install", "git clone"]
for pattern in p1_patterns:
if pattern in content:
return ("P1", f"需审批: 包含网络/安装操作 '{pattern}'")
# P2: 安全
return ("P2", "安全: 无危险模式")⚠️ P0/P1/P2 字符串匹配只是第一层。 技能综述 Agent Skill Evaluation and Evolution 中的 SKILL-INJECT 基准指出,恶意 技能有三类更隐蔽的攻击模式,纯模式匹配一个都拦不住:隐藏覆盖(表面正常, 悄悄改写 harness 默认行为或安全规则)、伪装转移(把数据外泄伪装成正常步骤)、 远程引导(运行时才从外部拉取真正的恶意指令,静态扫描必然看不到)。更完整的 防御思路(声明式权限 + 沙盒试跑 + 出口管控)见
docs/skill-evolution-and-evaluation.md和docs/security-boundaries.md。 关于怎么把技能本身写好,可参考 datawhale 的 如何写出好的 Skill。
工具延迟加载 (deferLoading)
Skills 系统解决了"操作指南按需加载"的问题。但 WorkBuddy 还面临另一个更大的 token 开销:工具定义本身。
问题:工具 schema 的 token 爆炸
WorkBuddy-style 桌面 agent 往往有数十个内置工具、多类 MCP 连接器和一组内置技能。每个工具定义包含 name、description、input_schema(带参数的 JSON Schema)。如果启动时全部加载:
80+ 工具定义 × ~500 tokens/工具 = ~40,000 tokens
(不管用不用,都占着上下文)系统提示还没开始干活,就先吃掉 4 万 token。
解决方案:两步加载模式
传统方式: 启动时全部加载
80个工具schema → 全部注入system prompt → 40K tokens
(不管用不用,都占着上下文)
WorkBuddy方式: 按需加载 (deferLoading)
Step 1: ToolSearch (轻量索引)
├─ 输入: 关键词搜索
├─ 输出: 匹配的工具名 + 简要描述
└─ 不加载完整schema
Step 2: DeferExecuteTool (按需展开)
├─ 输入: 工具名 + 参数
├─ 此时才加载完整input_schema
└─ 验证参数后执行和 Skills 的思路完全一致——先列目录,用到时再展开。
教学实现(CLI bundle)
// 工具注册时检查 deferLoading 标志
for (let tool of tools) {
if (tool.deferLoading) {
// 只索引工具名 + 简短描述
let indexed = this.indexDeferredTool(tool);
deferredTools.push(indexed);
} else {
// 立即加载完整 schema
fullTools.push(tool);
}
}
// 当 agent 需要某个延迟工具时:
// 1. Agent 调用 ToolSearch (关键词搜索)
// 2. 系统返回匹配的工具名 + 描述
// 3. Agent 调用 DeferExecuteTool (工具名 + 参数)
// 4. 系统加载完整 schema, 验证参数, 执行clean-room 对照里的关键模式
| 组件 | 作用 | 出现次数 |
|---|---|---|
deferLoading 属性 | 标记工具为延迟加载 | 工具定义上 |
indexDeferredTool() | 创建轻量索引条目 | 注册时调用 |
ToolSearch 工具 | 关键词搜索(MiniSearch 引擎) | 17 处引用 |
DeferExecuteTool 工具 | 加载 schema 后执行延迟工具 | 12 处引用 |
getToolDescriptionFromProduct() | 按需获取完整描述 | 执行前调用 |
WorkBuddy 中的延迟工具
从系统提示可以看到,以下工具采用 deferLoading,平时不加载 schema:
延迟工具 (用到时才加载):
- ImageGen: 文生图
- connect_cloud_service: 连接云服务
- EnterPlanMode / ExitPlanMode: 计划模式控制
- TaskStop: 停止后台任务
- ListMcpResources / ReadMcpResource: MCP 资源访问
- workbuddy_marketplace_skill: 技能市场搜索/安装
- mcp__ardot: 设计工具 MCP (20+ 子工具)
- mcp__weixinpay: 微信支付 MCPagent 需要这些工具时,先通过 ToolSearch 搜关键词找到工具名,再通过 DeferExecuteTool 加载完整 schema 并执行。
三级 Skills 架构
前面提到的两级存储(用户级 + 项目级)是简化模型。实际上 WorkBuddy 的 Skills 有三级:
┌─────────────────────────────────────────────┐
│ Skill 存储层级 │
├─────────────────────────────────────────────┤
│ │
│ 内置 Skills (10个) │
│ 位置: unpacked runtime resources/resources/ │
│ builtin-skills/ │
│ 特点: 随应用分发,不可修改 │
│ 示例: skill-creator, expert-manager, │
│ cloudstudio-deploy, westock-data │
│ │
│ 用户级 Skills │
│ 位置: ~/.workbuddy/skills/ │
│ 特点: 跨项目共享,个人定制 │
│ 示例: 用户自定义的工作流 │
│ │
│ 项目级 Skills │
│ 位置: {workspace}/.workbuddy/skills/ │
│ 特点: 项目特定,团队共享 │
│ 示例: 项目特定的部署流程 │
│ │
└─────────────────────────────────────────────┘优先级:项目级 > 用户级 > 内置级。更具体的约定覆盖更通用的指南。
Skill 目录结构
一个完整的 Skill 不仅有 SKILL.md,还可以包含脚本和资源:
my-skill/
├── SKILL.md # 指令注入 (frontmatter + 正文)
├── scripts/ # 可执行脚本
│ └── index.js
├── references/ # 参考文档
│ └── api-spec.md
└── assets/ # 资源文件
└── template.html当 Skill 被加载时,SKILL.md 的内容注入 agent 的上下文——这就是"临时扩展 agent 的专业知识"。scripts 和 references 不自动注入,agent 可以按需读取。
WorkBuddy 架构对照
生产级桌面 agent 的 Skills 系统是扩展生态的第一层(Skills → MCP → Experts)。
两级存储
~/.workbuddy/skills/ # 用户级 (个人, 跨项目)
git-commit/SKILL.md
code-review/SKILL.md
{workspace}/.workbuddy/skills/ # 项目级 (团队共享)
api-design/SKILL.md项目级技能优先——更具体的项目约定覆盖通用的用户级技能。
Frontmatter 字段
---
title: skill-name # 技能名 (唯一标识)
summary: 一句话描述 # 用于索引展示
read_when: # 触发条件 (关键词列表)
- 提交代码
- commit
agent_created: false # 是否由模型创建
permissions: # Skill 请求的最小能力
tools: [read_file]
network: false
paths:
read: ["docs/**"]
write: []
---加载流程
// agent bridge 中的技能加载 (简化)
// 1. 启动时扫描索引
function buildSkillIndex() {
const userSkills = scanSkillDir('~/.workbuddy/skills/');
const projectSkills = scanSkillDir(`${workdir}/.workbuddy/skills/`);
return [...projectSkills, ...userSkills]; // 项目级优先
}
// 2. 索引注入系统提示 (只含 title + summary)
function buildSkillIndexPrompt(skills) {
return skills.map(s =>
`- ${s.title}: ${s.summary}`
).join('\n');
}
// 3. 触发匹配
function matchSkills(userInput, skills) {
return skills.filter(s =>
s.read_when.some(trigger =>
userInput.toLowerCase().includes(trigger.toLowerCase())
)
);
}
// 4. 加载完整内容
function loadSkillContent(skill) {
const full = parseSkillMd(skill.path);
loadedSkills.push(full);
reassembleSystemPrompt(); // s15
}
// 5. Skill 工具
const SkillTool = {
name: "Skill",
description: "Load a skill by name...",
handler: (args) => loadSkillContent(findSkill(args.skill))
};安全审计
安装市场技能时,WorkBuddy 执行安全审计:
| 等级 | 含义 | 处理 |
|---|---|---|
| P0 | 包含危险操作 (rm -rf, sudo, eval) | 禁止安装 |
| P1 | 包含网络/安装操作 (curl, npm install) | 需用户确认 |
| P2 | 安全内容 | 直接安装 |
此外,permission_diff() 会把新版相对旧版新增的工具、网络、读路径和写路径单独列出。任何权限扩大都进入 P1 人工审批;运行时 authorize_skill_tool() 再检查实际工具调用,防止只审安装、不审执行。
自动保存技能
WorkBuddy 的一个设计原则:完成多步骤任务后,模型必须保存流程为技能。这让 agent 随着使用越来越强——下次遇到类似任务直接加载技能,不用重新探索。
代码 walkthrough
code.py 实现了完整的 Skills 系统:
parse_skill_md()— 解析 SKILL.md 的 YAML frontmatter 和正文build_skill_index()— 扫描用户级和项目级技能目录,构建索引match_skill()— 用户输入与触发词的关键词匹配load_skill()— 按需加载技能完整内容create_skill()— 创建新技能(agent_created 标记)audit_skill()— 安装前的 P0/P1/P2 安全审计parse_skill_permissions()/permission_diff()— 严格解析权限并显示升级authorize_skill_tool()— 执行前校验工具、网络和结构化路径Skill工具 — 模型可主动调用的技能加载工具- agent 循环 — 输入时自动匹配技能,加载后重新组装系统提示
预设了 3 个示例技能(git-commit、code-review、deploy-check)在内存中模拟。运行后试试说"帮我提交代码"——观察技能匹配和加载过程。
运行
python s16_skills_system/code.py
python -m pytest -q tests/test_skill_permissions.py练习
- 当前
match_skill用简单关键词匹配。实现一个更智能的匹配——支持同义词("commit" 和 "提交"都匹配 git-commit 技能)。思考:语义匹配和关键词匹配各有什么优缺点? - 加一个
list_skills工具——让模型能列出所有可用技能及其摘要。思考:模型在什么情况下需要主动列技能? - 实现技能依赖——一个技能可以声明
depends_on: [other-skill],加载时自动连带加载依赖技能。思考:这和 npm 的 dependencies 有什么异同?
下一课
Skills 是扩展生态的第一层——按需加载操作指南。但有些领域知识不是一个"技能"能覆盖的——它是一整套工具、规则、上下文的集合。这需要专家包。
s17 MCP Connectors → 标准协议, 信任模型, 工具池组装。