优化上下文:subAgents + Skills 为何是当前 AI 编程的黄金方案

115 阅读27分钟

为什么 AI 编程工具难以真正落地

AI 编程工具落地过程中暴露的三类共性问题

在总结大会上,大家都有聊到 AI 编程的落地痛点,核心问题可归纳为三点:成本高、响应慢、生成不准

这三大痛点直接制约了我们对 AI 编程工具的深度应用。

被忽略但长期存在的成本:学习与配置的快速沉没

结合我个人的实践经验,还想补充一点:工具用法迭代快,学习成本易 “沉没” —— 比如刚花精力吃透 Cursor 的 Rules 配置这类实用技巧,很快就会被新的最优方案替代,之前的学习投入往往打了水漂。

比如这里 1 月 5 号记录的内容,在 1 月 8 号 Claude 更新到 2.1.0 后就失效了,它支持自动更新了

以及因为 Claude 可以显示使用 skills 后,复杂的配置流程简化了

痛点表现汇总(示例)

痛点类型具体表现
成本高1. 商业 AI 编程工具按调用量 / 团队人数收费,长期订阅成本高 2. 私有化部署需投入 GPU 集群、运维等硬件与人力成本 3. 生成代码后需额外投入安全审计、合规校验的人力
响应慢1. 公有云模型在复杂需求下(如生成跨模块架构代码)响应延迟高 2. 私有化部署模型受硬件算力限制,大上下文场景下卡顿明显 3. 复杂需求需反复调试 prompt,沟通迭代耗时
生成不准1. 对项目私有架构、业务逻辑理解不足,生成代码易与现有系统冲突 2. 代码语法正确但逻辑漏洞多(如边界条件缺失),需大量人工校验 3. 对小众框架、自研工具链支持薄弱,生成代码可用性低
学习成本易 “沉没”1. 工具功能迭代快 2. 新的最优方案快速替代旧技巧,前期投入的学习时间与精力难以复用

这些痛点看似是工具体验问题,实则根源在于 Transformer 架构的无状态特性——模型无法稳定承载长期约束,只能依赖重复注入的上下文模拟记忆,这也让 Rules 等临时方案最终陷入上下文膨胀、规则失效的恶性循环。

问题的根源并不在工具,而在模型架构

Transformer 架构如何决定了 AI 编程工具的上下文形态

Transformer 架构是当前常见通用大模型(如 GPT、Claude、Qwen)上下文设计的底层基石 。

Transformer = 无状态的 next-token 预言机。即 “给定前面所有 token,预测下一个 token”

上下文是模型的唯一记忆形式

它没有内部状态,上下文即唯一内存:把全部历史一次性塞进输入,模型才能“接着写”,而非“重新写”。

AI 编程工具把你输入的需求、模型之前的输出打包成一个完整的 token 序列,只有这样。模型才能知道 :你现在要在之前那个模块的基础上,补充一个功能,而不是重新生成一个完全无关的组件。

为什么输入质量直接决定输出上限

Transformer 架构决定了,输入质量直接决定输出质量。

在当前模型能力相对稳定的阶段,让输入更准确,是 AI 编程工具性价比最高也最容易工程化落地的方向

为了稳定输入,AI 编程工具不得不“接管上下文”

问题在于,这些“高质量输入”并不是一次性就能给完的。

在真实的编程场景中,用户往往需要通过多轮对话不断补充细节、修正方向,而 Transformer 又无法在轮次之间保留任何状态。

让用户在每一轮对话中重复输入这些“长期有效”的约束与背景,显然既不现实,也极易出错。

**于是,AI 编程工具不得不接管这件事:
**把“需要反复提示的重要信息”从用户输入中抽离出来,固化为系统级上下文,并在每一次请求中自动注入。

接下来我们看看,这种“固化输入”的工程方案,是如何通过 system / rules 实现的。

大模型的入参

用 api 调用过大模型的都知道大体有两种传参方式

  1. system 塞进 messages 里

  1. system 跟 messages 同级

可以看出这里共有 3 类角色

角色类型说明
system系统指令、环境信息、项目规则
user用户输入、文件内容、工具调用结果
assistantAI 的回复、工具调用

看到 system 的说明后,很容易想到 cursor 提出的 rules

用 cursor 的实际行为 + API 来聊聊这件事 对话大概是这样的

