终端编码 Agent opencode 的 skills 用法与分工

2026-08-04

本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。

opencode 里的技能不是”插件”,它只是一段按需注入当前对话的 Markdown 正文,触发权在模型手上,不在你手上。 想清楚这一条,后面所有取舍都顺理成章:技能不带自己的模型、不带自己的权限集、不开新会话,它唯一能做的事,就是在模型自己判断”这个任务和某条描述对得上”的时候,把那份文件的正文塞进正在进行的这轮对话。你要做的判断题因此变得很具体——什么内容值得占这份注入名额,什么内容其实该写成命令或者交给子 agent。

opencode 是一个 MIT 许可证的开源项目(LICENSE 里写的是 Copyright (c) 2025 opencode),跑在终端里,仓库 packages/ 下有 32 个包,英文文档在 packages/web/src/content/docs/ 下共 36 份 mdx。本文说的 skills,对应的就是其中的 skills.mdx 那一份,以及 packages/opencode/src/skill/ 这个目录。

一、这套机制由哪几块拼起来

先把零件摆出来,后面讲行为时你能对上号。下面每一行的仓库位置都是实际存在的文件:

组成部分它负责什么对应仓库位置你什么时候会碰到它
技能索引与扫盘按几类模式扫出 SKILL.md、解析 frontmatter、以 name 为键建表packages/opencode/src/skill/index.ts会话启动、技能没被认出来时
skill 工具本体按名字取出技能正文,走一次权限询问,拼出注入内容packages/opencode/src/tool/skill.ts模型决定加载某个技能时
工具描述文本告诉模型这个工具是干什么的、什么时候调packages/opencode/src/tool/skill.txt每一轮请求都在上下文里
系统提示里的技能清单用较详细的形式列出每个技能的名字、描述、位置packages/opencode/src/session/system.ts每一轮请求都在上下文里
远程技能拉取从一个 URL 取 index.json,把技能文件下到缓存目录packages/opencode/src/skill/discovery.ts配置里写了技能 URL 时
配置字段定义skills 下的 pathsurls 两个可选数组packages/core/src/v1/config/skills.tsopencode.json
文档与权限说明放置位置、命名规则、权限模式的官方口径packages/web/src/content/docs/skills.mdx排查”为什么没生效”时

注意 packages/opencode/src/tool/ 下一共 25 个 .ts 和 15 个 .txtskill 只是其中一个普通工具,和 readgreptask 平级。这个位置本身就说明了它的分量:它不是框架层的特权机制,就是一个参数只有 name 的工具。

二、文件放哪会被发现

文档给出的六个位置是:项目内 .opencode/skills/<name>/SKILL.md、全局 ~/.config/opencode/skills/<name>/SKILL.md,兼容 Claude 生态的 .claude/skills/<name>/SKILL.md~/.claude/skills/<name>/SKILL.md,以及兼容 agent 约定的 .agents/skills/<name>/SKILL.md~/.agents/skills/<name>/SKILL.md

代码里的模式常量比文档更能说明边界:

const CLAUDE_EXTERNAL_DIR = ".claude"
const AGENTS_EXTERNAL_DIR = ".agents"
const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
const SKILL_PATTERN = "**/SKILL.md"

三点值得留意。第一,opencode 自己的配置目录下,单数的 skill/ 和复数的 skills/ 都认,这和配置文档里”单数目录名为向后兼容保留”的说法一致;而 .claude.agents 这两个外部目录只认复数 skills/。第二,模式都是 **/,也就是说技能目录可以再往下嵌一层分组目录,扫盘不会因此漏掉。第三,扫外部目录时开了 dot 选项,隐藏目录里的技能才扫得到。

项目内的查找不是只看当前目录。代码里对 .claude.agents 这两个目标是从当前工作目录往上走、走到 git worktree 为止,沿途每一层匹配到的都会被收进来。这意味着 monorepo 里子包目录下的技能和仓库根的技能会叠加生效,而不是就近覆盖。

