夜雨聆风学习资料网

ARTICLE · 995396

Skill和普通文档有什么区别:从 Codex 源码看 skill 的作用

Skill和普通文档有什么区别:从 Codex 源码看 skill 的作用

我之前一直在想,skill 到底有没有必要。即便没有 skill,一个项目已经可以在 AGENTS.md 里放索引,再指向更详细的文档、脚本和外部工具说明。模型照着索引读取文件、执行脚本,看起来也能完成同样的工作。

沿着 Codex 的源码看下来,我的结论是:skill 的价值是“文件目录结构协议 + 与协议耦合的 agent harness”所实现的渐进式展开。协议规定 SKILL.md、frontmatter、references/、scripts/ 和 assets/ 怎样组织;harness 据此发现 skill、构建 catalog、处理显式注入和调用观测,模型再根据 catalog 完成隐式选择。

技术领域有过很多类似的协议化、标准化工作,比如不同的编程语言、Kubernetes。编程语言先约定语法和语义,语言实现、工具链和业务代码因此有了共同基础;Kubernetes 抽象出 Pod、Deployment、Service 等核心资源并约定 API schema,大家统一用 YAML 描述期望状态,控制器和周边工具据此工作。

没有 skill,只用 AGENTS.md + 项目文档 + 脚本 也能表达相同的工作流。但要让模型事先知道有哪些可选工作流,所有入口都得预先写进随正文注入的 AGENTS.md 索引;新增或移除入口也要修改这份索引。

这套协议让 Codex 实现了 skill 级别的渐进式展开和目录级插拔:harness 自动发现符合约定的 skill,先只向模型提供 name、description 和 source locator。Skill 被选中后才加载完整 SKILL.md;显式路径由 harness 注入,隐式路径由模型读取。附属资料再由模型根据正文按需处理。增加或移除 skill 目录时,catalog 随之更新。

只服务当前仓库、可选流程又不多时,AGENTS.md 通常够用;需要管理大量可选流程、跨项目复用或动态接入时,skill 才更有价值。

分析范围

本文基于 OpenAI Codex 的 rust-v0.145.0 源码。

术语约定

术语
本文中的含义
agent harness
本文对 Codex 宿主运行层的统称,负责保存会话与每轮状态、组装模型输入、执行工具、路由 skill 并记录使用情况。
skill root
Codex 开始扫描 skill 的目录。项目、用户、system、plugin 和运行环境可以提供不同的 root。
snapshot
某一时刻 skill 发现结果的快照。Session 和 turn 可以复用它,不必反复扫描相同目录。
frontmatter
SKILL.md 开头的 YAML 元数据,包含 name、description 等字段。
skill catalog
Codex 已发现且当前可用的 skill 元数据目录。它向模型提供 name、description 和 source locator,正文在选中 skill 后另行加载。
source locator
skill 来源的位置标识。本地 skill 通常使用文件路径。
turn
从一次用户输入开始的一轮执行。每个 turn 都重新决定要使用的 skill。
invocation
Harness 记录的 per-turn skill 使用事件。
explicit invocation(显式指定)
用户通过 $skill-name 或 UI 点名 skill,harness 精确匹配并注入完整 SKILL.md。
implicit invocation(隐式触发)
用户没有点名 skill,模型根据 catalog 中的 description 自行判断并读取 SKILL.md,harness 再观测这次使用。
shadow selector
旁路运行的候选排序实验。排序结果只进入实验指标,生产 prompt 和选择链路保持原样。
渐进式展开(progressive disclosure)
Codex 先提供 catalog 元数据,选中后加载完整 SKILL.md,附属资料再由模型按需读取或执行。
目录级插拔
增加或移除符合协议的 skill 目录后,Codex 通过目录监听、缓存刷新和后续 turn 装配更新 catalog。

AGENTS.md 的链路是最简单的

