From 69c1fad7fb64f24d44fe05c9bb31cdf380f724ab Mon Sep 17 00:00:00 2001 From: clz Date: Wed, 17 Jun 2026 17:32:26 +0800 Subject: [PATCH] docs(blog): add RAG and Skills 101 drafts; refresh blog index Co-Authored-By: Claude Opus 4.8 --- blog/README.md | 14 ++ blog/rag-101.md | 352 ++++++++++++++++++++++++++++ blog/skills-101.md | 558 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 924 insertions(+) create mode 100644 blog/rag-101.md create mode 100644 blog/skills-101.md diff --git a/blog/README.md b/blog/README.md index cd6ec81..fbac261 100644 --- a/blog/README.md +++ b/blog/README.md @@ -14,3 +14,17 @@ M × N 问题的类比引入,说清 MCP 跟 Function Call 是不同层(模型↔应用 vs 应用↔工具),澄清三个常见误解,并用 OpenClaw 源码示意 MCP server 怎么被发现、何时连接、模型如何看到扁平 tool 列表。 + +- [rag-101.md](./rag-101.md) + RAG 是什么、怎么让 LLM 看着你的资料回答问题:从"AI 不知道公司内部数据" + 的真实问题出发,对比塞 prompt / fine-tuning / 自己搜 / RAG 四种解法, + 详细讲清三步骤(检索/增强/生成)和向量化的概念,澄清"向量塞 prompt" + 的常见误解,介绍切块/检索失败/embedding 不等于相关性/评估等工程坑, + 并对比 RAG 跟 MCP、Skill 的关系。 + +- [skills-101.md](./skills-101.md) + Skill 是什么、怎么让 agent 学会"什么时候用什么工具":从"接了 100 个工具 + 模型怎么选"的现实问题出发,讲清 progressive disclosure(prompt 里只放 + description 目录,正文按需 `read` 加载)、跟 RAG 的本质区别、"代码筛 + 可用性 / 模型选用哪个"的分层、description 工程、Skill Workshop 人审进化、 + 以及 Skill 跟 tool 是控制 vs 能力的关系。 diff --git a/blog/rag-101.md b/blog/rag-101.md new file mode 100644 index 0000000..8e3afcd --- /dev/null +++ b/blog/rag-101.md @@ -0,0 +1,352 @@ +# RAG 是什么?让 LLM 看着你的资料回答问题 + +> 本文是 RAG(Retrieval-Augmented Generation,检索增强生成)的科普介绍, +> 不依赖具体源码。完整技术细节见 +> [../rag-overview.md](../rag-overview.md)。 + +如果你看过 [Function Call 101](./function-call-101.md) 和 +[MCP 101](./mcp-101.md),你已经知道 LLM 怎么 "用工具" 了。但还有一类 +问题工具解决不了:**模型不知道你公司的数据**。 + +你问 ChatGPT "我们公司的报销额度是多少",它怎么可能知道? +报销政策没在它的训练数据里。 + +RAG 是目前最常用的解法。这篇讲它是什么、怎么工作、为什么需要专门搞一个 +概念叫 "向量化"。 + +--- + +## 从一个具体问题开始 + +假设你给公司做一个 AI 助手,员工问: + +> "我们公司的报销额度是多少?" + +你把这句话丢给 LLM,得到: + +> "我无法获取你们公司的具体政策。建议查询员工手册或联系 HR..." + +废话。员工知道员工手册里有,但 200 页谁愿意翻?他们就是想让 AI 直接 +告诉他们。 + +问题在于:**LLM 训练时根本没见过你公司的员工手册**。它的"知识"全部 +封存在训练权重里,新数据进不去。 + +你有几个选择: + +### 选项 A:把整本手册塞进 system prompt + +``` +System: 你是公司 AI 助手。以下是员工手册: +[200 页 PDF 文字...] + +User: 我们公司的报销额度是多少? +``` + +理论可行,但: + +- 200 页可能 50 万 token,每次对话都付这个钱,贵 +- 模型有"大海捞针"问题——内容太多,中间部分容易被忽略 +- 手册更新了得重新塞,而且系统提示太长会挤占其他重要指令的空间 + +### 选项 B:Fine-tuning(微调) + +把员工手册做成训练数据,微调一个版本的模型,让它"记住"这些政策。 + +可行,但: + +- 训练贵,得 GPU 集群 +- 政策一改要重训 +- 微调可能让模型遗忘其他能力(灾难性遗忘) +- 你需要一支 ML 团队 + +### 选项 C:让模型自己搜 + +加一个 search 工具,让模型自己决定要不要查、查什么、读哪条结果。 + +可行(其实就是 [Function Call](./function-call-101.md) 的应用),但: + +- 多了几轮往返,慢 +- 模型可能错过该查的时机 +- 还是需要一个搜索引擎能搜到内部文档 + +### 选项 D:RAG + +**在用户问问题之前,先去公司文档库里找相关段落,把段落和问题一起 +塞给模型**,让模型基于段落回答。 + +``` +User 输入: "我们公司的报销额度是多少?" + + ↓ 在用户看不到的后台: + ↓ [1] 检索:在公司文档库找最相关的几段 + ↓ → 找到:"差旅报销单笔上限 2000 元,需主管审批..." + + ↓ [2] 增强:把检索结果拼进 prompt + ↓ Prompt: "根据以下文档回答: + ↓ 差旅报销单笔上限 2000 元,需主管审批... + ↓ 问题:我们公司的报销额度是多少?" + + ↓ [3] 生成:LLM 基于资料回答 + +模型输出: "公司差旅报销单笔上限是 2000 元,需要主管审批。" +``` + +R + A + G。模型不"记住"任何东西,每次回答之前临时拿资料看一眼。 + +--- + +## 为什么这是个好主意 + +对比一下: + +| 方案 | 知识更新 | 成本 | 可解释 | 实施难度 | +| ---------------- | ----------------- | ----------------- | --------------------- | --------------------- | +| 全塞 prompt | 每次都要重塞 | 每次对话都贵 | 不行(模型从一堆里挑) | 简单 | +| Fine-tuning | 改了要重训 | 训练超贵,推理便宜 | 不行(知识进了权重) | 难 | +| 让模型自己搜 | 实时(搜的是新数据) | 多轮往返 | 能("我搜了 X 得到 Y") | 中 | +| **RAG** | 实时(查的是当前库) | 检索 + 一次推理 | 能("根据 X 文档") | 中 | + +RAG 几乎在所有维度上都不错。**它最大的优势是"知识跟模型解耦"**—— +你想换底层模型?随便换,检索逻辑没动;你想更新政策?改一下文档, +重新索引,搞定。 + +不用训练,不用 ML 团队,不用调几小时超参。 + +--- + +## 三个步骤拆开看 + +### [1] 检索:怎么找到 "相关" 的段落 + +最朴素的想法:**关键词搜**。 + +用户问 "退货",我在文档库 grep "退货",拿到所有包含"退货"的段落。 + +问题是: + +- 用户问 "退货" 但文档里只写 "退款政策" → 漏了 +- 用户问 "我不想要这东西了" → 字面不含 "退货",漏了 +- 用户问 "返货" → 也漏了 + +人类问问题是**语义的**,搜索却只能匹配**字面的**。 + +RAG 的核心创新就在这里:**用向量化把 "字面" 变成 "语义"**。 + +### 向量化:把文字变成一组数字 + +`text-embedding-3-small`(OpenAI 的 embedding 模型)接收一段文字, +吐出 1536 个浮点数: + +``` +"退货" → [0.21, -0.13, 0.88, 0.45, ..., 0.07] +"退款" → [0.23, -0.10, 0.85, 0.42, ..., 0.05] ← 数字很接近 +"购物车" → [0.91, 0.40, -0.20, 0.10, ..., -0.30] ← 数字差很远 +``` + +这 1536 个数字不是随便的——它们是 "语义坐标"。**意思相近的词, +数字也接近**。 + +为什么?embedding 模型预训练时的目标就是 "让意思相近的句子输出 +向量接近,意思不同的句子向量远"。你不需要懂它的训练原理,只需要把 +它当成一个黑盒:**输入文本,输出"语义指纹"**。 + +有了这个,检索就变成一件优雅的事: + +1. 离线时:把文档库里每段文字都向量化,把(向量,原文)成对存进 + 向量数据库(Pinecone、Qdrant、LanceDB 之类) +2. 用户问问题时:把问题也向量化 +3. 在数据库里找跟 "问题向量" 最接近的几个 "文档向量" +4. 把对应的**原文**拼进 prompt(注意是原文,不是向量) + +"接近"用 **余弦相似度** 算——把向量当 1536 维空间里的方向,方向越 +一致,夹角越小,余弦值越接近 1。 + +### 一个常见误解:向量和 prompt + +很多人初学时会问:**"向量化的结果是什么?是不是把向量塞进 prompt?"** + +**不是。** LLM 看不懂向量,它只懂自然语言。向量只在"检索"这一步用 +来匹配,匹配完之后用的是**原文**。 + +数据库里其实是两份数据并存: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ id │ vector │ text │ +│ #001 │ [0.21,-0.13,0.88,..]│ "退款政策:商品 7 天内可..." │ +│ #002 │ [0.91, 0.40,-0.20,..]│ "购物车使用指南..." │ +│ #003 │ [-0.55,0.70,-0.10,..]│ "隐私条款..." │ +└─────────────────────────────────────────────────────────────┘ + ↑ ↑ + 用来 "找" 用来 "还原" +``` + +类比:**向量像图书索引卡上的"主题分类号",原文像书的内容**。 +读者(LLM)读的是书,索引卡只是帮你找到该读哪本书。 + +最终发给 LLM 的 prompt 长这样,**从头到尾全是自然语言**: + +``` +System: You are a helpful assistant. Answer based on the provided context. + +Context: +--- +[Source: policies/refund.md] +退款政策:商品 7 天内可申请退货退款,需保持原包装完好,联系客服 400-xxx 办理。 +--- + +Question: 怎么退货? +``` + +向量没出现,因为它的使命在检索那一步就完成了。 + +### [2] 增强:把检索结果塞进 prompt + +这一步简单,就是把找到的几段原文按某种格式拼进 prompt。常见格式: + +``` +请基于以下资料回答用户问题。如果资料里没有相关信息,直接说"我不知道"。 + +[资料 1] {{ chunk_1 }} +[资料 2] {{ chunk_2 }} +[资料 3] {{ chunk_3 }} + +用户问题:{{ user_query }} +``` + +"如果资料里没有相关信息,直接说我不知道" 这一句很重要—— +没它的话模型容易在检索失败时强行编一个答案出来。 + +### [3] 生成:让 LLM 回答 + +普通 LLM 调用,但 prompt 里多了"参考资料"。模型在生成时会优先用 prompt +里给的内容,不太敢瞎编(因为提示明确告诉它"基于资料")。 + +这一步没什么特别的——RAG 的"魔法"全在前两步。 + +--- + +## 实际工程里的坑 + +讲到这里你可能觉得 RAG 很简单——"切块 + 向量化 + cosine + 拼 prompt" +四步搞定。**它的概念确实简单,工程却充满坑**。 + +### 坑 1:切块策略 + +文档不能整篇喂进去——一篇 50 页的政策手册一个向量,语义信息全被 +平均掉了。要切块,但怎么切? + +- 按固定长度切?会切断句子,信息丢 +- 按段落切?长度不均,有的几行有的几百行 +- 按章节切?太大,匹配不精确 + +实际常用做法:**滑动窗口 + 重叠**——每 500 字一块,相邻块重叠 100 字, +避免边界丢上下文。更高级的做语义切块,用 embedding 相似度判断切点。 + +### 坑 2:检索失败比幻觉更糟 + +如果检索没拉到相关文档,模型就只能瞎编了。它可能写出一个**听起来 +专业、引用煞有介事但完全错误**的答案。 + +防御措施: + +- prompt 里明确写 "如果资料里没有就说不知道" +- 设相似度阈值——top-1 的 cosine 低于 0.5 就直接拒绝回答 +- 召回后做 reranker 二次验证 + +### 坑 3:embedding ≠ 相关性 + +embedding 模型抓的是 "语义相似",不一定是 "对你这个问题有答案"。 + +例子:用户问 **"明天天气怎么样?"**,文档里有: + +- A. "天气预报通常由气象台发布..." +- B. "明天我们公司放假,大家不用上班..." + +embedding 可能把 A 排在前面(都讨论天气),但 A 其实没回答用户的问题; +B 字面不相关,但其实是用户该看的(他可能想问 "明天用不用上班")。 + +实战常见的加强: + +- **混合检索**:embedding(语义)+ BM25(关键词)双路召回合并 +- **Reranker**:先拉 50 个,用专门的重排模型(cohere-rerank、bge-reranker) + 精排到 top 5 +- **HyDE**:让 LLM 先 "想象" 一个理想答案,用想象答案去检索——比直接 + 用问题更接近文档表述 + +### 坑 4:评估超难 + +模型说错了,根因是什么? + +- 检索没拉到正确文档? +- 拉到了但模型没用? +- 用了但理解错了? + +需要分阶段评估: + +- 检索准确率(top-k 里有没有正确答案) +- 端到端准确率(最终回答对不对) +- 引用一致性(回答有没有 hallucinate,真的在检索结果里吗) + +很多生产 RAG 系统在这上面投入的精力不亚于 RAG 本身的实现。 + +--- + +## RAG 跟 MCP 是什么关系 + +[MCP 101](./mcp-101.md) 讲过 MCP 是 AI 应用怎么接入外部工具的协议。 +那 RAG 跟 MCP 是什么关系? + +**很多 MCP server 内部就是 RAG**。比如 `mcp-server-notion` 接到 query +后,在 Notion 里做向量检索,返回相关页面——这是个标准 RAG 系统,只是 +包装成了 MCP 接口。 + +但 RAG 也可以**不通过 MCP**——直接在 agent 框架里写死,或者每次 LLM +请求前手工拼 prompt。**MCP 是分发机制,RAG 是内部实现**,两者不冲突。 + +更广义看,RAG 是 "模型不知道怎么办" 这个问题的多种解法之一: + +``` +模型不知道怎么办? + ├─ 缺事实/知识(报销额度、产品手册) → RAG(查文档库,把原文拼进 prompt) + ├─ 缺实时数据(今天天气、最新 PR) → tool use(让模型自己调 API) + └─ 缺通用能力(根本不会写代码) → fine-tuning(改模型本身) +``` + +下一篇会讲第四类情况——**模型有工具但不知道什么时候用哪个**—— +这又是另一种 "知识" 的缺失,需要另一种解法。 + +--- + +## 总结 + +RAG 的核心思路一句话:**不让模型"凭记忆"回答,让它"看着资料"回答**。 + +``` +[索引阶段] 文档 → 切块 → 向量化 → 存数据库 + +[查询阶段] 问题 → 向量化 → 找近邻 → 拼原文 → LLM 生成 +``` + +它在四个维度上比其他方案漂亮: + +- **知识可实时更新**——改文档重新索引就行,不用重训模型 +- **可解释**——回答可以标注来源 +- **便宜**——只需要一次嵌入服务调用 + 一次 LLM 推理 +- **可控**——你完全决定文档库的内容和检索策略 + +如果你做的 AI 应用要回答 "基于我们的数据" 的问题,RAG 是默认应该考虑 +的方案。它不一定是最完美的,但它便宜、好懂、可演化。 + +--- + +## 想深入了解? + +- 完整技术细节:[../rag-overview.md](../rag-overview.md)——包括向量数据库 + 选型、常见混合检索 / rerank / HyDE 策略、向量化背后的数学 +- 系列前文:[function-call-101.md](./function-call-101.md)、 + [mcp-101.md](./mcp-101.md) +- 系列下一篇:[skills-101.md](./skills-101.md)——RAG 解决了 "知识" + 的问题,Skill 解决 "什么时候用哪个工具" 的问题,会借用 RAG 类似的 + 思路但走出一个有意思的变体 diff --git a/blog/skills-101.md b/blog/skills-101.md new file mode 100644 index 0000000..caf6f58 --- /dev/null +++ b/blog/skills-101.md @@ -0,0 +1,558 @@ +# Skill 是什么?让 agent 学会"什么时候用什么工具" + +> 本文以 [OpenClaw](https://github.com/openclaw/openclaw) 开源源码为例 +> (基于 2026.6.2 版本),结合实际代码说明 agent skill 系统的设计和工作原理。 +> 涉及源码路径均可在仓库中直接查阅。 + +如果你跟着这个系列读到这儿: +[Function Call](./function-call-101.md) 讲了模型怎么调工具, +[MCP](./mcp-101.md) 讲了工具怎么从哪儿来, +[RAG](./rag-101.md) 讲了怎么让模型看着资料回答。 + +但还有一个问题没解:**给 agent 接了 100 个工具,它怎么知道 +什么时候该用哪个?** 这次不是 "知识不知道",而是 "方法不知道"。 + +本文回答这个问题。答案叫 **Skill**(技能)——它会借用一些 RAG 的 +设计思想,但走出一个有意思的变体。 + +--- + +## 一个真实的麻烦 + +假设你正在做一个 AI 编程助手,接了一堆工具:`Read`、`Bash`、`Grep`、 +`Edit`、`gh`(GitHub CLI 包装)、`git`、`docker`、`kubectl`、`npm`、 +`pytest`、再加上 20 个 MCP server 暴露的工具。模型看到的 tool 列表有 +80 个。 + +用户说:**"帮我看看这个 PR 的评论"**。 + +模型该用哪个工具?是 `gh pr view --comments`?还是 `git log --grep`? +还是某个 MCP 工具?光看工具名和参数 schema,**模型其实不知道你的团队 +习惯用哪种方式**。 + +你可以这么做: + +### 选项 A:把"怎么做"全塞进 system prompt + +``` +You are a coding assistant. When the user asks about PR comments, +use `gh pr view --comments --json`. When asked about +recent commits, use `git log --oneline -20`. When deploying, use +kubectl with our cluster context "prod-east"... +``` + +这能解决问题——但 10 条指令还行,100 条就崩了:**system prompt 越来越长, +所有任务都付全套 token 成本,而且每条 instruction 都会跟其他 instruction +互相干扰**。 + +### 选项 B:让模型自己看 README + +把所有工作流写成 markdown 文档,告诉模型 "需要时去读"。问题是:它 +怎么知道现在该读哪份?80 个工具加 50 份操作文档,**索引到底在哪儿**? + +### 选项 C:用 RAG 查相关文档 + +把所有文档向量化,每次任务先 retrieval([RAG](./rag-101.md) 那一套)。 +可行,但有两个问题: + +- 要维护一套向量库——切块、embedding、向量数据库,基础设施成本不小 +- 程序操作类的精确指令上,**embedding 经常翻车**——"PR 评论怎么查" + 跟 "GitHub gh CLI 文档" 字面差很远,语义匹配可能拉到错的段落 + +对"知识检索"RAG 很合适,但对"流程指令"它有点重。 + +Skill 是第四个选项,而且很巧妙——**它借了 RAG 的"先找再用"思路, +但把"找"这一步做得更轻**。 + +--- + +## Skill 是什么 + +一句话:**Skill 是一份 markdown 文档,描述"什么时候、怎么完成某类 +任务",存在文件系统里,模型按需读取。** + +每个 skill 是一个目录,核心是带 YAML frontmatter 的 `SKILL.md`: + +```markdown +--- +name: github +description: Interact with GitHub via gh CLI - view PRs, issues, comments, etc. +metadata: {"openclaw": {"requires": {"bins": ["gh"]}}} +--- + +When the user asks about a PR's comments, run: + gh pr view --comments --json + +When asked about recent commits in a repo, use: + git log --oneline -20 + +If the user is asking about a closed PR, also check: + gh pr view --json mergedAt,closedAt +... +``` + +就这。整个"能力"就是这份 markdown 加上目录结构。OpenClaw 仓库里有约 +50 个内置 skill,每个都长这样——`github`、`pytest`、`docker`、 +`image-lab` 等等。 + +--- + +## 核心机制:渐进式披露(progressive disclosure) + +最聪明的部分在这里。 + +**模型的 prompt 里不放 skill 的正文,只放一个目录**: + +```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. + + + + github + Interact with GitHub via gh CLI - view PRs, issues... + /path/to/skills/github/SKILL.md + 3 + + + pytest + Run and interpret Python tests... + /path/to/skills/pytest/SKILL.md + 1 + + ...50 个 skill... + +``` + +每条只有几十个 token。模型看到这个目录后: + +1. 用户问 **"帮我看看这个 PR 的评论"** +2. 模型扫一遍 description,**"github" 这条匹配** +3. 模型调用 `read` 工具,读取那个 `` +4. 拿到 SKILL.md 正文,按指令执行 `gh pr view ... --comments` + +**只有匹配上的 skill 才付正文 token 成本**。50 个 skill 在目录里大概几千 +token,如果今天用了 github 和 pytest 两个,实际加载的正文也就 1000 tok。 +对比"全塞进 system prompt"那种动辄几万 token 的方案,差距巨大。 + +这个设计有个名字叫 **progressive disclosure**——一开始只给最少必要 +信息,需要细节时再加载。Anthropic 2025 年初提出的 [Agent Skills 规范](https://agentskills.io) +就是基于这个思路。 + +--- + +## OpenClaw 里是怎么落地的 + +讲完概念,看看 OpenClaw 源码里一次完整的 skill 调用从头到尾发生了什么。 +这一节回答几个具体问题:**skill 是怎么被找到的、prompt 长什么样、 +模型怎么"决定"用某个 skill、它返回的内容里怎么标识这次调用**。 + +### 1. 加载:六类来源,优先级合并 + +agent session 启动时,OpenClaw 扫描六类目录(`src/skills/loading/`): + +``` +优先级 1(最高) │ /skills 仅该 workspace +优先级 2 │ /.agents/skills 仅该 agent +优先级 3 │ ~/.agents/skills 本机所有 agent +优先级 4 │ ~/.openclaw/skills 本机所有 agent +优先级 5 │ 内置 bundled(约 50 个) +优先级 6(最低) │ extraDirs + 插件携带 +``` + +同名时高优先级覆盖低优先级——你可以在 workspace 里写一个 `github` +覆盖内置的 `github`,本地定制不动上游。 + +扫到的每个 SKILL.md 解析 YAML frontmatter,经过门控过滤(缺二进制 +`requires.bins`、缺环境变量 `requires.env`、不在 agent allowlist 等) +被剔除掉。 + +活下来的合到一份 **SkillSnapshot**(`src/skills/runtime/session-snapshot.ts`) +固化在 session 上。**会话中途用户改 skill 不影响进行中的对话**——这跟 +[MCP catalog 缓存](./mcp-101.md) 是同样的"热路径不轮询"思路。 + +### 2. Prompt:实际的 XML 长什么样 + +prompt 由 `formatSkillsForPrompt`(`src/skills/loading/skill-contract.ts:53`) +生成。**真实输出就是下面这段** 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. +When a skill file references a relative path, resolve it against the skill directory ... + + + + github + Interact with GitHub via gh CLI - view PRs, issues, comments, releases + /home/clz/.openclaw/skills/github/SKILL.md + 3 + + + pytest + Run and interpret Python tests, debug failures + /home/clz/.openclaw/skills/pytest/SKILL.md + 1 + + ... + +``` + +这段拼到 system prompt 里,跟 system instruction、工具列表(`tools` +字段)一起发给模型。这就是模型看到 skill 的**全部**——50 个 skill +也就几千 token,跟"全塞 skill 正文"动辄几万 token 差好几个数量级。 + +注意三点: + +1. **没有正文**,只有 ``(SKILL.md 绝对路径) +2. **`` 用于会话内缓存失效**——上轮和这轮版本不同时,模型 + 被指令要求重读 +3. **prompt 里有句明确指令**:"Use the read tool to load a skill's + file when the task matches its description"。**这一句就是全部 + 触发机制**,没有别的代码逻辑 + +### 3. LLM 怎么"决定"使用某个 skill + +**完全是 LLM 自己的语义判断,没有任何代码做匹配**。 + +OpenClaw 这一侧: + +- 不算 embedding +- 不做关键词索引 +- 不调用任何路由算法 +- 不告诉模型"现在该用 X skill" + +模型这一侧拿到 prompt 之后: + +1. 看到用户问题 "帮我看看 PR 123 的评论" +2. 扫 `` 里几十条 `` +3. 用语义判断:**"github" 这条 description 提到了 PR 和 comments,匹配** +4. 决定调 read 工具去读 `` 指向的文件 + +整个"路由智能"完全是 LLM 推理出来的。OpenClaw 只负责把候选目录端 +端正正放好。 + +**推论:`description` 的写法直接决定 skill 会不会被触发**——它是 +skill 作者手里最重要的"路由表项"。写得含糊(`"GitHub stuff"`)模型 +就匹配不到;写得准确(`"Interact with GitHub via gh CLI - view PRs, +issues, comments"`)模型几乎必中。 + +### 4. LLM 返回的内容里有什么 + +**没有"特殊的 skill 调用类型"**。模型返回的就是普通的 `tool_use`—— +一个 `read` 工具调用,参数是 SKILL.md 路径: + +```json +{ + "stop_reason": "tool_use", + "content": [ + { + "type": "text", + "text": "我用 github skill 来查 PR 评论。" + }, + { + "type": "tool_use", + "id": "toolu_01abc...", + "name": "Read", + "input": { + "file_path": "/home/clz/.openclaw/skills/github/SKILL.md" + } + } + ] +} +``` + +注意 `name` 是 `"Read"`,**不是 `"skill_github"` 或 `"invoke_skill"` +之类的特殊名字**。Skill 触发本质上就是一次 file read,跟模型读其他 +文件没有任何机制上的区别。 + +OpenClaw 这一侧收到 tool_use 后: + +1. 在 tool 列表里找到 `Read`,调它的 `execute` +2. 返回 SKILL.md 全文给模型 +3. 模型在下一轮看到正文,按指令开始执行真正的工作 + (`gh pr view 123 --comments --json`) + +整个流程**完全复用了 Function Call 的标准机制**——这是 progressive +disclosure 设计的另一个优点:**不需要新协议、不需要 provider 支持 +特殊字段、模型不需要任何专门的训练。** + +### 5. 走完一遍完整时序 + +``` +Session 启动 + │ + ├─ 扫六类目录,加载 SKILL.md frontmatter + ├─ 门控过滤,剔除不满足 requires 的 + ├─ 固化 SkillSnapshot + └─ formatSkillsForPrompt() → XML 片段 + +用户发问:"帮我看看 PR 123 的评论" + │ + ├─ OpenClaw 组装请求: + │ system: "......" + │ tools: [Read, Write, Bash, ...] + │ user: "帮我看看 PR 123 的评论" + │ + └─ 发给 LLM + +LLM 第 1 轮返回: + │ + └─ tool_use: Read(file_path="/path/to/skills/github/SKILL.md") + +OpenClaw 执行 Read,返回 SKILL.md 正文 + +LLM 第 2 轮返回: + │ + ├─ "我用 github skill,执行 gh pr view" + └─ tool_use: Bash(command="gh pr view 123 --comments --json") + +OpenClaw 执行 Bash,返回 JSON 结果 + +LLM 第 3 轮返回: + │ + └─ 用自然语言把 PR 评论整理给用户 +``` + +整个机制非常简洁:**两次额外 tool_use 往返(一次 read SKILL.md、 +一次执行 SKILL.md 里指的命令),换来零基础设施 + 任意可扩展的能力库**。 + +### 6. 一个有意思的细节:为啥不直接塞 SKILL.md 内容 + +读到这里你可能会想:"既然要 read 一次拿全文,为啥不在 prompt 里 +直接给正文,省一次往返?" + +OpenClaw 的设计就是为了避这个开销: + +- 50 个 skill 的正文加起来可能上万 token,**每轮都付这个钱**(prompt + cache 命中也得算 hash + 重新走 prefill) +- 用户这一轮可能根本不用任何 skill(普通问候/闲聊),正文白付 +- 多一次 read 往返,对比"每轮付全部 token",在大多数场景下都更划算 + +进一步,**OpenClaw 在 prompt 预算紧张时还会更激进地压缩目录** +(`applySkillsPromptLimits`)——只保留 name + location,丢掉 description。 +即使在这种最差情况下,模型仍然知道"有这些 skill 存在",可以试着读 +进来看看。 + +--- + +## 跟 RAG 有什么不一样 + +上一篇刚讲完 [RAG](./rag-101.md),你可能觉得这俩思路很像——都是 +"先找相关的、再让模型用"。**核心思想确实是 RAG 那一套 progressive +disclosure 的延伸**,但 Skill 在关键一步上换了实现: + +| | RAG | Skill | +| ---- | --------------------------------- | ----------------------------------- | +| 索引 | embedding 向量库 | 目录 + 文本 description | +| 匹配 | cosine similarity(数学) | 模型读 description 做语义判断(LLM) | +| 加载 | 检索器自动选 top-k | 模型自己决定调 `read` 工具 | +| 基础设施 | 向量数据库 + embedding 模型 | 文件系统 + 一个 read 工具 | +| 适合什么 | 大规模、模糊查询、长尾知识 | 中等数量、明确分类、操作指令 | + +关键差别在 **"找" 这一步谁来做**: + +- **RAG** 让向量库来找——准确但要基础设施(embedding 服务 + 向量数据库), + 适合海量、模糊、跨文档的知识检索 +- **Skill** 让模型自己来找——把目录直接塞进 prompt,让模型读 description + 做语义匹配,**省掉向量化整套链路** + +为什么 Skill 敢省掉向量化?因为它的场景跟 RAG 不一样: + +- RAG 要在几万段文档里找几个相关段——向量化是必要的,模型扫不过来 +- Skill 通常就 50~100 条——目录占几千 token,模型完全扫得过来 + +简言之:**RAG 适合"知识检索",Skill 适合"流程路由"**。规模小、目标 +明确的场景,直接让 LLM 充当"路由器"比搭一套向量库更轻、更准。 + +这是个有意思的工程权衡:**当 LLM 自己够聪明时,有些中间层就可以 +省掉**。 + +--- + +## "路由智能"留给模型,"可用性控制"留给代码 + +OpenClaw 的实现(`src/skills/`)有一个清晰的分工: + +**代码这一层管"哪些可用"**——会被加载进目录的 skill 必须通过这些过滤: + +- 缺少必需的二进制(`requires.bins: ["gh"]` 但本机没装 gh)→ 排除 +- 缺少必需的环境变量(`requires.env: ["GITHUB_TOKEN"]`)→ 排除 +- 平台不符(`requires.os: ["macos"]` 但跑在 Linux 上)→ 排除 +- 不在当前 agent 的 allowlist 里 → 排除 + +**模型这一层管"用不用、用哪个"**——目录里活下来的 skill,选哪个完全是 +模型的语义判断。代码不去做关键词匹配、不去算 embedding、不去做任何 +"推荐"。 + +这个分层很重要: + +- 代码擅长**确定性检查**(二进制装没装、env 设没设) +- 模型擅长**语义判断**("用户说的'PR 评论'对应 github 这个 skill") + +让各自做自己擅长的事。 + +--- + +## description 是 skill 作者最重要的产物 + +既然模型靠 description 做匹配,**description 写得好不好直接决定 skill +会不会被触发**。 + +好的 description: + +```yaml +description: Interact with GitHub via gh CLI - view PRs, issues, +comments, releases, run workflows, manage labels. +``` + +涵盖了关键名词("PR"、"issue"、"comment")和动词("view"、"run"、 +"manage"),用户问任何相关问题模型都能匹配上。 + +差的 description: + +```yaml +description: GitHub stuff +``` + +模型看到这个根本不知道你 skill 里写了什么。用户问 "看 PR 评论" 时模型 +不会想到调它。 + +这是 skill 工程的核心技能——不是写代码,而是 **写出让模型能精确匹配的 +英文描述**。 + +--- + +## 版本失效:会话中如何感知 skill 变化 + +`` 里每条带一个 ``,prompt 里有句指令: + +> If a skill's `` differs from a previous turn, re-read its SKILL.md before using it. + +会话开始时,OpenClaw 固化一份 SkillSnapshot(`src/skills/runtime/`), +中途用户改了 `SKILL.md`,新的 version 会进到目录里,模型看到版本变了 +就重读正文——**缓存失效靠版本号传导,而不是每轮都重读**。 + +这跟我们之前在 [MCP 内部实现](../mcp-client-internals.md) 里看到的 +"catalog 缓存 + listChanged 失效" 是同一个套路:**热路径缓存,显式信号 +触发刷新**。 + +--- + +## 还有几个有意思的设计 + +### 1. prompt 预算超限时怎么办 + +50 个 skill 写得详细的话,目录可能上万 token。OpenClaw 有梯度退化 +(`applySkillsPromptLimits`): + +| 梯度 | 行为 | 代价 | +| --- | --- | --- | +| 正常 | name + description + location + version | 无 | +| 超 `maxSkillsInPrompt` | 截断技能数量 | 后面的不可见 | +| 超字符预算 | 降级 compact:只剩 name + location | 丢 description,模型只能按名字猜 | +| 再超 | 截断 + 注入警告 | 提示用户 `openclaw skills check` | + +哪怕最差情况,模型也至少**知道有这个 skill 存在**,可以试着用 read +工具看看。这是 graceful degradation 的好例子——情况越糟,体验越糟, +但永远不彻底崩。 + +### 2. 绕过模型的两条路径 + +有时候你不想让模型自己决定: + +- `disable-model-invocation: true` → 不进目录,模型自主用不到;**只能 + 用户敲 `/skill-name` 触发** +- `command-dispatch: tool` → 斜杠命令**连模型都不经过**,直接分发到 + 注册工具(确定性操作) + +这是给敏感操作或者高频操作准备的——比如 `/deploy` 你绝对不希望模型 +"自己判断时机",必须人触发。 + +### 3. Skill Workshop:agent 提案 + 人审 + +agent 在工作中发现"这个流程值得做成 skill"时,**不直接写 SKILL.md**, +而是产出一份提案进队列。用户审批(`openclaw skills workshop apply `) +后才写进 skill 目录。 + +这是 "agent 可以建议、人类拥有最终所有权" 原则的体现——你不会希望模型 +偷偷改自己的指令集。 + +### 4. 安装是供应链问题 + +skill 可以从多个来源安装:ClawHub 注册表(类似 npm)、Git 仓库、本地 +目录、zip 上传。每个来源都被视为 **不可信代码**: + +- ClawHub 上架前过安全扫描(VirusTotal / ClawScan / 静态分析) +- 安装时本地可以配 `security.installPolicy` 跑自定义策略命令,fail-closed +- `openclaw skills verify` 拿信任信封做校验,适合接入 CI 做供应链门禁 + +**skill 不是"代码"但也不只是"文档"**——它能让模型执行任意命令, +所以安全模型按代码标准来。 + +--- + +## Skill 跟 native tool / MCP tool 是什么关系 + +你可能会想:"模型已经有 tool 了,skill 是不是又一层抽象?" + +**不是的**。skill 不是 tool,它**控制模型怎么用现有的 tool**。 + +- **Tool**(包括 native、plugin、MCP)是"能做什么"——读文件、跑命令、调 API +- **Skill** 是"什么时候做、怎么做"——遇到这种用户请求,该按什么顺序调哪些工具 + +类比一下: + +- Tool 是厨房里的刀、锅、铲——基础能力 +- Skill 是菜谱——告诉你做番茄炒蛋要先打蛋还是先炒西红柿 + +你完全可以只有 tool 没有 skill,模型靠常识也能用。但如果有领域特定的 +习惯(我们公司 PR 用 squash 合并 / 我们的数据库用这个连接字符串 / +这个 API 错误码 200 其实是失败)——这些是模型训练数据里没有的东西, +就需要 skill 来教。 + +--- + +## 总结:为什么 Skill 是个好主意 + +回到开头那个问题:**给 agent 接了 100 个工具,它怎么知道什么时候该 +用哪个?** + +``` + 塞进 system prompt 全部 token 成本 + ↑ ↓ + Skill ←─────────┴─ progressive disclosure ─────┘ + 模型语义匹配 + ↓ ↑ + 按需 read 加载 只付匹配上的成本 +``` + +Skill 在三个轴上找到了平衡: + +| 轴 | 极端 1 | 极端 2 | Skill 在哪儿 | +| --- | --- | --- | --- | +| 信息密度 | 全部塞进 prompt(贵) | 全部不给(模型不知道有) | 只给目录,正文按需加载 | +| 路由智能 | 代码决定(僵硬) | 模型决定(可能瞎选) | 代码筛可用性,模型选用哪个 | +| 演化方式 | 硬编码(改要发版) | 模型自改(危险) | 文件系统 + workshop 人审 | + +最关键的洞察是:**目录这个抽象足够便宜,可以放大量条目;模型的语义 +匹配能力足够强,可以按 description 准确路由;文本格式足够灵活,可以 +表达任意流程指令**。三者结合,得到了一套零基础设施、可扩展、可治理 +的"agent 能力库"。 + +这跟 [LSP](../lsp-tools.md) 把"语言能力"抽出来变成独立服务、跟 +[MCP](./mcp-101.md) 把"工具能力"抽出来变成独立 server 是同样的思路—— +**把可复用的智能抽到通用的接口背后**。Skill 抽的是"流程指令"。 + +--- + +## 想深入了解? + +- AgentSkills 规范: +- OpenClaw skills 系统源码:`src/skills/` +- 配套的技术笔记(更细): + - 整体设计:[../skills-management-design.md](../skills-management-design.md) + - 触发机制:[../skill-invocation-mechanism.md](../skill-invocation-mechanism.md) + - CLI 与样例:[../skills-cli-and-examples.md](../skills-cli-and-examples.md) +- 系列前文:[function-call-101.md](./function-call-101.md)、 + [mcp-101.md](./mcp-101.md)、[rag-101.md](./rag-101.md)