Back to Journal
02 / Entry· 11 min read

各家 Agent 的 Skills 加载机制:渐进披露为什么成了事实标准

调研 Claude Code、Codex CLI、OpenClaw、Cursor、Gemini CLI、GitHub Copilot 六家的技能加载机制:四种加载模式、三级渐进披露、1%/2% 上下文预算,最后给出一个约 100 行的技能加载器,三步接进你自己的 Agent Loop。

🔊 系统朗读

如今的 Agent 已经不只是调用几个工具:它们还会加载技能文件,把一套稳定的工作方法、领域知识和操作流程带进任务。但一个问题经常被忽略:技能文件是怎么被 Agent 发现、加载、决定使用的?

这个话题值得单独写一篇,因为"技能加载"的工程设计直接决定了三件事:上下文占用、技能数量上限、以及模型到底会不会真的用上你的技能。这次调研覆盖了六家主流实现——Claude Code、Codex CLI、OpenClaw、Cursor、Gemini CLI、GitHub Copilot / VS Code,资料以各家公开文档(截至 2026-09-02)为准。由于产品仍在快速迭代,下面会把“官方明确说明”和“跨产品抽象”分开写。先给结论:

主流实现正在收敛到一个共同骨架:Skill = 目录 + 带 frontmatter 的 SKILL.md;启动时优先保留 name + description,模型判定相关时才读全文,附属文件按需读取或执行。

核心分歧集中在三件事:放哪(目录层级与优先级)、谁触发(模型自主、用户显式、文件规则或目录事件)、占多少(上下文预算与截断策略)。权限、热更新和子代理继承则属于各产品自己的运行时设计。

如果你正在自建 Agent,这篇就是整套可抄的作业。

先看结论:四种加载模式

把六家的机制抽象掉产品外壳,底层可以归纳为四种基本模式;Gemini CLI 的目录扫描是“规则匹配”的 JIT 变体:

模式注入时机触发依据代表优点缺点
① 常驻注入启动即全量拼进 system prompt无(无条件)Codex AGENTS.md、Cursor alwaysApply确定性最高,模型一定看得见吃 token,规模一大就撑爆
② 按需检索(渐进披露)只驻元数据,用时读全文模型读 description 自主判断Claude Code / Codex / OpenClaw 的 Skills省 token,技能可以无限多依赖模型判断力,误判=漏用
③ 规则匹配命中 glob/paths 才注入文件 glob 匹配Cursor globs、部分实现的路径规则精准,不依赖模型需要维护匹配规则,非文件场景失效
③a 目录扫描(JIT)工具访问某目录时扫描其祖先链目录访问事件Gemini CLI 第三层覆盖整个目录树,无需维护 glob依赖"工具访问"事件,只在文件型场景有效
④ 显式触发用户敲命令才注入用户 /name$name@rule全部(各家都保留)确定性最高需要用户知道有这个技能

关键洞察:这四种不是互斥的,各家通常是组合提供。真正的设计决策是默认走哪一种——Claude Code / Codex / OpenClaw 的 Skills 以按需发现为主,Cursor 把选择权交给 frontmatter 组合(有 globs 走 ③,有 description 无 globs 走 ②,alwaysApply 走 ①)。

六家横向对比

维度Claude CodeCodex CLIOpenClawCursorGemini CLICopilot / VS Code
技能文件SKILL.mdSKILL.mdSKILL.mdSKILL.md + .mdcSKILL.mdSKILL.md
指令文件CLAUDE.mdAGENTS.mdAGENTS.mdAGENTS.md / CLAUDE.mdGEMINI.mdcopilot-instructions.md
目录发现启动目录+父目录;嵌套子目录触及文件时才动态加载从 CWD 逐级向上到仓库根6 级优先级,同名高优先级覆盖,最深 6 层递归发现,嵌套目录自动作用域4 级:内置 > 扩展 > 用户 > 工作区项目+个人目录;monorepo 向上扫到 .git
全局位置~/.claude/skills~/.agents/skills~/.openclaw/skills~/.agents/skills~/.cursor/skills~/.gemini/skills~/.copilot/skills
上下文预算列表约 上下文 1%;每条 listing 的组合文本上限 1536 字符列表 ≤ 上下文 2%(未知时 8000 字符)未公开未公开未公开未公开
正文建议上限< 5k tokens / 500 行未明确未明确≤500 行未公开未公开
模型自主触发✅ 默认✅ 默认(可关)✅ 默认(可关)✅ "Apply Intelligently"✅ 默认✅ 默认
用户显式触发/name/skills$name/name$name/name@rule/skills/name/skills(prompt 文件)
glob 限定paths环境条件过滤globsJIT 目录扫描applyTo glob + 语义匹配
脚本执行shell 执行,默认不自动读入上下文同规范{baseDir}同规范同规范shell 执行,默认不自动读入上下文
热更新✅ 文件监听,会话内生效需重启✅ watcher未确认/skills reload未确认