**在 Cursor 的聊天窗口中:
**每一轮新消息发送时,都会把你的 rules 重新塞进 system 消息,并和完整的对话历史一起发给大模型。
所以:

  • 第一轮:[system: rules, user: yyy]
  • 第二轮:[system: rules, user: yyy, assistant: aaa, user: zzz]
  • 第三轮:[system: rules, user: yyy, assistant: aaa, user: zzz, assistant: bbb, user: www]

**** 每次都塞!不是只塞一次!


场景模拟:在 Cursor 中聊天

假设你在 Cursor 的聊天窗口(非 /edit,是 Chat)做了以下操作:

****Step 1:设置 Rules

.cursor/rules 或 Settings 里写了:

你是一个 Python 专家。
- 必须使用类型注解
- 遵循 PEP8
- 只输出代码

****Step 2:第一轮提问

输入:

写一个加法函数

Cursor 实际发送的 API 请求(简化):

{
  "messages": [
    {"role": "system", "content": "你是一个 Python 专家。\n- 必须使用类型注解\n- 遵循 PEP8\n- 只输出代码"},
    {"role": "user", "content": "写一个加法函数"}
  ]
}

模型返回:

def add(a: int, b: int) -> int:
    return a + b

****Step 3:第二轮提问(关键!)

接着输入:

给它加个文档字符串

错误理解
“上一轮已经传过 system 了,这次只要加 user: 给它加个文档字符串 就行。”

真实行为(Cursor 实际做法)
Cursor 会把 完整的对话历史 + 重新注入的 rules 一起发送:

{
    "messages": [
        {"role": "system", "content": "你是一个 Python 专家。\n- 必须使用类型注解\n- 遵循 PEP8\n- 只输出代码"},
        {"role": "user", "content": "写一个加法函数"},
        {"role": "assistant", "content": "def add(a: int, b: int) -> int:\n    return a + b"},
        {"role": "user", "content": "给它加个文档字符串"}
    ]
}

rules 被重新塞进了 system!这是必须的,因为 API 是无状态的。

****Step 4:第 N 轮 —— 上下文变长了!

假设你聊了 20 轮,每轮都有代码和解释,总 token 达到 200K

→ 此时,API 客户端(或服务端)会自动截断最前面的内容,以腾出空间。

可能的截断结果:

原始 messages:

[system (200), user1, asst1, user2, asst2, ..., user10, asst10]

截断后变成:

[user3, asst3, user4, asst4, ..., user10, asst10, user11]
  • system 被砍 → 规则失效
  • 中间史丢失 → 失忆
  • 窗口越来越长 → 越聊越卡

Rules 的代价:输入稳定化带来的新问题

Rules 的出现绝非错误选择,恰恰相反,在当前大模型的架构约束下,它几乎是必然会诞生的工程方案。

Transformer 架构的核心问题,在于模型本身无状态——它无法在多轮对话中留存任何内部记忆,只能依靠每次请求时携带的完整上下文,来判断“当下正在处理什么事”。

Rules 的核心作用,就是把那些“多轮对话中始终生效的信息”,比如技术栈要求、代码风格规范、架构约束等,从用户每次的输入中抽离出来,固化为 system 级上下文,在每次发起请求时自动注入。

从这个角度来说,Rules 解决的不是模型“能不能生成好代码”的问题,而是一个更底层的难题:如何在无状态模型上,模拟出“长期记忆”的效果。

而麻烦,也恰恰从这里开始。

上下文膨胀不是实现缺陷,而是 Rules 成功后的必然结果

Rules 给人的第一直觉是“省事”,不用反复输入相同内容,能明显提效。但站在模型的角度,实际情况却刚好相反。

对模型而言,信息是用户手动输入的还是工具自动添加的,毫无区别,所有内容最终都会被拼接成一长串线性的 Token 序列。

所以 Rules 并没有缩短上下文长度,只是把开发者“每次都要显式输入的重复内容”,转化成了工具在系统后台“悄悄注入的隐性内容”。

随着需求日益复杂,Rules 会不断累积新增内容,最终陷入恶性循环:

  • 新增编码规范,适配更精细的开发要求;
  • 新增业务限制,贴合具体场景合规需求;
  • 新增默认上下文假设,减少手动输入成本。