Codex 把 AGENTS.md 当作项目级指令。agents_md.rs 记录的发现过程如下:

  • 从当前工作目录向上寻找项目根;
  • 再从项目根走回当前工作目录;
  • 读取沿途的 AGENTS.override.md、AGENTS.md或配置的候选文件;
  • 按从外到内的顺序拼接正文;
  • 不越过项目根。
读取时还有一个总字节预算。超过 project_doc_max_bytes 的部分会被截断,相关实现位于 read_agents_md()。

拼接后的文本最终以 user context 的形式进入模型,外层标记为 # AGENTS.md instructions ... <INSTRUCTIONS>,见 user_instructions.rs。

这条链路可以压缩成:

项目根 → 当前工作目录逐级发现 → 读取预算内正文 → 拼接 → 注入模型上下文

因此,AGENTS.md 适合放当前目录树中始终生效的约束,例如构建命令、代码风格、安全要求和验证步骤。

Skill 的发现是程序化的

AGENTS.md 正文与 skill catalog 走两条独立的上下文链路。Codex 源码中的 skills_are_not_appended_to_agents_md 测试固定了这个边界。

按约定扫描

这里的“发现”由 Codex 宿主端在模型推理前程序化完成。Rust loader 先按固化规则确定哪些文件有资格进入 skill catalog,模型随后再做相关性判断。

这套逻辑分为两步。第一步,skill_roots() 根据配置层、当前工作目录和 plugin 状态算出一组扫描根:

  • 项目配置目录下的 skills;
  • 从项目根到当前目录沿途的 .agents/skills;
  • 用户目录下的 $HOME/.agents/skills;
  • 为兼容旧版本保留的 $CODEX_HOME/skills;
  • Codex 随安装包提供、缓存到 $CODEX_HOME/skills/.system 的 system skills;
  • 管理员、plugin 和运行时额外配置的 skill roots。

第二步,discover_skills() 对每个根执行有界目录遍历。代码固化了扫描深度、目录数、文件数、隐藏目录和符号链接策略。

遍历结果中只有 basename 精确等于 SKILL.md 的文件会成为候选。随后 parse_skill_file() 读取正文和可选元数据,解析成功后生成 SkillMetadata。

扫描根、入口文件名、frontmatter 和附加元数据共同构成文件目录结构协议。这套协议给 loader 提供了稳定的探测边界:符合约定的目录可以在模型推理前被枚举、校验和缓存,其他 Markdown 继续按普通文档处理。

AGENTS.md 中的链接由模型根据自然语言判断对应哪个工作流、什么时候读取。Skill 把 name、description 和位置等信息变成结构化数据,loader 可以在模型推理前完成注册。

发现和调用是两个阶段。固化的 loader 逻辑只回答“有哪些合法 skill、它们的元数据是什么”;当前任务使用哪个 skill,由显式 mention 或模型判断决定。

解析结构化元数据

每个 SKILL.md 的 YAML frontmatter 至少要提供有效的 description,name 可以省略并由目录名推导。

Codex 会校验字段和长度。无效的非 system skill 会形成可见错误,相关代码在 parse_skill_frontmatter_metadata_inner()。

Skill 目录还可以带 agents/openai.yaml,其中包含三类可选元数据:

  • interface:展示名称、简述、图标、颜色和默认 prompt;

  • dependencies:工具依赖;

  • policy:例如是否允许 implicit invocation。

这些字段会进入专门的 SkillMetadata、SkillInterface、SkillDependencies 和 SkillPolicy 数据结构,见 model.rs。

Codex 由此得到一个可以枚举、校验、过滤和展示的 skill 元数据目录。

Agent harness 怎样消费已发现的 skill

Codex 在 agent harness 中为 skill 做了专用处理,把文件目录结构约定转成一套可缓存、可注入、可路由和可观测的机制。

这条主链路可以概括为:

文件目录结构约定  → SkillsService(发现、缓存与预热)  → HostSkillsSnapshot(保存本机发现结果)  → TurnSkillsContext(装配当前 turn 可用的 skill)  → developer skill catalog(模型先看到的元数据目录)     ├─ 显式指定 → mention 路由 → <skill> 正文注入    └─ 隐式触发 → 模型按 description 判断 → 工具读取 SKILL.md  → 普通工具执行  → invocation 记录、对话 history 与长期 memory 管理

生命周期由宿主持有。ThreadManager  创建独立的 SkillsService。session 初始化时,Codex 根据配置和 plugin roots 预热 skill snapshot,并与 AGENTS.md 刷新并行执行,见 warm_plugins_and_skills_for_session_init() 及其调用处。

每个 turn 又会按当时的 cwd、配置、plugin 和执行环境文件系统请求 snapshot,再放进 TurnSkillsContext,相关装配位于 turn_context.rs。宿主由此为模型准备好本轮可用 skill 的结构化视图。

App server 中的 SkillsWatcher 监听 skill 目录变化,清理缓存并发送 SkillsChanged。目录监听与后续 turn 的 snapshot 装配共同支持目录级插拔。

SkillsExtension 是把 skill 逻辑接入 agent harness 的专用组件。它把 catalog 放进模型可见的上下文,并处理配置变化和 invocation 记录。

执行阶段走普通工具。Harness 保证同一个 skill 在一个 turn 内只记录一次 invocation。Skill 正文作为当前 turn 的临时上下文传给模型;memory writer 会f="https://github.com/openai/codex/blob/rust-v0.145.0/codex-rs/memories/write/src/phase1.rs#L466-L473" data-draft-type="text-link">排除完整的 <skill>...</skill> 片段,长期记忆只接收其余内容。

为什么需要渐进式展开

Anthropic 在 2025 年的 Equipping agents for the real world with Agent Skills 中,把 progressive disclosure 称为 Agent Skills 保持灵活和可扩展的核心设计原则,并用“目录、章节、附录”解释三个层级:启动时只加载所有 skill 的 name 和 description;任务匹配后读取完整 SKILL.md;更细的参考文件只在具体场景需要时读取。Agent Skills 规范的概览 将这三个阶段概括为 discovery、activation 和 execution。

它首先解决的是上下文成本。全量加载时,常驻上下文需要容纳所有 skill 的正文;渐进式展开把它压缩为“全部 skill 的短元数据 + 当前 skill 的正文 + 当前任务需要的附属资料”。附属资料留在文件系统中,一个 skill 可以携带远多于单次上下文容量的内容,模型只读取当前任务需要的部分。这样既为用户任务保留更多上下文,也减少互斥或低频说明带来的 token 消耗。Anthropic 的文章也明确建议把很少同时使用的内容拆到不同文件中。

这种取舍在计算机系统中很常见。程序把当前调用直接需要的局部状态放在栈帧中,体量更大或生命周期更长的数据放在堆中,通过引用访问;计算机体系结构又用寄存器、缓存、内存和存储组成层次,只把当前工作集放进更快、容量更小的层级。Skill 的 catalog、SKILL.md 和附属资料采用相似的组织思路:把有限上下文留给当前任务,其余内容保留为可寻址、可按需读取的外部资料。

先注入 catalog,再按需读取正文

选中 skill 前,Codex 只渲染名称、描述和 source locator,形成 available skills catalog。

render.rs 为这份目录设置了独立预算:如果模型提供了 context window,skill 元数据默认最多使用其中的 2%;没有窗口信息时使用 8,000 字符的默认预算。描述过长时先缩短描述,仍然放不下时再省略条目并产生警告。