逐家拆解

Anthropic / Claude Code:规范的制定者

开放规范定义了 6 个 frontmatter 字段:name(≤64 字符、必须与目录名一致)、description(≤1024 字符,写"做什么 + 何时用")、licensecompatibility(≤500 字符)、metadataallowed-tools(实验性)。Claude Code 还扩展了 disable-model-invocationuser-invocablecontext: forkhooksargument-hint 等字段。要跨客户端分发,应该把规范字段当作可移植基线,扩展字段则视为客户端能力,不能默认互通。

发现优先级:Enterprise > Personal(~/.claude/skills)> Project(.claude/skills);插件用 plugin:skill 命名空间隔离。一个值得抄的细节:monorepo 里子包的 .claude/skills/ 启动时不加载,等 Claude 第一次读写该目录内的文件才动态激活——避免大仓启动时扫描全部子包。

三级渐进披露(核心抄作业点):

级别内容时机量级
1. Metadataname + description启动时常驻~100 tokens/个
2. InstructionsSKILL.md 全文模型判定相关时读入建议 <5k tokens
3. Resourcesscripts/references/assets按需读或执行无上限

最关键的认知:二级加载本身就是一个普通工具调用——Anthropic 工程博客明确写了模型通过 Bash 工具 cat pdf/SKILL.md 来加载。也就是说渐进披露不需要任何特殊机制,你的 Agent 只要会调工具,天然就支持

预算与截断:skill 列表占上下文窗口约 1%(skillListingBudgetFraction 可调),超预算会缩短或省略 description,name 仍会保留;当前文档还说明每条 listing 的组合文本默认最多 1,536 个字符。这里的“1%”是客户端预算,不是开放规范对所有实现的硬性要求。

脚本不必自动进上下文:scripts/ 里的代码可以由模型通过 shell 直接执行,默认只把执行结果返回给模型;如果模型主动读取脚本或数据,相关内容仍然会进入上下文。这是把确定性逻辑从概率性推理里剥离的关键手段。Claude Code 还支持动态上下文注入,可在命令执行后把结果内联到技能内容中。

标准化:Agent Skills 已形成开放规范,并被多个客户端采用;官方客户端列表会随版本变化,不能把某个时点的数量当成长期不变的事实。规范的最小结构和三级渐进披露见 Agent Skills specification

OpenAI Codex CLI:两代机制并存

Codex 同时保留了两代机制,正好暴露演进轨迹。

旧代:AGENTS.md 层级叠加——全局 ~/.codex/(有 AGENTS.override.md 则优先)→ 从仓库根沿目录一路向下走到 CWD,每层取一个文件(override > AGENTS.md > 备选名),按序拼接而非覆盖,越靠近 CWD 的越靠后,靠"后写胜出"的惯例实现覆盖语义。总大小上限默认 32 KiBproject_doc_max_bytes 可调)。这套设计像 MCP 的 stdio 一样"简单粗暴但有效":没有按需加载,全量注入,用大小上限兜底。

新代:Skills 渐进披露——目录为 $REPO_ROOT→$CWD 各级 .agents/skills/~/.agents/skills(USER)、/etc/codex/skills(ADMIN)、内置(SYSTEM,含 skill-creator、plan)。启动时只注入 name+description+文件路径,预算是上下文窗口的 2%(上下文未知时 8000 字符),超了先缩短 description、再省略并告警。

~/.codex/prompts 自定义命令已标记 deprecated——和 Claude Code 把 custom commands 合并进 skills 是同一个动作。

OpenClaw:工程细节最丰富

