Skills · 发现、选择与注入

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

作者 David更新于 第 36 篇(共 57 篇)

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 技能。

三方分工

位置负责
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。整体数据流:

图表加载中…

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

resolve_skill_roots(codex-rs/ext/skills/src/host_roots.rs:28)顺着配置层找根目录:每个项目层的 .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,解析器会先试着修一遍:

    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 这类常见环境变量不算;[$名字](路径) 形式的链接按路径匹配。只写名字时有一道防歧义检查:

        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 及其组合——只在影子模式下运行:

/// 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 角色的消息放进本轮输入:

    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》对照

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


上一篇:配置系统 · 分层加载与托管要求 · 下一篇:Hooks · 在关键节点插入你的脚本

本页目录