本指南面向编写、校验和审查 OpenClaw.NET MetaSkill 的作者和维护者。
用户指南:../meta-skill-user-guide.md。
什么是 MetaSkill#
一个 MetaSkill 是一个包含以下内容的 SKILL.md 文件:
kind: meta- 一个或多个自然语言
triggers - 一个
composition:块,定义有向无环步骤图
运行时,OpenClaw.NET 的 AgentRuntime.ExecuteMetaSkillAsync(使用 Microsoft Agent
Framework 适配器时为 MafAgentRuntime.ExecuteMetaSkillAsync)逐步执行声明的
composition——强制执行依赖顺序、模板渲染、meta 策略门禁、工具白名单、
暂停/恢复检查点和失败分支激活。用户的自然语言意图通过 Gateway 的 Skill
匹配层触发工作流。
运维人员可以按 Skill 或全局禁用 MetaSkill 调用:
{
"Skills": {
"MetaSkill": { "Enabled": false }
}
}
禁用后,MetaSkill 仍然加载用于目录和历史检查,但不会被激活。
何时使用 MetaSkill#
当任务可重复且自然分解为 3-12 步 DAG 时使用 MetaSkill:
- 分类用户请求,然后路由到正确的专用 Skill
- 并行运行两个独立分析 Skill,然后合并它们的输出
- 搜索或检查上下文,然后总结为用户可读的答案
- 执行确定性 CLI 支持的 Skill,然后审查或持久化结果
- 暂停等待结构化用户输入后再继续
不要在以下场景使用 MetaSkill:
- 一次性指令(使用标准 Skill)
- 应保持对话式的开放式规划
- 需要任意递归的流程
- 超过 12 步的任务(拆分为多个 MetaSkill 或使用 Microsoft Agent Framework / LangGraph 等外部编排器)
一个 MetaSkill 不能调用另一个 MetaSkill(TryValidateMetaPlan 拒绝
kind: meta 的委托 Skill)。
文件位置#
src/OpenClaw.Gateway/skills/<skill-name>/SKILL.md # Gateway 内置
~/.openclaw/skills/<skill-name>/SKILL.md # 本地管理
生成的提案审查通过后才安装。接受提案后,OpenClaw.NET 将其提升并刷新 Skill 加载器。
打包子 Skill#
MetaSkill 可通过 kind: agent 或 kind: skill_exec 委托标准 Skill。这些被委托的子 Skill
可以与 MetaSkill 一起打包在嵌套子目录中:
~/.openclaw/skills/my-meta/
SKILL.md # kind: meta (my-meta)
subskills/
fetcher/SKILL.md # kind: standard (fetcher)
reporter/SKILL.md # kind: standard (reporter)
pdf/SKILL.md # kind: standard (pdf)
在配置中启用递归扫描,使加载器能发现嵌套的 SKILL.md 文件:
{
"Skills": {
"Load": {
"ScanSubdirectories": true
}
}
}
当 ScanSubdirectories 为 true 时,SkillLoader 使用 SearchOption.AllDirectories
替代 TopDirectoryOnly。这使得每个嵌套的 SKILL.md 文件都可被发现,无需将每个子
Skill 目录单独注册为 ExtraDirs 条目。
默认为 false 以保持向后兼容。该标志统一应用于 ExtraDirs、Bundled、Managed、Plugin
和 Workspace 技能目录。
必需的前置元数据#
---
name: short-stable-name
kind: meta
description: 一句话告诉模型何时适用此工作流。
triggers:
- 用户自然输入的短语
meta_priority: 50
always: false
final_text_mode: auto
composition:
steps: []
---
| 字段 | 必需 | 用途 |
|---|---|---|
name |
是 | CLI 和跨 Skill 引用的稳定标识符 |
kind |
是 | 必须为 meta |
description |
是 | 面向模型的激活时机描述 |
triggers |
是 | 用于意图匹配的自然语言短语 |
meta_priority |
否 | 多个 MetaSkill 可能匹配时的排序键(默认 50) |
always |
否 | 应为 false。MetaSkill 不无条件注入 |
final_text_mode |
否 | 最终答案的派生方式(见下文) |
composition.steps |
是 | 有序 DAG 定义 |
步骤类型#
llm_chat#
一次有界 LLM 生成,无工具循环。最适合输入规范化、紧凑草稿或轻量综合。
- id: normalize
kind: llm_chat
with:
system: "提取请求字段。不要提问。"
task: "{{ input | xml_escape | truncate(1000) }}"
llm_classify#
从闭集合返回恰好一个值。最适合路由和分诊。
- id: classify
kind: llm_classify
output_choices: [BUG, FEATURE, QUESTION]
with:
text: "{{ input | xml_escape | truncate(512) }}"
agent#
通过 LLM 委托到另一个 Skill 的指令。面向用户的推理和综合的默认选择。
- id: summarize
kind: agent
skill: summarize
with:
text: "{{ outputs.search | truncate(2000) }}"
tool_call#
直接工具执行。声明 tool_allowlist 并保持参数精简。
- id: persist
kind: tool_call
tool: memory_save
tool_allowlist: [memory_save]
with:
text: "{{ outputs.summary | truncate(2000) }}"
skill_exec#
将 Skill 的 entrypoint 作为子进程运行。最适合确定性 CLI 支持的 Skill。
- id: render
kind: skill_exec
skill: html-to-pdf
skill_exec_entrypoint: scripts/render.py
skill_exec_args:
- "{{ outputs.report | truncate(12000) }}"
skill_exec_parse_mode: json
user_input#
暂停等待结构化人工输入,带 clarify schema 校验。
- id: collect_project
kind: user_input
when: "outputs.intake contains 'NEEDS_CLARIFICATION'"
clarify:
mode: form
fields:
- name: topic
type: string
required: true
min_length: 3
- name: priority
type: enum
options: [low, medium, high]
default: "medium"
cancel_words: [cancel, 取消]
timeout_seconds: 300
skip_if: "outputs.auto_approve == '1'"
支持的字段类型:string、enum、integer、boolean。使用 skip_if 在上下文
足够时跳过。
fan_out —— 动态步骤展开#
对运行时生成的列表进行迭代,为每个元素克隆步骤模板,并在并行批次中执行子步骤。 适用于子任务数量在编写时无法确定的情况——例如,对前一步 LLM 输出中提取的 N 个主题进行搜索。
必需字段:
| 字段 | 用途 |
|---|---|
kind |
必须为 fan_out |
iterable |
Jinja 表达式,求值为字符串 JSON 数组 |
fan_out_template |
每个元素克隆的步骤定义(必须声明 kind、tool 或 skill) |
可选字段:
| 字段 | 默认值 | 用途 |
|---|---|---|
fan_out_max_concurrency |
4 |
每批最大并发子步骤数 |
fan_out_merge_mode |
concat |
子步骤输出合并方式:concat、json_array、first、last |
- id: search_every_topic
kind: fan_out
iterable: "{{ outputs.extract_topics | from_json }}"
fan_out_max_concurrency: 3
fan_out_merge_mode: json_array
fan_out_template:
kind: tool_call
tool: web_search
with:
query: "{{ item }}"
continue_on_error: true
depends_on:
- extract_topics
每个子步骤接收 {{ item }} 作为其输入上下文。模板支持 tool_call 和
llm_chat 两种子步骤类型。子步骤失败会记录日志并反映在 stepResults 中;
设置 continue_on_error: true 使 fan_out 在单个子步骤失败后继续执行。
tool_call —— 直接工具执行#
绕过 LLM,直接调用注册的工具。使用 list_tools 在运行时发现可用工具:
- id: discover
kind: tool_call
tool: list_tools
依赖与并行#
没有 depends_on 的步骤可以并行执行(波次调度)。有 depends_on 的步骤等待
所有命名步骤完成。
steps:
- id: inspect_code
kind: agent
skill: code-reviewer
- id: inspect_tests
kind: agent
skill: test-engineer
- id: merge
kind: llm_chat
depends_on: [inspect_code, inspect_tests]
with:
task: |
Code: {{ outputs.inspect_code | truncate(2000) }}
Tests: {{ outputs.inspect_tests | truncate(2000) }}
图必须无环。一个步骤只能依赖同 composition 中声明的步骤 ID。
路由#
在 agent 或 skill_exec 步骤上使用 route 根据输出进行分支:
- id: classify
kind: llm_classify
output_choices: [DOCS, BUG, SECURITY]
- id: handle
kind: agent
skill: summarize
depends_on: [classify]
route:
- when: "outputs.classify == 'DOCS'"
to: writer
- when: "outputs.classify == 'BUG'"
to: debugger
- when: "outputs.classify == 'SECURITY'"
to: security-reviewer
无 when 的 route 充当默认 fallback。
错误处理#
on_failure —— 替代步骤#
- id: llm_summarize
kind: llm_chat
on_failure: fallback_template
timeout_seconds: 15
- id: fallback_template
kind: tool_call
tool: emit_text
with:
text: "摘要不可用——使用模板。"
5 条约束(parse + runtime 强制执行):
- fallback 目标必须在 composition 中存在
- 步骤不能引用自身
- fallback 不能有
on_failure(禁止链式) - 每个 fallback 只能服务于一个 primary
- fallback 不能有
depends_on
continue_on_error —— 失败时跳过#
- id: optional_step
kind: skill_exec
skill: analytics
with:
continue_on_error: true
失败时将步骤标记为 Continued: true,DAG 继续。
最终文本模式#
| 模式 | 行为 |
|---|---|
auto |
默认。运行时将步骤输出总结为简洁的最终答案 |
raw |
逐字返回最后一个非替代步骤的输出 |
step:<id> |
逐字返回一个特定步骤的输出 |
structured |
返回带 error_code 和每步 status/failure_code 的 JSON 信封 |
final_text_mode: auto
final_text_mode: raw
final_text_mode: "step:summarize"
final_text_mode: structured
模板安全#
模板是由 MetaTemplateRenderer 渲染的 Jinja2 表达式。只允许 4 个 filter:
xml_escape、slugify、truncate、tojson。
始终过滤用户输入和之前步骤的输出:
# 安全
query: "{{ input | xml_escape | truncate(512) }}"
text: "{{ outputs.search | truncate(2000) }}"
slug: "{{ input | slugify | truncate(80) }}"
payload: "{{ outputs.plan | tojson }}"
# 不安全——绝对不要这样做
query: "{{ input }}"
text: "{{ outputs.search }}"
Jinja2 沙箱实施三道防线:
- 经典逃逸向量(
__class__、__bases__、.GetType())被阻断 - 只有 4 个注册 filter 有效——38+ 个内置 Jinja2 filter 被覆盖
- 全局函数(
range()、dict())抛出NotSupportedException,由渲染器捕获
有界执行#
- id: api_call
kind: tool_call
tool: external_api
timeout_seconds: 30
retry:
max_attempts: 2
backoff_ms: 500
激活指导#
- 将触发词写成用户自然输入的短语:
总结最近历史,而不是运行内部 DAG 组合 meta skill - 使用 2-5 个触发词,除非有经过测试的理由使用更多
- 避免触发词与解释性问题冲突(如"这个 meta-skill 是如何工作的?")
- 设置
description引导模型选择。模型主要看到前置元数据和注入的 Skill 摘要
校验清单#
启用 MetaSkill 前:
- 前置元数据解析为有效 YAML
kind: meta和composition.steps存在- 所有
depends_on、route.to和on_failure目标存在 - 依赖图中无环路
- 所有用户输入和步骤输出都通过
xml_escape/truncate过滤 on_failure目标通过全部 5 条约束- 触发词通过误报测试
final_text_mode匹配预期的交付物形态
故障排查#
MetaSkill 未激活:
- 确认
SKILL.md在已加载的 Skill 目录下 - 确认
kind: meta且composition.steps非空 - 确认用户措辞匹配触发词或描述
- 检查
Skills.MetaSkill.Enabled不是false
解析失败:
- 检查重复的步骤 ID
- 检查未知的
kind值 - 检查
agent或skill_exec步骤缺少skill - 检查
llm_classify缺少output_choices - 检查
user_input缺少clarify.fields - 检查环路和未定义的
depends_on引用
Fallback 未激活:
- 检查 5 条
on_failure约束未被违反 - 验证 fallback 步骤存在且没有
depends_on