frontmatter 门控字段最有意思:

  • command-dispatch: tool + command-tool + command-arg-mode:斜杠命令直接路由到工具、完全绕过模型。对确定性的高频操作(如"部署到预发")是巨大优化——省一轮模型调用,也消除模型传参出错的可能
  • metadata.openclaw.requires(bins / anyBins / env / config)、osalways加载时就按环境过滤,环境不满足的技能根本不进列表——把兼容性判断从运行时前移到加载时
  • disable-model-invocation:对模型隐藏,但用户 $name 显式引用仍可用

6 级来源优先级:<workspace>/skills > <workspace>/.agents/skills > ~/.agents/skills > <state-dir>/skills~/.openclaw/skills)> 内置 > skills.load.extraDirs + 插件。同名技能高优先级直接覆盖,不合并。SKILL.md 可在技能根目录下任意深度被发现(最多 6 层)。

与 session / subagent 的关系(别家文档都没写透的部分):

  • 个人技能库按不可变 revision 管理,会话锁定选中的技能 ID + revision,新 revision 只影响新会话——避免"改技能把正在跑的会话改崩"
  • 子 agent 是独立 session,默认 context: isolated只注入 AGENTS.md,不注入人格文件
  • 子 agent 沿用目标 agent 的技能允许列表(agents.defaults.skills / agents.entries.*.skills,非空列表是最终集合,不与 defaults 合并)

Cursor:把选择权交给 frontmatter

.mdc 规则只有三个字段,却组合出四种行为:

frontmatter 组合类型行为
alwaysApply: trueAlways Apply每次会话注入,globs/description 被忽略
alwaysApply:false + globsApply to Specific Files匹配文件进入上下文时自动附加
alwaysApply:false + description(无 globs)Apply IntelligentlyAgent 依据 description 自主决定
三者皆无Apply Manual@rule 提及时注入

优先级 Team > Project > User。.cursorrules 已 legacy。

Cursor 的收敛动作最彻底:已支持 Agent Skills 开放标准,目录兼容 .agents/skills/.cursor/skills/~/.agents/skills/~/.cursor/skills/甚至兼容 .claude/skills/.codex/skills/,并提供 /migrate-to-skills 把 "Apply Intelligently" 规则和 slash commands 批量转成技能。这说明技能生态的战场已经从"谁的格式好"变成"谁能吃到别人的存量技能"。

Gemini CLI:三遍扫描 + 按需技能

两层机制:GEMINI.md 指令文件(常驻)+ Agent Skills(渐进披露)。

GEMINI.md 三层层级:

层级位置时机
1. 全局~/.gemini/GEMINI.md每个项目启动都注入
2. 环境/工作区工作区目录及其父目录中的 GEMINI.md随会话注入
3. JIT(按需)工具访问某文件/目录时,扫描该目录向上直到 trusted rootGEMINI.md用到才注入

所有找到的文件拼接后随每条 prompt 发送(/memory show 可查看)。它家独有的是第三层 JIT:触发点从"模型读文件"扩展到了"任意工具访问文件",比 Cursor 的 glob 更宽松。

Agent Skills 发现层级(低 → 高):内置 > 扩展 > 用户(~/.gemini/skills/~/.agents/skills/ 别名)> 工作区(.gemini/skills/.agents/skills/ 别名)。会话内 /skills list / reload / disable / enable / link 可以不重启就热重载;终端侧 gemini skills install <repo|.skill> 支持用 .skill 包分发。内置 skill-creator 元技能:一句话 prompt 就能生成带 name/description frontmatter 和 scripts/references/assets 目录的技能。

自定义命令:~/.gemini/commands/(用户级)+ <project>/.gemini/commands/(项目级覆盖同名),TOML v1 格式,子目录转命名空间(git/commit.toml/git:commit)。参数系统最有意思:{{args}} 上下文感知注入、!{...} 执行 shell、@{...} 注入文件内容。

⚠️ 注意:Gemini CLI 的产品路线和技能命令仍可能随版本变化;文章中的目录、命令和加载时机,使用时应以对应版本的官方文档为准。

GitHub Copilot / VS Code:五类定制并存,正在向 Skills 收敛

Copilot 的定制体系最庞大,分五类文件:

