docs: add architecture and function call study notes
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"vimMode": true
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Vendored
+31
@@ -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
|
||||||
|
}
|
||||||
Vendored
+185
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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 Cache:token 逐步生成的机制、prefill vs decode 阶段、
|
||||||
|
首 token 慢的原因,以及服务端 Prompt Cache 的命中条件与 OpenClaw 中保证顺序确定性的做法。
|
||||||
|
|
||||||
|
完整官方文档:<https://docs.openclaw.ai>
|
||||||
@@ -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)。
|
||||||
@@ -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>
|
||||||
@@ -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 和 name,input 为空)
|
||||||
|
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 文本追加进 system,tool 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 call,agent loop 当普通文本回复处理,直接返回给用户。没有报错,但用户可能拿到的是一个不准确的答案——这正是为什么工具的 `description` 要写清楚。
|
||||||
|
|
||||||
|
| 情形 | 处理方式 |
|
||||||
|
|---|---|
|
||||||
|
| 参数类型小错误 | 自动类型转换,尽量容忍 |
|
||||||
|
| 参数不符合 schema | 错误信息还给模型,让模型重试 |
|
||||||
|
| 模型输出纯文本 tool call | 解析并提升为结构化调用 |
|
||||||
|
| 模型完全没调工具 | 当普通文本回复,loop 结束 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 几个常见误区
|
||||||
|
|
||||||
|
**误区一:`description` 随便写就行**
|
||||||
|
|
||||||
|
`description` 是模型决定"要不要用这个工具"的唯一依据,也是它真实读到的文本。写得模糊,模型就可能在不该调用时调用,或者该调用时放弃。工具描述和参数说明值得认真打磨。
|
||||||
|
|
||||||
|
**误区二:一次只能调一个工具**
|
||||||
|
|
||||||
|
现代模型支持在一次响应里输出多个 tool call,框架可以并行执行。这对"分头查询再汇总"的任务(比如同时搜索多个关键词)效率提升很大。
|
||||||
|
|
||||||
|
**误区三:有顺序依赖的操作需要开发者干预**
|
||||||
|
|
||||||
|
"先创建文件再写入"这类有依赖的操作,LLM 会自然地分轮次处理——它看不到上一步的结果,就不会发出下一步的调用。开发者只需要处理**同批次内的并发安全**(用 `sequential`),跨轮次的逻辑顺序不需要干预。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 小结
|
||||||
|
|
||||||
|
Function Call 的本质是一个**协议**:开发者用 schema 描述工具,模型用结构化输出表达"想调哪个",代码执行并返回结果,模型用结果继续生成。这个循环让 LLM 从"知识库"变成了"能行动的 agent"。
|
||||||
|
|
||||||
|
你现在看到的各种 AI 助手能查天气、写代码、操作文件、发邮件,背后都是这套机制在支撑。如果你对 agent 如何管理多轮工具调用的完整循环感兴趣,可以进一步了解 agent loop 的设计;如果想知道工具列表如何按需过滤,可以看 tool planner 的实现。
|
||||||
@@ -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 原生格式(见下节)
|
||||||
|
│
|
||||||
|
│ 放入请求 body(params.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_control(prompt 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 不 terminate,loop 继续。
|
||||||
|
|
||||||
|
**串行降级**
|
||||||
|
并行模式下只要发现任意一个 tool 的 `executionMode === "sequential"`,整批立即降为串行执行。
|
||||||
@@ -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。
|
||||||
@@ -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 质量。
|
||||||
@@ -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 字段参考)
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user