当我们给一个 Agent "加能力"时,到底应该写一份 Markdown,还是写一段 TypeScript?这个问题看似工程细节,实则关乎 Agent 系统的架构边界。本文以 OpenClaw 的实现为样本,剖析 Skill 与 Tool 两种抽象的本质差异,并给出可落地的决策框架。
引言:一个真实的架构困境
arduino
用户:"帮我加一个部署能力。"
工程师 A:"写个 deploy.ts Tool。"
工程师 B:"写个 SKILL.md。"
两人都有道理。问题在于,这是一个 policy-mechanism separation 问题:部署涉及的"知识流程"(审批流程、回滚策略、哪个环境用什么策略)需要灵活表述,而"执行操作"(调 kubectl、docker push)需要安全校验------两边都要,但放错位置就会出问题。
OpenClaw 的设计选择是:Skill 承载知识层,Tool 承载执行层,二者通过 dispatch 正交桥接。本文从源码出发,拆开这个设计。
一、先看清两个东西"长什么样"
1.1 Tool:一段可被 LLM 调用的"函数"
OpenClaw 的核心工具清单定义在 src/agents/tool-catalog.ts:41-244:
typescript
const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [
{ id: "read", sectionId: "fs", profiles: ["coding"] },
{ id: "write", sectionId: "fs", profiles: ["coding"] },
{ id: "exec", sectionId: "runtime", profiles: ["coding"] },
{ id: "web_fetch", sectionId: "web", profiles: ["coding"] },
// ... 共 31 个核心工具,按 section 分组
];
每个 Tool 的实现形态是一段 TypeScript:name、description、TypeBox 描述的 parameters schema ,以及一个 execute() 函数。
转换层在 src/agents/pi-tool-definition-adapter.ts:16-58,把 AgentTool[] 打包成 ToolDefinition[],喂给底部 LLM 用于 function calling:
typescript
export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
return tools.map((tool) => ({
name, label, description, parameters,
execute: async (...args: ToolExecuteArgs): Promise<AgentToolResult<unknown>> => {
// 1. beforeToolCall 安全闸门
// 2. 执行 tool.execute()
// 3. 结果归一化
},
}));
}
Tool 的关键特征:
- 结构化输入输出:参数有强 schema(TypeBox),结果有规范化格式;
- 安全闸门 :
beforeToolCall钩子 + 沙箱 + 结果截断; - 模型隐式调用:模型根据上下文判断"该不该用";
- Profile 控制下发 :
minimal / coding / messaging / full决定工具列表长度。
1.2 Skill:一份可被 LLM 阅读的"操作手册"
Type 定义在 src/agents/skills/types.ts:51-71:
typescript
export type SkillCommandSpec = {
name: string;
skillName: string;
description: string;
dispatch?: SkillCommandDispatchSpec; // { kind: "tool", toolName: string }
};
export type SkillEntry = {
skill: Skill;
frontmatter: ParsedSkillFrontmatter;
metadata?: OpenClawSkillMetadata;
invocation?: SkillInvocationPolicy;
};
物理形态是一个目录:SKILL.md + 可选的 scripts/、references/、assets/。
加载逻辑在 src/agents/skills/workspace.ts:按工作区动态扫描 ,按预算(默认 30,000 字符 [1]、最多 150 个 skill)拼成 SkillSnapshot.prompt 作为系统提示词片段。
提示词预算 来自
src/agents/skills/workspace.ts:27-29:
typescriptconst DEFAULT_MAX_SKILLS_PROMPT_CHARS = 30_000; const DEFAULT_MAX_SKILLS_IN_PROMPT = 150; const DEFAULT_MAX_SKILL_FILE_BYTES = 256_000;
Skill 的关键特征:
- 以自然语言为主:给模型"读"的,不是给程序"调"的;
- 三级渐进披露:metadata(始终在上下文)→ SKILL.md body(被触发时加载)→ bundled resources(按需请求);
- 声明式依赖 :
requires.bins、requires.env、install[]声明依赖环境; - 支持
/slash命令显式触发 :见src/auto-reply/skill-commands.ts:166-204。
1.3 两者如何桥接?
关键字段在 src/agents/skills/types.ts:40-49:
typescript
export type SkillCommandDispatchSpec = {
kind: "tool";
toolName: string; // 调用的目标 Tool
argMode?: "raw"; // raw = 直接透传用户参数
};
skill-commands.ts:191-196 实现了 dispatch 逻辑:
typescript
if (command.dispatch?.kind === "tool") {
// 找到 toolName 对应的 Tool,执行它
const tool = findToolByName(command.dispatch.toolName);
return tool.execute(rawArgs);
}
这就是架构的精妙之处:Skill 提供语义层,Tool 提供执行层,二者通过 dispatch 正交桥接。
perl
┌─────────────────────────────────────────────────────────┐
│ Agent 运行时 │
├─────────────────────────────────────────────────────────┤
│ 系统 Prompt │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SkillSnapshot.prompt (30k budget) │ │
│ │ ├─ always: metadata (~100 chars) │ │
│ │ └─ on trigger: SKILL.md body │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ToolDefinitions[] (by Profile) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ read, write, exec, web_fetch, memory_search... │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ dispatch 桥接 │
│ Skill (描述层) ──dispatch──► Tool (执行层) │
└─────────────────────────────────────────────────────────┘
二、本质差异:知识 vs 能力
| 维度 | Skill | Tool |
|---|---|---|
| 抽象层级 | 知识 / 流程 / 约定 | 原子动作 / 副作用 |
| 载体 | Markdown(给模型读) | TypeScript(给程序执行) |
| 触发方式 | 描述匹配 + /slash |
模型 function call |
| 状态 | 无状态、纯文本 | 有副作用、可写文件/网络 |
| 变更成本 | 改文件即生效,无需发版 | 需要代码评审、发版 |
| 安全模型 | 进入 prompt,被模型自由解读 | beforeToolCall 闸门、沙箱、ACL |
| 下发预算 | 共享 30k 字符上下文预算 | Profile 控制工具数量 |
| ��观测性 | 出现在 prompt 即可见 | 每次调用有结构化日志 |
一句话:Skill 教模型"该怎么做",Tool 让模型"能够做"。
三、什么时候用 Skill?
3.1 典型案例:1Password Skill
skills/1password/SKILL.md 是 Skill 的最佳示范。核心知识是:
- "
op之前必须op signin"(操作顺序) - "OTP 字段路径是
op://Private/Npmjs/one-time password?attribute=otp"(领域细节) - "所有
op命令必须在 tmux 里跑"(环境约束)
这些不是新功能,而是 "领域专家知道、但模型不知道"的操作手册 。底层执行靠已有的 exec Tool 调 op 命令------Skill 的价值是把这类知识注入上下文。
→ 判据:能力是"知识 + 既有 CLI 的编排顺序" → Skill
3.2 典型案例:Skill Creator
skills/skill-creator/SKILL.md 定义了 skill 编写规范:
- 三级渐进披露结构(metadata → body → bundled resources)
- 建议的自由度分级(高/中/低)
- 目录结构规范(
SKILL.md+scripts/+references/+assets/)
这是meta 能力的典型:不是给机器执行,而是 "教模型如何写一个新的 Skill"。
→ 判据:能力是"生成或理解其他能力的方法论" → Skill
3.3 何时判定用 Skill?
| 问题 | 答案 → |
|---|---|
| Q1: 是不是"先 A 再 B,注意 C 陷阱"的流程知识? | 是 → Skill |
| Q2: 需要多种合法路径、让模型自己决策? | 是 → Skill |
| Q3: 项目/用户会频繁定制/修改这个能力? | 是 → Skill |
Q4: 需要暴露 /xxx 这样可读的命令入口? |
是 → Skill |
| Q5: 能力需要附带模板、示例、参考资料? | 是 → Skill |
四、什么时候用 Tool?
4.1 典型案例:Web Fetch Tool
src/agents/tools/web-fetch.ts 是复杂 Tool 的实现样本。关键特征:
- TypeBox 参数校验 :
url(必填)、extractMode?(枚举)、maxChars?(数值); - 执行安全保障:超时控制、响应截断、SSRF 黑名单;
- 可观测性:每次调用产生结构化日志。
→ 判据 :需要严格参数校验 + 安全闸门 + 审计 → Tool
4.2 典型案例:Core Tool Catalog
src/agents/tool-catalog.ts:41-244 定义了 31 个核心工具,分为 11 个 Section:
scss
fs (read/write/edit) runtime (exec/process)
web (web_search/fetch) memory (search/get)
sessions (list/history/spawn) ui (browser/canvas)
messaging (message) automation (cron/gateway)
nodes (nodes) agents (list)
media (image/tts)
这些是 平台级原子能力:所有 Agent 都需要、协议稳定、不随项目变化。
→ 判据 :能力的本质是 I/O、进程、网络等副作用 → Tool
4.3 何时判定用 Tool?
| 问题 | 答案 → |
|---|---|
| Q1: 能力的本质是读/写/网络/进程等副作用? | 是 → Tool |
| Q2: 参数错误会导致严重后果(删错文件、误付费)? | 是 → Tool |
Q3: 需要进入 beforeToolCall 安全闸门、ACL、可审计? |
是 → Tool |
| Q4: 结果需要被程序结构化消费(后续 Tool / UI)? | 是 → Tool |
| Q5: 能力是平台级、跨项目通用、契约需稳定? | 是 → Tool |
五、灰色地带:Skill 调 Tool 的复合模式
ini
┌─────────────────────────────────────────────────────────────┐
│ 复合能力示例:/deploy 命令 │
├─────────────────────────────────────────────────────────────┤
│ │
│ deploy Skill (知识层) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ ## 部署流程 │ │
│ │ 1. 运行测试 ─► 2. 构建镜像 ─► 3. 推送到仓库 │ │
│ │ 4. 滚动更新 ─► 5. 验证 Health │ │
│ │ │ │
│ │ ## 权限约束 │ │
│ │ - 生产环境只能 role=admin 的用户触发 │ │
│ │ - 必须在 deploy 前完成 code review │ │
│ │ │ │
│ │ ## 回滚策略 │ │
│ │ - 失败自动回滚到上一个 tag │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ dispatch="tool" │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ deploy-orchestrator Tool (执行层) │ │
│ │ ├─ run-tests.sh │ │
│ │ ├─ docker build && tag │ │
│ │ ├─ kubectl rolling-update │ │
│ │ └─ health-check loop │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
这种模式的架构价值:用 Skill 表达 policy(哪些人能在什么环境下做什么),用 Tool 实现 mechanism(底层执行细节)。Unix 的经久不衰的设计原则,在 Agent 系统里同样成立。
六、决策框架:四个问题
yaml
┌─────────────────┐
│ 加一个新能力 │
└────────┬────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Q1: 知识 │ │ Q2: 安全 │ │ Q3: 定制 │
│ 还是 │ │ 需要 │ │ 权在 │
│ 副作用? │ │ 闸门? │ │ 项目? │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
┌──────┴──────┐ │ ┌──────┴──────┐
▼ ▼ ▼ ▼ ▼
Skill Tool Tool Skill Tool
(编排知识) (I/O) (安全) (项目级) (平台级)
Q1:能力本质是知识 + 现有工具编排,还是对外部世界的新副作用?
- 知识/编排 → Skill
- 新副作用 → Tool
Q2:需要严格的参数校验 + beforeToolCall 安全闸门 + ACL 吗?
- 需要 → Tool(即使本质是知识,执行部分也要沉淀为 Tool)
Q3:能力需要项目/用户自由定制吗?
- 需要 → Skill(即使背后调 Tool,对外暴露的入口是 Skill)
Q4:结果是否需要被程序结构化消费?
- 是 → Tool
- 否(只给人/模型读) → Skill
复合判定 :如果 Q1=Tool 且 Q3=需要定制 → 两层设计:底层 Tool 稳定提供 + 上层 Skill 灵活编排。
七、常见反模式
反模式 1:把领域知识硬编码成 Tool
typescript
// ❌ 反例
const tool = {
name: "write-good-commit",
execute: async () => {
// 强制要求 "[type]: description" 格式
// 问题:模型失去灵活性,每次都被迫走固定路径
}
}
问题:强制路径 = 丧失模型的决策空间。领域知识应该是 Skill。
反模式 2:把副作用,塞进 Skill 的 Markdown
markdown
<!-- ❌ 反例 -->
## 执行部署
运行以下命令:
\`\`\`bash
kubectl apply -f deployment.yaml
\`\`\`
问题 :失去 beforeToolCall 保护,无法 ACL,无法审计。所有副作用必须经 Tool。
反模式 3:Skill 数量爆炸,没有 filter
typescript
// ❌ 反例:���做 skillFilter
const snapshot = buildWorkspaceSkillSnapshot(workspaceDir, {
// 没有 skillFilter,全部加载
});
// 结果:30k 预算被稀释,核心能力被淹没
正确姿势 :在 agent-scope.ts 中为每个 Agent 配置 skillFilter,按 Agent 类型加载不同 skill 集合。
反模式 4:Tool 数量爆炸,没有 Profile
typescript
// ❌ 反例:给 messaging Agent 加载 coding profile
const profile = "coding"; // 不该暴露给 messaging
// 结果:误调用风险 + 上下文浪费
正确姿势 :按 Agent 类型选 Profile:minimal(仅状态查询)、coding(有 fs/web)、messaging(仅 message)、full(全量)。
反模式 5:Skill 和 Tool 功能重复
ini
Skill name="read-file" ←─ dispatch ──► Tool name="read"
↑ ↑
重复! 去重!
正确姿势 :SkillCommandSpec.dispatch 指向前置 Tool,或者直接砍掉 Skill 变体。dedupeBySkillName() 只是兜底,不该被正常路径依赖。
八、结论
Tool 是 Agent 的"硬件指令集",Skill 是 Agent 的"操作系统教程"。前者决定能做什么,后者决定该怎么做。
OpenClaw 的设计之所以健壮,正是因为两层正交:
sql
┌─────────────────────────────────────────────────────────┐
│ Agent 能力体系 │
├─────────────────────────────────────────────────────────┤
│ │
│ Skill 层(收敛的、Markdown 的、用户可扩展的) │
│ - 位置:工作区 skills/ 目录 │
│ - 变更:git add + commit,无发版 │
│ - 触发:描述匹配 + /slash 命令 │
│ │
│ dispatch │
│ ──────► │
│ │
│ Tool 层(收敛的、TypeScript 的、平台维护的) │
│ - 位置:src/agents/tools/ │
│ - 变更:code review + CI + npm publish │
│ - 触发:模型 function call │
│ │
└─────────────────────────────────────────────────────────┘
工程上最容易犯的错,是把"加一个能力"当作一道单选题。真正的高级抽象,是知道 这个能力的哪部分属于知识层、哪部分属于执行层,然后把它们放到正确的位置上。
这,才是 Skill vs Tool 这道题的真正答案。
附录:源码索引
| 概念 | 文件路径 | 关键行号 |
|---|---|---|
| Core Tool 定义 | src/agents/tool-catalog.ts |
41-244 |
| Tool → ToolDefinition 转换 | src/agents/pi-tool-definition-adapter.ts |
16-58 |
| beforeToolCall 闸门 | 同上 | 22-27 |
| Skill 类型定义 | src/agents/skills/types.ts |
51-71 |
| SkillCommandDispatchSpec | src/agents/skills/types.ts |
40-49 |
| Skill 动态加载 | src/agents/skills/workspace.ts |
全文 |
| 提示词预算常量 | src/agents/skills/workspace.ts |
27-29 |
| /slash 命令解析 | src/auto-reply/skill-commands.ts |
166-204 |
| Profile 枚举 | src/agents/tool-catalog.ts |
1, 256-267 |
| 三级渐进披露 | skills/skill-creator/SKILL.md |
全文 |
| 1Password Skill 案例 | skills/1password/SKILL.md |
全文 |
| Web Fetch Tool 实现 | src/agents/tools/web-fetch.ts |
全文 |