需求复杂度上升, 带动 Rules 数量增加,进而导致系统层 Token 持续膨胀, 使得单次请求的上下文越来越长,最终上下文被截断的风险难以避免

这并非工具开发存在缺陷,而是由大模型上下文机制本身决定的结构性问题。

截断即崩塌:Rules 没有“部分生效”的可能

在实际落地时,你可能会跟我一样,容易陷入这样一个误区:“就算上下文被截断,至少核心规则还能生效吧?”

但 Transformer 的上下文根本不是有层级、分优先级的配置项,只是一长串毫无区别的线性 Token 序列——系统层的 Rules 也只是这串文本中的一段,不存在“核心规则优先保留”的机制。

这就意味着:Rules 本身没有内置优先级,被截断的位置完全由 Token 长度决定。一旦系统层的 Rules 被截断一部分甚至全部,模型不会“只丢失部分约束、勉强遵守剩余规则”,而是对被截断的规则完全失去认知,整个规则体系直接失灵。

模块化 Rules 也无法避免“截断即崩塌”

此时往往会出现一个更深层的疑问:
在实际项目中,我们通常会对 Rules 做模块化拆分;如果上下文截断时只裁掉其中少数几条, “Rules 没有部分生效”这一结论是否仍然成立?

cursor/rules/
├── project-guidelines.mdc # 通用规范 (你是一个前端架构师,使用 Vue + TypeScript)
├── vue-components.mdc # Vue 组件规范
├── code-style.mdc # 代码风格规范 (遵循项目的 ESLint 规范, 不要用 any)
├── api-design.mdc # API 设计规范 
├── …… # 一些别的规范,或者说明
└── testing.mdc # 测试规范

答案是肯定的。

核心原因在于:这些规则在我们脑中是一个完整、内在一致的体系,但对 Transformer 模型而言,它们只是 Token 流中的孤立文本片段。模型并不理解规则的适用范围、依赖关系或语义前提,只能对当前可见的自然语言逐句解读。

因此,一旦发生截断,即便部分规则在形式上仍然存在,也会出现典型现象:
表面上规则似乎“还在”,但生成结果开始出现风格漂移、约束失真、边界条件错乱。从工程语义上看,这已经不再是规则的“退化生效”,而是规则体系的实质性失效

更进一步,Transformer 并不具备判断规则重要性的能力。
system 角色并不会获得额外权重,模型也无法识别“第几条规则更核心”。它只基于当前可见的 Token 分布进行概率预测。一旦 Rules 被截断,受到影响的不是“少遵守几条规则”,而是整体约束分布被破坏,预测逻辑发生偏移。从输出行为上看,这正是规则体系“崩塌”的表现。

最致命的问题在于:Rules 的生效状态完全不可观测
即便在 20 条规则中截断后仍保留 10 条,工程上仍无法回答以下问题:

  • 模型是否真正理解了这些规则?
  • 是否仍将它们视为当前任务的全局约束?
  • 规则之间是否出现了隐性冲突?
  • 是否已被后续对话内容弱化或覆盖?

而在工程系统中,一项能力如果同时具备不可观测、不可验证、不可回滚这三个特征,其可靠性将迅速下降,几乎无法支撑复杂工程场景下的稳定运行。

从这个意义上说,Rules 一旦进入不可判定的生效状态,便已经失去了作为工程约束机制的可信基础

从工程角度来看,这会带来一个棘手的后果:Rules 的可靠性不会随着使用时间沉淀增强,反而会随着对话长度增加、上下文膨胀而持续下降

Rules 越多,系统的可理解性和可维护性越差

随着 Rules 数量的增加,AI 编程工具会逐渐呈现出一种熟悉的“反模式”:

  • 新成员无法判断当前哪些规则在生效
  • 规则之间可能存在隐性冲突,但没有任何静态分析手段
  • 当模型违反规则时,几乎无法定位是“哪条规则失效了”

从软件工程的角度来看,Rules 更像是:

一个没有类型系统、没有依赖图、没有作用域划分的全局宏集合。

它在规模较小时尚且可控,但一旦进入复杂工程场景,其可维护性会迅速恶化

小结:Rules 并非失败方案,而是过渡方案

综合来看,Rules 的问题不在于“做得不够好”,而在于它试图用重复注入的文本上下文,去模拟模型本身并不具备的结构化长期状态。

