# Skills · 发现、选择与注入

> 技能从哪些根目录被扫描出来、清单怎样在预算内挤进上下文、显式提及如何解析成要注入的 SKILL.md，以及“自动选用”在 v0.158.0 其实仍由模型自己决定——代码里的检索式选择器只在影子模式下跑实验指标。

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/skills
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

# Skills · 发现、选择与注入

技能（skill）是一个带 `SKILL.md` 的目录。Codex 用渐进式加载：上下文里平时只放每个技能的名字、描述和位置，真要用某个技能时才读它的全文。这一篇沿着“发现 → 清单 → 选择 → 注入”走一遍。

## 怎么用

`SKILL.md` 以 YAML frontmatter 开头，写 `name`、`description`，可选 `metadata.short-description`；同目录可放 `agents/openai.yaml`，声明界面信息、`policy.allow_implicit_invocation` 与依赖的 MCP 服务器。技能放在仓库的 `.agents/skills`、`~/.agents/skills`、`/etc/codex/skills` 等处；输入 `$技能名` 显式调用，任务与描述吻合时也可能被自动选用；`[[skills.config]]` 按路径或名字停用。详见手册 [Skills 技能](https://daiw.net/manual/codex/skills)。

## 三方分工

| 位置 | 负责 |
| --- | --- |
| `codex-skills`（`codex-rs/skills/`） | 纯逻辑：解析 frontmatter、提取 `$` 提及、显式选择、识别隐式调用，内置技能的打包与安装 |
| `codex-skills-extension`（`codex-rs/ext/skills/`） | 通过扩展 API 挂进内核：扫描根目录、维护技能目录（catalog）、按预算渲染清单、读取技能正文、提供 `skills.list` / `skills.read` 工具 |
| `codex-core` | 每轮收集提及并注入正文（`session/turn.rs` 的 `build_skills_and_plugins`）、安装技能依赖的 MCP 服务器、在执行命令时记录隐式调用 |

扩展在 `install_with_providers_and_metrics` 里一次注册了线程与回合生命周期、配置变更、上下文、本轮输入、技能调用和工具七类贡献者（`codex-rs/ext/skills/src/extension.rs:658`），接口本身见[扩展 API](https://daiw.net/manual/codex-source/extension-api)。整体数据流：

```mermaid
flowchart TB
  R[技能根目录<br/>repo · user · system · admin · 插件] --> L[扫描 SKILL.md<br/>解析 frontmatter 与 openai.yaml]
  L --> C[技能目录 SkillCatalog]
  C --> W[world state 的技能分区<br/>按预算渲染的清单]
  W --> M[模型]
  U[用户输入里的技能提及] --> S[collect_explicit_skill_mentions]
  C --> S
  S --> I[读取 SKILL.md<br/>以 skill 片段注入本轮]
  I --> M
  M -->|用 shell 读 SKILL.md 或跑 scripts| D[隐式调用识别<br/>只用于遥测]
```

## 发现：从哪些根目录找 SKILL.md

`resolve_skill_roots`（`codex-rs/ext/skills/src/host_roots.rs:28`）顺着[配置层](https://daiw.net/manual/codex-source/config-system)找根目录：每个项目层的 `.codex/skills`（作用域 Repo），用户层的 `$CODEX_HOME/skills`（已弃用但仍读）与 `~/.agents/skills`（User），`$CODEX_HOME/skills/.system`（System），system 配置层所在目录下的 `skills`（Admin，即 `/etc/codex/skills`）；再加上插件提供的根、从项目根到 cwd 每一级的 `.agents/skills`，最后按路径去重。

值得注意的是信任：`roots_from_layer_stack` 遍历的是包含 disabled 层在内的全部配置层，测试 `layer_roots_preserve_scope_precedence_and_disabled_projects` 明确断言未受信任项目的 `.codex/skills` 也会被扫描，`.agents/skills` 更是不看信任。项目配置、hooks、rules 要等项目受信任才加载，技能不在此列；不过技能本身只是说明文字，照着它跑命令仍要经过沙箱与审批。

每个根目录用一次受限的遍历：深度 6 层、最多 2000 个目录与 20000 个条目，跳过隐藏目录（`codex-rs/ext/skills/src/loader/mod.rs:31`）；User、Repo、Admin 作用域跟随目录符号链接，System 不跟随（`codex-rs/ext/skills/src/loader/host.rs:164`）。找到的 `SKILL.md` 交给 `parse_skill_frontmatter_metadata`：`name` 缺省取目录名、最长 64 字符，`description` 必填，多行值压成一行。第三方技能常写出不合法的 YAML，解析器会先试着修一遍：

```rust
    let parsed: SkillFrontmatter = match serde_yaml::from_str(&frontmatter) {
        Ok(parsed) => Ok(parsed),
        Err(original_error) => match repair_frontmatter_scalar_fields(&frontmatter) {
            // Some third-party skills use prose like `description: Build for AWS: ECS`
            // or `argument-hint: <duration: e.g. 7d>`. Keep the repair line-oriented
            // so unrelated invalid YAML still surfaces.
            Some(repaired_frontmatter) => {
                serde_yaml::from_str(&repaired_frontmatter).map_err(|_| original_error)
            }
            None => Err(original_error),
        },
    }
```

（`codex-rs/skills/src/parser.rs:50`）

修补是逐行的：给值里带 `: ` 的标量补上单引号再解析一次，还失败就报原来的错。`agents/openai.yaml` 则一律“失败即忽略”，可选元数据不能挡住技能本身。插件里的技能名加命名空间成为 `插件名:技能名`。各根的结果合并时只按 `SKILL.md` 的规范路径去重，同名技能并存，排序为 Repo、User、System、Admin（`codex-rs/ext/skills/src/loader/host_merge.rs:201`）。

内置的六个系统技能（`skill-creator`、`skill-installer`、`plugin-creator`、`openai-docs`、`imagegen`、`review-agent`）用 `include_dir!` 编进二进制，首次需要时写到 `$CODEX_HOME/skills/.system`，目录里留一个记录内容指纹的标记文件，指纹不变就不重写（`codex-rs/skills/src/lib.rs:77`）。

## 清单：在预算内挤进上下文

清单占多少上下文由 `skill_metadata_budget` 决定（`codex-rs/ext/skills/src/render.rs:123`）：显式配置的 `skills.max_context_tokens` 封顶 10000 token；否则取上下文窗口的 2%；窗口未知时按 8000 字符。每行形如 `- 名字: 描述 (file: 路径)`，描述先截到 1024 字符。放不下时分三级降级：全部完整；去掉描述后放得下，就把剩余预算一个字符一个字符地轮流分给各技能的描述，“免得一个技能独占”；连光秃秃的名字都放不下，就按顺序只留名字、放到预算用完为止，其余整行省略并发出警告（`codex-rs/ext/skills/src/render.rs:321`）。路径很长时，渲染器会试一种别名写法：在 `### Skill roots` 里定义 `r0`、`r1` 这样的根目录别名，条目只写短路径，哪种写法放进的技能更多就用哪种。

`policy.allow_implicit_invocation: false` 的技能在目录里标为 `hidden_from_prompt`：不进清单，模型看不见，但 `$名字` 仍能选中它。

清单本身作为 world state 的一个分区进入上下文，外面包 `<skills_instructions>` 标签、以 developer 消息出现。每一步都会重算，但只有正文与上一次快照不同时才追加新消息（`codex-rs/ext/skills/src/world_state.rs:93`），技能没变就不打扰前缀缓存。模型目录为该模型打开 `include_skills_usage_instructions` 时，清单后面再附一段“How to use skills”，规定触发条件：用户点名，或者任务明显符合某个技能的描述，就必须在本轮使用；决定使用后先完整读 `SKILL.md` 再动手，读说明这件事不许委派给子代理。

## 选择：显式提及与“自动选用”

显式选择由 `collect_explicit_skill_mentions` 完成（`codex-rs/skills/src/selection.rs:42`）。先处理结构化输入：TUI 里按 `$` 从列表选中的技能以 `UserInput::Skill { name, path }` 提交，按路径精确匹配。再扫文本：`$` 后面跟字母、数字、`_`、`-`、`:` 组成的名字，`$PATH`、`$HOME` 这类常见环境变量不算；`[$名字](路径)` 形式的链接按路径匹配。只写名字时有一道防歧义检查：

```rust
        let skill_count = selection_context
            .skill_name_counts
            .get(skill.name.as_str())
            .copied()
            .unwrap_or(0);
        let connector_count = selection_context
            .connector_slug_counts
            .get(&skill.name.to_ascii_lowercase())
            .copied()
            .unwrap_or(0);
        if skill_count != 1 || connector_count != 0 {
            continue;
        }
```

（`codex-rs/skills/src/selection.rs:178`）

同名的已启用技能不止一个，或者和某个应用连接器重名，纯名字就不选，要靠路径消歧。

“自动选用”在代码里没有对应的选择逻辑：清单摆在上下文里，是模型按上面那段规则自己决定去读哪个 `SKILL.md`。`codex-rs/ext/skills/src/dynamic_skill_selector/` 里那一组检索算法——加权词法、分字段 BM25、字符 n-gram、多查询、RRF 融合、routing card、LRU 及其组合——只在影子模式下运行：

```rust
/// Selects likely-relevant skills without changing the model-visible catalog.
///
/// Implementations must be deterministic, side-effect free, and cheap enough to run in shadow
/// mode on every turn. Callers must validate returned IDs against the supplied documents.
pub(crate) trait CheapSkillSelector: Send + Sync {
```

（`codex-rs/ext/skills/src/dynamic_skill_selector.rs:44`）

功能开关 `skill_search`（稳定、默认开）让 `ShadowSelectionExperiment` 每轮用这些选择器各选一遍，记录候选数量、能把清单缩小多少，以及模型真正读取的技能排在各方法候选里的第几位；文件头注释写明这是临时实验，评估完就删。

模型怎么“调用”一个本机技能？没有专门的工具，它直接用 shell 读 `SKILL.md`、跑 `scripts/` 里的脚本。`exec_command` 处理器每次执行前调用 `maybe_emit_implicit_skill_invocation`：用命令解析器认出“读某个 `SKILL.md`”，或“用 python、bash、node 等解释器运行某技能 `scripts/` 下的脚本”，就记一次隐式调用，同一轮同一技能只记一次（`codex-rs/core/src/skills.rs:121`）。这只影响遥测和影子实验，不改变执行。

## 注入：把 SKILL.md 放进本轮

被显式选中的技能，其 `SKILL.md` 全文会包成一个 `SkillInstructions` 片段，作为 user 角色的消息放进本轮输入：

```rust
    fn type_markers() -> (&'static str, &'static str) {
        ("<skill>", "</skill>")
    }

    fn body(&self) -> String {
        let name = &self.name;
        let path = &self.path;
        let contents = &self.contents;
        // ...
        format!("\n<name>{name}</name>\n<path>{path}</path>{resource_access}\n{contents}\n")
    }
```

（`codex-rs/ext/skills/src/fragments.rs:89`）

本机技能由内核直接读取注入，一般不截断（可移植 Agent Plugin 格式的技能例外）；经扩展读取的执行环境、云端技能，正文截到 8000 字节并给出警告。几条边界规则：

- 自动审查（guardian）会话不解析提及：它的输入里嵌着父会话的对话记录，被当作不可信证据（`codex-rs/core/src/session/turn.rs:1013`）。
- 技能在 `openai.yaml` 里声明了尚未安装的 MCP 服务器时，`maybe_prompt_and_install_mcp_dependencies` 先按 requirements 过滤，再用 `request_user_input` 问一句 `Install MCP servers?`；同意就写进全局 `config.toml`、需要时走 OAuth 登录并立刻刷新 MCP 连接。同一会话里问过的不再问，只对官方客户端开放（`codex-rs/core/src/mcp_skill_dependencies.rs:40`）。
- 技能不在本机文件系统上（远程执行环境或云端）时，模型没法 `cat`，扩展就提供 `skills` 命名空间下的 `list` 与 `read` 两个工具来读取（`codex-rs/ext/skills/src/tools/mod.rs:59`）。

## 和《从 LLM 到 Coding Agent》对照

那本书的[系统提示与上下文注入](https://daiw.net/manual/llm-to-agent/context-injection)讲“静态放高处、动态放低处”。Codex 的技能系统把这条原则拆成两半：相对稳定的清单进 world state，只在变化时追加；每轮才需要的 `SKILL.md` 全文跟着本轮输入走，不回头改历史。对比同类实现：[Grok Build](https://daiw.net/manual/grok-build/skills) 有 `paths:` 门控，碰到匹配文件才让技能现身；[OpenCode](https://daiw.net/manual/opencode/agents-and-commands) 由 `skill` 工具返回正文；Codex 对本机技能不设专门工具，让模型用 shell 直接读文件，再从命令里反推“用了哪个技能”。

---

上一篇：[配置系统 · 分层加载与托管要求](https://daiw.net/manual/codex-source/config-system) · 下一篇：[Hooks · 在关键节点插入你的脚本](https://daiw.net/manual/codex-source/hooks)