类型文件加载方式
常驻指令.github/copilot-instructions.mdAGENTS.mdCLAUDE.md、组织级指令每次聊天请求自动注入
文件级指令.github/instructions/*.instructions.md(也兼容 .claude/rules/applyTo glob 匹配或 description 语义匹配,递归搜索子目录
Prompt 文件.github/prompts/*.prompt.md用户手动 /name 调用;frontmatter 可指定 agent/model/tools
Agent Skills.github/skills/.claude/skills/.agents/skills/ + 个人级按需加载,与 Anthropic 同一开放标准
自定义 Agent.github/agents/*.agent.md切换 agent harness 时整包生效

几个关键细节:

  • Copilot 同样支持 Agent Skills 开放标准,格式与 Claude Code 完全互通;context: fork 可以在子代理上下文里跑技能、只把最终结果带回主会话(和 OpenClaw 的 subagent 思路一致)
  • Agent Host(云端 agent)不用 prompt files:官方提供一次性迁移把 .prompt.md 转成 Agent Skills——"prompt → skill"的迁移在每个产品里都发生了
  • monorepo:开启 chat.useCustomizationsInParentRepositories 后从工作区文件夹向上走到 .git 根,收集沿途所有定制文件——与 Claude Code"触及才加载"互补的另一种解法
  • 用户级指令兼容 ~/.claude/rules——和 Cursor 一样,主动兼容竞品目录

收敛出的四个共识

  1. 格式正在统一:目录 + SKILL.md(YAML frontmatter + Markdown),name + description 是开放规范的最小必需集,已经成为跨客户端分发的主流格式
  2. 渐进披露是主流答案:多个新一代实现选择"先驻元数据、再按需读正文";Anthropic 和 Codex 的预算参数是客户端策略,不应外推成行业标准
  3. 加载即工具调用:二级加载不需要特殊机制,模型用读文件工具即可,自建 Agent 里渐进披露几乎是零成本
  4. 脚本不进上下文:把确定性逻辑关进脚本,模型只看到输入输出——省 token 和提准确率的最大杠杆

自建 Agent 该抄哪一套

场景推荐理由
技能 < 10 个,都是通用指令Codex 式全量注入实现最简单,一个文件拼接 + 32 KiB 上限兜底
技能 10–100 个,需要按场景区分渐进披露启动注入 name+description(约 100 token/个),用时读全文
有强文件类型相关性(如前端组件规范)叠加 Cursor 式 glob 匹配命中文件才注入,比模型判断更可靠
高频确定性操作(如"部署到预发")OpenClaw 式 command-dispatch: tool绕过模型,省一轮调用且零出错
技能依赖特定环境(需要 docker/psql 等)OpenClaw 式加载期过滤不满足的技能一开始就不出现在列表里

一个最小可用的技能加载器

把核心流程压到约 100 行,核心是三级加载 + 预算控制;模型自主、文件规则和用户显式触发,都可以复用 activate()

python
"""skill_loader.py — 自建 Agent 的技能加载器(渐进披露 + glob + 预算)"""
import fnmatch
import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterable

import yaml  # pip install pyyaml

SKILL_DIRS = (".agents/skills", ".claude/skills", "skills", "~/.agents/skills")
LISTING_BUDGET_FRACTION = 0.02   # 参考 Codex:列表最多占上下文 2%
CHARS_PER_TOKEN = 4              # 粗略估算;生产环境应使用目标模型 tokenizer


@dataclass
class Skill:
    name: str
    description: str
    path: Path
    paths: list[str] = field(default_factory=list)   # glob:命中才自动注入
    requires: dict = field(default_factory=dict)     # 环境要求:bins/env/config
    loaded: bool = False

    def satisfied(self) -> bool:
        """加载期过滤:环境不满足直接不进列表(抄 OpenClaw)"""
        for b in self.requires.get("bins", []):
            if not any(os.access(os.path.join(p, b), os.X_OK)
                       for p in os.environ["PATH"].split(os.pathsep)):
                return False
        return all(k in os.environ for k in self.requires.get("env", []))

    def matches(self, files: Iterable[str]) -> bool:
        """规则匹配:命中 glob 才自动注入(抄 Cursor)"""
        if not self.paths:
            return False
        return any(fnmatch_any(f, self.paths) for f in files)


def discover(roots=SKILL_DIRS) -> list[Skill]:
    """按 roots 从高到低扫描;同名技能由高优先级版本覆盖。"""
    found: dict[str, Skill] = {}
    for root in roots:
        d = Path(root).expanduser()
        if not d.exists():
            continue
        for skill_md in sorted(d.rglob("SKILL.md")):
            meta = _frontmatter(skill_md)
            if not isinstance(meta, dict) or not meta.get("name") or not meta.get("description"):
                continue
            paths = meta.get("paths", [])
            if isinstance(paths, str):
                paths = [paths]
            elif not isinstance(paths, list):
                paths = []
            requires = meta.get("requires", {})
            if not isinstance(requires, dict):
                requires = {}
            s = Skill(name=str(meta["name"]), description=str(meta["description"]),
                      path=skill_md, paths=list(paths), requires=requires)
            found.setdefault(s.name, s)      # 先出现的高优先级胜出
    return [s for s in found.values() if s.satisfied()]


def listing(skills: list[Skill], context_window: int) -> str:
    """一级:只渲染 name + description,受预算约束(抄 Anthropic/Codex)"""
    # context_window 用 token 表示,字符串长度用字符表示,不能直接相乘。
    budget = max(1, int(context_window * LISTING_BUDGET_FRACTION * CHARS_PER_TOKEN))
    lines, used = [], 0
    for s in skills:
        prefix = f"- {s.name}: "
        line = prefix + s.description
        if used + len(line) > budget:
            remaining = budget - used
            if remaining <= len(prefix) + 1:
                continue                                          # name 太长也无法放入预算
            line = prefix + s.description[:remaining - len(prefix) - 1] + "…"
        lines.append(line)
        used += len(line) + 1
    return "\n".join(lines)


def activate(skill: Skill) -> str:
    """二级:模型/规则/用户触发后才读全文——就是一次普通的文件读取"""
    body = _strip_frontmatter(skill.path.read_text(encoding="utf-8"))
    skill.loaded = True
    return body


def fnmatch_any(file: str, patterns: list[str]) -> bool:
    return any(fnmatch.fnmatch(file, p) for p in patterns)


def _frontmatter(p: Path) -> dict:
    text = p.read_text(encoding="utf-8")
    if not text.startswith("---\n"):
        return {}
    end = text.find("\n---", 4)
    if end == -1:
        return {}
    return yaml.safe_load(text[4:end]) or {}

def _strip_frontmatter(text: str) -> str:
    return re.sub(r"\A---\n.*?\n---\n", "", text, count=1, flags=re.S)

接进你的 Agent Loop 只需三步:

  1. 启动时 discover() 扫一遍,listing() 的结果拼进 system prompt——这是唯一的常驻开销
  2. activate(skill) 注册成一个普通工具(比如 load_skill),模型要用时自己调——渐进披露到此完成
  3. 可选:每轮根据当前处理的文件 skill.matches(files) 自动注入(glob 模式),或把技能名注册成斜杠命令(显式模式)

踩坑提醒

  • description唯一影响模型是否调用的信号,写得含糊等于技能不存在。各家都强调"写清做什么 + 何时用"
  • 技能正文控制在 5k tokens / 500 行内,超了就拆 references 走三级加载
  • 会话内改动技能文件要做热更新或提示用户 /new,否则旧会话还用着旧索引(OpenClaw 的解法是 revision 锁定)

别把技能当成权限边界

“能发现并加载”不等于“有权执行”。技能正文、脚本和资源可能携带提示注入或恶意命令;真正的 Agent 还应把技能可见性、工具授权、沙箱和凭据隔离分别设计。尤其是第三方技能,先审阅再启用;脚本的执行权限不能因为它位于 SKILL.md 目录里就自动放开。

动手建议

  • 用上面的 skill_loader.py 起一个 demo:写 3 个技能(一个带 paths、一个带 requires.bins、一个普通),观察 listing 里谁出现、谁被过滤,再让模型通过 load_skill 工具触发二级加载
  • listing() 喂一个 128k 上下文的 Agent 跑 50 个技能,分别记录实际 token 占用和触发率;示例中的字符换算只是近似值
  • 对比实验:同一任务,全部技能常驻 vs 渐进披露,同时记录触发准确率、任务成功率、延迟和 token 成本——不要只看上下文省了多少

参考来源

分享
← 返回博客列表
🎁 有邀请福利哦,点击查看
🎁