它在中小规模场景下是高性价比的解法,能快速解决无状态模型的记忆难题,但随着系统复杂度不断上升,其带来的上下文膨胀、可靠性下降、可维护性恶化等代价,会以不可忽视的方式持续累积。

问题的根源很明确:上下文本身就无法稳定承载长期状态。在这种情况下,继续堆叠 Rules 只会放大现有问题,而非从根本上解决问题。

这也正是我们需要跳出“单纯堆叠上下文”的固有思路,转向分层上下文、Agents 与 Skills 组合式设计的核心原因——这是应对复杂场景、破解 Rules 固有困境的必然方向。

为什么必须引入「分层上下文」

理解了 Rules 带来的结构性代价后,一个更根本的问题便无法回避:

如果问题根源不在于“提示不够多”,而在于“上下文本身无法稳定承载长期状态”,那么继续通过堆叠 Rules 优化输入质量,本质上已经偏离了正确方向。

要理清这个问题,首先要明确一个易被忽略却至关重要的事实:上下文并不等于状态

上下文的两种“记忆”含义:机制级 vs 工程级

在模型机制层面,上下文确实是 Transformer 的唯一“记忆”载体。
模型无法在多轮请求之间保留任何内部状态,所有历史信息都必须通过上下文重新注入,模型才能基于此前内容继续生成。

但关键在于,上下文并不是工程意义上的记忆系统——它只是一次性输入的 Token 序列,并非可读写、可更新的内部状态,既无法结构化引用、修改已有内容,也天然缺失软件工程所需的核心能力:

  • 无法表达生命周期
  • 无法表达作用域
  • 无法表达依赖关系
  • 无法区分长期稳定信息与短期临时信息

而实际开发中,我们依赖这些概念,这也是单一上下文无法适配复杂软件开发场景的核心原因。

单一上下文正在承载本不属于它的职责

实际使用时,我们常无意识地把不同性质信息塞进同一上下文,主要包括:

  • 项目级长期约束(技术栈、架构原则、合规要求)
  • 模块级稳定知识(领域模型、接口约定)
  • 当前任务即时目标(本次实现/修改内容)
  • 对话中间信息(解释、试错路径、临时假设)

这些信息的稳定性、复用性、生命周期差异极大,但模型会一视同仁地视为线性 Token。最终,本应承载“当前思考过程”的上下文,被迫兼任“长期记忆、系统配置、执行记录”多重角色,上下文膨胀、规则失效等问题随之而来,这是职责错配的必然结果。

解法:拆分职责,实现精准输入

回到我们之前反复强调的核心观点:这是一个关于输入的第一性问题。

相同模型能力前提下,决定 AI 编程工具效果上限的关键因素,在于输入是否足够准确、是否足够可控。

相比纠结于扩大上下文窗口,或不断堆叠 Rules,在既定模型能力下提升输入质量,才是性价比最高、也最容易工程化落地的优化方向

当我们从这一前提出发重新审视问题时,解决思路会发生根本性的转变。
真正的瓶颈并不在于上下文“不够大”,也不在于规则“写得不够细”,而在于我们试图用一个线性、一次性消费的上下文结构,去承载一个本应具备长期状态与层级关系的系统

在这样的结构错配下,无论上下文窗口多大、Rules 多复杂,问题都只会被延后,而不会被消除。

因此,核心解法是拆分上下文职责:分离不同生命周期、稳定性的信息,仅向当前任务注入必要推理信息。这是分层上下文的核心出发点,并非新提示技巧,而是工程化践行“输入第一性原理”的方式,通过精准控输入,构建可扩展、可演进的系统基础。

分层上下文的核心思想

分层上下文并不是对模型架构的改造,而是一种纯工程层面的设计思想。

它的核心原则可以概括为一句话:

让上下文只承载当前这一轮推理“必须参与思考的信息”,而不是所有“可能有用的信息”。

基于这一原则,我们可以自然地将上下文拆分为不同层级。

全局稳定上下文:环境与约束层

这一层包含的是跨任务、跨对话长期稳定的信息,例如:

  • 技术栈与框架选型
  • 架构与安全原则
  • 编码规范与工程约定

它们的共同特征是:

  • 更新频率极低
  • 对所有任务都成立
  • 不直接参与推理过程