Codex harness 会把这份 catalog 渲染成一段 developer 消息,随本轮请求发送给模型,模型由此知道当前有哪些可用 skill,见 available_skills_instructions.rs。如果启用了 skill 使用说明,Codex 还会附上以下规则:

  • 用户点名某个 skill,或者任务明显匹配其描述时,应在当前 turn 使用它;
  • 选中后要完整读取 SKILL.md;
  • 相对路径按 skill 目录解析;
  • 只读取当前任务需要的 references、scripts 和 assets;
  • 每个 turn 重新选择需要使用的 skill。

渐进式展开止于 skill 边界

Codex 的结构化展开止于 skill 一级:SKILL.md 先被程序化发现,匹配当前任务时再被整体加载。其余内容走 agent harness 的普通工具链;模型根据 SKILL.md 判断需要哪些 references/、scripts/ 和 assets/,再调用文件或 shell 工具读取、查看或执行,处理方式与普通项目文档和脚本相同。

一个 skill 内的子流程共用同一组 name、description 和 invocation。具备一定复杂度,并且需要独立进入 catalog、匹配、启停或观测的流程,应拆成独立 skill。因此,我倾向于使用扁平、细粒度的薄 skill:每个 skill 独立按需加载;多个 skill 共用的说明只在项目文档或脚本中维护一份,以减少文档复制和信息冗余。

路径一:显式指定

显式指定由 harness 确定性处理。用户通过 UI 选择 skill 时,输入里会出现结构化的 UserInput::Skill;文本中的 $skill-name 也会被解析。

collect_explicit_skill_mentions() 会:

  • 优先按结构化输入中的路径匹配;
  • 过滤已经禁用的 skill;
  • 对路径去重;
  • 对重名 skill 避免含糊匹配;
  • 再处理文本中的 $skill-name。

匹配成功后,build_skill_injections() 读取完整 SKILL.md,记录这次 invocation,并把正文包装成:

<skill><name>...</name><path>...</path>...完整 SKILL.md 正文...</skill>

这个片段以 user context 进入当前 turn。集成测试 user_turn_includes_skill_instructions 直接检查了请求中是否同时包含 skill 名称、路径和正文。

路径二:隐式触发

用户未显式指定 skill 时,模型根据 catalog 中的 description 自行判断,再主动读取 SKILL.md。Codex 注入的 developer instructions 明确要求使用与任务明显匹配的 skill,见 SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS。

当前生产链路可以写成:

用户任务 + available skills catalog    → 模型在普通推理中比较任务与 name / description        ├─ 判断匹配 → 通过普通工具读取 SKILL.md → 按正文行动        └─ 判断不匹配 → 不加载该 skill

当前生产接口只提供 catalog 和普通工具,相关性判断留在模型内部,harness 再根据后续工具调用观测结果。源码没有定义 skill=A, confidence=0.83 一类结构化结果、独立 evaluator 模型或统一置信度阈值。description 写得是否准确,直接影响这次普通模型决策。

源码里的自动 selector 当前运行在 shadow mode。SkillsExtension 先生成用于正文注入的 selected_entries,再运行 shadow selection。排名不会写回 selected_entries,也不会进入 prompt;它只保存在当前 turn 的实验状态中,等模型实际使用 skill 后交给 record_invocation() 记录是否命中和排在第几位。配置结构也明确将其定义为“不改变 prompt 内容”的 shadow selector,见 config.rs。

实验使用四种廉价的词法排序方法:

  • weighted lexical:统计词和短语的重合,并让 name 的权重高于 short description 和 description;

  • fielded BM25:分别计算三个字段的 BM25 分数,同时考虑词频、字段长度和词的稀有程度;

  • character n-gram:比较二到五个字符组成的片段,对局部拼写和词形差异更宽容;

  • multi-query lexical:把用户输入拆成多个句子或子句,分别排序后合并结果。

这项旁路实验不改变生产 skill 选择和 prompt,只增加本地排序时间与观测指标。它输出候选排名、耗时、候选缩减比例和命中情况,用来评估这些方法对模型实际 skill 使用的预测效果,见 ShadowSelectionExperiment。

