From 7a0801caefae976cd690fbeea4591dcb9c5613ff Mon Sep 17 00:00:00 2001 From: clz Date: Mon, 15 Jun 2026 15:51:11 +0800 Subject: [PATCH] docs: add architecture and function call study notes --- .obsidian/app.json | 3 + .obsidian/appearance.json | 1 + .obsidian/core-plugins.json | 31 +++ .obsidian/workspace.json | 185 +++++++++++++++++ README.md | 40 ++++ agent-loop.md | 90 ++++++++ architecture-overview.md | 162 +++++++++++++++ blog/function-call-101.md | 376 ++++++++++++++++++++++++++++++++++ function-call-flow.md | 235 +++++++++++++++++++++ llm-autoregressive-kvcache.md | 48 +++++ skill-invocation-mechanism.md | 102 +++++++++ skills-cli-and-examples.md | 234 +++++++++++++++++++++ skills-management-design.md | 188 +++++++++++++++++ 13 files changed, 1695 insertions(+) create mode 100644 .obsidian/app.json create mode 100644 .obsidian/appearance.json create mode 100644 .obsidian/core-plugins.json create mode 100644 .obsidian/workspace.json create mode 100644 README.md create mode 100644 agent-loop.md create mode 100644 architecture-overview.md create mode 100644 blog/function-call-101.md create mode 100644 function-call-flow.md create mode 100644 llm-autoregressive-kvcache.md create mode 100644 skill-invocation-mechanism.md create mode 100644 skills-cli-and-examples.md create mode 100644 skills-management-design.md diff --git a/.obsidian/app.json b/.obsidian/app.json new file mode 100644 index 0000000..0281356 --- /dev/null +++ b/.obsidian/app.json @@ -0,0 +1,3 @@ +{ + "vimMode": true +} \ No newline at end of file diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.obsidian/appearance.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json new file mode 100644 index 0000000..b977c25 --- /dev/null +++ b/.obsidian/core-plugins.json @@ -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 +} \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json new file mode 100644 index 0000000..c921f58 --- /dev/null +++ b/.obsidian/workspace.json @@ -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" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0cee51 --- /dev/null +++ b/README.md @@ -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 中保证顺序确定性的做法。 + +完整官方文档: diff --git a/agent-loop.md b/agent-loop.md new file mode 100644 index 0000000..95d2732 --- /dev/null +++ b/agent-loop.md @@ -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)。 diff --git a/architecture-overview.md b/architecture-overview.md new file mode 100644 index 0000000..cbaea32 --- /dev/null +++ b/architecture-overview.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
Baileys] + TG[Telegram
grammY] + DC[Discord / Slack /
Signal / iMessage / 微信...] + WC[WebChat] + end + + subgraph GW["Gateway 守护进程(单实例,launchd/systemd 托管)"] + WS[WebSocket 服务
typed req/res + event,JSON Schema 校验] + RT[路由层
渠道/账号/对端 → agent] + AG[Agent 运行时
会话、队列、工具调用循环] + CRON[Cron / Webhook
自动化触发] + CANVAS["Canvas Host
/__openclaw__/canvas + a2ui"] + DB[(SQLite 状态库
state/openclaw.sqlite
+ 每 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 节点
语音唤醒/Canvas] + AND[Android 节点
相机/录屏/语音] + end + + subgraph LLM["模型提供商"] + P1[Anthropic / OpenAI /
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/
WS 服务与协议方法] + CHN[channels/
渠道传输实现] + AGS[agents/
agent 循环/工具/会话] + SKL[skills/ cron/ memory/
media/ tts/ ...] + CFG[config/ state/
canonical 配置 + SQLite] + SDK[plugin-sdk/
唯一对外门面 barrel] + end + + subgraph Ext["extensions/(插件,对外叫 plugins)"] + E1[telegram / discord /
slack / whatsapp ...] + E2[codex / copilot /
bedrock / vertex ...
模型提供商] + E3[memory-lancedb /
voice-call / ...] + end + + PKG[packages/*
gateway-protocol, llm-core,
agent-core, sdk ...] + UI[ui/ — Control UI] + APPS[apps/ — macOS/iOS/Android] + DOCS[docs/ → docs.openclaw.ai] + end + + Ext -->|"仅允许 import
openclaw/plugin-sdk/*"| SDK + SDK --> Core + Core --> PKG + Core -.->|"禁止 import 插件内部
(只走 manifest/registry)"| Ext + APPS -->|"Swift 模型由
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//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 渠道插件
(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 配对检查:陌生人
收到 pairing code,消息不处理 + RT->>A: 投递到目标 agent 会话(队列) + A->>A: 组装 prompt:系统提示 + skills
快照 + 记忆 + 会话历史 + loop Agent 循环 + A->>LLM: completion 请求 + LLM-->>A: 文本 / 工具调用 + A->>A: 执行工具(浏览器/bash/canvas...
非 main 会话可进沙箱) + end + A->>GW: 回复 payload(分块/媒体处理) + GW->>CH: 映射为渠道原生格式 + CH->>U: 回复送达 + GW-->>GW: 会话/状态写入 SQLite,
事件推送给 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 协议接入。 + +完整官方文档: diff --git a/blog/function-call-101.md b/blog/function-call-101.md new file mode 100644 index 0000000..41bd235 --- /dev/null +++ b/blog/function-call-101.md @@ -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... +... +[/SYSTEM] + +[HUMAN]北京今天天气怎么样?[/HUMAN] + +[ASSISTANT]我来查一下。 +{"name": "get_weather", "input": {"location": "北京"}} +[/ASSISTANT] ← 模型生成到这里停止 + +[HUMAN] +北京今天晴,28℃,东南风 3 级 +[/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 风格: + +``` + +北京 + +``` + +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 的实现。 diff --git a/function-call-flow.md b/function-call-flow.md new file mode 100644 index 0000000..e557842 --- /dev/null +++ b/function-call-flow.md @@ -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 } +``` +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"`,整批立即降为串行执行。 diff --git a/llm-autoregressive-kvcache.md b/llm-autoregressive-kvcache.md new file mode 100644 index 0000000..fc0bc2f --- /dev/null +++ b/llm-autoregressive-kvcache.md @@ -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。 diff --git a/skill-invocation-mechanism.md b/skill-invocation-mechanism.md new file mode 100644 index 0000000..63813f8 --- /dev/null +++ b/skill-invocation-mechanism.md @@ -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 differs from a previous turn, re-read its SKILL.md before using it. + + + + github + Interact with GitHub via gh CLI... + /path/to/skills/github/SKILL.md + 3 + + ... + +``` + +### 第二层:模型决定"用不用、用哪个" + +prompt 里那句指令就是全部机制—— +*"当任务与某个 skill 的 description 匹配时,用 read 工具加载它的文件"*。即: + +1. 模型拿用户任务和目录里的 `description` 做语义比对(纯 LLM 推理); +2. 认为匹配 → 调 `read` 工具读取 `` 指向的 `SKILL.md` 全文; +3. 按读到的指令行事(正文里通常写"遇到 X 情况用 Y 工具/命令")。 + +**推论:`description` 写得好不好直接决定技能会不会被触发** —— +它是 skill 作者手里最重要的"路由表项"。 + +## 决策流程图 + +```mermaid +flowchart TB + T[用户任务到达] --> M{模型读 available_skills 目录
任务 ≈ 某条 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[绕过模型
直接分发到注册工具] +``` + +## 三个细节设计 + +### 1. 预算降级(`applySkillsPromptLimits`) + +技能太多时按梯度退化,尽量保住"模型知道技能存在"这件事: + +| 梯度 | 行为 | 代价 | +| --- | --- | --- | +| 正常 | name + description + location + version | 无 | +| 超出 `maxSkillsInPrompt` | 截断技能数量 | 后面的技能不可见 | +| 超字符预算 | 降级 compact 格式:只有 name + location | 丢 description,模型只能按名字匹配 | +| 仍超 | 截断 + 注入警告 | `⚠️ Skills truncated... Run openclaw skills check` | + +### 2. 版本失效信号 + +目录里带 ``,prompt 指令要求 +"若 version 与上一轮不同,使用前必须重读 SKILL.md"。 +这是会话内技能内容更新的缓存失效机制—— +技能集快照(SkillSnapshot)本身在会话内固定,但正文变更可以通过版本号传导。 + +### 3. 绕过模型的路径 + +| frontmatter 配置 | 效果 | +| --- | --- | +| `disable-model-invocation: true` | 不进 ``,模型永远不会自主使用;仅用户可通过 `/skill-name` 触发 | +| `command-dispatch: tool` | 斜杠命令连模型都不经过,确定性分发到注册工具(`SkillCommandDispatchSpec`,`argMode: raw` 原样转发参数) | +| `metadata.openclaw.always: true` | 反向操作:跳过 requirements 门控,无条件进目录 | + +## 设计渊源 + +这与 Anthropic 官方 Agent Skills 的设计同构: +目录条目只占几十 token/技能,正文按需加载; +**"路由智能"留给模型,"可用性控制"留给代码**。 +好处是零检索基础设施、技能数量可扩展(token 成本近似常数), +代价是触发可靠性依赖模型能力与 description 质量。 diff --git a/skills-cli-and-examples.md b/skills-cli-and-examples.md new file mode 100644 index 0000000..2d91014 --- /dev/null +++ b/skills-cli-and-examples.md @@ -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 /
propose-update / revise] + WA[apply / reject / quarantine] + end + + LIST & INFO & CHECK --> STATUS["discovery/status.ts
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 `,解析顺序是:显式 `--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 ` | 单个技能的详情: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` | 进 `` 目录 | 第二层:模型能看到的范围(`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 + 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
+ .clawhub/origin.json + else slug 是源安装规格
(git:owner/repo@ref, ./dir, ../dir, ~/dir, 绝对路径) + U->>CLI: skills install git:owner/repo --as myskill + CLI->>SRC: installSkillFromSource
(isSkillSourceInstallSpec 判定) + SRC->>SRC: 写 .openclaw/source-origin.json
(source: "path"|"git", git.commit) + end + + U->>CLI: skills update --all + CLI->>SRC: readTrackedClawHubSkillSlugs
(只能追踪 ClawHub 来源) + CLI->>HUB: updateSkillsFromClawHub + + U->>CLI: skills verify weather [--card] + CLI->>HUB: resolveClawHubSkillVerificationTarget
+ fetchClawHubSkillVerification + HUB-->>U: 信任信封(decision: pass/...)
--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 ` | `inspectSkillProposal` | 看提案正文 + 附带的 support files + 扫描状态(`record.scan.state`) | +| `workshop propose-create --name --description` | `proposeCreateSkill` | 为**新**技能创建提案(`createdBy: "cli"`) | +| `workshop propose-update ` | `proposeUpdateSkill` | 为**已存在**技能创建更新提案 | +| `workshop revise ` | `reviseSkillProposal` | 替换 pending 提案的正文/描述/目标(产生新 `proposedVersion`) | +| `workshop apply ` | `applySkillProposal` | 人工批准后落盘到真实 `SKILL.md`(`workspace-skill-write`) | +| `workshop reject ` / `quarantine ` | `rejectSkillProposal` / `quarantineSkillProposal` | 拒绝或隔离(带 `--reason`) | + +提案正文来源统一是 `--proposal `(单文件)或 `--proposal-dir ` +(含 `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`(不进 ``),但仍可 `/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 里 +`` 的内容是同一份真相;`install/update/verify` 覆盖 ClawHub 与 +git/本地源两条安装路径,但只有 ClawHub 路径可追踪更新;`workshop` 子命令把 +"agent 提案 → 人工审批 → 落盘"的每一步都暴露成独立命令,方便脚本化审核。 + +完整官方文档:(命令参考)、 +(frontmatter 字段参考) diff --git a/skills-management-design.md b/skills-management-design.md new file mode 100644 index 0000000..d88c02a --- /dev/null +++ b/skills-management-design.md @@ -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/
多源扫描、frontmatter 解析、
优先级合并、序列化进 prompt] + DISC[discovery/
门控过滤、skill 索引、
斜杠命令生成、agent 过滤] + LIFE[lifecycle/
安装/更新:ClawHub、Git、
本地目录、zip 上传] + RUN[runtime/
会话/cron 快照、env 注入、
tool 直接分发、远程节点 eligibility] + SEC[security/
安装扫描器、ClawHub 信任裁决、
workspace 审计] + WORK[workshop/
agent 提案队列:
store / policy / service] + RES[research/
autocapture:从会话中
捕捉可复用工作流信号] + CFG[config/
skills.* 配置变更] + end + + LOAD --> DISC --> RUN + LIFE --> LOAD + SEC -.->|安装前置闸门| LIFE + RES --> WORK -->|apply 写回| LOAD + CFG --> LOAD + + CLI[openclaw skills CLI
+ Gateway skills.* 方法] --> LIFE + CLI --> WORK + HUB[(ClawHub
clawhub.ai 注册表)] <--> LIFE +``` + +## 2. 加载管线:从磁盘到 agent prompt + +技能从六类来源加载,**同名时高优先级覆盖低优先级**; +任何根目录下出现 `SKILL.md` 即被发现(子目录层级仅作组织用)。 + +| 优先级 | 来源 | 路径 | 可见范围 | +| --- | --- | --- | --- | +| 1(最高) | Workspace skills | `/skills` | 仅该 agent | +| 2 | 项目 agent skills | `/.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 解析
loading/] + PATH{路径收敛检查
realpath 必须在根内
除非 allowSymlinkTargets} + GATE{门控过滤 discovery/filter
requires.bins / env / config / os} + MERGE[同名合并:高优先级覆盖] + ALLOW{agent allowlist
agents.list#91;#93;.skills} + + Sources --> SCAN --> PATH --> GATE --> MERGE --> ALLOW + + subgraph Outputs["产出(同一份有效集,四处一致)"] + SNAP["SkillSnapshot
(prompt 片段 + env 需求 + 版本号)
会话级固定,cron 另有快照"] + CMD["斜杠命令注册
/skill-name(渠道可见)"] + SBX[沙箱同步
非 main 会话的技能文件] + REG[运行时注册表
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
(可选本地策略命令) + participant HUB as ClawHub 注册表 + participant FS as 技能目录 + + U->>CLI: skills install [--global] [--as name] + alt ClawHub 来源 + CLI->>HUB: 解析 slug,下载归档 + HUB-->>CLI: 包 + 安全扫描状态
(VirusTotal / ClawScan / 静态分析) + else Git / 本地 / 上传 + CLI->>CLI: git:owner/repo@ref 克隆 /
本地目录 / zip 分块上传
(上传需显式 allowUploadedArchives) + end + CLI->>POL: 元数据 + 暂存路径 + Note over POL: fail-closed:策略命令
无法给出有效决定即拒绝 + POL-->>CLI: allow / deny + CLI->>FS: 写入 workspace/skills
(--global → ~/.openclaw/skills) + CLI->>FS: 记录 .clawhub/origin.json
(版本 + 注册表来源) + + U->>CLI: skills update --all + CLI->>HUB: 按 origin.json 对比版本(仅 ClawHub 安装可追踪) + U->>CLI: skills verify + 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
捕捉可复用信号| P[提案草稿] + P --> Q[(workshop/store
提案队列)] + Q -->|workshop list / inspect| H{用户审批} + H -->|apply| W[写入活动技能文件
workspace-skill-write] + H -->|拒绝/忽略| X[丢弃] + W -.->|符号链接目标写入需
allowSymlinkTargetWrites| W +``` + +这是整个系统"agent 可以建议、人类拥有最终所有权"原则的具体化: +`workshop/policy.ts` 定义什么可以被提案,`service.ts` 管理生命周期, +CLI 入口为 `openclaw skills workshop list / inspect / apply `。 + +## 5. 与插件系统的关系 + +- 插件通过 `openclaw.plugin.json` 的 `skills` 字段携带技能目录,随插件启用而加载, + 合并在最低优先级层(与 `extraDirs` 同级)—— 任何同名的内置/托管/agent/workspace 技能都能覆盖它。 +- 插件技能可用 `metadata.openclaw.requires.config` 绑定到插件自己的配置项做门控。 +- 核心通过 `plugin-sdk/skills-runtime` barrel 向插件暴露技能运行时能力, + 符合"插件只走 SDK 门面"的总边界。 + +## 一句话总结 + +Skill 系统 = **"markdown 即能力 + 多源覆盖加载 + 声明式门控 + 快照固化 + 不可信供应链治理 + 人审进化"**: +能力以纯文本契约描述,从六层来源按优先级合并,按环境声明过滤, +会话内固化为快照保证确定性;安装链路全程有信任校验与策略闸门, +而 agent 对技能库的修改永远经过 Workshop 人工审批。 + +完整官方文档: