一、Skill 的数据模型export const Info z.object({ name: z.string(), description: z.string(), location: z.string(), content: z.string(), })nameskill 的唯一标识名在frontmatter 中提取description描述用于AI判断locationSKILL.md文件在磁盘上的绝对路径content指令内容SKILL.md正文内容二、Skill 的发现与加载Skill 的发现逻辑通过 state 函数懒初始化按优先级依次扫描 4 类来源后加载的会覆盖同名 skill项目级覆盖全局级外部兼容目录先扫全局 home 目录下的.claude/skills/和.agents/skills/const EXTERNAL_DIRS [.claude, .agents] const EXTERNAL_SKILL_PATTERN skills/**/SKILL.md if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) { for (const dir of EXTERNAL_DIRS) { const root path.join(Global.Path.home, dir) if (!(await Filesystem.isDir(root))) continue await scanExternal(root, global) } for await (const root of Filesystem.up({ targets: EXTERNAL_DIRS, start: Instance.directory, stop: Instance.worktree, })) { await scanExternal(root, project) } }.opencode 自有目录扫描 opencode 原生的 skill 目录支持 skill/ 和 skills/ 两种命名。即.opencode/skill/和.opencode/skills/const OPENCODE_SKILL_PATTERN {skill,skills}/**/SKILL.md for (const dir of await Config.directories()) { const matches await Glob.scan(OPENCODE_SKILL_PATTERN, { ... }) for (const match of matches) await addSkill(match) }用户自定义路径用户可以在opencode.json中声明自定义的 skill 路径支持~/前缀和相对路径展开远程 URL 下载通过Discovery.pull(url)从远程服务器拉取 skill三、Skill的注册每发现一个 SKILL.md都用这个addSkill函数解析并注册到内存 map 中注册表是一个以 name 为 key 的 Recordstring, Info 对象存储在 state 中。const addSkill async (match: string) { // 解析 SKILL.mdYAML frontmatter 正文 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, { ... }) } // 注册到内存 map skills[parsed.data.name] { name: parsed.data.name, description: parsed.data.description, location: match, // 文件路径 content: md.content, // 正文内容 } dirs.add(path.dirname(match)) }四、Skill的初始化Instance.state() 底层调用 State.create()实现基于项目路径的单例懒加载// state.ts export function createS(root: () string, init: () S, ...) { return () { const key root() const exists entries.get(init) if (exists) return exists.state const state init() entries.set(init, { state, dispose }) return state } }五、Skill写入系统提示Skill 的执行分为两个阶段系统提示阶段预告和 工具调用阶段加载详情。export async function skills(agent: Agent.Info) { // 检查 agent 权限若 skill 工具被完全禁用则跳过 if (PermissionNext.disabled([skill], agent.permission).has(skill)) return const list await Skill.available(agent) // 按 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 }), // 以 XML 格式列出所有 skill 摘要 ].join(\n) }Skill.fmt(list, { verbose: true })生成的 XML 片段被注入系统提示AI 可以提前感知到有哪些 skill 可用六、Skill完整信息获取export const SkillTool Tool.define(skill, async (ctx) { // --- 动态生成工具描述含当前可用 skill 列表--- const list await Skill.available(ctx?.agent) const description list.length 0 ? No skills are currently available. : [ Load a specialized skill..., Skill.fmt(list, { verbose: false }), // 简洁列表工具描述用简洁版系统提示用详细版 ].join(\n) return { description, parameters: z.object({ name: z.string().describe(The name of the skill from available_skills (e.g., skill-a, ...)) }), async execute(params, ctx) { // 1. 从注册表查找 skill 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}) } // 2. 权限确认可能触发用户交互弹窗 await ctx.ask({ permission: skill, patterns: [params.name], always: [params.name], metadata: {}, }) // 3. 扫描 skill 目录的附属文件最多 10 个排除 SKILL.md 本身 const dir path.dirname(skill.location) const base pathToFileURL(dir).href const files await iife(async () { const arr [] for await (const file of Ripgrep.files({ cwd: dir, follow: false, hidden: true, signal: ctx.abort })) { if (file.includes(SKILL.md)) continue arr.push(path.resolve(dir, file)) if (arr.length 10) break } return arr }).then((f) f.map((file) file${file}/file).join(\n)) // 4. 拼装完整输出注入对话上下文 return { title: Loaded skill: ${skill.name}, output: [ skill_content name${skill.name}, # Skill: ${skill.name}, , skill.content.trim(), // 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., , skill_files, files, // 附属文件列表供 AI 进一步读取 /skill_files, /skill_content, ].join(\n), metadata: { name: skill.name, dir }, } }, } })以上代码的主要流程execute方法查找 Skill根据 LLM 传入的 name 参数通过 Skill.get(name) 查找对应的 skill。如果找不到就抛出错误并列出所有可用的 skill 名称。权限检查调用 ctx.ask() 发起一个权限请求类型为 skill如果用户配置了需要确认会弹出交互式确认。always 字段设置为 skill 名称意味着一旦用户允许过一次后续不再重复询问。收集附带文件通过 Ripgrep.files() 列出 skill 所在目录下的所有文件排除 SKILL.md 本身最多收集 10 个。这些文件路径会以 file.../file 标签的形式包含在输出中让 LLM 知道 skill 目录下还有哪些脚本、模板等资源可以配合使用。构造输出最终返回给 LLM 的是一个结构化的文本块
深度剖析OpenCode中的Skills的实现原理
发布时间:2026/7/2 1:22:12
一、Skill 的数据模型export const Info z.object({ name: z.string(), description: z.string(), location: z.string(), content: z.string(), })nameskill 的唯一标识名在frontmatter 中提取description描述用于AI判断locationSKILL.md文件在磁盘上的绝对路径content指令内容SKILL.md正文内容二、Skill 的发现与加载Skill 的发现逻辑通过 state 函数懒初始化按优先级依次扫描 4 类来源后加载的会覆盖同名 skill项目级覆盖全局级外部兼容目录先扫全局 home 目录下的.claude/skills/和.agents/skills/const EXTERNAL_DIRS [.claude, .agents] const EXTERNAL_SKILL_PATTERN skills/**/SKILL.md if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) { for (const dir of EXTERNAL_DIRS) { const root path.join(Global.Path.home, dir) if (!(await Filesystem.isDir(root))) continue await scanExternal(root, global) } for await (const root of Filesystem.up({ targets: EXTERNAL_DIRS, start: Instance.directory, stop: Instance.worktree, })) { await scanExternal(root, project) } }.opencode 自有目录扫描 opencode 原生的 skill 目录支持 skill/ 和 skills/ 两种命名。即.opencode/skill/和.opencode/skills/const OPENCODE_SKILL_PATTERN {skill,skills}/**/SKILL.md for (const dir of await Config.directories()) { const matches await Glob.scan(OPENCODE_SKILL_PATTERN, { ... }) for (const match of matches) await addSkill(match) }用户自定义路径用户可以在opencode.json中声明自定义的 skill 路径支持~/前缀和相对路径展开远程 URL 下载通过Discovery.pull(url)从远程服务器拉取 skill三、Skill的注册每发现一个 SKILL.md都用这个addSkill函数解析并注册到内存 map 中注册表是一个以 name 为 key 的 Recordstring, Info 对象存储在 state 中。const addSkill async (match: string) { // 解析 SKILL.mdYAML frontmatter 正文 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, { ... }) } // 注册到内存 map skills[parsed.data.name] { name: parsed.data.name, description: parsed.data.description, location: match, // 文件路径 content: md.content, // 正文内容 } dirs.add(path.dirname(match)) }四、Skill的初始化Instance.state() 底层调用 State.create()实现基于项目路径的单例懒加载// state.ts export function createS(root: () string, init: () S, ...) { return () { const key root() const exists entries.get(init) if (exists) return exists.state const state init() entries.set(init, { state, dispose }) return state } }五、Skill写入系统提示Skill 的执行分为两个阶段系统提示阶段预告和 工具调用阶段加载详情。export async function skills(agent: Agent.Info) { // 检查 agent 权限若 skill 工具被完全禁用则跳过 if (PermissionNext.disabled([skill], agent.permission).has(skill)) return const list await Skill.available(agent) // 按 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 }), // 以 XML 格式列出所有 skill 摘要 ].join(\n) }Skill.fmt(list, { verbose: true })生成的 XML 片段被注入系统提示AI 可以提前感知到有哪些 skill 可用六、Skill完整信息获取export const SkillTool Tool.define(skill, async (ctx) { // --- 动态生成工具描述含当前可用 skill 列表--- const list await Skill.available(ctx?.agent) const description list.length 0 ? No skills are currently available. : [ Load a specialized skill..., Skill.fmt(list, { verbose: false }), // 简洁列表工具描述用简洁版系统提示用详细版 ].join(\n) return { description, parameters: z.object({ name: z.string().describe(The name of the skill from available_skills (e.g., skill-a, ...)) }), async execute(params, ctx) { // 1. 从注册表查找 skill 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}) } // 2. 权限确认可能触发用户交互弹窗 await ctx.ask({ permission: skill, patterns: [params.name], always: [params.name], metadata: {}, }) // 3. 扫描 skill 目录的附属文件最多 10 个排除 SKILL.md 本身 const dir path.dirname(skill.location) const base pathToFileURL(dir).href const files await iife(async () { const arr [] for await (const file of Ripgrep.files({ cwd: dir, follow: false, hidden: true, signal: ctx.abort })) { if (file.includes(SKILL.md)) continue arr.push(path.resolve(dir, file)) if (arr.length 10) break } return arr }).then((f) f.map((file) file${file}/file).join(\n)) // 4. 拼装完整输出注入对话上下文 return { title: Loaded skill: ${skill.name}, output: [ skill_content name${skill.name}, # Skill: ${skill.name}, , skill.content.trim(), // 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., , skill_files, files, // 附属文件列表供 AI 进一步读取 /skill_files, /skill_content, ].join(\n), metadata: { name: skill.name, dir }, } }, } })以上代码的主要流程execute方法查找 Skill根据 LLM 传入的 name 参数通过 Skill.get(name) 查找对应的 skill。如果找不到就抛出错误并列出所有可用的 skill 名称。权限检查调用 ctx.ask() 发起一个权限请求类型为 skill如果用户配置了需要确认会弹出交互式确认。always 字段设置为 skill 名称意味着一旦用户允许过一次后续不再重复询问。收集附带文件通过 Ripgrep.files() 列出 skill 所在目录下的所有文件排除 SKILL.md 本身最多收集 10 个。这些文件路径会以 file.../file 标签的形式包含在输出中让 LLM 知道 skill 目录下还有哪些脚本、模板等资源可以配合使用。构造输出最终返回给 LLM 的是一个结构化的文本块