第 8 章 Skills:可复用能力与渐进式加载¶
上一章把"改代码"这只手讲透了。这一章往外走一圈,进入"能力"这一层。工具是"一个动作",而 skill(技能)是一套成型的专家流程——把"遇到这类活该怎么一步步做"打包成可复用、可组合的包。
但这一章真正的硬核,是你之前点名要讲透的那个问题:当你有几十上百个技能时,怎么让模型"知道有哪些技能",又不把上下文撑爆? 这就是渐进式加载(progressive loading),也就是 Anthropic 工程博客里反复强调的 progressive disclosure(渐进式披露)。
我们会沿用本书的老规矩——失败 → 机制 → 源码 → 抽象——从"如果天真地全塞会怎样"这个失败场景出发,一路拆到 Codex 源码里那套精确到"按字符分配描述预算"的降级算法,最后抽象成一个你能带走、复用到任何 agent 系统的模式。
引子:把"老师傅的套路"装进包里¶
工具能让 agent 做单个动作(跑命令、改文件)。但很多任务不是单个动作,而是一套有讲究的流程。
比如"做一次像样的 code review":要先看改动规模、再看有没有破坏性变更、再看测试覆盖、再看上下文是否齐全……这套流程,是有经验的人才知道的"套路"。如果每次都靠模型临场发挥,质量飘忽——这一轮它想起来要看测试,下一轮就忘了;这次它检查了破坏性变更,换个 PR 又漏了。套路之所以叫套路,正是因为它不能依赖临场记忆。
skill 就是把这种套路写下来、装进包里。 Codex 自己的仓库里就有一批真实的 skill(在仓库的 .codex/skills/ 下,属于后面要讲的 Repo 作用域),随手一看就有十来个:
.codex/skills/
├── code-review/ # 评审编排者:分派给各子技能
├── code-review-breaking-changes/ # 专查破坏性变更
├── code-review-change-size/ # 专查改动规模
├── code-review-context/ # 专查上下文是否齐全
├── code-review-testing/ # 专查测试覆盖
├── babysit-pr/ # 盯着 PR 一路跑到合并
├── codex-issue-digest/ # 汇总 issue
├── codex-pr-body/ ... # 还有一串
注意这里的设计巧思:code-review 本身不亲自评审,而是编排——它的 SKILL.md 第一句就是"用子代理调用本仓库里所有 code-review-* 技能,一个技能一个子代理"。这正是"技能可组合"的活样本:一个总技能把活拆给若干专项技能。
这思路并非 Codex 独有:上一章提过的 Hermes,技能放在 ~/.hermes/skills/,也是 markdown + YAML;pi 则给每个能力配"一句话描述"。Anthropic 在工程博客里也用同一个词来定义它——
"Skills are reusable, filesystem-based resources that provide Claude with domain-specific expertise: workflows, context, and best practices that transform general-purpose agents into specialists." (技能是可复用、基于文件系统的资源,为模型提供领域专长——工作流、上下文与最佳实践,把通用 agent 变成专家。) ——Anthropic, Agent Skills overview
官方文档还用了一个很贴切的比喻——技能目录"organized like an onboarding guide you'd create for a new team member"(像你给新同事写的入职手册那样组织)。这个比喻值得品:入职手册不是给老员工天天背的,而是新人遇到具体场景时去翻对应那一节。技能正是如此——它不该常驻在模型"脑子里",而该躺在文件系统上,等模型遇到对应场景时再翻。这一句就埋下了下文渐进式加载的全部动机。
把专家流程沉淀成可复用的包,是这一代 agent 的共识。 而它背后那个"模型是商品、harness 是杠杆"的主线在这里格外清楚:底层模型谁都能调用,真正让你的 agent 在某个领域比别人强的,是你沉淀进 skill 的那套流程。 技能就是 harness 这根杠杆上,最容易由用户和团队自己加长的那一段。
一、一个 skill 长什么样¶
一个 SKILL.md 大致是这样(取自 Codex 真实的 code-review/SKILL.md,裁剪):
---
name: code-review
description: Run a final code review on a pull request
---
Use subagents to review code using all code-review-* skills in this
repository other than this orchestrator. One subagent per skill...
(正文:一步步怎么做这次评审……可能还引用 references/、调用 scripts/)
关键就两段:
- YAML frontmatter:
name+description——一句话说清"这技能是干嘛的、什么时候该用它"。Codex 的协议里(codex-rs/protocol/src/protocol.rs的SkillMetadata),name和description是必填的核心字段。 - 正文:真正的流程说明(可能很长,还带脚本和参考资料)。
记住这个"一句话描述 + 长正文"的结构,它正是下一节那套省上下文机制的支点。Anthropic 的官方文档把"什么该写进 description"讲得很直白:
"The
descriptionshould include both what the Skill does and when Claude should use it." (description 要同时写清"这技能做什么"和"什么时候该用它"。) ——Anthropic, Agent Skills overview
为什么强调"什么时候该用"?因为后面你会看到,这句描述是常驻上下文里唯一代表该技能的东西——模型靠它(而不是靠正文)来判断这一轮要不要展开这个技能。描述写得含糊,技能就等于隐身。
skill 目录除了 SKILL.md,还可能挂两类东西:
scripts/:可执行脚本(.py/.sh/…)。这是个关键设计——脚本的代码本身永远不进上下文,模型只是bash运行它、拿回输出。确定性的操作交给脚本,省下大量 token,也比让模型每次现写代码更可靠。references/(或assets/、其他 markdown):参考资料、模板、schema。用到哪份才读哪份。
〔配图 1:一个 SKILL.md 的解剖〕——一张"技能卡":顶部 YAML frontmatter(高亮
name和description一句话)、下面是长长的正文(流程步骤),旁边挂两个小文件夹scripts/(齿轮)和references/(书本)。scripts/那个齿轮旁边特别标一行:"脚本代码不进上下文,只跑、只取输出。" 底部一行标注:"一句话描述(轻)+ 长正文(重)——这正是省上下文的关键。"
二、失败现场:技能越多,上下文越危险¶
先按本书的规矩,把"如果不做渐进式加载会怎样"演一遍。
假设你给 agent 装了 50 个 skill,每个正文 2000 字。如果一上来就把 50 份正文全塞进上下文——10 万字瞬间吃光预算(回想第 4 章:上下文是预算)。模型还没开始干活,窗口就满了;就算没满,50 份流程糊在一起,反而让它抓不住这一轮真正该用哪个。
把数字摆出来更直观。一个像样的 SKILL.md,正文动辄一两千字(几千 token);50 个就是十几万 token。而主流模型的上下文窗口虽然在变大,但那是要留给真正的任务的——用户的需求、代码文件、命令输出、多轮对话历史。如果开场白就把六位数 token 砸在"备用流程"上,等价于你还没开口,秘书先把一整面墙的操作手册全摊在你桌上,把你要看的合同盖得严严实实。更糟的是:这些手册里绝大多数这一轮根本用不上——你这次只是想做个 code review,却被另外 49 套无关流程包围。
这不是危言耸听,而是 Anthropic 在《Effective context engineering for AI agents》里反复点名的核心约束:
"Context is a critical but finite resource for AI agents." …… "Context, therefore, must be treated as a finite resource with diminishing marginal returns." …… "Every new token introduced depletes this budget by some amount." (上下文是 agent 关键却有限的资源……必须被当作一种边际收益递减的有限资源来对待……每多引入一个 token,都会消耗掉一部分预算。) ——Anthropic, Effective context engineering for AI agents
更要命的是,塞得多不只是"占地方",还会直接拉低模型表现:
"LLMs have an 'attention budget' that they draw on when parsing large volumes of context." …… "as the number of tokens in the context window increases, the model's ability to accurately recall information from that context decreases." (大模型有一份"注意力预算",解析大段上下文时要从里面支取……上下文里 token 越多,模型从中准确回忆信息的能力反而越下降。) ——Anthropic, Effective context engineering for AI agents
这就是第 4 章"上下文腐烂"在技能这一层的具体形态:50 份你以为"备着总没错"的流程,不仅没帮上忙,还稀释了模型对真正相关那一份的注意力。
但你又不能不让它知道这些技能存在——这是本书的另一条主线:"对 agent 可见的才存在"(第 4 章)。不告诉它有 code-review 这个技能,它就永远不会用,那这技能写了等于没写。
于是矛盾摆在这:
既要让模型"知道有哪些技能"(可见性),又不能把"每个技能怎么做"全塞进去(预算)。
这正是 Anthropic 给出的那条优化目标——
"good context engineering means finding the smallest possible set of high-signal tokens that maximize the likelihood of some desired outcome." (好的上下文工程,就是找到能最大化期望结果的、最小的那一组高信号 token。) ——Anthropic, Effective context engineering for AI agents
怎么破?答案就是下一节。
三、机制:渐进式加载——只放"名录",正文按需取¶
Codex 的解法,简单到优雅:上下文里只放技能的"名录"(名字 + 一句话描述 + 文件路径),不放正文;正文等到真要用某个技能时,才让模型自己用 bash 把那一份 SKILL.md 读进来。
这和 Anthropic 工程博客给出的"三级渐进式披露"是同一套机制,几乎一一对应:
| 层级 | Anthropic 的说法 | Codex 的实现 |
|---|---|---|
| Level 1 元数据 | 启动时把每个技能的 name + description 预载进系统提示 |
AvailableSkillsInstructions 把名录以 developer 角色注入 |
| Level 2 正文 | 判断相关后,读取完整 SKILL.md 进上下文 |
显式提及 / 命令隐式触发 → build_skill_injections 读正文 |
| Level 3+ 资源 | 按需导航 scripts/、references/ 等附加文件 |
SKILL.md 引用相对路径,模型再按需读 |
Anthropic 对 Level 1 的描述,几乎就是 Codex 名录机制的设计说明书:
"At startup, the agent pre-loads the
nameanddescriptionof every installed skill into its system prompt. … This lightweight approach means you can install many Skills without context penalty; Claude only knows each Skill exists and when to use it." (启动时,agent 把每个已安装技能的 name 和 description 预载进系统提示……这种轻量做法意味着你可以装很多技能而几乎不付上下文代价;模型只知道每个技能存在、以及何时该用它。) ——Anthropic
它还给了一个量级感:官方文档明确算过,Level 1 元数据约 100 token/技能,Level 2 正文一般 5k token 以下,Level 3+ 资源"实际上无上限"。50 个技能的名录也就 5000 token 量级,相比把 50 份正文全塞(10 万字)省了一个数量级以上。
而那句最漂亮的总结是:
"Agents with a filesystem and code execution tools don't need to read the entirety of a skill into their context window … the amount of context that can be bundled into a skill is effectively unbounded." (有文件系统和代码执行能力的 agent,不必把整个技能读进上下文……因此一个技能里能打包的上下文实际上是无上限的。) ——Anthropic, Equipping agents for the real world with Agent Skills
"无上限"的秘密,恰恰是"几乎不加载"。 这就是渐进式加载的全部魔法。
那个"像查手册"的比喻也来自这里,值得记住:
"Like a well-organized manual that starts with a table of contents, then specific chapters, and finally a detailed appendix, skills let Claude load information only as needed." (就像一本编排得当的手册,先是目录、再是具体章节、最后是详细附录——技能让模型只在需要时加载信息。) ——Anthropic, Equipping agents for the real world with Agent Skills
把它和第 4 章那个"AGENTS.md 是目录而非手册"的比喻摆在一起看:同一个比喻,在指令层和技能层各用了一次。 这不是巧合,是 harness 设计的统一手法。
值得把账算清:Level 1 名录是每一轮都在场的常驻成本,Level 2/3 是这一轮按需的可变成本。这正是"索引常驻、正文按需"那句话的精确含义——常驻的那部分被死死按在 2% 以内(下一节会看到它怎么被精打细算地守住),可变的那部分只为"这一轮真用到的技能"付费。换句话说:你装一百个技能,付的常驻代价仍然只是一百行名录;真正昂贵的正文,只在你这一轮碰到它时才计费。 这种"固定小开销 + 按使用量计费"的结构,正是它能横向扩展到几百个技能而不崩的根本原因,也是为什么官方敢说"装很多技能而几乎不付上下文代价"。
〔配图 2:全塞 vs 索引+按需(本章核心)〕——左右对照的"上下文预算条"。左(砖红)"全塞":50 份技能正文把预算条撑爆,真正的任务被挤没,标"10 万字,窗口爆了 + 注意力被稀释"。右(主色)"索引 + 按需":预算条里只占一小条"50 行名录(名字+一句话,约 100 token/个)",旁边一个"按需加载"的抽屉柜,模型用到
code-review时才把那一份正文(<5k)抽出来放进预算条;抽屉柜底层还有一格"scripts/ 资源(无上限)——只跑不读"。底部:"知道有哪些技能 ≠ 把每个技能怎么做都塞进去。"
四、源码:名录是怎么"省"出来的¶
到这里都还是"理念"。Codex 真正硬核的地方,是它没有止步于"只放名录",而是连名录本身都做了一套精打细算的预算管理。这部分代码集中在 codex-rs/core-skills/src/render.rs,是本章最值得逐层拆的标本。
4.1 名录预算:2% 双轨制¶
第一个问题:名录要给多大预算?写死一个数字太蠢——上下文窗口有大有小。Codex 的答案是 default_skill_metadata_budget(render.rs):
const DEFAULT_SKILL_METADATA_CHAR_BUDGET: usize = 8_000;
const SKILL_METADATA_CONTEXT_WINDOW_PERCENT: usize = 2;
pub fn default_skill_metadata_budget(context_window: Option<i64>) -> SkillMetadataBudget {
context_window
.and_then(|window| usize::try_from(window).ok())
.filter(|window| *window > 0)
.map(|window| SkillMetadataBudget::Tokens(
(window * SKILL_METADATA_CONTEXT_WINDOW_PERCENT / 100).max(1))) // 按 token 算:窗口的 2%,至少 1
.unwrap_or(SkillMetadataBudget::Characters(DEFAULT_SKILL_METADATA_CHAR_BUDGET)) // 回退:8000 字符
}
解读:名录预算 = 上下文窗口 × 2%(按 token 计),这是个相对量——窗口越大,留给技能名录的空间越大,但永远只占一个零头。只有在拿不到窗口大小时(None 或非正数),才回退到固定的 8000 字符预算(按字符计)。源码里的测试钉死了这两条轨:default_budget_uses_two_percent_of_full_context_window 验证 20 万窗口 → 4000 token;default_budget_falls_back_to_characters_without_context_window 验证 None → 8000 字符。
这个 SkillMetadataBudget 是个枚举,Tokens(n) 走近似 token 计数、Characters(n) 走字符计数——同一套降级逻辑能在两种"货币"下工作。2% 这个数字本身就是一句设计宣言:技能名录是配角,最多占舞台的 2%。
4.2 超预算了怎么办:先截描述,再丢技能¶
真正精彩的是超预算时的降级。假设你装了几百个技能,连名录都装不下 2% 了,怎么办?render.rs 里 render_skill_lines_from_lines 走的是一个三段式策略(从慷慨到吝啬):
第一段——全量。 如果所有技能的完整名录行(- 名字: 完整描述 (file: 路径))加起来 ≤ 预算,皆大欢喜,原样输出。
第二段——等比截描述。 放不下全量、但"最小行"(只留名字和路径、描述清空)能放下时,就把省下的预算一个字符一个字符地、公平地分给各技能的描述。这段算法(render_lines_with_description_budget)有个巧思,注释写得很清楚:
// Distribute description space one character at a time across skills.
// Short descriptions naturally drop out, so their unused share can go to
// longer descriptions instead of being stranded in a fixed per-skill quota.
解读:不是给每个技能"固定配额"(那样短描述用不完的额度会被浪费),而是逐字符轮流分配——短描述自然先"满"退出,把没用完的空间让给长描述。测试 budgeted_rendering_redistributes_unused_description_budget 钉死了这个行为:短技能描述 "x" 原样保留,省下的额度全给了长技能。这是一种"按需公平"的预算再分配,连一个字符都不浪费。
第三段——丢技能。 连"最小行"都放不下时,才开始真正丢弃技能(render_minimum_skill_lines_until_budget):按优先级顺序逐个塞最小行,塞不下的累加进 omitted_count。注意它丢弃也讲优先级——靠 prompt_scope_rank 排序,System(0) → Admin(1) → Repo(2) → User(3),越靠前越优先保留。
4.3 降级要"看得见":警告与遥测¶
降级不能悄悄发生——否则用户会困惑"我装的技能怎么不见了"。Codex 在这里很克制地做了两件事:
一是给模型/用户一句警告。截描述截多了,会附一句 SKILL_DESCRIPTION_TRUNCATED_WARNING:"技能描述被缩短以适配预算,Codex 仍能看到每个技能,但部分描述变短了,建议禁用不用的技能腾出空间。" 若是 2% token 预算触发,文案还会专门点出"the 2% skills context budget"。真丢了技能,则是 SKILL_DESCRIPTIONS_REMOVED_WARNING_PREFIX:"超出技能上下文预算,所有描述已移除,另有 N 个技能未纳入模型可见列表。"
二是打遥测。record_available_skills_side_effects 在降级时 tracing::info! 一条结构化日志(预算上限、技能总数、保留数、丢弃数、平均截断字符数),并在线程启动时把这些数字喂给 THREAD_SKILLS_* 系列指标(启用总数 / 保留总数 / 是否截断 / 截断字符数)。降级是可观测的——这正是第 14 章可观测性的伏笔:你能从指标上看出"哪些用户被技能挤爆了预算"。
〔配图 3:名录预算的三段式降级〕——一张漏斗/瀑布图。顶部一个标"名录预算 = 窗口 × 2%(无窗口则 8000 字符)"的水箱。水流向下经过三道闸:① 全量(绿,"放得下就原样")→ ② 等比截描述(黄,画几条长短不一的描述被"逐字符公平分配",短的先满、富余让给长的)→ ③ 丢技能(红,按 System→Admin→Repo→User 优先级,溢出的技能掉出漏斗,旁边亮起 ⚠️ 警告 + 📊 遥测)。底部一行:"连'目录'都精打细算——省预算这件事,一个字符都不放过。"
4.4 还能再榨一点:把长路径折叠成别名¶
名录里每一行除了"名字 + 描述",还带一个文件路径——好让模型知道去哪儿读正文。可这路径有时长得离谱:插件装下来的技能往往住在
几十个这样的技能,光是重复的长前缀就能把名录预算吃掉一大块。Codex 在 render.rs 里又补了一层优化(build_alias_plan):当绝对路径方案出现截断或丢技能时,改用"别名表 + 短路径"再渲染一遍,谁更省(能装下更多技能、截断更少)就用谁。 渲染结果会先列一张别名表,再让每行只写短路径:
### Skill roots
- `r0` = `/Users/you/.codex/plugins/cache/openai-curated`
...
- github:gh-fix-ci: ... (file: r0/github/hash123/skills/gh-fix-ci/SKILL.md)
解读:把共享的长前缀抽成 r0 这样的一个字母别名,几十行就共用一份前缀。它甚至会判断"这个插件版本下只有一个技能"时进一步上提到 marketplace 根目录(plugin_marketplace_base),让别名覆盖更多路径。名录里配套的 SKILLS_HOW_TO_USE_WITH_ALIASES 指令会教模型"用 ### Skill roots 表把短路径展开成绝对路径再打开"。这层优化平时你完全感觉不到——但它体现了 Codex 对"名录这 2%"的极致抠门:连路径里的重复字符都不肯白占。
五、源码:正文是怎么被"触发"的¶
名录解决了"知道有什么"。那"知道怎么做"——完整正文——是怎么、何时被拉进来的?这是 core-skills/src/injection.rs 与 invocation_utils.rs 的活儿。正文只在两种触发下才加载。
5.1 显式提及¶
模型(或你)在消息里点名某个技能。collect_explicit_skill_mentions(injection.rs)扫描两类输入:
- 结构化选择(
UserInput::Skill):UI 里直接选中的技能,按路径精确匹配,最可靠。 - 文本提及(
$skill-name形式):扫文本里的$技能名token(extract_tool_mentions)。
文本匹配有个谨慎的细节:纯名字匹配只在"无歧义"时才生效。select_skills_from_mentions 里有这么一道关卡——
let skill_count = ...skill_name_counts.get(skill.name)...; // 同名技能有几个
let connector_count = ...connector_slug_counts...; // 是否撞上连接器名
if skill_count != 1 || connector_count != 0 {
continue; // 有歧义就不靠名字匹配
}
解读:如果有两个技能同名、或这名字撞上了某个连接器(connector)的 slug,光凭 $名字 就不自动触发——避免"猜错技能"。这是 harness 在"自动便利"和"不出错"之间的一个典型权衡。
这里还有两个值得留意的工程取舍。其一,结构化选择先于文本提及:collect_explicit_skill_mentions 先处理 UserInput::Skill(UI 选中、按路径精确匹配),再扫文本里的 $名字,并用 seen_paths/seen_names 去重——同一个技能不会因为既被选中又被提到而重复注入。其二,保序:选出的技能严格按 skills 列表既有顺序排列(函数注释专门点了这条复杂度与"preserve prior ordering semantics"),一次提到多个技能时,注入顺序是确定、可预期的,而不是看哈希表心情。名录渲染的 SKILLS_HOW_TO_USE_* 指令也呼应了这点:"若多个技能都适用,挑覆盖请求的最小集合,并说明你打算用它们的顺序。"确定性,是 harness 必须替模型守住的底线。
5.2 命令隐式触发¶
这是更隐蔽、也更巧妙的入口。注意:它不是简单地"按命令首 token 匹配",而是 detect_implicit_skill_invocation_for_command(invocation_utils.rs)盯着模型真实跑的 shell 命令,看它是否"无意中"在用某个技能。两种情形:
① 跑了技能里的脚本(detect_skill_script_run)。命令首 token 是已知的解释器(python/bash/node/deno/ruby/… 共 10 种),且参数里有个脚本文件(.py/.sh/.js/…)。Codex 把脚本路径逐级往上找父目录,若命中某技能的 scripts/ 目录,就认定"这是在用那个技能"。
② 读了技能的 SKILL.md(detect_skill_doc_read)。命令是已知的读文件命令(cat/sed/head/tail/less/more/bat/awk 共 8 种),且参数里的某个路径正好是某技能的 SKILL.md。
pub fn detect_implicit_skill_invocation_for_command(...) -> Option<SkillMetadata> {
if let Some(c) = detect_skill_script_run(...) { return Some(c); } // 先看是否跑了脚本
detect_skill_doc_read(...) // 再看是否读了文档
}
解读:这个机制的意义在于——就算模型没有"正式声明"在用某技能,只要它的实际动作(跑脚本、读文档)落在某技能的范围里,Codex 也把这次算作一次技能调用并打点。 这让遥测口径更真实(第 14 章),也让"按需展开"不依赖模型乖乖喊口令。
5.3 注入正文 + 打点¶
一旦确定要展开(无论哪种触发),build_skill_injections(injection.rs)就真正去读 SKILL.md 的内容塞进上下文,每读一份打一条遥测:
otel.counter("codex.skill.injected", 1,
&[("status", status), ("skill", skill.name.as_str())]); // status = ok / error
同时记一条 SkillInvocation(带 InvocationType::Explicit/隐式、scope、路径、plugin_id)交给 analytics。读失败(文件没了、权限不够)不会让整轮崩,而是记一条 warning 继续——这是 harness 的"优雅降级"本能。
这里还藏着一个角色选择的讲究:名录不是以 system 角色、也不是以 user 角色注入,而是以 developer 角色(AvailableSkillsInstructions::role() 返回 "developer"),并用一对 <skills_instructions>...</skills_instructions> 标签(SKILLS_INSTRUCTIONS_OPEN_TAG/CLOSE_TAG)框起来。为什么是 developer 而不是 system?因为这层信息是"集成方(harness)给模型的操作说明",介于"不可违抗的系统底线"和"用户当下的具体请求"之间——它该被模型当作权威指引,但不该凌驾于真正的系统级安全约束之上。用明确的 XML 标签框起来,则让模型一眼就能把"技能名录"和上下文里其他内容区分开,便于聚焦。角色和标签都是在替模型标注"这段话是谁说的、有多重"——这正是上下文工程里最不该含糊的一笔。
注意名录里给出的路径不是摆设:渲染名录的 SKILLS_HOW_TO_USE_* 指令明确教模型"决定用某技能后,打开它的 SKILL.md,只读够用的部分;引用到 references/ 时只加载需要的那几份,别全量加载;有 scripts/ 就优先跑或改脚本,而不是重打一遍代码"。渐进式披露不只是机制,也被写成了给模型的明文规矩。
〔配图 4:两个触发入口〕——中间一个"完整正文"的抽屉,两条箭头指向它。上箭头(显式提及):一条用户/模型消息里高亮
$code-review,旁注"无歧义才靠名字"。下箭头(命令隐式触发):一个终端窗口里跑着python .codex/skills/foo/scripts/run.py和cat .../SKILL.md,旁注"看实际动作落在哪个技能里"。抽屉被拉开后,旁边弹出一个遥测点codex.skill.injected(标"第 14 章")。底部:"喊口令会展开,'手滑'用到也算——按需加载不靠模型自觉。"
六、四级作用域:技能也分"层"¶
技能从哪来?Codex 把技能来源分成四级作用域——SkillScope 是个有 4 个独立变体的枚举(codex-rs/protocol/src/protocol.rs,对外 API 在 app-server-protocol/src/protocol/v2/plugin.rs 也有对应的 4 变体):
对照 loader.rs 里的实际目录映射,这四级各有出身:
| 作用域 | 来自哪里(目录) | 谁的技能 |
|---|---|---|
| User | $HOME/.agents/skills(以及兼容旧版的 $CODEX_HOME/skills) |
你个人的技能 |
| Repo | 仓库的 .codex/skills/、以及项目根到 cwd 之间各级的 .agents/skills |
这个仓库/团队的技能,随仓库走 |
| System | 内置技能缓存到 $CODEX_HOME/skills/.system |
Codex 预置(自带)的技能 |
| Admin | 系统配置目录下的 skills(Unix 上如 /etc/codex/skills) |
管理员/企业统一下发的技能 |
这里要分清一个常见误读:System 和 Admin 不是同一回事。System 是"软件自带"的内置技能(缓存在用户目录下的
.system里),Admin 是"组织通过系统级配置强制下发"的技能(住在/etc/codex这类管理域)。两者来源、信任级别、谁能改都不同,所以是两个独立变体。
这四级有个明确的优先级顺序,而且名录渲染和真正加载用的还是两套(一致但分开定义的)排序:
- 名录里的展示与"丢技能"优先级(
render.rs的prompt_scope_rank):System(0) → Admin(1) → Repo(2) → User(3),越靠前越优先保留、越早展示。 - 加载去重时的优先级(
loader.rs的scope_rank):Repo(0) → User(1) → System(2) → Admin(3),同名时离你越近的(Repo/User)越优先生效。
两套顺序不矛盾:一个管"展示和保命",一个管"撞名时谁说了算"。举个具体场景体会差别——假设 System 里有个内置的 code-review,你的仓库 .codex/skills/ 里又写了一个同名的 code-review:
- 撞名时谁生效(
scope_rank,Repo 优先):用你仓库那份。这符合直觉——团队为这个项目特调的流程,应当压过软件自带的通用版本,正如"专用在下层、压过通用"。 - 预算紧张要丢谁(
prompt_scope_rank,System 优先保留):System 那份更晚被丢。这也合理——内置技能是软件能正常工作的基线保障,不能因为你装了一堆技能就把基线挤没。
同一个 scope 值,在"谁覆盖谁"和"谁先被牺牲"两个问题上扮演不同角色,所以需要两套独立的 rank。这种"看似重复、实则各司其职"的小设计,正是成熟 harness 里随处可见的精细。
这套分层和第 4 章 AGENTS.md 的"从根到叶分层拼接"是同一个思路:通用的放上层、专用的放下层,各级叠加。而"这个环境实际允许用哪些技能",则由 config_rules(core-skills/src/config_rules.rs)做最后一道过滤——它从用户配置层里读出一串 SkillConfigRule(按名字或路径选中、enabled: true/false),允许你显式启用或禁用某些技能。分层 + 渐进披露,是 Codex 组织一切"给模型的知识"的统一手法——指令(AGENTS.md)如此,技能(skills)也如此。
〔配图 5:四级作用域层叠〕——四层叠放的卡片,从下到上 Admin(标"/etc/codex,组织下发")→ System(标"内置 .system 缓存")→ Repo(标".codex/skills,随仓库走、团队共享")→ User(标"~/.agents/skills,个人"),每层贴几个技能名便签。右侧一个漏斗
config_rules(按名字/路径 enable/disable)过滤出"这个环境实际可用"的技能。卡片侧面标两个小箭头:一个"展示/保命优先级 System→Admin→Repo→User",一个"撞名优先级 Repo→User→System→Admin"。一行标注:"和 AGENTS.md 一样:通用在上层、专用在下层,叠加生效;四级各有出身与信任级别。"
七、加载流水线与工程细节¶
把上面串成一条流水线(core-skills crate 的 loader.rs/render.rs/injection.rs、config/skills_config.rs、app-server/src/skills_watcher.rs):
发现技能(四级作用域目录 + 插件技能根)
→ 渲染名录(名字+描述+路径,受 2% 预算约束、必要时降级)以 developer 角色注入上下文
→ 模型显式提及 / 命令隐式触发
→ 读取该技能完整 SKILL.md 正文 + 打 codex.skill.injected
→ 模型按 SKILL.md 指引,按需再读 references/、跑 scripts/,完成流程
几个值得一提的工程细节:
-
热更新(
skills_watcher.rs):基于FileWatcher盯着所有技能根目录(递归),一有变动就skills_manager.clear_cache()并向客户端发SkillsChanged通知。你加一个新技能、改一句描述,不必重启就能被发现。为了不被频繁文件事件刷爆,它用ThrottledWatchReceiver做了 10 秒节流(测试里压到 50ms)。远程环境(is_remote)则不注册本地文件监听。 -
首次使用要审批:带可执行
scripts/的技能并不"免检"——脚本是通过 Codex 正常的命令执行通道跑的,该走审批就走审批(core/tests/suite/skill_approval.rs专门覆盖了"技能里的 shell 脚本触发ExecApprovalRequestEvent"这条路径)。这呼应了官方文档那句很重的话:
"Treat like installing software. Only use Skills from trusted sources." …… "a malicious Skill can direct Claude to invoke tools or execute code in ways that don't match the Skill's stated purpose." (把它当成安装软件来对待,只用可信来源的技能……恶意技能可以诱导模型以偏离其声称用途的方式调用工具或执行代码。) ——Anthropic, Agent Skills overview
"装新软件先问一声"——技能越像可执行能力,这道审批就越不该省。
-
可遥测:除了
codex.skill.injected,还有降级时的THREAD_SKILLS_*指标和SkillInvocation事件(第 14 章可观测性会用到),让你知道哪些技能真在被用、谁被预算挤掉、用得对不对。 -
技能会声明依赖:
SkillMetadata里有个可选的dependencies(SkillDependencies,protocol.rs),技能可以声明"我需要哪些工具/MCP 才能跑"。Codex 在一轮开始时会据此maybe_prompt_and_install_mcp_dependencies(core/src/mcp_skill_dependencies.rs)——必要时提示并帮你把缺的 MCP 装上。这把"技能"和下一章的"MCP 外部工具"接到了一起:技能不只是文档,它能声明并拉起自己跑起来所需的外部能力。 第 9 章会专门讲 MCP 这一侧。 -
技能也能来自插件:除了四级作用域的本地目录,技能还能由插件(plugin)带进来——
effective_plugin_skill_roots()(core-plugins)把插件携带的技能根并进skill_roots,watcher 也会一并监听。前面 4.4 那套"把超长插件路径折叠成r0/别名"的优化,正是为这类插件技能准备的。 -
能自举:Codex 内置了一组样例技能,注意它们不住在
.codex/skills/,而是打包在skillscrate 里(codex-rs/skills/src/assets/samples/),随软件分发、属于 System 作用域。其中就有skill-creator(造技能)和skill-installer(装技能),还有plugin-creator、openai-docs、imagegen等。用技能来造和装技能,颇有"自己生长"的意思——这正是第 1 章 Hermes"会成长的 agent"同一股潮。
别把样例技能和仓库里真实的
.codex/skills/搞混:前者是软件自带的"种子"(System),后者是这个仓库自己写的活技能(Repo),如code-review、babysit-pr、codex-issue-digest。〔配图 6:技能加载流水线〕——一条从左到右的流水线:① 发现(四级作用域目录 + 插件根,一只 watcher 眼睛盯着,标"热更新·10s 节流")→ ② 渲染名录注入(一小条进预算条,标"受 2% 预算约束,超了就降级",developer 角色)→ ③ 触发(两个入口:显式提及 / 命令隐式触发)→ ④ 读正文 + 打遥测点(标"codex.skill.injected,第 14 章")→ ⑤ 按需读 references/、跑 scripts/(标"scripts 走审批,如装软件")。中文清晰。
八、同一个模式,你已经见过四次¶
退一步看,你会发现"渐进式加载"这个模式,在本书里已经反复出现:
| 在哪一层 | "索引"(常驻) | "正文/细节"(按需加载) |
|---|---|---|
| 指令(第 4 章 AGENTS.md) | 100 行的"目录" | 需要时去 docs/ 翻 |
| 工具(第 6 章 tool_search) | 工具名 | 用到时取它的 schema |
| 技能(本章 skills) | 名字 + 一句话 + 路径(约 100 token/个) | 触发时读正文,再按需读 references/、跑 scripts/ |
| 外部工具(第 9 章 MCP) | 工具名录 | deferred 按需拉取 |
同一个省预算思想,在四个层面各来一次。 这不是巧合——它们都在回答第 4 章那个母题问题:上下文是稀缺预算,怎么让模型"知道得多"却"占得少"。 Anthropic 把这个通用做法直接命名为"just in time":
"agents built with the 'just in time' approach maintain lightweight identifiers and use these references to dynamically load data into context at runtime." …… "progressive disclosure … allows agents to incrementally discover relevant context through exploration." (采用"即时"策略的 agent 只保留轻量标识符,运行时再用这些引用把数据动态加载进上下文……渐进式披露让 agent 通过探索逐步发现相关上下文。) ——Anthropic, Effective context engineering for AI agents
答案永远是:把"知道有什么"和"知道怎么做"分开,前者常驻、后者按需。记住这个模式,你设计任何 agent 系统都用得上。
还有一条更深的主线值得点破:harness 随模型演化。当模型获得了稳定的文件系统和代码执行能力,"把整套流程读进上下文"就从必需变成了浪费——正因为模型能自己 bash 去读、去跑,skill 才敢把正文留在磁盘上、把脚本代码挡在上下文之外。skill 这套机制,本身就是"模型能力变强后,harness 顺势把活儿外包给文件系统"的产物。模型进一寸,harness 就能省一分。
不妨设想倒推:如果模型没有文件系统和 bash,渐进式加载就无从谈起——你只能把所有可能用到的内容预先塞进提示,回到"全塞"的老路。是模型这一侧的能力升级(会用工具、会读文件、会跑脚本),才让 harness 这一侧得以"把知识留在外面、按需取用"。反过来,harness 也在替模型省下它本来要为'什么都得记住'付出的注意力税——两边互相成全。这正是本书反复强调的那句:模型与 harness 不是一静一动,而是协同演化。你今天为某个模型设计的渐进式加载策略,下一代模型可能让你能更激进地外包、也可能要求你换一套触发启发式。理解机制背后的"为什么",比记住某个常量是 2% 还是 8000,重要得多。
本章小结¶
- skill = 打包好的专家流程:一个目录 + 一份
SKILL.md(YAML 一句话描述 + 长正文 + 可选scripts//references/)。Codex 自己就用code-review(编排者,分派给code-review-*子技能)、babysit-pr、codex-issue-digest等真实技能。 - 失败现场:技能越多,正文全塞就越会撑爆上下文、稀释注意力(第 4 章 + Anthropic 上下文工程);但不让模型知道,技能就等于不存在。
- 渐进式加载(本章核心):索引常驻、正文按需——只注入"名字 + 一句话 + 路径"的名录(Level 1,约 100 token/个),完整正文(Level 2)仅在显式提及或命令隐式触发时由模型读入,附加资源(Level 3+)再按需展开。与 Anthropic 的三级 progressive disclosure 一一对应。
- 名录预算双轨 + 三段式降级:预算 = 窗口 × 2%(无窗口回退 8000 字符);超了先等比截描述(逐字符公平再分配,不浪费),再按 scope 优先级丢技能,并附警告 + 打遥测。
- 四级作用域(User / Repo / System / Admin,四个独立变体)+ 热更新(FileWatcher,10s 节流)+ 首用审批(脚本走正常 exec 审批,如装软件)+ 遥测 + 自举(
skill-creator/skill-installer是skillscrate 内的样例技能,不在.codex/skills/),和AGENTS.md同属"分层 + 渐进披露"的统一手法。 - 同一个模式四次:AGENTS.md 目录、tool_search、skills 名录、MCP 延迟加载——都在回答"知道得多、占得少",也都是"harness 随模型演化"的注脚。
下一章,我们看这套"渐进式加载"的第四个现场,也是 agent 接入外部世界的关口——MCP:Codex 怎么既当客户端接入别人的工具,又当服务端把自己变成别人的工具。
参考来源¶
解剖标本(codex-rs 源码,HEAD = ad2012d6)
core-skills/src/render.rs— 名录渲染、2% 预算与三段式降级core-skills/src/injection.rs— 显式提及、歧义保护、注入与遥测core-skills/src/invocation_utils.rs— 命令隐式触发检测core-skills/src/loader.rs— 四级作用域目录映射与scope_rankcore-skills/src/config_rules.rs—SkillConfigRuleenable/disablecore/src/context/available_skills_instructions.rs— developer 角色注入名录protocol/src/protocol.rs—SkillScope/SkillMetadata/SkillDependenciesapp-server/src/skills_watcher.rs—FileWatcher热更新与 10s 节流skills/src/assets/samples/— 内置样例技能(System,非.codex/skills/).codex/skills/*/SKILL.md— 仓库真实技能(Repo)core/tests/suite/skill_approval.rs— 脚本走 exec 审批的测试
极简 agent 参照
- pi(earendil-works) — 每个能力配一句话描述
- Hermes Agent(Nous Research) — 技能存为 markdown,自创自改
方法论
- Anthropic, Agent Skills overview
- Anthropic, Effective context engineering for AI agents
- Anthropic, Equipping agents for the real world with Agent Skills
注:技能子系统还涉及
SkillDependencies(技能声明依赖哪些工具/MCP,第 9 章)、插件技能根(plugin skill roots)、别名路径压缩(render.rs里build_alias_plan把超长插件路径折叠成r0/...以省名录预算)等;本章聚焦"渐进式加载"主干,类型/常量名以 HEAD=ad2012d6 为准。