除了这六个位置,配置里还有两条扩展路:skills.paths 接受额外的技能目录数组,以 ~/ 开头会展开成用户主目录,相对路径按当前目录解析,路径不是目录时只记一条 skill path not found 的警告,不会中断启动;skills.urls 则交给远程拉取那一层去处理。

还有一个容易被忽略的存在:opencode 内置了一个名为 customize-opencode 的技能,注册在磁盘扫描之前,源码注释写得很直白——模型对 opencode.json 该长什么样的直觉经常是错的,而配置非法会硬失败,所以内置这份技能是为了在模型要动 opencode 自身配置时给它真实的 schema 而不是猜测。因为注册在前,磁盘上同名的技能可以把它顶掉。

三、加载那一刻,上下文里多了什么

技能清单有两种格式化形态:一种带标签,逐条给出名字、描述和位置;一种是精简的 Markdown 列表,只有名字和描述。session/system.ts 里那段注释写了取舍理由——模型对更详细的那份吸收得更好,所以详细版放系统提示、精简版留给工具描述,而不是反过来。

这里有一处口径与代码的落差值得单独记一笔:当前代码里真正被调用的只有系统提示那一路的详细版,skill.txt 这份工具描述本身并没有内嵌技能清单,它只是让模型「在系统提示列出的技能里挑一个」。官方文档里那句「技能清单在 skill 工具的描述里」,按现在的代码读已经对不上了。这条落差有实际用处:排查「模型看不见我的技能」时,该盯的是系统提示那一段,别去翻工具描述。

模型真正调用工具时,tool/skill.ts 的流程是:先按名字取技能,取不到就带着可用技能名列表报错;然后走一次权限询问;接着以技能文件所在目录为基准目录,用 ripgrep 找出这个目录里除 SKILL.md 之外的文件;最后拼出一段带标签的输出。这段拼装的写法很说明设计意图:

`Base directory for this skill: ${base}`,
"Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
"Note: file list is sampled.",

也就是说,技能正文里写 scripts/xxx.py 这类相对路径是被官方支持的用法,工具会告诉模型基准目录在哪;但同时它明说文件清单是采样的——查找时限制了返回条数,也不跟随符号链接。技能目录里塞几十个附件,模型看到的只是其中一部分,剩下的得靠正文里明确点名,让模型自己去读。

这就引出一条实际写法:技能正文应该是”路线图 + 判断规则”,不是”资料库”。真正的长材料留在同目录的文件里,正文负责在什么条件下该读哪个文件。这条思路和站内讲检索式技能组织的那篇是同一路数,可以对照 ECC 的技能检索 看。

四、技能、自定义命令、子 agent 的分工线

这三样东西在 opencode 里是并存的,边界其实很清楚,混用是大部分”技能不生效”体感的真正来源。

自定义命令是你触发的。它是 commands/ 目录下的 Markdown 文件,或者配置里 command 字段的条目,在 TUI 里用 / 加名字调起。它的模板支持 $ARGUMENTS$1$2 这样的位置参数,支持用反引号语法把一条 shell 命令的输出注进提示词,也支持用 @ 引用文件内容。frontmatter 里还能指定 agentmodel,以及一个 subtask 布尔值强制走子 agent。命令解决的是”这段提示词我每周要敲二十遍”。

子 agent是另一个执行体。它有自己的 modemodelpromptpermission,可以被主 agent 通过任务工具调起,也可以你在消息里用 @ 点名。内置的就有全能型、只读探索型、外部资料调研型几种,各自的工具范围不同。子 agent 解决的是”这活儿要单独的上下文和单独的权限边界”。

技能是模型触发的,落点在当前对话。它的 frontmatter 只认五个字段:文档口径是 namedescription 必填,licensecompatibilitymetadata 可选,未知字段一律忽略(加载代码实际只强制 name 是字符串,这处落差留到第六节说)——注意这里没有 model,也没有 tools。技能不能给自己换模型、不能给自己开权限。它解决的是”这类任务有固定做法,但我事先不知道你什么时候会撞上”。

