Opencode Skills 架构剖析:从实现细节到上下文虚拟化
1. 概述
在 Opencode 项目中,Skill (技能) 不仅仅是一份份静态的 Markdown 文档,它是一套精密的动态上下文管理机制。本文将从代码实现层面由浅入深,探讨 opencode 如何通过“渐进式发现”原则,实现对 LLM 有限 Context 的极致优化。
2. 基础实现:SKILL 的发现与注册
Opencode 不会将所有技能直接硬编码进系统提示词,而是采用文件系统扫描的方式进行动态发现。
2.1 扫描机制
位于 packages/opencode/src/skill/skill.ts 的核心逻辑负责在启动时扫描以下路径:
- 全局目录:
~/.claude/skills或~/.agents/skills。 - 项目目录:沿着项目根目录向上查找
.claude/.agents。 - 配置目录:
.opencode/skill以及配置文件中指定的路径。
这种设计确保了技能的可移植性和层级覆盖(项目级覆盖全局级)。
// packages/opencode/src/skill/skill.ts 核心扫描逻辑
const addSkill = async (match: string) => {
// 解析 Markdown Frontmatter
const md = await ConfigMarkdown.parse(match);
// ... 校验与去重 ...
skills[parsed.data.name] = {
name: parsed.data.name,
description: parsed.data.description,
location: match, // 记录物理路径,暂不加载全部内容
content: md.content,
};
};
3. 核心机制:渐进式披露 (Progressive Disclosure)
这是 Skill 架构中最精彩的部分。系统不会在对话开始时将几万字的技能文档扔给 LLM,而是利用 Tool Definition 构建了一个轻量级的“索引表”。
3.1 索引表的构建
在 packages/opencode/src/tool/skill.ts 中,SkillTool 的定义至关重要。
代码实现细节:
// packages/opencode/src/tool/skill.ts
export const SkillTool = Tool.define("skill", async (ctx) => {
const skills = await Skill.all()
// 1. 权限过滤:确保 Agent 只能看到它有权访问的技能
const accessibleSkills = ...
// 2. 构建描述符(Descriptor):这是渐进式披露的关键
// 拼出一段给 LLM 的提示,说明“当识别任务匹配时再调用工具加载完整 skill”
// 并列出 <available_skills> 清单(仅包含名称、描述、位置)。
const description = [
"Load a specialized skill that provides domain-specific instructions and workflows.",
"When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
"<available_skills>",
...accessibleSkills.flatMap((skill) => [
` <skill>`,
` <name>${skill.name}</name>`,
` <description>${skill.description}</description>`, // 仅暴露描述,隐藏具体内容
` <location>${pathToFileURL(skill.location).href}</location>`,
` </skill>`,
]),
"</available_skills>",
].join("\n")
return { description, parameters, ... }
})
设计意图: LLM 此时只持有“指针”(Skill Name)和“摘要”(Description),Context 占用极小。
3.2 按需加载 (Page-In)
当 LLM 决定调用 skill 工具时,才会触发真正的加载逻辑。
// packages/opencode/src/tool/skill.ts
async execute(params: z.infer<typeof parameters>, ctx) {
// 1. 获取完整内容
const skill = await Skill.get(params.name)
// 2. 权限校验
await ctx.ask({ permission: "skill", patterns: [params.name], ... })
// 3. 文件采样:对 skill 目录进行采样,在提示中展示有限示例
// 帮助 LLM 理解目录结构,同时避免 Context 爆炸
const files = await iife(async () => {
// ... Ripgrep 扫描前 10 个文件 ...
})
// 4. 注入完整上下文
return {
output: [
`<skill_content name="${skill.name}">`,
`# Skill: ${skill.name}`,
skill.content.trim(), // 此刻,庞大的知识库才真正进入 Context
"<skill_files>",
files,
"</skill_files>",
"</skill_content>",
].join("\n"),
}
}
4. 数据流闭环
Skill 的内容通过以下链路最终影响模型行为:
- Tool Call: LLM 发起工具调用请求。
- Tool Execution:
SkillTool.execute返回包含<skill_content>的文本。 - Message Serialization:
- 结果被封装为 Message 的
toolpart。 packages/opencode/src/session/message-v2.ts中的toModelMessages方法将其序列化为模型可读的tool_result。
- 结果被封装为 Message 的
- Context Injection:
SessionPrompt将包含 Skill 结果的消息历史发送给 LLM。- LLM 在下一轮推理中,“看到”了刚刚加载的系统级指令。
5. 顶层抽象:上下文虚拟化 (Context Virtualization)
透过代码细节,我们可以看到 Opencode Skill 机制的本质是对 LLM 稀缺注意力资源和昂贵上下文窗口的一种极致优化。
5.1 提示词的分页机制 (Prompt Paging)
这与操作系统的虚拟内存机制有着异曲同工之妙:
- 压缩态 (Page Out): 默认状态下,System Prompt 不含任何领域知识,仅保留一份索引表(Skill List)。
- 缺页中断 (Page Fault): 当 LLM 的“指令指针”运行到特定领域(如“需要编写 Rust 代码”)时,触发工具调用。
- 展开态 (Page In): 系统将磁盘上的
SKILL.md加载到当前的 Context Window 中。
5.2 复杂度的降维
这种设计将 Token 消耗从 O(N)(N为所有技能的总长度)降低到了 O(1)(仅加载当前任务需要的技能)。
5.3 动态系统提示 (Dynamic System Prompting)
传统开发中,System Prompt 往往在对话开始前就写死。Opencode 通过 Skill 实现了后置注入 System Prompt。虽然技术载体是 tool_result,但在语义上,它动态地重塑了 LLM 在当前 Session 中的行为准则、工作流和注意事项。
总结: Opencode 的 Skill 架构不仅是一个插件系统,更是一套注意力管理系统。它允许 Agent 在保持极简初始状态的前提下,通过工具化的检索机制,拥有理论上无限的技能库,并确保注意力始终聚焦在当前最相关的上下文上,有效减少了模型幻觉与上下文被稀释的风险。
继续阅读: Skills 的演进与终局