docs: add architecture and function call study notes

This commit is contained in:
clz
2026-06-15 15:51:11 +08:00
commit 7a0801caef
13 changed files with 1695 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"vimMode": true
}
+1
View File
@@ -0,0 +1 @@
{}
+31
View File
@@ -0,0 +1,31 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"properties": false,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": true,
"webviewer": false
}
+185
View File
@@ -0,0 +1,185 @@
{
"main": {
"id": "459ad3a32aad4276",
"type": "split",
"children": [
{
"id": "5f97bf8400cae9a8",
"type": "tabs",
"children": [
{
"id": "d0cf7351ba09fde6",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "blog/function-call-101.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "function-call-101"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "b6108a14266af7ba",
"type": "split",
"children": [
{
"id": "58c3efc6201f5a69",
"type": "tabs",
"children": [
{
"id": "e748b7d3d15f7bf9",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": false
},
"icon": "lucide-folder-closed",
"title": "文件列表"
}
},
{
"id": "5aaea1cf2403cdc9",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "搜索"
}
},
{
"id": "885aa17fd321b938",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "书签"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "16ed2280b2c0f5fb",
"type": "split",
"children": [
{
"id": "27e5e389f8fb98dc",
"type": "tabs",
"children": [
{
"id": "5e702d324eac08f1",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"file": "blog/function-call-101.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "function-call-101 的反向链接列表"
}
},
{
"id": "607e0346c7c7ac6b",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"file": "blog/function-call-101.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "function-call-101 的出链列表"
}
},
{
"id": "10e2aad3cce4362c",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "标签"
}
},
{
"id": "5e263de838d80777",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"file": "blog/function-call-101.md",
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "function-call-101 的大纲"
}
}
],
"currentTab": 3
}
],
"direction": "horizontal",
"width": 300
},
"left-ribbon": {
"hiddenItems": {
"switcher:打开快速切换": false,
"graph:查看关系图谱": false,
"canvas:新建白板": false,
"daily-notes:打开/创建今天的日记": false,
"templates:插入模板": false,
"command-palette:打开命令面板": false
}
},
"active": "d0cf7351ba09fde6",
"lastOpenFiles": [
"llm-autoregressive-kvcache.md",
"blog/function-call-101.md",
"README.md",
"function-call-flow.md",
"blog",
"skills-management-design.md",
"skills-cli-and-examples.md",
"skill-invocation-mechanism.md",
"architecture-overview.md",
"agent-loop.md",
"README.md.tmp.7973.29117344d8e6"
]
}
+40
View File
@@ -0,0 +1,40 @@
# OpenClaw 笔记
对 [OpenClaw](https://github.com/openclaw/openclaw) 代码库的架构与设计梳理笔记,
基于 2026.6.2 版本源码及根 `AGENTS.md``docs/` 整理而成。
## 目录
- [architecture-overview.md](./architecture-overview.md)
整体架构概览:运行时拓扑(单 Gateway 控制平面)、核心/插件代码分层边界、
一条消息从渠道到 agent 再到回复的完整生命周期。
- [skills-management-design.md](./skills-management-design.md)
Skill 管理整体设计:`SKILL.md` 契约、六类来源的加载与合并管线、
ClawHub 安装/校验生命周期、Skill Workshop 人审进化机制。
- [skill-invocation-mechanism.md](./skill-invocation-mechanism.md)
Skill 触发机制:agent 如何判断是否使用以及使用哪个 skill —— 代码侧过滤可用集,
模型侧基于 `description` 做语义匹配并按需 `read` 正文。
- [skills-cli-and-examples.md](./skills-cli-and-examples.md)
Skill CLI 与真实样例:`openclaw skills` 命令族(list/info/check、
search/install/update/verify、workshop 子命令)与内置 `SKILL.md`
frontmatter 实例图谱。
- [agent-loop.md](./agent-loop.md)
Agent loop 核心引擎:双重循环结构(steering 消息外层 + tool call 内层)、
三个核心输入(AgentContext/AgentLoopConfig/AgentMessage[])、
全部 hook 一览(beforeToolCall/afterToolCall/steering/followUp 等)、
AgentEvent 完整事件序列。
- [function-call-flow.md](./function-call-flow.md)
Function call 完整流程:各 provider 原生格式差异(Anthropic/OpenAI/Google/Mistral)、
内部归一化格式、从 ToolPlan 构建到 LLM 请求、流式解析、执行调度(并行/串行)、
hook 拦截、结果写回 transcript 的端到端流程,以及 deferred tool 与 terminate 语义。
- [llm-autoregressive-kvcache.md](./llm-autoregressive-kvcache.md)
LLM 自回归生成与 KV Cachetoken 逐步生成的机制、prefill vs decode 阶段、
首 token 慢的原因,以及服务端 Prompt Cache 的命中条件与 OpenClaw 中保证顺序确定性的做法。
完整官方文档:<https://docs.openclaw.ai>
+90
View File
@@ -0,0 +1,90 @@
# OpenClaw Agent Loop
> 本文基于 2026.6.2 版本源码,关键文件:
> `packages/agent-core/src/agent-loop.ts`、`packages/agent-core/src/types.ts`。
---
## 是什么
Agent loop 是 OpenClaw 的核心驱动引擎,控制一次 agent 运行的完整生命周期。
一次用户请求进来后,不是只请求一次 LLM 就结束——模型可能要多次调用工具、每次看完工具结果再继续思考,直到给出最终答复。Agent loop 就是控制这个"请求 → 工具执行 → 再请求"反复循环的引擎。
**两个入口:**
- `agentLoop(prompts, context, config)` — 带新消息启动,返回 `EventStream`
- `agentLoopContinue(context, config)` — 从现有 context 继续(用于重试)
两者都是异步流,调用方通过监听 `AgentEvent` 来驱动 UI 更新。
---
## 双重循环结构(`runLoop`
```
外层 while(true)
│ 等待 steering 消息(用户在 agent 工作时插入的消息)
│ 没有新消息且无 tool call → 检查 getFollowUpMessages()
│ 还是空 → 退出
└── 内层 while(hasMoreToolCalls || pendingMessages.length > 0)
① 注入 pending steering 消息
② streamAssistantResponse() —— 一次 LLM 流式请求
③ 检测 toolCall 块
④ executeToolCalls() —— 执行这批 tool,结果写回 transcript
⑤ prepareNextTurn() —— 可替换下一轮的 model/context
⑥ shouldStopAfterTurn() —— 可优雅退出
```
---
## 三个核心输入
| 输入 | 类型 | 说明 |
|------|------|------|
| `AgentContext` | 当前状态 | `systemPrompt` + `messages`(完整 transcript+ 可用 `tools` |
| `AgentLoopConfig` | 行为配置 | model、各种 hook、转换函数 |
| `AgentMessage[]` | 新消息 | 这轮用户输入 |
---
## AgentLoopConfig 关键 hook
| hook | 作用 |
|------|------|
| `convertToLlm` | AgentMessage[] → LLM Message[](必填,过滤 UI-only 消息) |
| `transformContext` | 发给 LLM 前压缩/裁剪 context window |
| `beforeToolCall` | 可拦截 tool 执行(返回 `{ block: true }` |
| `afterToolCall` | 可修改 tool result,或设 `stopAfterBatch` 终止本批 |
| `resolveDeferredTool` | 动态加载初始列表之外的 tool |
| `getSteeringMessages` | 每轮工具执行完后注入的插队消息 |
| `getFollowUpMessages` | agent 即将退出时追加的后续消息 |
| `prepareNextTurn` | 每轮结束后替换 model 或 context |
| `shouldStopAfterTurn` | 优雅退出(如 context 快满时) |
---
## AgentEvent 事件序列
调用方(UI、channel、测试)订阅事件流来渲染界面或做断言,不需要关心内部循环细节。
```
agent_start
turn_start
message_start (user)
message_end (user)
message_start (assistant, 流式开始)
message_update × N (流式 delta)
message_end (assistant)
tool_execution_start
tool_execution_update × N (进度回调)
tool_execution_end
message_start (toolResult)
message_end (toolResult)
turn_end
turn_start ← 有 tool call 时进入下一轮
...
agent_end
```
tool call 的具体执行流程见 [function-call-flow.md](./function-call-flow.md)。
+162
View File
@@ -0,0 +1,162 @@
# OpenClaw 整体架构设计
> 本文是对 OpenClaw 代码库的架构梳理笔记(基于 2026.6.2 版本源码与 `docs/concepts/architecture.md`、根 `AGENTS.md`),
> 从三个视角描述系统:运行时拓扑、代码分层、消息流转。
## 1. 运行时拓扑:单 Gateway 控制平面
整个系统的核心是一个**单实例、长驻的 Gateway 守护进程**(每台主机一个,默认绑定 `127.0.0.1:18789`,
由 launchd/systemd 托管)。它是唯一的控制平面:所有消息渠道连接、所有控制面客户端、所有设备节点都汇聚到这里。
```mermaid
flowchart TB
subgraph Channels["消息渠道(用户在这里说话)"]
WA[WhatsApp<br/>Baileys]
TG[Telegram<br/>grammY]
DC[Discord / Slack /<br/>Signal / iMessage / 微信...]
WC[WebChat]
end
subgraph GW["Gateway 守护进程(单实例,launchd/systemd 托管)"]
WS[WebSocket 服务<br/>typed req/res + event,JSON Schema 校验]
RT[路由层<br/>渠道/账号/对端 → agent]
AG[Agent 运行时<br/>会话、队列、工具调用循环]
CRON[Cron / Webhook<br/>自动化触发]
CANVAS["Canvas Host<br/>/__openclaw__/canvas + a2ui"]
DB[(SQLite 状态库<br/>state/openclaw.sqlite<br/>+ 每 agent 独立库)]
end
subgraph Clients["控制面客户端(role: client)"]
CLI[openclaw CLI]
MAC[macOS 菜单栏 App]
WIN[Windows Hub]
ADMIN[Web 管理 UI]
end
subgraph Nodes["设备节点(role: node,设备配对)"]
IOS[iOS 节点<br/>语音唤醒/Canvas]
AND[Android 节点<br/>相机/录屏/语音]
end
subgraph LLM["模型提供商"]
P1[Anthropic / OpenAI /<br/>Gemini / 本地模型...]
end
Channels <--> RT
RT --> AG
AG <--> P1
AG --> DB
Clients <-->|WebSocket| WS
Nodes <-->|WebSocket| WS
WS --> AG
CRON --> AG
AG --> CANVAS
```
### 协议关键不变量
- 每台主机只有一个 Gateway,它是唯一打开 WhatsApp(Baileys)会话的进程。
- WebSocket 握手第一帧必须是 `connect`,否则硬关闭连接。
- 所有客户端(操作端 + 节点)在 `connect` 时携带设备身份,新设备需配对审批
(签名 challenge nonce + 签发设备 token);本机回环连接可自动批准,非本地连接必须显式审批。
- 副作用方法(`send``agent`)要求幂等键,服务端维护短期去重缓存。
- 事件不重放;客户端检测到序号断档后需主动刷新快照。
## 2. 代码分层:核心与插件的边界
这是根 `AGENTS.md` 中最强调的设计约束:**core 保持 plugin-agnostic**,
插件只能通过 `openclaw/plugin-sdk/*` 公开门面进入核心,禁止反向或越界 import。
```mermaid
flowchart LR
subgraph Repo["pnpm workspace 单仓"]
subgraph Core["核心 src/(发布进 dist)"]
GWY[gateway/<br/>WS 服务与协议方法]
CHN[channels/<br/>渠道传输实现]
AGS[agents/<br/>agent 循环/工具/会话]
SKL[skills/ cron/ memory/<br/>media/ tts/ ...]
CFG[config/ state/<br/>canonical 配置 + SQLite]
SDK[plugin-sdk/<br/>唯一对外门面 barrel]
end
subgraph Ext["extensions/(插件,对外叫 plugins)"]
E1[telegram / discord /<br/>slack / whatsapp ...]
E2[codex / copilot /<br/>bedrock / vertex ...<br/>模型提供商]
E3[memory-lancedb /<br/>voice-call / ...]
end
PKG[packages/*<br/>gateway-protocol, llm-core,<br/>agent-core, sdk ...]
UI[ui/ — Control UI]
APPS[apps/ — macOS/iOS/Android]
DOCS[docs/ → docs.openclaw.ai]
end
Ext -->|"仅允许 import<br/>openclaw/plugin-sdk/*"| SDK
SDK --> Core
Core --> PKG
Core -.->|"禁止 import 插件内部<br/>(只走 manifest/registry)"| Ext
APPS -->|"Swift 模型由<br/>JSON Schema 生成"| PKG
```
### 边界规则(核心设计哲学)
- **插件 → 核心**:只能走 `openclaw/plugin-sdk/*`、manifest 元数据、注入的 runtime helper、
文档化的 barrel(`api.ts``runtime-api.ts`);禁止 import 核心 `src/**` 或其他插件内部。
- **核心 → 插件**:核心代码中不允许出现任何具体插件的 id/默认值/策略,
只通过 manifest/registry/capability 这类通用契约发现插件。
- **渠道是纯传输层**:只负责渲染可移植的 presentation/action、执行传输限制、映射原生回调封套;
产品命令树、provider 策略、功能菜单归核心/owner 插件所有。
- **提供商插件**拥有自己的 auth/catalog/runtime hooks;核心只拥有通用 agent 循环。
- **状态只进 SQLite**(Kysely 访问,禁止裸 SQL 字符串,DDL/迁移除外):
全局状态在 `state/openclaw.sqlite`,agent 级状态在 `agents/<agentId>/agent/openclaw-agent.sqlite`
旧文件格式只在 `openclaw doctor --fix` 迁移代码中处理,运行时无兼容分支、无 fallback 读取。
- **配置只有 canonical 形态**:运行时只读当前配置形态;旧配置由 doctor 迁移,不做静默兼容。
- **协议链**:TypeBox schema → 生成 JSON Schema → 生成 Swift 模型;
协议变更必须先做加法兼容,不兼容变更需要版本化 + 文档 + 客户端跟进。
## 3. 一条消息的生命周期
```mermaid
sequenceDiagram
participant U as 用户(Telegram 等)
participant CH as 渠道插件<br/>(transport-only)
participant GW as Gateway
participant RT as 路由
participant A as Agent 会话
participant LLM as 模型提供商
U->>CH: 发消息
CH->>GW: 标准化入站 envelope
GW->>RT: 渠道/账号/对端匹配
Note over RT: DM 配对检查:陌生人<br/>收到 pairing code,消息不处理
RT->>A: 投递到目标 agent 会话(队列)
A->>A: 组装 prompt:系统提示 + skills<br/>快照 + 记忆 + 会话历史
loop Agent 循环
A->>LLM: completion 请求
LLM-->>A: 文本 / 工具调用
A->>A: 执行工具(浏览器/bash/canvas...<br/>非 main 会话可进沙箱)
end
A->>GW: 回复 payload(分块/媒体处理)
GW->>CH: 映射为渠道原生格式
CH->>U: 回复送达
GW-->>GW: 会话/状态写入 SQLite,<br/>事件推送给 WS 订阅者
```
### 安全模型要点
- 入站 DM 默认视为**不可信输入**:主流渠道默认 `dmPolicy="pairing"`,
陌生发送者只收到配对码,消息不进 agent;公开 DM 需要显式 `dmPolicy="open"` + 通配 allowlist。
- `main` 会话默认在宿主机直接执行工具(单用户场景);群组/多人场景可配
`agents.defaults.sandbox.mode: "non-main"`,非 main 会话进 Docker/SSH/OpenShell 沙箱,
并按工具族 allow/deny。
- 第三方 skill 视为不可信代码:安装走 ClawHub 信任信封 + 安全扫描,
可配 `security.installPolicy` 本地策略命令,失败即拒绝(fail-closed)。
## 一句话总结
OpenClaw = **"一个本地 Gateway 进程 + 插件化的渠道/提供商生态 + 通用 agent 循环"**。
所有外部世界(聊天软件、设备、模型 API)都被插件适配成统一契约,
核心只做路由、会话、工具循环和状态管理;
客户端和设备节点统一走带配对认证的 WebSocket 协议接入。
完整官方文档:<https://docs.openclaw.ai/concepts/architecture>
+376
View File
@@ -0,0 +1,376 @@
# Function Call 是什么?LLM 如何学会使用工具
> 本文以 [OpenClaw](https://github.com/openclaw/openclaw) 开源源码为例(基于 2026.6.2 版本),结合实际代码说明 Function Call 的工作原理。涉及源码路径均可在仓库中直接查阅。
如果你用过 ChatGPT 的联网功能,或者让 AI 帮你查股票、操作文件,你已经在使用 Function Call 的成果了。但它背后是怎么运作的?模型是怎么"调用"工具的?本文从头说清楚。
---
## 从一个问题开始
> "今天北京天气怎么样?"
把这个问题丢给 LLM,它大概率会回答:
> "抱歉,我的训练数据有截止日期,无法获取实时天气信息。"
这暴露了 LLM 的本质局限:**它只能生成文本,没有任何与外部世界交互的能力**。它不能查数据库、不能调 API、不能执行代码——所有"知识"都封存在训练时的权重里。
Function Call(也叫 Tool Use)就是为了解决这个问题而生的。
---
## Function Call 是什么
一句话:**让开发者提前告诉模型"有哪些工具可以用",模型在需要时决定调用哪个工具、传什么参数,再由代码来真正执行它。**
有一个关键细节值得提前说清楚:**模型本身不执行任何工具**。它只是输出一段结构化的"调用请求",真正的执行发生在你的代码里。这个区别很重要,后面会反复提到。
---
## 一个完整的例子
还是那个天气问题,看看有了 Function Call 之后发生了什么。
**第一步:开发者注册工具**
在调用 LLM 之前,把可用的工具告诉模型:
```json
{
"name": "get_weather",
"description": "获取指定城市的实时天气",
"parameters": {
"type": "object",
"properties": {
"location": { "type": "string", "description": "城市名称" }
},
"required": ["location"]
}
}
```
**第二步:用户提问,模型决定调用工具**
模型收到"北京今天天气怎么样"后,意识到自己没有实时数据,但有 `get_weather` 工具可以用。于是它不直接回答,而是输出一个调用请求:
```json
{
"type": "tool_use",
"name": "get_weather",
"input": { "location": "北京" }
}
```
**第三步:代码执行工具,结果还给模型**
你的程序接收到这个请求,真正去调天气 API,拿到结果后告诉模型:
```json
{
"type": "tool_result",
"content": "北京今天晴,气温 28℃,东南风 3 级"
}
```
**第四步:模型拿到结果,给出最终回答**
> "北京今天天气不错,晴天,气温 28℃,东南风 3 级,适合出门。"
整个过程对用户是透明的,感觉就像模型"知道"天气一样。
---
## 工作原理
理解了"是什么",再看"怎么做到的"。
### 工具的定义与注册
在 OpenClaw 里,一个工具用 TypeBox schema 描述参数,并提供一个 `execute` 函数负责实际执行(`packages/agent-core/src/types.ts`):
```ts
const weatherTool: AgentTool = {
name: "get_weather",
label: "获取天气",
description: "获取指定城市的实时天气信息",
parameters: Type.Object({
location: Type.String({ description: "城市名称,如「北京」" }),
}),
execute: async (toolCallId, args) => {
const data = await fetchWeatherApi(args.location);
return { content: [{ type: "text", text: data }] };
},
};
```
三个核心字段:`name` 是模型调用时用的名字,`description` 是**模型决定"要不要用它"的唯一依据**,`parameters` 是模型生成参数时要遵守的 JSON Schema。
工具注册到 `AgentContext.tools` 后,每次请求 LLM 前经过 provider 的 `convertTools()` 转换,写入请求 body 的 `tools` 字段。
### 各家 API 的格式差异
不同厂商的格式不统一,是接入多个 provider 时最头疼的地方:
| | 工具声明字段 | 调用响应格式 | 结果角色 |
|---|---|---|---|
| **Anthropic** | `input_schema` | `{ type: "tool_use", input: {...} }` | `tool_result`role: user |
| **OpenAI** | `parameters` | `{ function: { arguments: "字符串" } }` | role: `tool` |
| **Google** | `parametersJsonSchema` | `{ functionCall: { args: {...} } }` | `functionResponse` |
| **Mistral** | `parameters` | 同 OpenAI,但字段名 camelCase | role: `tool` |
几个容易踩的坑:
- **OpenAI 的 `arguments` 是字符串**,不是对象,需要 `JSON.parse()` 才能用
- **Anthropic 的工具结果 role 是 `user`**,不是独立角色,多个工具结果会合并进同一条 user 消息
- **Google 不保证 tool call 有稳定 id**,需要在缺失时自动生成
### 完整请求体长什么样
把上面这些拼在一起,实际发给 Anthropic 的请求体是这样的(`src/llm/providers/anthropic.ts`):
```json
{
"model": "claude-sonnet-4-6",
"max_tokens": 8096,
"stream": true,
"system": [
{
"type": "text",
"text": "You are a helpful assistant...",
"cache_control": { "type": "ephemeral" }
}
],
"messages": [
{ "role": "user", "content": "北京今天天气怎么样?" }
],
"tools": [
{
"name": "get_weather",
"description": "获取指定城市的实时天气信息",
"input_schema": {
"type": "object",
"properties": {
"location": { "type": "string", "description": "城市名称" }
},
"required": ["location"]
},
"cache_control": { "type": "ephemeral" }
}
]
}
```
注意 `cache_control` 只加在 tools 数组的**最后一个工具**上——这是 Anthropic 的 prompt cache 标记方式,缓存整个 tools 块,下次相同前缀的请求直接命中。
### Provider 返回体与 transcript 拼接
请求发出后,Anthropic 以 SSE 流式返回。一次含 tool call 的响应,事件序列大致如下:
```
// 1. 消息开始,返回 token 用量(含 prompt cache 命中数)
event: message_start
data: { "message": { "id": "msg_01xxx", "usage": { "input_tokens": 312, "cache_read_input_tokens": 280 } } }
// 2. 文本块流式输出
event: content_block_start → data: { "index": 0, "content_block": { "type": "text" } }
event: content_block_delta → data: { "index": 0, "delta": { "type": "text_delta", "text": "我来查一下。" } }
event: content_block_stop → data: { "index": 0 }
// 3. tool_use 块开始(携带 id 和 nameinput 为空)
event: content_block_start
data: { "index": 1, "content_block": { "type": "tool_use", "id": "toolu_01xxx", "name": "get_weather", "input": {} } }
// 4. 参数以 JSON 字符串碎片流式输出,需要本地拼接
event: content_block_delta → data: { "index": 1, "delta": { "type": "input_json_delta", "partial_json": "{\"loc" } }
event: content_block_delta → data: { "index": 1, "delta": { "type": "input_json_delta", "partial_json": "ation\": \"北京\"}" } }
event: content_block_stop → data: { "index": 1 }
// 5. 消息结束,stop_reason 说明停止原因
event: message_delta → data: { "delta": { "stop_reason": "tool_use" }, "usage": { "output_tokens": 42 } }
event: message_stop
```
三个值得注意的细节:
- **tool 参数是字符串碎片**,需要在本地逐片拼接后解析,解析失败就暂时返回 `{}`,等待更多碎片
- **`stop_reason: "tool_use"`** 告诉框架这轮因调工具而停止,agent loop 据此进入执行流程
- **`cache_read_input_tokens: 280`** 说明 system + tools 部分命中了 prompt cache,跳过了 prefill 计算
工具执行完毕后,结果带着原始的 `tool_use_id` 写回 transcript,下一轮整段重发给 LLM
```json
[
{ "role": "user", "content": "北京今天天气怎么样?" },
{
"role": "assistant",
"content": [
{ "type": "text", "text": "我来查一下。" },
{ "type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": { "location": "北京" } }
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01",
"content": [{ "type": "text", "text": "北京今天晴,28℃,东南风 3 级" }],
"is_error": false
}
]
}
]
```
`tool_use_id` 的作用是让模型把"这个结果"和"那次调用"对应起来——一次响应里可能有多个 tool call,每个 result 都要通过 id 说明自己是哪次调用的答复。
### 模型实际"看到"的是什么
API 接收到结构化 JSON 后,服务端会把它展平成一段带特殊 token 的连续文本序列,才是真正喂进 transformer 的内容。各家格式没有完整公开,但大致规律如下。
**Anthropic**:tools 被序列化成 XML 追加在 system 末尾,含 tool call 的完整对话在 token 层面长这样:
```
[SYSTEM]
You are a helpful assistant...
<tools><tool_description>...</tool_description></tools>
[/SYSTEM]
[HUMAN]北京今天天气怎么样?[/HUMAN]
[ASSISTANT]我来查一下。
<tool_use>{"name": "get_weather", "input": {"location": "北京"}}</tool_use>
[/ASSISTANT] ← 模型生成到这里停止
[HUMAN]
<tool_result tool_use_id="toolu_01">北京今天晴,28℃,东南风 3 级</tool_result>
[/HUMAN]
[ASSISTANT] ← 服务端追加,模型从这里继续生成
```
**OpenAI**:使用 ChatML 格式(`<|im_start|>` / `<|im_end|>`),tools 序列化为类 JSON 文本追加进 systemtool result 作为 `tool` 角色注入。
从模型视角看,它每次看到的都是一段完整历史,**完全不知道中间发生过"执行工具"这件事**——工具执行是框架层的行为,对模型透明。
这也解释了两件事:**`description` 写得好不好直接影响模型判断**(它就是模型读到的那段文字);以及对话越长首 token 延迟越高(每次都要从头 prefill 整段历史),prompt cache 缓存不变的前缀正是为了解决这个问题。
### Agent Loop 与执行调度
真实场景中,模型往往不是调一次工具就结束,而是经历多轮。OpenClaw 的 agent loop 核心是一个双层循环(`packages/agent-core/src/agent-loop.ts`):
```ts
while (true) { // 外层:等待用户追加消息
let hasMoreToolCalls = true;
while (hasMoreToolCalls) { // 内层:处理 tool call 批次
const message = await streamAssistantResponse(context);
const toolCalls = message.content.filter(c => c.type === "toolCall");
if (toolCalls.length === 0) {
hasMoreToolCalls = false; // 没有 tool call,本轮结束
break;
}
const results = await executeToolCalls(toolCalls);
context.messages.push(...results); // 带着结果进入下一轮请求
}
}
```
模型可以**一次输出多个 tool call**,框架默认并行执行以节省时间。比如"帮我查北京和上海的天气",模型同时发出两个 `get_weather` 调用,结果都回来后再统一回答。
**并行还是串行,由开发者在定义工具时决定,不是 LLM 控制的:**
```ts
const weatherTool: AgentTool = {
name: "get_weather",
executionMode: "parallel", // 查询类,并发没问题
};
const writeFileTool: AgentTool = {
name: "write_file",
executionMode: "sequential", // 有副作用,强制串行
};
```
框架降级规则:同一批里**只要有一个**工具标了 `sequential`,整批立即降为串行。
| 工具类型 | 推荐模式 | 原因 |
|---|---|---|
| 查询、读取、幂等操作 | `parallel` | 并发安全,节省时间 |
| 写文件、发消息、调支付 | `sequential` | 有副作用,避免竞态 |
| 有依赖关系的操作 | LLM 跨轮次自然处理 | 模型看不到上一步结果就不会发下一步调用 |
---
## 如果模型"没调对"怎么办
Function Call 依赖模型生成结构化输出,但模型并不总是完美的,实际上有三种失败情形。
**情形一:调用了工具,但参数不符合 schema**
OpenClaw 的处理分两步:先尝试自动类型转换(比如 schema 要求数字,模型给了字符串 `"28"`,会尝试自动转成 `28`);转换后仍然不通过,才把详细错误信息作为 tool result 还给模型:
```
Validation failed for tool "get_weather":
- /location: Expected string
Received arguments: { "city": "北京" }
```
模型通常能读懂并修正参数重新调用,整个过程用户无感知。
**情形二:模型把 tool call 写成了普通文本**
部分模型(尤其是较早版本或上下文过长时)会退化,不输出结构化的 `tool_use`,而是直接在回复文本里写:
```
[get_weather]
{"location": "北京"}
```
或者 XML 风格:
```
<function=get_weather>
<parameter=location>北京</parameter>
</function>
```
OpenClaw 有专门的 `tool-call-repair` 模块识别这些格式,解析成功后提升为正式的结构化调用继续执行,同时把这段文本从用户可见的回复里抹掉。
**情形三:模型完全没有识别到需要调工具**
没有任何 tool callagent loop 当普通文本回复处理,直接返回给用户。没有报错,但用户可能拿到的是一个不准确的答案——这正是为什么工具的 `description` 要写清楚。
| 情形 | 处理方式 |
|---|---|
| 参数类型小错误 | 自动类型转换,尽量容忍 |
| 参数不符合 schema | 错误信息还给模型,让模型重试 |
| 模型输出纯文本 tool call | 解析并提升为结构化调用 |
| 模型完全没调工具 | 当普通文本回复,loop 结束 |
---
## 几个常见误区
**误区一:`description` 随便写就行**
`description` 是模型决定"要不要用这个工具"的唯一依据,也是它真实读到的文本。写得模糊,模型就可能在不该调用时调用,或者该调用时放弃。工具描述和参数说明值得认真打磨。
**误区二:一次只能调一个工具**
现代模型支持在一次响应里输出多个 tool call,框架可以并行执行。这对"分头查询再汇总"的任务(比如同时搜索多个关键词)效率提升很大。
**误区三:有顺序依赖的操作需要开发者干预**
"先创建文件再写入"这类有依赖的操作,LLM 会自然地分轮次处理——它看不到上一步的结果,就不会发出下一步的调用。开发者只需要处理**同批次内的并发安全**(用 `sequential`),跨轮次的逻辑顺序不需要干预。
---
## 小结
Function Call 的本质是一个**协议**:开发者用 schema 描述工具,模型用结构化输出表达"想调哪个",代码执行并返回结果,模型用结果继续生成。这个循环让 LLM 从"知识库"变成了"能行动的 agent"。
你现在看到的各种 AI 助手能查天气、写代码、操作文件、发邮件,背后都是这套机制在支撑。如果你对 agent 如何管理多轮工具调用的完整循环感兴趣,可以进一步了解 agent loop 的设计;如果想知道工具列表如何按需过滤,可以看 tool planner 的实现。
+235
View File
@@ -0,0 +1,235 @@
# OpenClaw Function Call 完整流程
> 本文基于 2026.6.2 版本源码分析,关键文件:
> `packages/agent-core/src/agent-loop.ts`、`src/tools/planner.ts`、
> `src/tools/types.ts`、`src/llm/providers/anthropic.ts`、
> `src/llm/providers/openai-completions.ts`、`src/llm/providers/openai-responses-tools.ts`、
> `src/llm/providers/google-shared.ts`。
---
## Tool 如何传递给 Provider
### 传递路径
```
AgentTool(注册在 AgentContext.tools
│ streamAssistantResponse() packages/agent-core/src/agent-loop.ts:416
Context.tools: Tool[] ← llm-core 层通用接口
{ name, description, parameters } TypeBox / JSON Schema
│ 每个 provider 的 convertTools()
各 Provider 原生格式(见下节)
│ 放入请求 bodyparams.tools
LLM API
```
`AgentTool` 继承自 `Tool`provider 只使用三个字段:`name` / `description` / `parameters`
### 各 Provider 的 `convertTools()` 差异
**Anthropic**`src/llm/providers/anthropic.ts:1480`
```ts
{
name: tool.wireName, // OAuth 模式下做名称转换
description: tool.description,
input_schema: tool.inputSchema, // 字段名是 input_schema
cache_control: ..., // 最后一个 tool 加 cache_controlprompt cache
eager_input_streaming: true, // 部分模型支持,提前流式返回 arguments
}
```
**OpenAI Completions**`src/llm/providers/openai-completions.ts:1174`
```ts
{
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters, // 字段名是 parameters
strict: false, // 可选,部分 provider 不接受此字段
}
}
```
**OpenAI Responses API**`src/llm/providers/openai-responses-tools.ts:60`
比 Completions 多两步:按 `name` 排序(保证 prompt cache 字节确定性)+ `normalizeOpenAIStrictToolParameters()` 处理 strict 模式兼容性。
```ts
{
type: "function",
name: tool.name,
description: tool.description,
parameters: normalizeOpenAIStrictToolParameters(...),
strict: true | false | null,
}
```
**Google**`src/llm/providers/google-shared.ts:367`
格式差异最大:tool 列表包在 `functionDeclarations` 数组里再套一层对象;有新旧两个 schema 字段,`useParameters` 参数控制选哪个(Cloud Code Assist 等场景需要旧字段)。
```ts
[{
functionDeclarations: tools.map(tool => ({
name: tool.name,
description: tool.description,
parametersJsonSchema: tool.parameters, // 新字段,完整 JSON Schema
// 或 parameters: sanitizeForOpenApi(...) // 旧字段,OpenAPI 3.0 格式
}))
}]
```
### 值得注意的细节
**Prompt cache 排序:** OpenAI Responses 和 Anthropic 都要求 tool 列表顺序固定才能命中 prompt cache。Responses API 在 `convertResponsesToolPayload()` 里显式按 name 排序;`src/tools/planner.ts``buildToolPlan()` 也用 `sortKey ?? name` 保证 planner 输出有序。
**Tool name projection** Anthropic OAuth 模式下,内部 tool 名称用 `toClaudeCodeName()` 转成 wire name,响应里再用 `resolveOriginalAnthropicToolName()` 反查回来,避免与 Claude.ai 原生工具名冲突。OpenAI 侧同样有 `projectOpenAITools()` 做类似映射。
---
## 各 Provider 原生格式对比
### Anthropic
**发出(assistant message):**
```json
{ "type": "tool_use", "id": "toolu_xxx", "name": "tool_name", "input": { ... } }
```
**结果(tool result):**
```json
{ "type": "tool_result", "tool_use_id": "toolu_xxx", "content": [...], "is_error": false }
```
**流式:** `content_block_start``tool_use` 类型)+ `content_block_delta``input_json_delta`,流式 JSON 片段),arguments 需要边收边拼接解析。
### OpenAI Completions
**发出:**
```json
{ "type": "function", "function": { "name": "tool_name", "arguments": "{\"key\":\"val\"}" } }
```
`arguments` 是**字符串**,不是对象。
**结果:** 消息角色为 `tool`,带 `tool_call_id`
```json
{ "role": "tool", "tool_call_id": "call_xxx", "content": "..." }
```
**流式:** `delta.tool_calls[]` 数组,按 `index` 对齐,`function.arguments` 是流式字符串片段。
### Google Generative AI
**发出:**
```json
{ "functionCall": { "name": "tool_name", "args": { ... } } }
```
`args` 是对象(非字符串);工具声明用 `function_declarations`
**结果:**
```json
{ "functionResponse": { "name": "tool_name", "response": { ... } } }
```
注意:Google 不保证 tool call 有稳定 id,代码会在缺失时自动生成(`needsNewId` 逻辑)。
### Mistral
格式最接近 OpenAI Completions,差异:
- 字段名为 camelCase`delta.toolCalls`(非 `tool_calls`
- id 可能是字符串 `"null"` 而非真 null,代码有专门判断
- `arguments` 可能已经是对象,代码做 `typeof === "string"` 判断后再决定是否 parse
### 内部统一格式
所有 provider adapter 最终都归一化为:
```ts
{ type: "toolCall", id: string, name: string, arguments: Record<string, unknown> }
```
adapter 的主要工作:把流式碎片 JSON 字符串拼好、把各家字段(`input`/`args`/`arguments`)统一解析成对象、补全缺失 id。
---
## 完整执行流程
```
用户消息
① 构建 tool 列表(ToolPlan
src/tools/planner.ts → buildToolPlan()
根据当前 auth/config/插件状态过滤出 visible tools
→ toToolProtocolDescriptors() 转成各 provider 格式
② LLM 流式请求
packages/agent-core/src/agent-loop.ts → streamAssistantResponse()
把 messages + tools 发给 provider
③ Provider 层解析流式响应(各家格式不同,见上节)
全部归一化为内部 { type: "toolCall", id, name, arguments } 格式
④ agent-loop 检测 tool call
message.content.filter(c => c.type === "toolCall")
有 toolCall → 进入执行;没有 → 结束本轮
⑤ resolveToolCallTool()
先在 context.tools 里按 name 查找
找不到 → 调 config.resolveDeferredTool()(动态加载)
仍找不到 → 返回 "Tool not found" error result
⑥ prepareToolCall()
a. prepareArguments() —— tool 可预处理入参
b. validateToolArguments() —— 按 JSON Schema 校验
c. beforeToolCall() hook —— 可返回 { block: true } 拦截执行
⑦ 执行模式判断
┌─ 串行(sequential):tool 标记了 executionMode="sequential",或全局配置强制
└─ 并行(parallel):默认,批次内所有 tool call 同时 Promise.all
⑧ executePreparedToolCall()
调 tool.execute(id, args, signal, onPartialResult)
执行中可发 tool_execution_update 事件(流式进度回调)
⑨ finalizeExecutedToolCall()
afterToolCall() hook —— 可覆盖 content / isError / details
可返回 stopAfterBatch: true 让 loop 在这批后停止
⑩ 结果写回 transcript
createToolResultMessage()
→ { role: "toolResult", toolCallId, content, isError }
push 到 context.messages
⑪ 下一轮 LLM 请求(回到步骤②)
带着 tool results 再请求,直到:
- 没有新的 toolCall(模型给出最终回复)
- terminate = true(所有 tool 都返回 terminate: true
- AbortSignal 触发
- stopAfterBatch 标记
```
---
## 关键设计点
**双重循环**
外层等待用户 steering 消息,内层处理 tool call batch。模型可以一次返回多个 tool call,执行完全部后再请求一次 LLM。
**Deferred tool**
不在初始 tool 列表里的 tool 可在执行时动态 resolve`resolveDeferredTool`),用于权限控制或懒加载场景。recover 后会追加到 `currentContext.tools`,让本轮后续 provider continuation 可见。
**terminate 语义**
tool result 可带 `terminate: true`。当批次内所有 call 都 terminate 时 loop 停止,不再请求 LLM。只要有一个 call 不 terminateloop 继续。
**串行降级**
并行模式下只要发现任意一个 tool 的 `executionMode === "sequential"`,整批立即降为串行执行。
+48
View File
@@ -0,0 +1,48 @@
# LLM 自回归生成与 KV Cache
> 通用 LLM 原理,与 OpenClaw 代码无关,但解释了 streaming 延迟和 prompt cache 的底层依据。
---
## 自回归生成(Autoregressive Generation
LLM 每次只生成一个 token,然后把它追加到输入序列末尾,重新跑一遍前向传播,生成下一个 token,循环直到 `<|EOS|>`
```
输入: [token1, token2, ... tokenN] → Transformer → tokenN+1
输入: [token1, token2, ... tokenN+1] → Transformer → tokenN+2
...
```
这就是为什么 LLM 的输出是"流式逐字出现"的——每个 token 生成后立即可以返回给调用方,不需要等全部生成完。
---
## KV Cache
如果每次都把整个序列从头重算,代价随序列长度线性增长。实际上 transformer 每层 attention 会把历史 token 的 Key/Value 矩阵缓存下来,新 token 只需计算自己的 Q 与历史 KV 的 attention,不重算历史。
```
第1步(prefill):完整计算所有输入 token 的 KV,存入缓存
第2步以后(decode):只算新 token,复用缓存中的历史 KV
```
**这解释了两个常见现象:**
- **首 token 慢、后续 token 快**prefill 是全量计算,耗时与输入长度成正比;decode 是增量计算,每步耗时基本固定
- **长 system prompt 影响首 token 延迟**system prompt 越长,prefill 越慢,但后续 decode 速度不受影响
---
## Prompt Cache(服务端 KV Cache
Anthropic、OpenAI 等服务商把 KV Cache 做到了服务端——把 system prompt + tools 的 KV 矩阵持久化存储,下次相同前缀的请求直接命中缓存,跳过 prefill 阶段。
**命中条件:前缀必须完全一致(逐 token 匹配)。**
这就是 OpenClaw 里多处保证顺序确定性的原因:
- `src/tools/planner.ts``buildToolPlan()``sortKey ?? name` 对 tool 列表排序
- `src/llm/providers/openai-responses-tools.ts``convertResponsesToolPayload()` 里再次按 name 排序
- Anthropic 的 `cache_control` 打在 tools 数组最后一个 tool 上,标记缓存边界
顺序一旦变化,前缀不匹配,缓存失效,重新 prefill。
+102
View File
@@ -0,0 +1,102 @@
# OpenClaw Skill 触发机制:agent 如何判断是否使用、使用哪个 skill
> 本文是对 OpenClaw skill 调用决策机制的源码分析笔记(基于 2026.6.2 版本),
> 关键源码:`src/skills/loading/skill-contract.ts`(`formatSkillsForPrompt`)、
> `src/skills/loading/workspace.ts`(`formatSkillsCompact` / `applySkillsPromptLimits`)。
> 配套文档:[skills-management-design.md](./skills-management-design.md)。
## 核心结论
**OpenClaw 不做任何代码层面的"技能匹配/检索"——没有 embedding、没有关键词索引、没有路由算法。
选择哪个 skill 完全交给模型自己语义判断**,采用渐进式披露(progressive disclosure)设计:
prompt 里只放每个技能的元数据目录,正文由模型按需用 `read` 工具加载。
## 两层决策
### 第一层:代码决定"哪些可用"
加载管线(见 skill 管理设计笔记)先把不合格的技能筛掉:
- 门控不满足(缺二进制 `requires.bins`、缺环境变量 `requires.env`、缺配置 `requires.config`、平台不符 `os`)
- agent allowlist 之外(`agents.list[].skills`)
- `enabled: false``disable-model-invocation: true`
活下来的技能只把**元数据**序列化进系统提示——不含正文:
```xml
The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.
If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.
<available_skills>
<skill>
<name>github</name>
<description>Interact with GitHub via gh CLI...</description>
<location>/path/to/skills/github/SKILL.md</location>
<version>3</version>
</skill>
...
</available_skills>
```
### 第二层:模型决定"用不用、用哪个"
prompt 里那句指令就是全部机制——
*"当任务与某个 skill 的 description 匹配时,用 read 工具加载它的文件"*。即:
1. 模型拿用户任务和目录里的 `description` 做语义比对(纯 LLM 推理);
2. 认为匹配 → 调 `read` 工具读取 `<location>` 指向的 `SKILL.md` 全文;
3. 按读到的指令行事(正文里通常写"遇到 X 情况用 Y 工具/命令")。
**推论:`description` 写得好不好直接决定技能会不会被触发** ——
它是 skill 作者手里最重要的"路由表项"。
## 决策流程图
```mermaid
flowchart TB
T[用户任务到达] --> M{模型读 available_skills 目录<br/>任务 ≈ 某条 description?}
M -->|匹配| R[read 工具读取 SKILL.md 正文]
R --> V{version 与上轮一致?}
V -->|否| R
V -->|是| E[按正文指令执行工具调用]
M -->|不匹配| N[正常回答,不用 skill]
U[用户敲 /skill-name] -->|user-invocable| R
U -->|command-dispatch: tool| D[绕过模型<br/>直接分发到注册工具]
```
## 三个细节设计
### 1. 预算降级(`applySkillsPromptLimits`)
技能太多时按梯度退化,尽量保住"模型知道技能存在"这件事:
| 梯度 | 行为 | 代价 |
| --- | --- | --- |
| 正常 | name + description + location + version | 无 |
| 超出 `maxSkillsInPrompt` | 截断技能数量 | 后面的技能不可见 |
| 超字符预算 | 降级 compact 格式:只有 name + location | 丢 description,模型只能按名字匹配 |
| 仍超 | 截断 + 注入警告 | `⚠️ Skills truncated... Run openclaw skills check` |
### 2. 版本失效信号
目录里带 `<version>`,prompt 指令要求
"若 version 与上一轮不同,使用前必须重读 SKILL.md"。
这是会话内技能内容更新的缓存失效机制——
技能集快照(SkillSnapshot)本身在会话内固定,但正文变更可以通过版本号传导。
### 3. 绕过模型的路径
| frontmatter 配置 | 效果 |
| --- | --- |
| `disable-model-invocation: true` | 不进 `<available_skills>`,模型永远不会自主使用;仅用户可通过 `/skill-name` 触发 |
| `command-dispatch: tool` | 斜杠命令连模型都不经过,确定性分发到注册工具(`SkillCommandDispatchSpec`,`argMode: raw` 原样转发参数) |
| `metadata.openclaw.always: true` | 反向操作:跳过 requirements 门控,无条件进目录 |
## 设计渊源
这与 Anthropic 官方 Agent Skills 的设计同构:
目录条目只占几十 token/技能,正文按需加载;
**"路由智能"留给模型,"可用性控制"留给代码**。
好处是零检索基础设施、技能数量可扩展(token 成本近似常数),
代价是触发可靠性依赖模型能力与 description 质量。
+234
View File
@@ -0,0 +1,234 @@
# OpenClaw Skill CLI 与真实样例
> 本文是对 `openclaw skills` 命令族与内置 skill 实例的梳理笔记(基于 2026.6.2 版本源码
> `src/cli/skills-cli.ts`、`src/cli/skills-cli.format.ts`、仓库 `skills/`(58 个内置技能)、
> `docs/tools/creating-skills.md`)。
> 配套文档:[skills-management-design.md](./skills-management-design.md)(加载/安装/Workshop 设计)、
> [skill-invocation-mechanism.md](./skill-invocation-mechanism.md)(模型如何选用 skill)。
## 1. 命令总览
`openclaw skills` 下的子命令按职责分三组,分别落到 `src/skills/` 的三个子模块:
```mermaid
flowchart LR
subgraph View["视图命令(只读)"]
LIST[list]
INFO[info]
CHECK[check]
end
subgraph Install["安装链路命令"]
SEARCH[search]
INST[install]
UPD[update]
VER[verify]
end
subgraph Workshop["workshop 子命令"]
WL[list / inspect]
WP[propose-create /<br/>propose-update / revise]
WA[apply / reject / quarantine]
end
LIST & INFO & CHECK --> STATUS["discovery/status.ts<br/>buildWorkspaceSkillStatus"]
SEARCH & INST & UPD --> HUB["lifecycle/clawhub.ts"]
INST -.->|"git: / ./ / ../ / ~/ / 绝对路径"| SRC["lifecycle/source-install.ts"]
VER --> HUB
WL & WP & WA --> SVC["workshop/service.ts"]
```
不带子命令时(`openclaw skills`)等价于 `skills list`(`skills-cli.ts:857`)。
所有命令都接受 `--agent <id>`,解析顺序是:显式 `--agent` > 按 cwd 反推 workspace 所属 agent
> 配置里的默认 agent(`resolveSkillsWorkspace`,`skills-cli.ts:71-88`)。
## 2. 三个视图命令:list / info / check
三者共享同一份数据源 `SkillStatusEntry[]`(`discovery/status.ts`),区别只是渲染粒度:
| 命令 | 输出 | 用途 |
| --- | --- | --- |
| `skills list [--eligible] [-v]` | 表格:Status / Skill / Description / Source(`-v` 加 Missing 列) | 浏览所有发现到的技能,`--eligible` 只看可用的 |
| `skills info <name>` | 单个技能的详情:requirements 逐项 ✓/✗、install 选项、API key 设置提示 | 排查某个技能为什么不可用,或它需要什么 env/二进制 |
| `skills check` | 汇总计数 + 分类清单 | 一眼看出"模型能看到几个技能、有几个被挡住" |
### 状态字段含义(`SkillStatusEntry`,决定 list/check 的分类)
`formatSkillStatus`(`skills-cli.format.ts:42-56`)按优先级判定单个技能状态,
`check` 命令(`skills-cli.format.ts:338-492`)按同一组字段做汇总:
| 字段 | 含义 | 对应 [skill-invocation-mechanism.md](./skill-invocation-mechanism.md) 中的概念 |
| --- | --- | --- |
| `disabled` | frontmatter `enabled: false` | 第一层门控,直接不进任何输出 |
| `blockedByAllowlist` | 未通过 `requires.*` 门控 | 第一层门控(缺二进制/env/config/os) |
| `blockedByAgentFilter` | 不在该 agent 的 `agents.list[].skills` 内 | 第一层门控的 allowlist 分支 |
| `eligible` | 安装齐全、requirements 满足 | 是否"活下来"进入候选集 |
| `modelVisible` | 进 `<available_skills>` 目录 | 第二层:模型能看到的范围(`disable-model-invocation` 为 false 才会是 true) |
| `commandVisible` | 注册为 `/skill-name` 斜杠命令 | `user-invocable` 是否为真 |
| `missing.{bins,anyBins,env,config,os}` | 具体缺什么 | `info` 命令逐项渲染 ✓/✗ 的依据 |
`check``notInjected`(代码里叫 `promptHidden`)专门标出
"**已就绪但不在模型 prompt 里**"的技能——即 `eligible && !blockedByAgentFilter && !modelVisible`,
对应 `disable-model-invocation: true` 的技能:它们仍可通过 `/skill-name`、cron 或工具分发使用。
## 3. 安装链路:search / install / update / verify
```mermaid
sequenceDiagram
participant U as 用户
participant CLI as openclaw skills CLI
participant HUB as ClawHub
participant SRC as 本地/Git 源
U->>CLI: skills search <query>
CLI->>HUB: searchSkillsFromClawHub
HUB-->>U: slug + version + summary 列表
alt slug 是 ClawHub 名(如 "weather")
U->>CLI: skills install weather [--global] [--agent x]
CLI->>HUB: installSkillFromClawHub
HUB-->>CLI: 包 + 安全扫描状态
CLI->>SRC: 写入 workspace/skills 或 ~/.openclaw/skills<br/>+ .clawhub/origin.json
else slug 是源安装规格<br/>(git:owner/repo@ref, ./dir, ../dir, ~/dir, 绝对路径)
U->>CLI: skills install git:owner/repo --as myskill
CLI->>SRC: installSkillFromSource<br/>(isSkillSourceInstallSpec 判定)
SRC->>SRC: 写 .openclaw/source-origin.json<br/>(source: "path"|"git", git.commit)
end
U->>CLI: skills update --all
CLI->>SRC: readTrackedClawHubSkillSlugs<br/>(只能追踪 ClawHub 来源)
CLI->>HUB: updateSkillsFromClawHub
U->>CLI: skills verify weather [--card]
CLI->>HUB: resolveClawHubSkillVerificationTarget<br/>+ fetchClawHubSkillVerification
HUB-->>U: 信任信封(decision: pass/...)<br/>--card 时额外拉取 Skill Card Markdown
```
关键细节(`skills-cli.ts`):
- **来源判定是字符串前缀规则**(`isSkillSourceInstallSpec`,`source-install.ts:391-400`):
`git:` 前缀、`./``../``~/`、绝对路径 → 走源安装;否则视为 ClawHub slug。
`--version` 只对 ClawHub 安装有效,`--as` 只对源安装有效,两者混用会报错(`skills-cli.ts:316-348`)。
- **`--global` vs `--agent`**:`--global` 写入共享的 `~/.openclaw/skills`(`CONFIG_DIR`),
`--agent` 写入该 agent 的 workspace `skills/`;两者互斥(`resolveClawHubTargetWorkspaceDir`,`skills-cli.ts:129-143`)。
- **`update --all` 只能更新 ClawHub 来源的技能**——源安装(git/本地)不写 `.clawhub/origin.json`,
因此 `readTrackedClawHubSkillSlugs` 看不到它们,`--all` 时这类技能会被静默跳过。
- **`verify`** 不依赖本地文件内容,而是把 `.clawhub/origin.json` 记录的 `slug@version` 发给 ClawHub
做信任信封校验;`shouldFailSkillVerification` 只看响应里的 `ok``decision === "pass"`,
失败时 CLI 以非零退出码结束,适合接入 CI/`openclaw skills check` 之外的供应链门禁。
## 4. workshop 子命令:提案生命周期的 CLI 入口
[skills-management-design.md](./skills-management-design.md#4-skill-workshopagent-自我演化的审批闸门)
中的提案流程在 CLI 上的映射(均落到 `workshop/service.ts`):
| CLI | service.ts 函数 | 作用 |
| --- | --- | --- |
| `workshop list [--json]` | `listSkillProposals` | 列出 pending/completed 提案(id, status, kind, skillKey, title) |
| `workshop inspect <id>` | `inspectSkillProposal` | 看提案正文 + 附带的 support files + 扫描状态(`record.scan.state`) |
| `workshop propose-create --name --description` | `proposeCreateSkill` | 为**新**技能创建提案(`createdBy: "cli"`) |
| `workshop propose-update <skill>` | `proposeUpdateSkill` | 为**已存在**技能创建更新提案 |
| `workshop revise <id>` | `reviseSkillProposal` | 替换 pending 提案的正文/描述/目标(产生新 `proposedVersion`) |
| `workshop apply <id>` | `applySkillProposal` | 人工批准后落盘到真实 `SKILL.md`(`workspace-skill-write`) |
| `workshop reject <id>` / `quarantine <id>` | `rejectSkillProposal` / `quarantineSkillProposal` | 拒绝或隔离(带 `--reason`) |
提案正文来源统一是 `--proposal <path>`(单文件)或 `--proposal-dir <path>`
(含 `PROPOSAL.md` + UTF-8 support files 的目录),二者互斥、必选其一
(`readSkillProposalInput`,`skills-cli.ts:221-237`)。这一层 CLI 本身不做内容审查——
`record.scan.state` 来自 `security/scanner.ts` 的安装前扫描,`apply` 时才真正写入活动技能目录。
## 5. SKILL.md frontmatter 实例图谱
仓库 `skills/`(58 个内置技能)展示了 frontmatter 字段的几种典型组合,
从最简到最完整:
### 5.1 最简形态:只有 `name` + `description`
`skills/skill-creator/SKILL.md:1-4`——本身就是"如何写 skill"的元技能,刻意保持最小:
```yaml
---
name: skill-creator
description: "Create, edit, audit, tidy, validate, or restructure AgentSkills and SKILL.md files."
---
```
### 5.2 声明式门控 + 安装引导:`requires` + `install`
`skills/blucli/SKILL.md:1-20`——缺 `blu` 二进制时,`info`/`list -v` 会展示这条安装选项:
```yaml
---
name: blucli
description: "BluOS CLI (blu) for discovery, playback, grouping, and volume."
homepage: https://blucli.sh
metadata:
{
"openclaw":
{
"emoji": "🫐",
"requires": { "bins": ["blu"] },
"install": [{ "id": "go", "kind": "go", "module": "github.com/steipete/blucli/cmd/blu@latest",
"bins": ["blu"], "label": "Install blucli (go)" }],
},
}
---
```
`skills/session-logs/SKILL.md` 是多二进制版本(`requires.bins: ["jq", "rg"]`,两条 brew 安装选项)。
### 5.3 斜杠命令 + API key 提示:`user-invocable` + `primaryEnv`
`skills/gh-issues/SKILL.md:1-22`——`user-invocable: true` 让它出现在 `commandVisible` 里,
`primaryEnv``skills info gh-issues``GH_TOKEN` 缺失时打印"API key setup"区块:
```yaml
---
name: gh-issues
description: "Fetch GitHub issues, select candidates, spawn background fix agents, open PRs, ..."
user-invocable: true
metadata:
{
"openclaw":
{
"requires": { "bins": ["git", "gh"] },
"primaryEnv": "GH_TOKEN",
"install": [{ "id": "brew", "kind": "brew", "formula": "gh", "bins": ["gh"],
"label": "Install GitHub CLI (brew)" }],
},
}
---
```
### 5.4 绕过模型的字段(仓库内置技能暂无样例,引自 `docs/tools/creating-skills.md:106-113`)
这三个字段决定[skill-invocation-mechanism.md](./skill-invocation-mechanism.md#3-绕过模型的路径)
里"绕过模型"的具体写法:
| 字段 | 默认值 | 效果 |
| --- | --- | --- |
| `disable-model-invocation` | `false` | `true``modelVisible=false`(不进 `<available_skills>`),但仍可 `/skill` 调用 |
| `command-dispatch` | — | 设为 `tool` 时,斜杠命令直接路由到工具,绕过模型 |
| `command-tool` | — | `command-dispatch: tool` 时要调用的工具名 |
| `command-arg-mode` | `raw` | 工具分发模式下,把斜杠命令的参数原文转发给该工具 |
## 6. 命令 → 源码速查表
| 命令 | 入口函数(`skills-cli.ts`) | 核心实现 |
| --- | --- | --- |
| `skills list/info/check` | `runSkillsAction` + `format*` | `discovery/status.ts: buildWorkspaceSkillStatus` |
| `skills search` | `searchSkillsFromClawHub` | `lifecycle/clawhub.ts` |
| `skills install` | 分支:`installSkillFromClawHub` / `installSkillFromSource` | `lifecycle/clawhub.ts` / `lifecycle/source-install.ts` |
| `skills update` | `updateSkillsFromClawHub` + `readTrackedClawHubSkillSlugs` | `lifecycle/clawhub.ts` |
| `skills verify` | `resolveClawHubSkillVerificationTarget` + `fetchClawHubSkillVerification` | `infra/clawhub.ts` |
| `skills workshop *` | 见第 4 节表格 | `workshop/service.ts` |
## 一句话总结
`openclaw skills` CLI 是 [skills-management-design.md](./skills-management-design.md)
里那套加载/安装/Workshop 设计的**操作面**:`list/info/check` 直接复用 agent prompt 用的同一份
`SkillStatusEntry`,所以 CLI 看到的"ready/visible/blocked"状态与模型 prompt 里
`<available_skills>` 的内容是同一份真相;`install/update/verify` 覆盖 ClawHub 与
git/本地源两条安装路径,但只有 ClawHub 路径可追踪更新;`workshop` 子命令把
"agent 提案 → 人工审批 → 落盘"的每一步都暴露成独立命令,方便脚本化审核。
完整官方文档:<https://docs.openclaw.ai/cli/skills>(命令参考)、
<https://docs.openclaw.ai/tools/creating-skills>(frontmatter 字段参考)
+188
View File
@@ -0,0 +1,188 @@
# OpenClaw Skill 管理整体设计
> 本文是对 OpenClaw skill(技能)系统的设计梳理笔记(基于 2026.6.2 版本源码 `src/skills/`、
> `docs/tools/skills.md` 与根 `AGENTS.md`)。
> Skill 是教 agent "如何及何时使用工具"的 markdown 指令单元,遵循 [AgentSkills](https://agentskills.io) 规范。
## 0. 基本单元:SKILL.md
每个 skill 是一个目录,核心是带 YAML frontmatter 的 `SKILL.md`:
```markdown
---
name: image-lab
description: Generate or edit images via a provider-backed image workflow
metadata: {"openclaw": {"requires": {"bins": ["ffmpeg"]}, "emoji": "🎨"}}
user-invocable: true
---
When the user asks to generate an image, use the `image_generate` tool...
```
关键契约(见 `src/skills/types.ts`):
- `name` / `description` 必填;name 决定技能名、斜杠命令名、allowlist 键(缺省取目录名)。
- `metadata.openclaw.requires`:门控声明 —— `bins` / `anyBins`(命令行二进制)、`env`(环境变量)、
`config`(配置项)、`os`(平台),不满足则该 skill 不进 prompt。
- `metadata.openclaw.install`:声明式安装规格(`brew | node | go | uv | download`),
缺二进制时引导安装。
- 调用策略:`user-invocable`(暴露为斜杠命令)、`disable-model-invocation`(不进模型 prompt)、
`command-dispatch: tool`(斜杠命令绕过模型直接分发到注册工具)。
- frontmatter 解析器只支持**单行键**,`metadata` 必须是单行 JSON;正文可用 `{baseDir}` 引用技能目录。
## 1. 模块地图(src/skills/)
```mermaid
flowchart LR
subgraph SK["src/skills/"]
LOAD[loading/<br/>多源扫描、frontmatter 解析、<br/>优先级合并、序列化进 prompt]
DISC[discovery/<br/>门控过滤、skill 索引、<br/>斜杠命令生成、agent 过滤]
LIFE[lifecycle/<br/>安装/更新:ClawHub、Git、<br/>本地目录、zip 上传]
RUN[runtime/<br/>会话/cron 快照、env 注入、<br/>tool 直接分发、远程节点 eligibility]
SEC[security/<br/>安装扫描器、ClawHub 信任裁决、<br/>workspace 审计]
WORK[workshop/<br/>agent 提案队列:<br/>store / policy / service]
RES[research/<br/>autocapture:从会话中<br/>捕捉可复用工作流信号]
CFG[config/<br/>skills.* 配置变更]
end
LOAD --> DISC --> RUN
LIFE --> LOAD
SEC -.->|安装前置闸门| LIFE
RES --> WORK -->|apply 写回| LOAD
CFG --> LOAD
CLI[openclaw skills CLI<br/>+ Gateway skills.* 方法] --> LIFE
CLI --> WORK
HUB[(ClawHub<br/>clawhub.ai 注册表)] <--> LIFE
```
## 2. 加载管线:从磁盘到 agent prompt
技能从六类来源加载,**同名时高优先级覆盖低优先级**;
任何根目录下出现 `SKILL.md` 即被发现(子目录层级仅作组织用)。
| 优先级 | 来源 | 路径 | 可见范围 |
| --- | --- | --- | --- |
| 1(最高) | Workspace skills | `<workspace>/skills` | 仅该 agent |
| 2 | 项目 agent skills | `<workspace>/.agents/skills` | 仅该工作区 agent |
| 3 | 个人 agent skills | `~/.agents/skills` | 本机所有 agent |
| 4 | 托管/本地 skills | `~/.openclaw/skills` | 本机所有 agent |
| 5 | 内置 skills | 随安装包发布(仓库 `skills/`,约 50 个) | 本机所有 agent |
| 6(最低) | 额外目录 + 插件 skills | `skills.load.extraDirs`、插件 manifest 的 `skills` 字段 | 本机所有 agent |
```mermaid
flowchart TB
subgraph Sources["技能来源(优先级从高到低)"]
S1[workspace/skills]
S2[workspace/.agents/skills]
S3[~/.agents/skills]
S4[~/.openclaw/skills]
S5[内置 bundled]
S6[extraDirs + 插件 skills]
end
SCAN[扫描 + frontmatter 解析<br/>loading/]
PATH{路径收敛检查<br/>realpath 必须在根内<br/>除非 allowSymlinkTargets}
GATE{门控过滤 discovery/filter<br/>requires.bins / env / config / os}
MERGE[同名合并:高优先级覆盖]
ALLOW{agent allowlist<br/>agents.list#91;#93;.skills}
Sources --> SCAN --> PATH --> GATE --> MERGE --> ALLOW
subgraph Outputs["产出(同一份有效集,四处一致)"]
SNAP["SkillSnapshot<br/>(prompt 片段 + env 需求 + 版本号)<br/>会话级固定,cron 另有快照"]
CMD["斜杠命令注册<br/>/skill-name(渠道可见)"]
SBX[沙箱同步<br/>非 main 会话的技能文件]
REG[运行时注册表<br/>tool 直接分发]
end
ALLOW --> SNAP & CMD & SBX & REG
```
设计要点:
- **位置与可见性分离**:目录优先级只决定"谁覆盖谁";agent 能看到什么由 allowlist 决定
(`agents.defaults.skills` 为共享基线,`agents.list[].skills` 非空时**整体替换**而非合并,
`[]` 表示零技能,省略则不限制)。
- **快照(snapshot)语义**:会话开始时固化一份 `SkillSnapshot`(prompt 文本 + 所需 env + 版本号),
会话中途技能变更不影响进行中的会话;cron 任务有独立的快照路径(`runtime/cron-snapshot.ts`)。
这与 AGENTS.md "hot path 不做 freshness 轮询" 的原则一致 —— 技能集是进程稳定元数据,
变更走显式 refresh(`runtime/refresh.ts`)。
- **远程节点 eligibility**(`runtime/remote.ts`):门控检查可以基于远程执行环境的平台/二进制集合,
而不是 Gateway 宿主机本身。
## 3. 安装生命周期:ClawHub 与多源安装
```mermaid
sequenceDiagram
participant U as 用户
participant CLI as openclaw skills CLI
participant POL as security.installPolicy<br/>(可选本地策略命令)
participant HUB as ClawHub 注册表
participant FS as 技能目录
U->>CLI: skills install <slug> [--global] [--as name]
alt ClawHub 来源
CLI->>HUB: 解析 slug,下载归档
HUB-->>CLI: 包 + 安全扫描状态<br/>(VirusTotal / ClawScan / 静态分析)
else Git / 本地 / 上传
CLI->>CLI: git:owner/repo@ref 克隆 /<br/>本地目录 / zip 分块上传<br/>(上传需显式 allowUploadedArchives)
end
CLI->>POL: 元数据 + 暂存路径
Note over POL: fail-closed:策略命令<br/>无法给出有效决定即拒绝
POL-->>CLI: allow / deny
CLI->>FS: 写入 workspace/skills<br/>(--global → ~/.openclaw/skills)
CLI->>FS: 记录 .clawhub/origin.json<br/>(版本 + 注册表来源)
U->>CLI: skills update --all
CLI->>HUB: 按 origin.json 对比版本(仅 ClawHub 安装可追踪)
U->>CLI: skills verify <slug>
CLI->>HUB: 请求 clawhub.skill.verify.v1 信任信封
HUB-->>U: 校验结果(失败则非零退出)
```
安全模型(skill 被明确视为**不可信代码**):
- **信任信封**:`openclaw skills verify` 对照 `.clawhub/origin.json` 记录的版本与注册表做校验;
ClawHub 页面在安装前即暴露最新扫描状态。
- **操作员安装策略**:`security.installPolicy` 配一个本地策略命令,
覆盖 ClawHub/上传/Git/本地/更新/依赖安装全部路径,fail-closed。
- **路径收敛**:workspace/项目/extraDirs 的 skill 根必须 realpath 收敛在配置根内
(防符号链接逃逸),例外需 `skills.load.allowSymlinkTargets` 显式信任。
- **密钥注入范围**:`skills.entries.*.env` / `.apiKey` 只注入**宿主进程的当轮 agent 调用**,
不进沙箱、不进 prompt。
## 4. Skill Workshop:agent 自我演化的审批闸门
agent 在工作中发现可复用的流程时,**不直接写 SKILL.md**,而是产出提案进队列,人工审批后才落盘:
```mermaid
flowchart LR
A[Agent 会话] -->|research/autocapture<br/>捕捉可复用信号| P[提案草稿]
P --> Q[(workshop/store<br/>提案队列)]
Q -->|workshop list / inspect| H{用户审批}
H -->|apply| W[写入活动技能文件<br/>workspace-skill-write]
H -->|拒绝/忽略| X[丢弃]
W -.->|符号链接目标写入需<br/>allowSymlinkTargetWrites| W
```
这是整个系统"agent 可以建议、人类拥有最终所有权"原则的具体化:
`workshop/policy.ts` 定义什么可以被提案,`service.ts` 管理生命周期,
CLI 入口为 `openclaw skills workshop list / inspect <id> / apply <id>`
## 5. 与插件系统的关系
- 插件通过 `openclaw.plugin.json``skills` 字段携带技能目录,随插件启用而加载,
合并在最低优先级层(与 `extraDirs` 同级)—— 任何同名的内置/托管/agent/workspace 技能都能覆盖它。
- 插件技能可用 `metadata.openclaw.requires.config` 绑定到插件自己的配置项做门控。
- 核心通过 `plugin-sdk/skills-runtime` barrel 向插件暴露技能运行时能力,
符合"插件只走 SDK 门面"的总边界。
## 一句话总结
Skill 系统 = **"markdown 即能力 + 多源覆盖加载 + 声明式门控 + 快照固化 + 不可信供应链治理 + 人审进化"**:
能力以纯文本契约描述,从六层来源按优先级合并,按环境声明过滤,
会话内固化为快照保证确定性;安装链路全程有信任校验与策略闸门,
而 agent 对技能库的修改永远经过 Workshop 人工审批。
完整官方文档:<https://docs.openclaw.ai/tools/skills>