在传统工具中,这一层通常被强行塞进 system Rules;而在分层设计中,它更像是运行环境的配置,而不是对话内容的一部分。

能力上下文:Skills 层

这一层描述的不是“当前要做什么”,而是“系统具备哪些可复用能力”。

例如:

  • 生成符合公司规范的前端页面
  • 设计标准化的数据库表结构
  • 对既有模块进行结构性重构

这些能力不应以零散 prompt 的形式存在,而应被抽象为具备明确输入输出的能力单元

在工程实现上,这一层通常以 Skills 的形式存在:
一组可调用、可组合、可测试的能力函数。

任务上下文:Agent 层

真正进入模型上下文、参与推理的,是这一层的信息,包括:

  • 当前任务目标
  • 必要的代码与上下文片段
  • 已完成的中间决策
  • Skills 的调用结果

它具有非常明确的特征:

  • 生命周期短
  • 可被频繁创建和销毁
  • 长度严格受控

Agent 的职责,正是围绕当前任务,按需组装这一层上下文,而不是把系统中所有信息一次性暴露给模型。

小结:工程视角的核心转向

从 Rules 到分层上下文、Agents 与 Skills 的组合设计,绝非 Prompt 技巧的简单升级,而是 AI 编程工具的系统设计转向——核心是从“追求更聪明的提示话术”,转向“构建可控、可演进的输入管理系统”。

这种转向的核心逻辑,是聚焦输入精准度而非信息数量,通过明确信息交付的时机、范围与形式,让 AI 工具真正适配复杂工程场景。

接下来,我想从 OpenCode 的源码入手,具体聊聊 Skills 与 Agents 是如何被工程化实现,进而承载分层上下文思想的。

聊聊 OpenCode 中的 Agents、Skills 实现

为什么是 OpenCode,因为它开源

从表格可清晰看出,OpenCode 对上下文的优化绝不仅于对 Skills、Agents 的处理,这里我们聚焦二者的核心原因是,它们是这类 AI 编程工具中,为数不多可由用户主动控制的核心功能模块。

而 Compaction、Summary 等功能均为系统自动执行,用户完全无感知,暂不纳入本次拆解范围。

优化方式用户控制自动执行作用
AGENTS.md/CLAUDE.md添加项目级指令到 system 消息
SKILL.md按需加载特定技能
instructions 配置加载额外的规则文件
Compaction⚙️自动压缩对话历史
Summary自动生成对话摘要
Prune⚙️自动删除旧工具调用输出
read 工具参数控制文件读取范围
Agent 配置定义特定行为的 agent

接下来,我们就深入源码,具体拆解 Agents 与 Skills 的实现逻辑。

Agent 系统:任务分解与并行执行的核心引擎

概述

Agent 系统是 OpenCode 实现复杂任务自动化和智能分解的核心机制。通过将大型任务拆解为多个独立的子任务,每个 Agent 在隔离的上下文中专注于特定职责,系统实现了高效的并行执行和精确的权限控制。

核心能力

  • 任务分解:将复杂任务拆解为多个独立的子任务
  • 并行执行:支持多个 Agent 并行执行,提高效率
  • 上下文隔离:每个 Agent 在独立的子会话中执行,避免污染主会话
  • 权限控制:每个 Agent 有独立的权限配置,实现精细的访问控制
  • 动态提示:每个 Agent 有自己的系统提示,优化特定场景的行为

在 Agent 系统的核心能力中,上下文隔离是支撑所有功能落地的关键技术基础,而并行执行则是其提升任务效率的核心价值体现。受限于篇幅,本文将重点聚焦上下文隔离的实现细节展开拆解。

Agent 子代理的独立对话上下文实现

1. Session 层级结构

每个 agent 子代理都有自己独立的 session,通过 parentID 字段关联到父 session:

在 tool/task.ts 中,创建子 session:

2. 消息存储隔离

每个 session 的消息是独立存储的,通过 sessionID 进行隔离:

在 session/index.ts 中:

存储路径结构:

Storage:
├── ["message", sessionID1, messageID1]
├── ["message", sessionID1, messageID2]
├── ["message", sessionID2, messageID1]  # 不同 session 的消息独立存储
├── ["part", messageID1, partID1]
├── ["part", messageID1, partID2]
└── ...

3. 消息读取机制

