如今的 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 Code | Codex CLI | OpenClaw | Cursor | Gemini CLI | Copilot / VS Code |
|---|---|---|---|---|---|---|
| 技能文件 | SKILL.md | SKILL.md | SKILL.md | SKILL.md + .mdc | SKILL.md | SKILL.md |
| 指令文件 | CLAUDE.md | AGENTS.md | AGENTS.md 等 | AGENTS.md / CLAUDE.md | GEMINI.md | copilot-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 | — | 环境条件过滤 | globs | JIT 目录扫描 | applyTo glob + 语义匹配 |
| 脚本执行 | shell 执行,默认不自动读入上下文 | 同规范 | {baseDir} | 同规范 | 同规范 | shell 执行,默认不自动读入上下文 |
| 热更新 | ✅ 文件监听,会话内生效 | 需重启 | ✅ watcher | 未确认 | ✅ /skills reload | 未确认 |
逐家拆解
Anthropic / Claude Code:规范的制定者
开放规范定义了 6 个 frontmatter 字段:name(≤64 字符、必须与目录名一致)、description(≤1024 字符,写"做什么 + 何时用")、license、compatibility(≤500 字符)、metadata、allowed-tools(实验性)。Claude Code 还扩展了 disable-model-invocation、user-invocable、context: fork、hooks、argument-hint 等字段。要跨客户端分发,应该把规范字段当作可移植基线,扩展字段则视为客户端能力,不能默认互通。
发现优先级:Enterprise > Personal(~/.claude/skills)> Project(.claude/skills);插件用 plugin:skill 命名空间隔离。一个值得抄的细节:monorepo 里子包的 .claude/skills/ 启动时不加载,等 Claude 第一次读写该目录内的文件才动态激活——避免大仓启动时扫描全部子包。
三级渐进披露(核心抄作业点):
| 级别 | 内容 | 时机 | 量级 |
|---|---|---|---|
| 1. Metadata | name + description | 启动时常驻 | ~100 tokens/个 |
| 2. Instructions | SKILL.md 全文 | 模型判定相关时读入 | 建议 <5k tokens |
| 3. Resources | scripts/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 KiB(project_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)、os、always:加载时就按环境过滤,环境不满足的技能根本不进列表——把兼容性判断从运行时前移到加载时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: true | Always Apply | 每次会话注入,globs/description 被忽略 |
alwaysApply:false + globs | Apply to Specific Files | 匹配文件进入上下文时自动附加 |
alwaysApply:false + description(无 globs) | Apply Intelligently | Agent 依据 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 root 的 GEMINI.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.md、AGENTS.md、CLAUDE.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 一样,主动兼容竞品目录
收敛出的四个共识
- 格式正在统一:目录 +
SKILL.md(YAML frontmatter + Markdown),name+description是开放规范的最小必需集,已经成为跨客户端分发的主流格式 - 渐进披露是主流答案:多个新一代实现选择"先驻元数据、再按需读正文";Anthropic 和 Codex 的预算参数是客户端策略,不应外推成行业标准
- 加载即工具调用:二级加载不需要特殊机制,模型用读文件工具即可,自建 Agent 里渐进披露几乎是零成本
- 脚本不进上下文:把确定性逻辑关进脚本,模型只看到输入输出——省 token 和提准确率的最大杠杆
自建 Agent 该抄哪一套
| 场景 | 推荐 | 理由 |
|---|---|---|
| 技能 < 10 个,都是通用指令 | Codex 式全量注入 | 实现最简单,一个文件拼接 + 32 KiB 上限兜底 |
| 技能 10–100 个,需要按场景区分 | 渐进披露 | 启动注入 name+description(约 100 token/个),用时读全文 |
| 有强文件类型相关性(如前端组件规范) | 叠加 Cursor 式 glob 匹配 | 命中文件才注入,比模型判断更可靠 |
| 高频确定性操作(如"部署到预发") | OpenClaw 式 command-dispatch: tool | 绕过模型,省一轮调用且零出错 |
| 技能依赖特定环境(需要 docker/psql 等) | OpenClaw 式加载期过滤 | 不满足的技能一开始就不出现在列表里 |
一个最小可用的技能加载器
把核心流程压到约 100 行,核心是三级加载 + 预算控制;模型自主、文件规则和用户显式触发,都可以复用 activate():
"""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 只需三步:
- 启动时
discover()扫一遍,listing()的结果拼进 system prompt——这是唯一的常驻开销 - 把
activate(skill)注册成一个普通工具(比如load_skill),模型要用时自己调——渐进披露到此完成 - 可选:每轮根据当前处理的文件
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 成本——不要只看上下文省了多少
参考来源
- Claude Code Skills:https://code.claude.com/docs/en/skills↗
- Agent Skills 开放规范:https://agentskills.io/specification↗|客户端列表:https://agentskills.io/clients↗
- Anthropic 工程博客:https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills↗
- Codex AGENTS.md:https://learn.chatgpt.com/docs/agent-configuration/agents-md.md↗|Skills:https://learn.chatgpt.com/docs/build-skills.md↗
- OpenClaw Skills:https://docs.openclaw.ai/tools/skills↗|https://docs.openclaw.ai/tools/creating-skills.md↗
- Cursor Rules:https://cursor.com/docs/rules↗|Skills:https://cursor.com/docs/skills↗
- Gemini CLI:GEMINI.md https://geminicli.com/docs/cli/gemini-md/↗|自定义命令 https://geminicli.com/docs/cli/custom-commands/↗|Agent Skills https://geminicli.com/docs/cli/using-agent-skills/↗
- VS Code / Copilot:Agent Skills https://code.visualstudio.com/docs/agent-customization/agent-skills↗|Instructions https://code.visualstudio.com/docs/agent-customization/custom-instructions↗|Prompt Files https://code.visualstudio.com/docs/agent-customization/prompt-files↗