模型读取 SKILL.md 或运行 scripts/ 后,detect_implicit_skill_invocation_for_command() 识别这次使用,maybe_emit_implicit_skill_invocation() 再通知 extension 并记录实际调用。任务开始前的选择仍由显式 mention 或模型完成。

工程边界:必须每次执行、漏掉一次就可能造成事故的规则,应放在始终可见的 AGENTS.md,或者下沉到 hook、CI、命令包装器等机械门禁。Implicit skill 更适合按任务启用的可选流程。

我准备尝试的项目组织设想

我准备按以下三层组织:AGENTS.md 保存粗粒度的项目索引,SKILL.md 保存按任务加载的工作流合同,项目文档和脚本保存共享内容的权威正文。AGENTS.md 和 skill 都可以长期维护;区别在于前者随适用目录的上下文注入,后者只在被选中后加载。

  • AGENTS.md:粗粒度的项目索引

    • 放什么:最重要的项目级约束,以及构建、测试、发布、故障处理、权威文档和 skill 的入口。

    • 原则:保持粗粒度和精简,只放模型在选择具体工作流前必须知道的内容;详细规则继续指向 skill、项目文档或脚本。

  • 项目文档和脚本:共享内容的权威来源
    • 放什么:完整设计、操作步骤、参数解释,以及负责执行或校验的脚本。
    • 原则:AGENTS.md 和 skill 提供入口,人、Codex、CI 和其他工具统一引用这一来源。
  • 扁平、细粒度的薄 skill:按任务加载的合同
    • 拆分边界:具备一定复杂度、需要独立匹配、启停或观测的工作流,对应一个独立 skill。
    • 内容:name 和 description 负责发现与匹配;SKILL.md 可以长期维护某类任务的规则、入口、执行顺序和验收标准;references/、scripts/ 和 assets/ 承载当前 skill 需要的执行材料。
    • 加载边界:SKILL.md 是一级加载单位,其中的 Markdown 链接保持普通链接语义。必要文档要明确写成“执行前必须读取”,再由模型通过普通工具打开。
    • 何时引入:工作流需要跨仓库复用;可选流程太多、持续占用上下文;需要 $skill 或 UI 选择;需要打包 references、scripts 和 assets;或者需要启停配置、依赖声明、更新监听与 invocation 观测。

Skill 不能解决的问题:稳定复杂工作流 和 信息治理

用工作流结构驱动 agent

在本文分析的版本里,Codex harness 固化的是 skill 的发现、加载和 invocation 观测。它把 SKILL.md 作为自然语言上下文交给模型,不会将其解析为由节点、连线、条件和信号组成的可执行工作流,也不会根据流程状态调度 agent。

控制方向仍然是 agent 驱动流程:模型读取 skill 和当前上下文,自行决定下一步及工具调用,harness 执行工具并返回结果,模型再继续推进。Skill 可以用自然语言描述顺序、循环、多级条件和多信号驱动关系,流程结构本身却不能决定何时进入下一节点、重复当前节点、等待信号或转入某个分支。上下文变长、分支增多或中途发生异常时,模型可能漏掉步骤、规约或退出条件。

大量skill治理

有可能发生的是,第三方 skill 各自携带一套说明和规约。同一条要求可能以不同措辞重复散落在多个 SKILL.md 中;不同 skill 也可能对工具选择、验证步骤或发布策略给出相互冲突的要求。随着 skill 本身变复杂、上下文规模膨胀,模型会更难判断不同表述是否语义等价、各自适用什么范围,以及发生冲突时谁应优先。

项目需要为这类信息建立权威来源和优先级。共用规约只在 AGENTS.md、项目文档、hook 或 CI 中维护一份,skill 引用这份统一定义。引入第三方 skill 时记录来源、版本和适用范围,并在启用前检查重复与冲突;发生冲突时,按项目预先声明的权威来源和机械门禁处理。

相关学习资料