
1. 从一次 Skill 加载失败说起OpenCode Skills 的加载链路到底怎么走如果你最近在折腾 OpenCode大概率会遇到这样一个场景明明在.opencode/skills/下放好了SKILL.mdfrontmatter 也写了name和description可模型就是看不见这个技能调用skill工具时直接报Skill xxx not found。我第一次碰到这个问题时盯着目录结构看了半小时最后才发现是 frontmatter 的 YAML 缩进多了一个空格导致ConfigMarkdown.parse解析出来的data里根本没有name字段Info.pick({ name: true, description: true }).safeParse(md.data)直接success: false注册环节被静默跳过。这就是 OpenCode Skills 机制的一个典型特征发现、解析、注册、注入、调用五个阶段里任何一个环节失败都不会给你特别醒目的报错而是技能凭空消失。所以想真正跑通一个自定义 Skill光会写SKILL.md不够得把整条链路拆开看。OpenCode 的 Skills 本质上是一套按需加载的指令包系统。它和传统的插件、工具调用不太一样工具是模型主动调用的函数而 Skill 更像是一份说明书平时只把标题和描述挂在系统提示里让模型知道有这么个东西等模型判断当前任务匹配某个 Skill 的描述时才通过skill工具把完整正文加载进上下文。这种设计的好处是省 token——你放 20 个 Skill系统提示里也只占几十行 XML坏处是调试链路变长出问题不好定位。这篇文章我会按 OpenCode 源码里的真实执行顺序把SKILL.md的 frontmatter 解析、四类来源的扫描优先级、内存注册表的构建、系统提示注入、以及skill工具的完整执行流程讲清楚。同时以接入 TaoToken 统一 Key/API 通道为实战场景给你一份可以直接复制的config.toml、settings.json骨架和一个能跑通的自定义 Skill 示例。适合已经在用 OpenCode、想搞清楚 Skills 内部机制、或者想把自己的工作流封装成 Skill 的开发者。2. TaoToken 前置准备统一 Key 与 API 通道怎么配在讲 Skill 注册之前得先把模型通道打通否则你连验证 Skill 的工具调用都跑不起来。OpenCode 支持多种 provider我这里用 TaoToken 作为统一入口原因是它把多家模型的 Key 收敛成一个 API Key配置一次就能在 OpenCode 里切换模型省得每个 provider 单独维护环境变量。TaoToken 的 API 地址是https://taotoken.net/api注意这个地址不带任何查询参数直接作为 Base URL 用。官网入口是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册后在控制台生成 API Key格式通常是一串以sk-开头的字符串。这里有个容易踩的坑OpenCode 的 provider 配置里Base URL 到底要不要带/v1取决于你用的 SDK 类型。TaoToken 的兼容层同时支持 OpenAI 风格和 Anthropic 风格如果你走 OpenAI 兼容模式Base URL 填https://taotoken.net/apiSDK 内部会自己拼/v1/chat/completions如果你手动填了/v1反而会变成/v1/v1/chat/completions直接 404。我实测下来最稳的做法是 Base URL 只写到/api让 SDK 处理路径拼接。配置方式有两种一种是写进 OpenCode 的全局配置文件另一种是用环境变量。环境变量适合临时切换配置文件适合长期使用。下面这份config.toml是 OpenCode 读取 provider 的标准位置路径在~/.config/opencode/config.tomlLinux/macOS或%APPDATA%\opencode\config.tomlWindows。# ~/.config/opencode/config.toml # TaoToken 统一通道配置Base URL 只写到 /api [provider.taotoken] name TaoToken npm ai-sdk/openai-compatible options { baseURL https://taotoken.net/api } [provider.taotoken.models.claude-sonnet-4-5] name Claude Sonnet 4.5 [provider.taotoken.models.gpt-4o] name GPT-4o [provider.taotoken.models.deepseek-v3] name DeepSeek V3注意npm字段指定的是ai-sdk/openai-compatible这是 OpenCode 用来对接 OpenAI 兼容接口的适配器。如果你的 OpenCode 版本较老可能字段名是sdk而不是npm以你本地opencode --version对应的文档为准。API Key 不要硬编码进config.toml用环境变量注入更安全。在~/.zshrc或~/.bashrc里加一行export TAOTOKEN_API_KEYsk-你的实际key然后在config.toml里引用[provider.taotoken] name TaoToken npm ai-sdk/openai-compatible options { baseURL https://taotoken.net/api, apiKey {env:TAOTOKEN_API_KEY} }{env:TAOTOKEN_API_KEY}是 OpenCode 的变量插值语法启动时会从环境变量读取。这样配置文件可以安全地提交到 dotfiles 仓库Key 留在本地环境里。如果你更习惯用settings.json风格部分 OpenCode 发行版或插件生态会读这个文件对应的骨架是这样{ provider: { taotoken: { name: TaoToken, npm: ai-sdk/openai-compatible, options: { baseURL: https://taotoken.net/api, apiKey: {env:TAOTOKEN_API_KEY} }, models: { claude-sonnet-4-5: { name: Claude Sonnet 4.5 }, gpt-4o: { name: GPT-4o } } } } }配好之后用opencode models命令应该能看到taotoken/claude-sonnet-4-5这类条目。如果看不到先检查config.toml的 TOML 语法有没有写错——TOML 对引号和方括号很敏感[provider.taotoken]这种表头写错一个字符整个 provider 都不会加载。3. SKILL.md 的 frontmatter 解析与四类来源扫描优先级现在进入正题。OpenCode 里一个 Skill 的最小单元就是一个SKILL.md文件它由两部分组成顶部的 YAML frontmatter 和下面的正文。frontmatter 用---包裹里面至少要有name和description两个字段。--- name: taotoken-api-helper description: 当用户需要查询 TaoToken 可用模型、生成 API 调用示例或排查 401 错误时使用此技能 --- # TaoToken API 助手 你是一个专门帮助用户接入 TaoToken 的技能。 ## 可用模型列表 - claude-sonnet-4-5 - gpt-4o - deepseek-v3 ## 常见错误处理 如果遇到 401检查 API Key 是否以 sk- 开头以及环境变量 TAOTOKEN_API_KEY 是否已导出。name是 Skill 的唯一标识注册表以它为 key重复的name会触发duplicate skill name警告后加载的覆盖先加载的。description是给模型看的模型靠它判断当前任务要不要加载这个 Skill所以描述要写清楚触发场景别写成这是一个很有用的技能这种废话。frontmatter 的解析由ConfigMarkdown.parse完成它内部用 YAML 解析器把---之间的内容转成对象正文部分单独提取。解析失败时addSkill里的.catch()会捕获错误并通过Bus.publish(Session.Event.Error, ...)发一个事件但不会中断整个扫描流程——这就是为什么一个坏文件不会影响其他 Skill 加载但也容易被忽略。接下来是发现逻辑。OpenCode 的 Skill 扫描是懒初始化的通过state函数在第一次访问时触发按优先级依次扫描四类来源后加载的覆盖同名的先加载的。这个后覆盖先的规则很关键意味着项目级 Skill 可以覆盖全局级同名 Skill方便你在具体项目里定制行为。第一类是外部兼容目录。OpenCode 会先扫全局 home 目录下的.claude/skills/和.agents/skills/这两个目录是为了兼容其他 AI 工具的 Skill 格式。源码里的常量是const EXTERNAL_DIRS [.claude, .agents] const EXTERNAL_SKILL_PATTERN skills/**/SKILL.md扫描时先检查Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS是否开启如果没开就遍历这两个目录用Filesystem.isDir确认存在后再scanExternal。然后还会从当前工作目录向上查找直到worktree根目录把项目级的.claude和.agents也扫一遍。所以你的 Skill 可以放在~/.claude/skills/作为全局也可以放在项目根目录的.claude/skills/作为项目级。第二类是 OpenCode 自有目录。这是最推荐放自定义 Skill 的地方支持skill/和skills/两种命名const OPENCODE_SKILL_PATTERN {skill,skills}/**/SKILL.md也就是说.opencode/skill/my-skill/SKILL.md和.opencode/skills/my-skill/SKILL.md都能被识别。扫描时通过Config.directories()拿到所有配置目录再用Glob.scan匹配模式。这里有个细节**/SKILL.md意味着你可以嵌套多层目录比如.opencode/skills/backend/api-helper/SKILL.mdname字段才是唯一标识目录层级不影响注册。第三类是用户自定义路径。你可以在opencode.json里声明额外的 Skill 路径支持~/前缀和相对路径展开。这对把 Skill 放在非标准位置的人很有用比如你想把团队共享的 Skill 放在一个 git submodule 里。第四类是远程 URL 下载。通过Discovery.pull(url)从远程服务器拉取 Skill适合团队统一分发。不过远程拉取涉及网络和缓存调试阶段建议先用本地文件跑通。四类来源的扫描顺序决定了覆盖关系。实际执行时外部兼容目录先扫然后是 OpenCode 自有目录再是用户自定义路径最后是远程。同名 Skill 后扫到的会覆盖先扫到的。所以如果你在.claude/skills/和.opencode/skills/放了同名 Skill.opencode里的会生效。这个规则可以用来做全局默认 项目覆盖的分层设计。4. 注册表构建与系统提示注入Skill 怎么被模型看见每发现一个SKILL.mdaddSkill函数就负责解析并注册到内存 map。注册表是一个以name为 key 的Recordstring, Info对象存在state里。Info的数据模型是export const Info z.object({ name: z.string(), description: z.string(), location: z.string(), content: z.string(), })name从 frontmatter 提取description用于 AI 判断location是SKILL.md在磁盘上的绝对路径content是正文内容。注册逻辑大致是const addSkill async (match: string) { const md await ConfigMarkdown.parse(match).catch((err) { Bus.publish(Session.Event.Error, { ... }) return undefined }) const parsed Info.pick({ name: true, description: true }).safeParse(md.data) if (!parsed.success) return if (skills[parsed.data.name]) { log.warn(duplicate skill name, { ... }) } skills[parsed.data.name] { name: parsed.data.name, description: parsed.data.description, location: match, content: md.content, } dirs.add(path.dirname(match)) }注意Info.pick({ name: true, description: true })只校验这两个字段location和content是注册时手动填的。如果 frontmatter 缺name或descriptionsafeParse返回success: false直接returnSkill 被静默丢弃。这就是开头那个技能凭空消失的根因。注册完成后Skill 的执行分两个阶段系统提示阶段预告和工具调用阶段加载详情。系统提示阶段由skills(agent)函数负责export async function skills(agent: Agent.Info) { if (PermissionNext.disabled([skill], agent.permission).has(skill)) return const list await Skill.available(agent) return [ Skills provide specialized instructions and workflows for specific tasks., Use the skill tool to load a skill when a task matches its description., Skill.fmt(list, { verbose: true }), ].join(\n) }Skill.fmt(list, { verbose: true })会把所有可用 Skill 以 XML 格式列出来注入系统提示。verbose: true是详细版包含description让模型能判断匹配度。这个 XML 片段就是模型看见Skill 的唯一途径——如果注册表里没有系统提示里就不会出现模型自然不知道有这个 Skill。这里有个权限过滤的细节PermissionNext.disabled([skill], agent.permission)会检查当前 agent 的权限配置如果skill工具被完全禁用整个 Skills 提示都不注入。所以如果你发现模型完全不提 Skill先检查 agent 的 permission 配置里有没有把skill禁掉。Skill.available(agent)会按 agent 权限过滤可用 Skill不同 agent 看到的 Skill 列表可能不同。这给了你按 agent 定制能力集的空间比如给代码审查 agent 只开放审查相关的 Skill。系统提示里的 XML 大概长这样available_skills skill nametaotoken-api-helper/name description当用户需要查询 TaoToken 可用模型、生成 API 调用示例或排查 401 错误时使用此技能/description /skill /available_skills模型看到这个列表后如果判断当前任务匹配某个description就会调用skill工具传入name参数进入第二阶段加载完整正文。5. skill 工具执行全流程与常见报错排查skill工具的定义在SkillTool里它的execute方法分四步查找 Skill、权限检查、收集附属文件、构造输出。第一步查找根据 LLM 传入的name通过Skill.get(name)从注册表查找。找不到就抛错并列出所有可用名称const skill await Skill.get(params.name) if (!skill) { const available await Skill.all().then((x) x.map((s) s.name).join(, )) throw new Error(Skill ${params.name} not found. Available: ${available}) }这个报错信息很实用Available:后面会列出所有已注册的 Skill 名对照一下就知道是名字拼错还是根本没注册上。第二步权限检查ctx.ask({ permission: skill, patterns: [params.name], always: [params.name], ... })。如果用户配置了需要确认会弹交互式确认框。always字段设为 Skill 名意味着用户允许过一次后后续同名 Skill 不再重复询问。第三步收集附属文件用Ripgrep.files()列出 Skill 目录下所有文件排除SKILL.md本身最多收集 10 个。这些文件路径以file.../file标签形式包含在输出里让模型知道目录下还有哪些脚本、模板可以配合使用。第四步构造输出返回一个结构化文本块包含 Skill 名、完整正文、基准目录路径、附属文件列表skill_content nametaotoken-api-helper # Skill: taotoken-api-helper SKILL.md 完整正文 Base directory for this skill: file:///path/to/skill/dir Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. Note: file list is sampled. skill_files file/path/to/skill/dir/scripts/check-key.sh/file /skill_files /skill_content这个输出被注入对话上下文模型就能按正文指令执行任务了。现在说几个我实际踩过的报错。第一个是Skill xxx not found. Available: ...最常见原因是 frontmatter 的name和调用时传的不一致或者 YAML 缩进错误导致name没解析出来。排查方法用opencode的调试模式看注册表或者临时在addSkill里加日志。更简单的办法是检查SKILL.md的 frontmatter 是不是严格以---开头和结尾中间不能有空行。第二个是401 Unauthorized这个通常不是 Skill 的问题而是 TaoToken 的 Key 没配好。检查TAOTOKEN_API_KEY环境变量是否导出config.toml里的{env:TAOTOKEN_API_KEY}拼写是否正确。如果用的是settings.json确认 JSON 没有尾逗号。第三个是local proxy failed或连接超时这多半是 Base URL 写错了。记住 TaoToken 的 Base URL 是https://taotoken.net/api不要加/v1也不要加尾部斜杠。如果你在config.toml里写了baseURL https://taotoken.net/api/v1SDK 会拼成/v1/v1/...直接失败。第四个是reading choices相关错误这通常出现在流式响应解析阶段说明返回的 JSON 结构不符合 OpenAI 兼容格式。先确认你选的模型在 TaoToken 控制台是启用的再确认npm字段用的是ai-sdk/openai-compatible而不是别的适配器。第五个是 OAuth 相关报错如果你用的是需要 OAuth 的 provider但配置里混了 API Key 模式会报OAuth token missing之类。TaoToken 走的是 API Key 模式不需要 OAuth所以config.toml里不要配auth相关字段。排查顺序建议先opencode models确认 provider 加载成功再发一条最简单的对话确认通道通最后才测 Skill 调用。这样能把通道问题和Skill 问题分开定位。6. 跑通自定义 Skill 并接入 TaoToken 的完整验证最后给你一个端到端的验证流程。先建目录和文件mkdir -p .opencode/skills/taotoken-api-helper写入SKILL.md--- name: taotoken-api-helper description: 当用户需要查询 TaoToken 可用模型、生成 API 调用示例或排查 401 错误时使用此技能 --- # TaoToken API 助手 你是一个专门帮助用户接入 TaoToken 的技能。 ## 可用模型列表 - claude-sonnet-4-5 - gpt-4o - deepseek-v3 ## 调用示例 Base URL 使用 https://taotoken.net/api不要追加 /v1。 ## 常见错误处理 如果遇到 401检查 API Key 是否以 sk- 开头以及环境变量 TAOTOKEN_API_KEY 是否已导出。确认config.toml里的 provider 配置正确环境变量已导出echo $TAOTOKEN_API_KEY启动 OpenCode发一条会触发 Skill 的消息比如帮我查一下 TaoToken 有哪些可用模型。模型应该会调用skill工具传入name: taotoken-api-helper然后按正文指令回答。如果模型没调用 Skill先检查系统提示里有没有available_skills片段。可以在 OpenCode 的调试日志里找或者临时把description写得更明确比如加上当用户提到 TaoToken 时必须使用此技能。验证成功后你可以把这个 Skill 目录提交到项目仓库团队成员拉下来就能用。如果想让全局生效把目录移到~/.config/opencode/skills/或~/.claude/skills/。想统一管理多个项目的 Skill可以在opencode.json里声明自定义路径或者用Discovery.pull从团队仓库拉取。一个实用技巧把 Skill 的description当成触发条件来写而不是功能描述。模型是靠description做匹配的写清楚什么时候用比写能做什么更有效。比如当用户需要查询 TaoToken 可用模型、生成 API 调用示例或排查 401 错误时使用此技能就比TaoToken 助手技能匹配率高得多。如果你想把 Skill 和 Coding Plan 结合做长期的编码 Agent 工作流可以在 TaoToken 控制台配置 Coding Plan把常用模型和额度固定下来再配合 Skill 封装团队的代码规范、审查清单、部署流程。这样每次新项目初始化只要把.opencode/skills/目录复制过去Agent 就自动具备团队约定的能力集。API Key 管理在控制台的 API Keys 页面接入细节可以对照官方文档模型能力验证可以直接在模型对话页面测。