一句话的判断法:你知道什么时候要用它,写成命令;它需要独立的上下文和权限,做成子 agent;只有模型比你更早知道该用它,才写成技能。

站内几篇相近文章的分工也顺带说清楚:讲 Claude Code 那套技能体系的看 Claude Code 的 skills,讲技能文件本身该怎么写、契约怎么定的看 superpowers 的技能文件契约,本篇只管 opencode 这一个项目的发现路径、注入行为与三者分工,不重复那两篇的写作方法论。跨框架横着比的话,三套技能机制对比 那篇的坐标系更合适。

五、边界与代价:它明确不管的事

它不管技能之间的冲突。 索引以 name 为键,撞名了只会记一条 duplicate skill name 的警告,不会报错、不会中断,最终留下哪一份没有稳定承诺。文档给的建议也是直接的:保证技能名在所有位置内唯一。你要是同时用了项目 .opencode/、全局 ~/.config/opencode/.claude/ 三处,重名迟早撞上。

它不管技能正文的可信度。 技能被加载后就是模型上下文里的一段指令。远程拉取那条路尤其要想清楚:拉的是别人服务器上的一份 index.json 和一堆文件,落到本地缓存目录后与本地技能一视同仁。更实际的一点是,下载函数在目标文件已存在时直接返回成功,不重新取;只有条目带了版本标识且和本地记录的不一致时,才会走”下到暂存目录、校验 SKILL.md 存在、原子换目录”的完整刷新流程。所以远程技能源如果不给版本标识,你本地那份就会一直停在第一次拉下来的样子。相关的注入风险,提示注入防御 那篇讲得更细。

它不管权限升级,也不替你兜底破坏性操作。 技能能做的事等于当前 agent 能做的事。这类终端工具本来就会在你机器上跑 shell 命令、直接改你的代码文件、把代码内容发给模型服务商。一份技能正文写着”发现构建失败就清理产物目录重来”,在权限放成全允许的 agent 上就是真的会删。反过来,skill 是有独立权限键的:可以按技能名做通配符匹配,设成允许、询问或拒绝;设成拒绝的技能会从模型可见列表里被过滤掉。文档给的配置形态是这样:

{
  "permission": {
    "skill": {
      "*": "allow",
      "pr-review": "allow",
      "internal-*": "deny",
      "experimental-*": "ask"
    }
  }
}

也可以在某个 agent 上单独覆盖,或者干脆把 skill 工具关掉——关掉之后,可用技能那一段在提示里整段消失,不占位置。至于权限该收到什么程度,最小权限设计 那篇的分级方法可以直接套。

它不管上下文预算。 技能描述是常驻的:每一个被认出来的技能,它的名字和描述每一轮都在上下文里。技能一多,这部分开销是固定支出。而正文只在加载时进来一次,之后一直留在对话里。这意味着技能数量的成本和技能正文的成本,是两笔账,得分开算。

它明确不承担的角色还包括:不做技能之间的依赖编排、不做版本约束求解、不给技能单独的模型或工具白名单。想要这些,得往子 agent 或者命令那边走。

六、上手与避坑清单

目录名和 name 对不上,真正生效的是 frontmatter 里那个名字。 文档明确要求两者一致,代码里也定义了一个专门表示名称不匹配的错误类型;但当前的扫盘与入库流程并不比对目录名,索引直接以 frontmatter 的 name 为键。结果就是:改名时只改了 frontmatter、忘了改文件夹,技能不会消失,它会以新名字挂在列表里,你按目录名去调它才会扑空。别把这当福利——文档已经把「必须一致」写成硬要求,补上校验是随时可能发生的事,届时这批技能会集体失效。会踩是因为凭目录名去猜技能名。避法:改名当成「改目录 + 改字段」两步的原子操作,改完立刻在一个新会话里确认它以你预期的名字出现在可用列表里。