在 message-v2.ts 中,通过 sessionID 读取消息:

在 session/index.ts 中:

4. 消息传递到 LLM

在 prompt.ts 中,只将当前 session 的消息传递给 LLM:

5. Session Fork(会话分支)

在 session/index.ts 中,fork 函数展示了如何复制消息到新的 session:

6. 子 Session 查询

在 session/index.ts 中,可以查询某个 session 的所有子 session:

总结:独立对话上下文的实现机制

机制实现方式作用
Session ID 隔离每个 session 有唯一的 sessionID确保不同 agent 的消息不会混淆
ParentID 关联子 session 通过 parentID 关联到父 session维护 session 的层级关系
存储路径隔离消息存储在 ["message", sessionID, messageID]物理隔离不同 session 的消息
按 SessionID 读取Storage.list(["message", sessionID])只读取指定 session 的消息
独立消息传递MessageV2.toModelMessage(sessionMessages)只将当前 session 的消息传递给 LLM
Session Fork复制父 session 的消息到新 session支持会话分支和独立对话

关键点

  • ✅ 每个 agent 子代理都有独立的 session
  • ✅ 消息按 sessionID 物理隔离存储
  • ✅ 读取和传递消息时都按 sessionID 过滤
  • ✅ 子 session 不会继承父 session 的消息历史(除非通过 fork 复制)
  • ✅ 通过 parentID 维护 session 的层级关系,但不共享消息上下文

Agents 的上下文隔离,本质是通过‘任务级输入隔离’实现精准控制,避免不同任务的上下文相互干扰
这种设计确保了每个 agent 子代理都有完全独立的对话上下文,互不干扰,同时可以通过 parentID 追踪会话的层级关系。

Skill 的按需加载和递进式加载机制

概述

OpenCode 的 Skill 系统通过按需加载(On-Demand Loading)和递进式加载(Progressive Loading)机制,实现了高效的技能管理和上下文优化。这种机制确保 AI 只在需要时才加载完整的 Skill 内容,避免了不必要的资源消耗和上下文浪费。

按需加载机制

工具注册时的延迟初始化

Skill 工具通过 init 方法实现延迟初始化,只有在实际需要时才生成工具描述:

export async function tools(providerID: string, agent?: Agent.Info) {
  const tools = await all()
  const result = await Promise.all(
    tools
      .filter((t) => {
        return true
      })
      .map(async (t) => {
        using _ = log.time(t.id)
        return {
          id: t.id,
          ...(await t.init({ agent })),
        }
      }),
  )
  return result
}

动态工具描述生成

Skill 工具的描述是动态生成的,根据当前代理的权限过滤可用技能:

export const SkillTool = Tool.define("skill", async (ctx) => {
  const skills = await Skill.all()

  const agent = ctx?.agent
  const accessibleSkills = agent
    ? skills.filter((skill) => {
        const rule = PermissionNext.evaluate("skill", skill.name, agent.permission)
        return rule.action !== "deny"
      })
    : skills

  const description =
    accessibleSkills.length === 0
      ? "Load a skill to get detailed instructions for a specific task. No skills are currently available."
      : [
          "Load a skill to get detailed instructions for a specific task.",
          "Skills provide specialized knowledge and step-by-step guidance.",
          "Use this when a task matches an available skill's description.",
          "<available_skills>",
          ...accessibleSkills.flatMap((skill) => [
            `  <skill>`,
            `    <name>${skill.name}</name>`,
            `    <description>${skill.description}</description>`,
            `  </skill>`,
          ]),
          "</available_skills>",
        ].join(" ")

按需加载的关键特性

  1. 延迟初始化:工具描述不是静态的,而是在每次 resolveTools 时动态生成
  2. 权限感知:根据当前代理的权限过滤可用技能
  3. 元数据优先:工具描述中只包含 Skill 的名称和描述,不包含完整内容
  4. 动态更新:每次调用 init 时都会重新扫描 Skill 目录

递进式加载机制

Skill 的递进式加载分为三个层次,每个层次提供不同粒度的信息:

第一层:工具描述层

在初始化阶段,只加载 Skill 的元数据信息:

<available_skills>
  <skill>
    <name>test-skill</name>
    <description>use this when asked to test skill</description>
  </skill>
</available_skills>

特点

  • 只包含 Skill 的元数据(名称和描述)
  • 不加载 Skill 的完整内容
  • 作为工具描述的一部分发送给 AI
  • 初始化成本低

第二层:工具执行层

当 AI 决定使用某个 Skill 时,才加载完整内容:

async execute(params: z.infer<typeof parameters>, ctx) {
  const skill = await Skill.get(params.name)

  if (!skill) {
    const available = await Skill.all().then((x) => Object.keys(x).join(", "))
    throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
  }

  await ctx.ask({
    permission: "skill",
    patterns: [params.name],
    always: [params.name],
    metadata: {},
  })

  const parsed = await ConfigMarkdown.parse(skill.location)
  const dir = path.dirname(skill.location)

  const output = [
    `## Skill: ${skill.name}`,
    "",
    `**Base directory**: ${dir}`,
    "",
    parsed.content.trim()
  ].join("\n")

  return {
    title: `Loaded skill: ${skill.name}`,
    output,
    metadata: {
      name: skill.name,
      dir,
    },
  }
}

特点

  • 只有当 AI 调用 skill 工具时才加载完整内容
  • 从磁盘读取 SKILL.md 文件
  • 解析 frontmatter 和内容
  • 返回格式化的 Skill 内容

第三层:对话历史层

Skill 内容被添加到对话历史中,供后续对话使用:

if (part.type === "tool" && part.state.status === "completed") {
  assistantMessage.parts.push({
    type: ("tool-" + part.tool) as `tool-${string}`,
    state: "output-available",
    toolCallId: part.callID,
    input: part.state.input,
    output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
    callProviderMetadata: part.metadata,
  })
}

特点

  • Skill 内容作为工具结果被添加到对话历史
  • 可以在后续对话中被 AI 引用
  • 受保护机制保护,不会被修剪

完整流程

┌─────────────────────────────────────────────────────────────┐
│ 第一层:工具描述层(初始化时)                                │
├─────────────────────────────────────────────────────────────┤
│ resolveTools() 被调用                                        │
│   ↓                                                          │
│ SkillTool.init({ agent }) 被调用                             │
│   ↓                                                          │
│ 动态生成工具描述,包含:                                      │
│   - 所有可用 Skill 的名称                                    │
│   - 每个 Skill 的描述                                        │
│   - 格式化为 XML 结构                                        │
│   ↓                                                          │
│ 工具描述发送给 AI                                            │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 第二层:工具执行层(按需触发)                                │
├─────────────────────────────────────────────────────────────┤
│ AI 分析任务,发现匹配的 Skill                                │
│   ↓                                                          │
│ AI 调用 skill(name="test-skill")                            │
│   ↓                                                          │
│ SkillTool.execute() 被执行                                  │
│   ↓                                                          │
│ 从磁盘读取 SKILL.md 文件                                     │
│   ↓                                                          │
│ 解析 frontmatter 和内容                                      │
│   ↓                                                          │
│ 返回完整的 Skill 内容                                        │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 第三层:对话历史层(持久化)                                  │
├─────────────────────────────────────────────────────────────┤
│ Skill 内容作为工具结果被添加到对话历史                        │
│   ↓                                                          │
│ 后续对话中 AI 可以引用已加载的 Skill                          │
│   ↓                                                          │
│ Skill 内容受保护,不会被修剪                                  │
└─────────────────────────────────────────────────────────────┘

优势分析

性能优化

  1. 第一层只加载元数据:避免在初始化时加载大量 Skill 内容
  2. 第二层按需加载:只有需要的 Skill 才会被加载
  3. 第三层持久化:已加载的 Skill 不需要重复加载
  4. 内存效率:渐进式内存占用,避免一次性加载所有 Skill

上下文管理

  1. 渐进式信息暴露:AI 先看到 Skill 摘要,再决定是否加载完整内容
  2. 权限感知:不同代理看到不同的可用 Skill 列表
  3. 保护机制:Skill 内容不会被修剪,确保持续可用
  4. 智能过滤:根据代理权限动态调整可用 Skill 列表

灵活性

  1. 动态更新:每次调用 init 时都会重新扫描 Skill 目录
  2. 权限过滤:根据代理权限动态调整可用 Skill 列表
  3. 多层级支持:支持项目级和全局级 Skill 目录
  4. 易于扩展:可以轻松添加新的 Skill 而不影响现有系统

技术实现细节

Skill 发现机制

Skill 系统支持多层级扫描:

const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md")
const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")

const claudeDirs = await Array.fromAsync(
  Filesystem.up({
    targets: [".claude"],
    start: Instance.directory,
    stop: Instance.worktree,
  }),
)

const globalClaude = `${Global.Path.home}/.claude`
if (await exists(globalClaude)) {
  claudeDirs.push(globalClaude)
}

for (const dir of claudeDirs) {
  const matches = await Array.fromAsync(
    CLAUDE_SKILL_GLOB.scan({
      cwd: dir,
      absolute: true,
      onlyFiles: true,
      followSymlinks: true,
      dot: true,
    }),
  )
  for (const match of matches) {
    await addSkill(match)
  }
}

for (const dir of await Config.directories()) {
  for await (const match of OPENCODE_SKILL_GLOB.scan({
    cwd: dir,
    absolute: true,
    onlyFiles: true,
    followSymlinks: true,
  })) {
    await addSkill(match)
  }
}

扫描优先级

  1. 项目级 .claude/skills/
  2. 全局 ~/.claude/skills/
  3. 项目级 .opencode/skill/

保护机制

Skill 工具的结果受到保护,不会被自动修剪:

const PRUNE_PROTECTED_TOOLS = ["skill"]

export async function prune(input: { sessionID: string }) {
  for (const msg of msgs) {
    for (const part of msg.parts) {
      if (part.type === "tool" && part.state.status === "completed") {
        if (PRUNE_PROTECTED_TOOLS.includes(part.tool)) continue

        const estimate = Token.estimate(part.state.output)
        total += estimate
        if (total > PRUNE_PROTECT) {
          pruned += estimate
          toPrune.push(part)
        }
      }
    }
  }
}

工具上下文

Skill 工具通过上下文获取代理信息:

export const SkillTool = Tool.define("skill", async (ctx) => {
  const agent = ctx?.agent
  const accessibleSkills = agent
    ? skills.filter((skill) => {
        const rule = PermissionNext.evaluate("skill", skill.name, agent.permission)
        return rule.action !== "deny"
      })
    : skills
})

最佳实践

1. Skill 文件组织

建议按照功能模块组织 Skill 文件:

.opencode/
  skill/
    development/
      code-review/
        SKILL.md
      testing/
        SKILL.md
    deployment/
      docker/
        SKILL.md
      kubernetes/
        SKILL.md

2. Skill 描述编写

编写清晰、准确的 Skill 描述:

---
name: code-review
description: Use this for reviewing code quality, identifying bugs, and suggesting improvements. Best for Python, JavaScript, and TypeScript projects.
---

# Code Review Skill

## Checklist
- [ ] Code follows style guidelines
- [ ] No obvious bugs
- [ ] Proper error handling
- [ ] Documentation is complete

3. 权限配置

为不同代理配置适当的 Skill 权限:

const generalAgent = {
  permission: {
    skill: {
      "code-review": "allow",
      testing: "allow",
      "*": "deny",
    },
  },
}

const adminAgent = {
  permission: {
    skill: {
      "*": "allow",
    },
  },
}

总结

Skill 的按需加载和递进式加载机制是 OpenCode 实现高效上下文管理的关键技术。通过三层递进的加载策略,系统确保:

  1. 性能优化:只在需要时加载完整内容
  2. 上下文效率:渐进式信息暴露,避免不必要的上下文消耗
  3. 灵活性:支持动态更新和权限控制
  4. 可扩展性:易于添加新的 Skill 而不影响现有系统

这种机制使得 OpenCode 能够高效地管理大量 Skill,同时确保 AI 可以在需要时快速访问相关的专业知识。

操作演示

1. Skills 目前为我的项目带来了哪些实际变化

2. 蓬勃的社区

skillsmp.com/

最后

确实如最开始所说,学习与配置的快速沉没,但这也恰恰说明 AI 编程工具进步。

功能与体验都在快速的迭代,会越来越容易满足我们工程化的需求。

这也是我认为可以开始投入 AI 编程使用的原因之一。

AMA

按以往大家分享的经验,会有个问我任何问题的环节。 我这里预设了几个问题

1. 合规性

出于信创的考量,可以用 OpenCode 然后配置国产大模型