skill_md 型 · 文档型(含 API 型)
什么时候读:不写代码,让隔离子代理照 SKILL.md 的步骤操作;或要把一组第三方 HTTP API 包成技能。 适合:多步操作流程、需要判断的工作流、调现有工具的组合。不适合:需要确定性结果的计算。 执行面按
roles_chat_skill_runtime.py/roles_chat_api_skill_executor.py核对(2026-10)。
工作机制:三条出口
trigger-type: explicit 才进模型的工具清单。模型选中后,平台按文档形态走三条路之一:
| 出口 | 条件 | 执行者 |
|---|---|---|
| 纯指令型 | 平台判定无需执行 | 文档直接交还主模型,主模型自己照做,不起子代理 |
| API 型 | 见下文「API 型」 | _ApiToolFace:每个端点一个工具,httpx 确定性发请求,工具面里没有 shell |
| 执行型(默认) | 其余 | _ShellToolFace:子代理 + read_skill_file + container_shell(无容器时退为 run_code) |
子代理没有调用方的对话历史,文档必须自足:输入从哪来、用什么工具、产出什么、什么情况下停。
链 / 定时任务(allow_skill_md=False)直接拒绝这类技能。
执行型子代理的实际约束
| 项 | 值 |
|---|---|
| 轮数 / 墙钟 | 12 轮 / 420 秒;判定为「样例脚本型」(有 scripts/ 但脚本不吃参数)时放宽到 20 轮 / 900 秒 |
| 卡死判定 | 同一 (工具, 参数) 3 次 |
| 工具 | read_skill_file(file=相对路径)(自动定位当前技能,单次上限 40000 字符;脚本文件 120000)+ container_shell 或 run_code |
run_code 模式 | 宿主 Python 沙箱:无 shell、不能装依赖、没有密钥注入通道——要 $密钥 的步骤在该模式下不可用 |
| 平台追加的系统提示 | 「你唯一的任务是完成【用户指令】那一句;SKILL.md 的简介/功能列表不是任务清单」「不要用 shell 测密钥是否存在」「拿不到数据如实说」 |
{baseDir} | 改写成容器内技能目录路径;资源文件清单自动附在提示里 |
| 依赖 | 声明了 pip / npm 依赖则已预置;否则提示子代理 pip install -r <技能目录>/requirements.txt |
| 文档导航 | 存在 references/recipes/_index.md 时按「配方」导航;否则列出技能真实包含的文档、预读相关的几份 |
frontmatter 里执行面读的是 metadata.openclaw:
metadata:
openclaw:
primaryEnv: XXX_API_KEY # 子代理被告知用 -H "Authorization: Bearer $XXX_API_KEY"
requires: {env: [XXX_API_KEY]} # 没写 primaryEnv 时取第一个
requiresContainer: true # 或 sandbox: container / docker
outputType: file # 产出契约(可选)
写法上的推论:
- 不要写「先 echo $KEY 确认一下」「[ -n "$KEY" ] 判断有没有配」——平台明令子代理别做,写了只会冲突。
- 「功能列表」写成能力说明,别写成待办清单;子代理只做用户这一句要的事。
- 要被预读,reference 文件名和标题就要和任务词对得上。
硬限制:8000 字符
SKILL.md 超过 8000 字符会被截头部注入,尾部丢失——丢的恰好是 checklist 和收尾步骤(规范 §2.2)。
对策:
- 主文档只放:触发场景、总流程、分支判断、何时读哪份 reference。
- 细节放
references/*.md,正文写明「执行 X 前先读 references/xxx.md」。 - 单个 reference 别超过 40000 字符(
read_skill_file单次上限),超了拆。
写法
写给执行它的子代理看,不是写给选工具的模型看:
- 祈使句。「先调 list_tables 取表 id,再……」而不是「本技能可以……」。
- 每步写清用什么工具、传什么参数、期望返回什么形状、返回不符时怎么办。
- 分支用表格:左列是判断条件,右列是动作。
- 明确「停下来问用户」的条件,否则子代理会自己编参数往下走。
- 动作型(以 action 分发)的文档明写「action 必须传中文原词」+ 至少三个调用示例。
- 帮助表每行左列是触发场景(「连得上数据库吗」),右列是它回答什么。不要左列写动作名。
给用户看的「下一步」
「下一步:说拉数据」会被子代理当成自己的指令直接执行。改为「可继续的操作:……」并标注 for_user。
frontmatter
---
name: xxx
description: 一句话说清什么时候该调它
skill_type: skill_md # 没有 handler.py 时建议显式写;有 handler.py 一律判执行型
skill_md: SKILL.md # 可选,主文档不叫 SKILL.md 时指定
trigger-type: explicit
category: general # 只认 13 个分类值,见 templates/SKILL.md.tmpl
---
API 型(包一组第三方 HTTP API)
怎么被判成 API 型(任一命中):
metadata.openclaw.skillClass以api-skill结尾;metadata.openclaw.authType: bearer_env;- 技能目录里有规格文件:
openapi.yaml|yml|json(根目录、references/、api/、spec/),或endpoints_whitelist.yaml|yml(references/或根目录)——找不到还会递归全目录搜同名文件。
推论:普通文档型技能里只要任何位置放了一份名为
openapi.yaml|yml|json/endpoints_whitelist.yaml|yml的文件 (哪怕只是参考资料),就会被当成 API 型执行、失去 shell。参考用的 OpenAPI 改个名(如api-reference.yaml)。
规格怎么变成工具:
| 来源 | 规则 |
|---|---|
| OpenAPI | 端点 = paths 下 get/post/put/delete/patch;工具名 = operationId(没有则 method_path);参数 = parameters + requestBody 第一个 content 的 properties;base_url = servers[0].url;域名白名单 = 全部 servers 的 host |
| whitelist | base_url + endpoints: [{id, method, path, required: [..] 或 {oneOf: [..]}, optional: [..]}];参数一律按 string;白名单 = base_url 的 host |
# references/endpoints_whitelist.yaml
base_url: https://api.example.com/v1
endpoints:
- {id: search_notes, method: GET, path: /notes/search, required: [keyword], optional: [page]}
- {id: note_detail, method: GET, path: /notes/detail, required: {oneOf: [note_id, url]}}
执行规则(_http_call):
- URL =
base_url + path,路径模板{id}不做替换——端点设计成参数全走 query / body; GET 参数进 query string,其它方法参数整体作 JSON body。 - 鉴权只有一种:配了密钥时加
Authorization: Bearer <密钥>(没配则不带头)。其它鉴权方式(签名、query key、自定义头)用执行型或 handler 型。 - 目标 host 不在白名单 → 拒绝(防 SSRF)。超时 25 秒;429 按
Retry-After退避最多 2 次(单次 ≤5 秒);5xx 重试 1 次。 - 401 / 402 / 403 / 404 → 立即停(提示检查密钥 / 端点过期)。
- 子代理最多 6 轮 / 90 秒,同参重复 2 次判卡死;端点多时先按用户指令预筛一批再给模型。
- 模型没给最终文本时,平台用最后一次返回(截 12000 字符)自动整理成中文结果。
调别的技能
文档型拿不到 call_skill,子代理靠自己的工具清单调。
需要「先取数再落库」且落库要 db 的,把落库写成进程内 Python 技能,文档里写明调它。