尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OpenClaw Skill 系统工作流程(代码层分析):从 SKILL.md 到 Frontmatter 的加载链路拆解

OpenClaw Skill 系统工作流程(代码层分析):从 SKILL.md 到 Frontmatter 的加载链路拆解 1. 从一次 Skill 不生效说起OpenClaw SKILL.md 加载链路到底卡在哪如果你在本地跑 OpenClaw大概率遇到过这种场景明明把SKILL.md放进了~/.openclaw/workspace/skills/目录Frontmatter 也照着示例写了可模型就是看不见这个技能或者用户输入/feishu-doc毫无反应。更迷惑的是日志里既没有报错也没有任何加载提示仿佛这个文件根本不存在。这类问题的根源几乎都出在 Skill 系统的加载链路上。OpenClaw 的 Skill 不是放进去就生效的简单机制它要经过目录发现 → Frontmatter 解析 → 运行时资格校验 → 注册进快照 → 注入 Prompt五个环节任何一环被过滤掉技能都会静默消失。而 OpenClaw Skill 系统工作流程代码层分析的价值就是把这五个环节拆开让你知道每一步在检查什么、失败时该看哪个文件。我试过在 Windows 和 macOS 上分别复现这套链路发现最容易踩的坑集中在两处一是 Frontmatter 的requires.env声明了环境变量但实际没注入导致evaluateRuntimeEligibility直接返回 false二是openclaw.json里配了allowBundled白名单却没把自定义技能加进去结果被isBundledSkillAllowed拦下。这两个问题都不会抛异常只会让技能人间蒸发。这篇文章面向需要在本地复现 Skill 调度行为的开发者我会给出可复制的目录结构、Frontmatter 字段配置示例以及通过日志与断点验证 Skill 被正确加载与触发的具体动作。核心检索词是 OpenClaw Skill 加载链路、SKILL.md 解析、Frontmatter 元数据读取适合已经跑通 OpenClaw 基础环境、想深入理解扩展机制的人。读完之后你应该能自己定位技能为什么不生效而不是靠反复重启碰运气。2. 前置准备TaoToken 接入与 OpenClaw 运行环境搭建在拆解加载链路之前得先把运行环境准备好。OpenClaw 本身是本地 Agent 运行时但它需要调用大模型来完成技能匹配和工具调用决策所以你得有一个可用的模型接入点。这里我用 TaoToken 作为模型网关它兼容 OpenAI 风格的接口配置起来比较直接。先说清楚 TaoToken 是什么它是一个模型 API 聚合服务提供统一的 Base URL 和 Key让你在 OpenClaw 里通过一份配置就能切换不同模型。对 Skill 系统调试来说这点很关键——因为技能是否被触发取决于模型能不能正确读取注入的available_skills列表并生成 Tool Call所以你需要一个稳定的模型端点来观察行为。第一步拿到 API Key。访问 https://taotoken.net/api-keys 创建密钥注意这个页面是控制台的一部分创建后 Key 只显示一次记得立刻保存。如果你还没注册先走 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 完成账号初始化。第二步确认 OpenClaw 的模型配置位置。OpenClaw 的模型接入通常写在openclaw.json的models或providers段里不同版本字段名略有差异。你需要把 Base URL 指向https://taotoken.net/api注意 API 地址不带 UTM 参数Key 填上一步创建的密钥Model ID 填你实际要用的模型名。第三步准备 Skill 目录。OpenClaw 会从多个路径搜索技能优先级从高到低大致是搜索路径用途典型场景~/.openclaw/workspace/skills/用户自定义技能自己写的业务技能~/.openclaw/workspace/.agents/skills/工作区技能项目级共享技能~/.openclaw/skills/系统配置技能全局安装的技能~/.agents/skills/用户主目录技能跨工作区复用plugins/*/skills/插件提供的技能第三方插件附带我建议调试阶段统一放在~/.openclaw/workspace/skills/下因为它的加载日志最完整出问题时最容易定位。目录结构长这样~/.openclaw/workspace/skills/ └── feishu-doc/ └── SKILL.md注意每个技能必须是独立子目录SKILL.md放在子目录根下。如果你直接把SKILL.md丢在skills/根目录loadSkillsFromDirSafe()递归查找时虽然能找到文件但loadSingleSkillDirectory()会用父目录名作为技能名容易和预期不符。环境准备好之后先别急着写复杂技能。用一个最小可用的SKILL.md验证链路是否通比一上来就配依赖、配环境变量要高效得多。下一节我会给出完整的 Frontmatter 配置和对应的openclaw.json片段。3. 可复制配置SKILL.md Frontmatter 与 openclaw.json 完整片段这一节是全文的核心操作部分。我会给出一个能直接跑通的SKILL.md然后逐字段解释它在加载链路里被谁读取、做什么校验。你可以把下面的内容原样复制到~/.openclaw/workspace/skills/feishu-doc/SKILL.md。--- name: feishu-doc description: Feishu document read/write operations. Activate when user mentions Feishu docs, cloud docs, or docx links. user-invocable: true disable-model-invocation: false primaryEnv: FEISHU_APP_ID os: [win32, darwin, linux] requires: bins: [node] env: [FEISHU_APP_ID] install: - kind: node package: feishu-sdk --- # Feishu Doc Skill 当用户提到飞书文档、云文档或 docx 链接时使用本技能读取或写入文档内容。 ## 使用方式 用户输入 /feishu-doc docx_url 时直接调用 feishu_doc 工具。这段 Frontmatter 里每个字段都有明确的消费方name和description会被parseFrontmatter()解析后写入 Skill 对象最终由formatSkillsForPrompt()转成 XML 注入模型上下文。description尤其重要模型就是靠它判断这个任务该不该用这个技能所以描述里要写清楚触发条件。user-invocable: true决定用户能否用/feishu-doc这种斜杠命令直接调用。如果设成 false用户手动输入命令会被忽略但模型仍可能自动调用取决于disable-model-invocation。disable-model-invocation: false表示允许模型自动调用。如果你写了一个只该由用户显式触发的危险操作技能把它设成 true。os字段由evaluateRuntimeEligibility()检查只有当前系统在列表里技能才会被注册。这个检查在shouldIncludeSkill()里执行失败时静默跳过。requires.bins和requires.env是依赖检查。bins会调用hasBinary()检查可执行文件是否存在env会检查process.env或技能配置里的env注入。这里声明了FEISHU_APP_ID意味着你必须通过配置或环境变量提供它否则技能不加载。install段声明安装方式kind: node对应normalizeSafeNpmSpec()校验包名合法性。支持的 kind 包括node、brew、go、uv、download分别对应不同的安装器。接下来是openclaw.json的配置片段。这个文件通常在工作区根目录或~/.openclaw/下{ skills: { entries: { feishu-doc: { enabled: true, env: { FEISHU_APP_ID: cli_xxxxxxxxxxxx } } }, allowBundled: [feishu-doc], limits: { maxSkillsInPrompt: 150, maxSkillsPromptChars: 18000 } } }这里有几个关键点。entries.feishu-doc.enabled为 true 是必须的如果设成 falseshouldIncludeSkill()第一步就返回 false。env字段是运行时注入的环境变量它会和process.env一起被hasEnv检查所以即使系统环境里没有FEISHU_APP_ID只要这里配了技能依然能通过校验。allowBundled是 bundled 技能白名单。如果你用的是 OpenClaw 内置技能必须把技能名加进这个数组否则isBundledSkillAllowed()会拦截。自定义技能通常不受这个限制但为了保险建议统一加上。limits控制注入 Prompt 的技能数量和字符上限。maxSkillsInPrompt: 150意味着最多 150 个技能会被写进available_skills超出部分会被截断。maxSkillsPromptChars: 18000是字符上限如果你的技能描述写得太长可能挤掉后面的技能。调试阶段建议把这两个值调小比如 10 和 2000这样日志里更容易看清哪些技能被注入了。如果你用的是插件提供的技能还需要在插件的manifest.json里声明技能路径{ id: feishu, skills: [skills/feishu-doc, skills/feishu-drive] }resolvePluginSkillDirs()会读取这个 manifest检查插件激活状态然后把技能目录加入搜索路径。注意插件技能只有在插件被激活时才会加载如果插件被禁用技能也不会出现。配置写完后别急着启动。先用一个简单的 Node 脚本验证 Frontmatter 能否被正确解析这样能把解析失败和校验失败两类问题分开。下一节我会给出验证请求的具体命令和预期结果。4. 验证请求用日志与断点确认 Skill 被正确加载与触发配置写好了接下来要验证链路是否真的走通。OpenClaw 的 Skill 加载是静默的所以你需要主动打开日志或打断点。我推荐三个层次的验证文件解析层、注册层、Prompt 注入层。第一层验证 Frontmatter 解析。写一个最小脚本直接调用解析逻辑。如果你有 OpenClaw 源码可以这样const { parseFrontmatter } require(./src/agents/skills/frontmatter); const fs require(fs); const raw fs.readFileSync( process.env.HOME /.openclaw/workspace/skills/feishu-doc/SKILL.md, utf8 ); const fm parseFrontmatter(raw); console.log(JSON.stringify(fm, null, 2));预期输出里应该包含name: feishu-doc、description、user-invocable: true等字段。如果name是 undefined说明 YAML 分隔符---没写对或者字段缩进有问题。Frontmatter 必须是文件开头第一行就是---前面不能有空行或 BOM。第二层验证技能注册。在loadSingleSkillDirectory()里打断点或者加一行日志function loadSingleSkillDirectory(params) { const skillFilePath path.join(params.skillDir, SKILL.md); console.log([skill-loader] trying:, skillFilePath); const raw readSkillFileSync({ /* ... */ }); const frontmatter parseFrontmatter(raw); console.log([skill-loader] parsed name:, frontmatter.name); // ... }启动 OpenClaw 后如果日志里出现了[skill-loader] trying: .../feishu-doc/SKILL.md说明目录发现成功。如果紧接着出现parsed name: feishu-doc说明解析成功。如果只有 trying 没有 parsed问题在解析环节。第三层验证资格校验。在shouldIncludeSkill()里加日志export function shouldIncludeSkill(params) { const { entry, config, eligibility } params; const skillKey resolveSkillKey(entry.skill, entry); const skillConfig resolveSkillConfig(config, skillKey); if (skillConfig?.enabled false) { console.log([skill-filter] disabled:, skillKey); return false; } if (!isBundledSkillAllowed(entry, allowBundled)) { console.log([skill-filter] not in allowBundled:, skillKey); return false; } const eligible evaluateRuntimeEligibility({ /* ... */ }); console.log([skill-filter] eligibility for, skillKey, :, eligible); return eligible; }这段日志能直接告诉你技能被哪个条件拦下了。如果看到not in allowBundled就去openclaw.json里补白名单如果eligibility是 false检查os、requires.bins、requires.env三项。第四层验证 Prompt 注入。在formatSkillsForPrompt()里打印结果export function formatSkillsForPrompt(skills) { if (skills.length 0) return ; const lines [ \n\nThe following skills provide specialized instructions for specific tasks., Use the read tool to load a skills file when the task matches its description., , available_skills, ]; for (const skill of skills) { lines.push( skill); lines.push( name${escapeXml(skill.name)}/name); lines.push( description${escapeXml(skill.description)}/description); lines.push( location${escapeXml(skill.filePath)}/location); lines.push( /skill); } lines.push(/available_skills); const result lines.join(\n); console.log([skill-prompt] injected skills count:, skills.length); console.log([skill-prompt] preview:, result.slice(0, 500)); return result; }如果这里打印的skills.length是 0说明前面的过滤把所有技能都拦掉了。如果数量对但模型不调用问题在模型侧——可能是description写得不够明确模型没识别出任务匹配。第五层验证实际触发。启动 OpenClaw 后在对话里输入一个明确匹配技能描述的任务比如帮我读一下这个飞书文档 https://xxx.feishu.cn/docx/xxx。观察日志里是否出现 Tool Call工具名是否是feishu_doc。如果模型生成了调用但执行失败检查dispatch.toolName和argMode配置。实测下来这套五层验证能把 90% 的 Skill 不生效问题定位到具体环节。剩下的 10% 通常是热更新没生效——refresh.ts用 chokidar 监听文件变化但awaitWriteFinish有防抖延迟改完文件后等一两秒再试。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 报错对照调试 Skill 链路时报错往往不直接指向 Skill 本身而是模型接入层的问题。因为技能触发依赖模型生成 Tool Call如果模型请求失败你会误以为是技能没加载。这一节我把几类高频报错和对应的排查方向列出来。401 Unauthorized。这个最常见通常是 TaoToken 的 Key 没配对或者 Base URL 写错了。检查openclaw.json里的模型配置确认 Base URL 是https://taotoken.net/apiKey 是https://taotoken.net/api-keys里创建的那一串。注意 Key 不要有多余空格也不要误用了其他服务的 Key。如果 Key 刚创建确认账号状态正常。local proxy failed / connection refused。这个报错说明 OpenClaw 尝试连接模型端点时被拒绝。可能原因有三个一是 Base URL 端口写错二是本地网络策略拦截三是模型服务临时不可用。先确认https://taotoken.net/api能正常访问再检查 OpenClaw 的 provider 配置里有没有多余的代理设置。如果你在openclaw.json里配了proxy字段先注释掉试试。reading choices 报错。这个错误通常出现在解析模型响应时response.choices是 undefined。根因是模型返回的 JSON 结构不符合 OpenAI 格式或者请求根本没成功但代码继续往下解析了。排查方向先看原始响应体确认返回的是标准 chat completion 格式。如果用的是非 OpenAI 兼容的模型需要在 provider 配置里加适配层。另外如果max_tokens设得太小模型可能返回空响应也会触发这个错误。OAuth 相关报错。如果你用的是需要 OAuth 的模型服务报错可能出现在 token 刷新环节。检查openclaw.json里的 OAuth 配置确认client_id、client_secret、refresh_token都在有效期内。OAuth token 过期后不会自动刷新需要重新授权。如果你不确定先用 API Key 方式接入排除 OAuth 变量。技能加载了但模型不调用。这类问题不报错但技能形同虚设。排查三步第一确认formatSkillsForPrompt()输出的 XML 里确实有这个技能第二检查description是否写清楚了触发条件模型需要明确的语义线索第三确认disable-model-invocation不是 true。如果技能是user-invocable: true但disable-model-invocation: true只有用户手动输入斜杠命令才会触发模型自动调用会被禁止。热更新不生效。改完SKILL.md后技能行为没变通常是 watcher 没触发。refresh.ts里awaitWriteFinish.stabilityThreshold默认有几百毫秒延迟改完等两秒再试。如果还是不行检查resolveWatchTargets()返回的路径是否包含你的技能目录。有些编辑器保存时会先写临时文件再重命名chokidar 可能捕获不到change事件这时手动重启 OpenClaw 最稳妥。路径逃逸警告。日志里出现warnEscapedSkillPath说明resolveContainedSkillPath()检测到技能文件不在允许的根目录内。这通常是符号链接导致的——你的技能目录是个 symlink指向了根目录之外。解决办法是把技能文件实际复制到根目录内或者调整rootRealPath配置。排查时有个原则先确认模型接入正常再确认技能加载正常最后确认触发正常。顺序反了会把模型问题误判成技能问题浪费大量时间。6. 继续深入从 Skill 加载到 Coding Plan 的实践路径把加载链路跑通之后你会发现 OpenClaw 的 Skill 系统真正的价值在于可扩展性。每个SKILL.md就是一个独立的能力单元通过 Frontmatter 声明依赖和权限通过目录约定被自动发现通过 Prompt 注入被模型感知。这套设计让你可以在不改动 OpenClaw 核心代码的前提下持续增加新能力。如果你打算把这套机制用在长期编码或 Agent 场景里建议下一步做两件事。一是把常用技能整理成模板固定 Frontmatter 字段规范避免每次手写出错二是把模型接入配置标准化用统一的 Base URL 和 Key 管理减少环境切换成本。TaoToken 的 Coding Plan 适合这种长期使用场景你可以在 https://taotoken.net/coding-plan 查看具体方案它针对编码类任务做了优化配合 OpenClaw 的技能调度会比较顺手。调试过程中如果遇到模型响应格式问题可以用模型对话页面快速验证接口是否正常地址是 https://taotoken.net/chat。这个页面能直接看到原始响应比在 OpenClaw 里翻日志要快。接入文档在 https://taotoken.net/doc里面有各语言的调用示例和字段说明配置openclaw.json时可以参考。最后提醒一点Skill 的description字段值得反复打磨。它不是给人看的注释而是模型判断是否调用的唯一依据。描述里写清楚触发关键词、适用场景、输入输出格式模型调用的准确率会明显提升。我见过太多技能因为描述写得太笼统导致模型要么不调用要么乱调用。把描述当成 Prompt 的一部分来写这个习惯能省下大量排查时间。
返回列表