文件名没全大写,扫不到。 模式串里写死的就是 SKILL.md。macOS 上文件系统大小写不敏感,本地能用,推到 Linux 的机器上就没了。避法:建文件时就用全大写,不要靠编辑器自动补全。

description 漏了,技能”存在但看不见”。 这是最隐蔽的一条:解析时只强制 name 是字符串,description 是可选的;但格式化可用技能清单时,会先把没有描述的条目过滤掉。结果就是这份技能确实进了索引,用名字直接要也能取到,可模型在列表里根本看不见它,永远不会主动调。避法:把”有 description”当成硬性检查项,而且描述要写清适用条件,文档给的长度区间是 1 到 1024 个字符,别只写四个字的标题。

名字不合规,眼下能跑、长期是坑。 文档给的规则是 1 到 64 个字符、小写字母数字、用单个连字符分隔、不能以连字符开头或结尾、不能出现连续两个连字符,等价正则是 ^[a-z0-9]+(-[a-z0-9]+)*$。和上一条一样,加载时并不跑这个正则,用下划线或大驼峰命名照样能进索引——这是「文档说不行、实现暂时没拦」的灰色地带,不是可以依赖的特性。会踩是因为习惯了用下划线或大驼峰。避法:新建目录时就照这个正则起名,别把「现在能跑」当成许可。

frontmatter 里写了别的字段,以为生效了。 只有那五个字段被识别,其余一律忽略。会踩是因为把别的工具的技能文件直接搬过来,里面带着一堆自定义键。避法:搬运时先按这五个字段裁一遍,别指望它替你读别的键。

环境里被关掉了还在查文件。 有几个开关会直接影响扫盘:一个总开关关掉外部技能目录,一个开关关掉全部 .claude 支持,还有一个只关 .claude/skills。会踩是因为团队的机器初始化脚本里统一设过这些变量,本人不知情。避法:技能怎么都不出现时,先确认这几个环境变量的状态,再去查文件。

YAML 解析失败是「静默」的。 解析出错会记一条加载失败的日志、往会话推一个错误事件,然后这份技能被跳过,启动不会崩。顺带纠一个常见误判:描述里写了冒号又没加引号,多数时候并不会挂——解析层带一次兜底重试,会把这类不带引号、值里含冒号的行改写成 YAML 块标量再解析一遍,源码注释写明这是为了兼容别的编码 Agent 那种宽松写法。真正打挂它的是块标量也救不回来的结构性错误,比如缩进错位、映射与列表混写、三横线分隔漏写。避法:改完 SKILL.md 后看一眼日志,别只看界面上有没有报红。

技能目录塞太多附件。 加载时给模型看的文件清单是采样的、有条数上限的,附件多了模型看到的只是一部分。避法:正文里对关键附件逐个点名并说明什么时候读,不要指望模型自己把目录翻完。

收束

把这套东西装进脑子里,判断一份内容该不该写成 opencode 技能,只需要过三道自检:第一,触发时机是不是模型比你更早知道?不是的话写成命令。第二,它需不需要独立的权限或独立的上下文?需要的话做成子 agent。第三,它的正文是不是”怎么做的判断规则”而不是”一大堆资料”?是资料就拆成附件,正文只留索引。

想继续往下挖,按这个顺序读仓库最省力:先 packages/web/src/content/docs/skills.mdx 拿到官方口径,再 packages/opencode/src/skill/index.ts 看扫盘与索引的真实边界,然后 packages/opencode/src/tool/skill.ts 看注入那一刻到底发生了什么,最后 packages/opencode/src/skill/discovery.ts 判断远程技能源值不值得你信。四个文件读完,“为什么我的技能没生效”这类问题基本能自己定位。

本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 开源终端 Agent opencode 接 MCP:两类接法与认证避坑把开源编码 Agent opencode 挂到代码托管平台:权限与边界

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。