在正文之间提前说明下Skill 的工程化架构设计与 Skill 的内容设计本质上属于两个不同层面的能力建设但二者共同决定了 Skill 体系最终能否真正发挥作用。工程化架构设计关注的是一套 Skill 机制如何被构建出来即系统如何完成 Skill 的组织、触发、加载、注入与治理Skill 的内容设计关注的则是当这套机制真正运行起来之后注入给模型的内容本身是否足以稳定、准确地引导模型完成目标任务。文本核心聚焦于Codex底层对于Skills的工程化架构设计。Skills的本质非常简单它是一段动态注入到大模型请求上下文中的Prompt。Codex对这个思路的工程化设计可以概括为三点渐进式披露不是一次性把所有Skill内容塞给模型而是先给目录元数据模型按需时才注入完整内容。分层来源不同权威度的Skill来自不同位置系统内置 vs 用户自建对应不同加载策略预算控制模型上下文窗口有限Skills有字符预算超了就裁剪或省略Codex Skill的目录结构规范从上面三点中分层来源直接引出了一个问题Codex怎么知道去哪里找Skill这就涉及Skill的目录规范设计——Codex定义了一套固定的目录结构让每个Skill都住在规定的地方有规定的形状。Codex规定一个Skill必须是一个目录而非单个文件。这个目录的核心标识是一个固定名称的文件SKILL.md。Codex扫描时只有找到SKILL.md的目录才会被识别为Skill。一个标准的完整Skill目录结构如下my-skillskill目录名称/ ├── SKILL.md ← 【必需】Skill的唯一标识和主内容 ├── openai.yaml ← 【可选】UI展示元数据图标、分类、显示名 ├── scripts/ ← 【可选】可执行脚本模型可调用 │ ├── init_skill.py ← 创建/初始化类脚本 │ └── validate.py ← 验证类脚本 ├── references/ ← 【可选】参考文档模型可读取 │ └── spec.md ← 规范说明文档 ├── assets/ ← 【可选】静态资源模板文件等 ├── hooks/ ← 【可选】Hook脚本 └── .mcp.json ← 【可选】MCP服务器配置 └── .app.json ← 【可选】App配置这个结构的设计思想是SKILL.md是灵魂其余一切都是辅助工具。模型首先通过SKILL.md了解我能做什么然后按需从工具里取用具体的脚本、文档、资源。1SKILL.md只是一个Skill的说明书是一个Markdown文档也是大模型识别技能的唯一入口文件主要分两段name: 技能名称description: 技能描述name 和 description 是必选的。description 是触发判据——模型根据它决定这个任务该不该用这个技能。所以 description里要包含什么时候用的信息写在正文里没用因为正文是触发后才读的。正文是给模型看的操作手册。Codex 官方建议控制在 500 行以内详细内容放到 references/ 里正文只引用路径和触发条件。这里不过多说明一个Skill内容如何设定这里我们需要深刻的认识到name 和 description重要性因为他们是大模型优先读取到的且用来识别是否使用该技能的关键说明。2openai.yaml这个配置信息主要是给 Codex UI 界面用的元数据模型并不直接读它它控制技能在 Codex App 侧栏中怎么显示名字、图标、颜色。没有这个文件技能仍然能被模型发现和使用只是 UI 展示会不够美观。其本质上就是一个约定的数据结构interface: display_name: Deploy Skill short_description: Generate K8s deployment configs icon_small: ./assets/my-skill-small.svg icon_large: ./assets/my-skill.png default_prompt: Use $my-skill to generate deployment configs3scripts/该目录下是用于存储可执行代码脚本文件例如python脚本模型在对话中可以决定调用这些脚本——通过Shell工具执行它们。设计意图是模型可以直接运行脚本而不把脚本内容读进 context。比如一个 rotate_pdf.py 脚本模型只需要 python scripts/rotate_pdf.py直接运行而不需要先花几百 token 读一遍脚本源码。这节省了大量 context。4references/SKILL.md是精简的说明书references是完整的技术手册。模型不会自动读取这些文件只有在SKILL.md中明确引导时才会去读。比如数据库 schema 文件、API 文档、公司政策。SKILL.md 正文中写清楚什么时候该去读哪个 references文件模型根据任务自行决定是否深入。如果文件超过 100 行建议在文件开头加目录索引。5assets/该目录下是存储输出资源模板、图标、字体。这些文件不读进 context模型只是把它们复制或修改后用在最终输出里。比如一个前端模板assets/frontend-template/模型不需要理解模板内容只需要复制到目标目录通俗的说就是一个资源文件。了解了单个Skill的内部结构后自然产生下一个问题这些Skill目录放在哪里Codex怎么知道去哪个路径找它们这涉及到Skill的分层来源设计。四个理论层级Codex在架构上定义了四个Skill来源层级按权威度从高到低需要注意的是当发生上下文预算不足时会按照优先级进行裁剪通常来说system级别skill不会被裁剪user级别的skill优先被裁剪1System层级在Windows系统下位于C:\Users\你的用户名\.codex\skills\.system\里面包含Codex自带的5个系统Skill这些是codex自身提供的Skill这些技能赋予 Codex 自我扩展能力例如用户说帮我创建一个技能模型就会读到 skill-creator 的 SKILL.md知道怎么用 init_skill.py 手架创建新技能说安装某个GitHub 上的技能模型就会读到 skill-installer 的指令。2User层级用户自己创建的Skill就在这里通常位于C:\Users\你的用户名\.agents\skills\。比如创建了一个叫my-helper的Skill完整路径就是C:\Users\你的用户名\.agents\skills\my-helper\SKILL.md3Admin层级在Unix/Linux系统上管理员可以把Skill放在/etc/codex/skills/让整台机器的所有用户都能使用。但Windows没有/etc这个路径概念Codex在Windows上不实现Admin层级。所以不会看到这个层级。4Repo层级这个层级不是全局生效的而是取决于当前打开的项目。只有当一个Git项目目录中存在.agents/skills/子目录时Codex才会加载其中的Skill。如果当前的项目没有这个目录就看不到Repo层级的Skill。它的工作方式是Codex会从项目根目录到当前工作目录之间的路径上查找所有.agents/skills/目录。想象一个团队在维护一个API项目他们在项目的.agents/skills/中放了一个api-designSkill描述了团队API设计的规范和检查清单。任何团队成员在Codex中打开这个项目时都会自动获得这个Skill的指导。所以通常在Windows环境中通常可见的是两个层级System内置和User个人现在清楚了Skill的结构规范和来源层级。接下来要回答最核心的问题这些Skill在Codex运行时到底是怎么被加载、解析、注入到大模型的上下文中的原理设计在Codex架构设计中已经说过Codex引擎层处理请求的核心流程大致如下收到用户输入 → 进入turn_run → 执行各种处理 →返回结果。Skill的整个运作就是这个流程中的一个环节由SkillManager处理这里我们可以聚焦SkillManager了解codex对于skill工程化能力的设计。1SkillsManager如何加载Skill目录 确定扫描路径在turn_run初期当Codex为当前这次对话构建运行环境时SkillsManager负责把所有可能需要的Skill收集起来。SkillsManager是一个长期驻留的组件持有Codex的home路径和配置信息并且有缓存机制避免重复扫描即上述说明的不同层级的划分和目录位置。SkillsManager并不是不是盲目搜索整个文件系统。它根据当前配置确定一组明确的Skill根路径来扫描。SKILL.md目录扫描BFS算法它在每个根路径下进行广度优先扫描BFS扫描算法寻找包含SKILL.md的目录。这里就引出了一个核心论证只有含有SKILL.md的目录才会被识别为一个skill。需要注意的是codex对于这种扫描操作并不是无限制的它扫描有明确的限制最大深度6层防止扫描过深。每个根最多扫描2000个目录防止路径过多跳过以.开头的目录除了.agents等特殊目录减少不必要的扫描消耗这些限制是为了在性能和完整性之间取平衡。Codex不能在每次对话启动时花几十秒扫描整个文件系统所以设了合理的上限。核心实现在loader.rs:162 的 load_skills_from_roots() 对每个 SkillRoot 做 BFS// loader.rs:478 async fn discover_skills_under_root(fs, root, scope, ...) { let mut queue: VecDeque(AbsolutePathBuf, usize) ...; while let Some((dir, depth)) queue.pop_front() { for entry in fs.read_directory(dir) { if metadata.is_file file_name SKILL.md { // 找到了解析它 parse_skill_file(fs, path, scope, ...); } if metadata.is_directory { enqueue_dir(mut queue, ..., depth 1); // 继续深入最多6层 } } } } 解析每个找到的SKILL.md找到SKILL.md文件后SkillsManager会读取它的内容做两件事提取YAML前置元数据frontmatter—— 这里面有Skill的name、description、metadata等结构化信息此时并不会读取完整的内容所以也印证了另一个论证name、description非常重要它取决了你skill是否能力LLM识别到。读取可选的openai.yaml— 如果存在提取UI展示元数据在读取完这些信息后codex底层会包装为一个SkillMetadata对象包括nameSkill名称如imagegendescription完整描述short_description简短描述用于目录展示path_to_skills_mdSKILL.md文件的绝对路径scope层级标记System/Admin/Repo/Userplugin_id如果是Plugin提供的Skill标记来源Plugin核心实现在loader.rs:623 的 parse_skill_file() 解析每个找到的 SKILL.mdloader.rs:623 的 parse_skill_file() 解析每个找到的 SKILL.md // loader.rs:623 async fn parse_skill_file(fs, path, scope, ...) - ResultSkillMetadata { let contents fs.read_file_text(path).await; let frontmatter extract_frontmatter(contents); // 提取 --- 之间的 YAML let parsed: SkillFrontmatter serde_yaml::from_str(frontmatter); // name 从 frontmatter 取没有则用目录名 // description 从 frontmatter 取 let metadata load_skill_metadata(fs, path); // 读取 agents/openai.yaml Ok(SkillMetadata { name, description, policy, scope, path_to_skills_md, ... }) } 过滤和组装SkillsManager不会直接把所有扫描结果一股脑交给后续环节。它还会做产品过滤某些Skill可能标记为仅特定产品、租户可用因此SkillsManager此时会根据当前产品或者租户类型过滤配置禁用用户可以在配置文件中禁用特定Skill因此SkillsManager根据配置规则排除被禁用的Skill。隐式调用索引构建为每个Skill的scripts/目录和SKILL.md文件建立索引用于后续的隐式调用检测最终SkillsManager产出一个SkillLoadOutcome对象这是当前这次对话中所有可用Skill的完整清单带着元数据、路径、层级信息。核心实现在 manager.rs:185 的 build_skill_outcome()// manager.rs:185 async fn build_skill_outcome(roots, skill_config_rules) - SkillLoadOutcome { let outcome filter_skill_load_outcome_for_product( load_skills_from_roots(roots).await, // 上面第二步的结果 self.restriction_product, // 产品门控Codex vs 其他 ); let disabled_paths resolve_disabled_skill_paths(outcome.skills, skill_config_rules); finalize_skill_outcome(outcome, disabled_paths) } 缓存机制SkillsManager不会每次turn_run都重新扫描。它根据配置和当前工作目录做缓存——如果配置和路径都没变直接用上次的结果。只有当配置改变比如你安装了新Plugin或切换了工作目录进入了新项目才重新扫描。这段逻辑对应的源码核心在codex-rs/core-skills/src/manager.rs中skills_for_config()方法负责加载和缓存// manager.rs中的核心加载方法 pub fn skills_for_config(self, input: SkillsLoadInput) - ResultArcSkillLoadOutcome, ... { // 1. 检查缓存——如果配置没变直接返回缓存结果 // 2. 配置变了重新扫描调用load_skills_from_roots() // 3. 过滤、禁用检查、构建隐式索引 // 4. 缓存新结果 }2如何选出本次需要注入的完整Skill.mdSkillsManager产出了所有可用Skill的清单但模型上下文窗口有限不能每次都把所有Skill的完整内容塞进去。所以需要一个选择环节即选择出来哪些Skill的完整信息注入给LLM也是渐进式披露的开始。codex设计中在用户输入被解析之后、模型请求内容被构建之前由SkillsExtension实现了一个叫TurnInputContributor的Contributor接口。Contributor是codex扩展层提供的一种扩展类接口用于在不同能力下扩展额外的能力在Java体系中也有很多这样的扩展机制的实现例如拦截器、SPI等等。TurnInputContributor这个接口的含义是在每个turn的输入构建阶段可以插入一段自定义的扩展内容SkillsExtension通过两种方式确定本次需要注入哪些完整的Skill方式一显式注入用户在输入中明确提到了Skill即我们通过$指定的或者描述中说明SkillsExtension会扫描用户输入文本找出这些提及然后去Skill清单中匹配对应的Skill。方式二隐式注入用户没有明确提到Skill名但模型在执行过程中做出了识别到当前行为与某个Skill关联因为已经有了所有skill描述此时codex系统就会加载这个Skill完整信息进行注入。隐式触发发生在turn执行过程中不是预先选择的而是事后检测的。它的作用不是注入内容因为模型已经在用了而是发送分析事件记录这个Skill被使用了。3渐进式披露机制这是Skills系统最精巧的设计分三层来说明 第一层注入Skill目录始终注入SkillsManager产出了所有可用Skill目录还包括了名称简短描述路径格式化成一段类似目录索引的文字这些信息每次turn_run都会注入无论用户是否提到了任何Skill。通俗的说就是告诉模型你有这些能力可用预算控制目录内容有字符预算限制。Codex默认分配模型上下文窗口的2%给Skill目录如果上下文窗口是128K tokens那目录大约占2560tokens的等效字符空间。如果目录内容超出预算Codex有三级裁剪策略所有Skill都能完整展示 → 不裁剪超了但还能裁剪描述 → 逐字符公平分配每个Skill的描述长度超了连裁剪描述都不够 → 按层级优先级省略SkillSystem优先保留 Admin Repo User因为系统Skill是Codex的基础能力不能丢而用户Skill是个人扩展优先级最低超预算时最先被省略。对应的源码在codex-rs/core-skills/src/render.rs的build_available_skills()函数中它接收一个SkillMetadataBudget参数来控制预算// render.rs - 预算控制的逻辑骨架 fn build_available_skills(skills, budget) - RenderedSkills { // 1. 计算所有Skill完整展示的总字符数 // 2. 如果 budget全部展示 // 3. 如果 budget尝试裁剪描述公平分配每个Skill的描述字符数 // 4. 如果连裁剪都不够按scope优先级省略Skill } 第二层注入注入选中的Skill完整SKILL.md内容在用户显式触发该Skill时注入SKILL.md文件的完整文本内容。注入角色这段内容以user角色注入并用skill和/skill标记包裹格式如下skill nameimagegen/name pathC:\Users\你\.codex\skills\.system\imagegen\SKILL.md/path [SKILL.md的完整357行内容] /skill为什么用不同角色来输入Skill的目录清单Codex使用developer角色系统指令语气你有这些工具而完整内容用user角色用户请求语气请按照这个Skill的指导来工作。这种角色区分核心是帮助模型理解目录是参考信息具体Skill内容是本次需要遵循的指令。 第三层读取scripts和references的按需读取这里指的是Skill目录中的scripts/、references/等子目录里的文件读取时机这不是预先注入的。模型在生成回复的过程中如果根据SKILL.md的指导决定需要读取某个脚本或参考文档它会自主通过文件读取工具或Shell执行工具这是渐进式披露的最后一步。Codex不预先把每个Skill的所有资源文件都塞给模型——太浪费上下文了。而是让SKILL.md起到导航作用里面会说如果你想了解X请读取references/X.md或执行python3 scripts/Y.py来完成Z。模型根据需要自己决定是否去读。(4) 架构原理总结在turn_run初期扫描并缓存所有可用Skill的元数据构建目录索引始终注入模型上下文用户触发时按需注入完整SKILL.md内容资源文件由模型自主读取整个过程受上下文预算控制超预算时按层级优先级裁剪。Codex Skill工程化设计思考总结Skill系统要解决的根本矛盾是大模型的上下文窗口是稀缺资源而可扩展的能力描述是无限增长的。如果没有Skill系统扩展模型能力的方式只能是每次对话都把所有可能的指导信息塞进去。但这在工程上不可行——128K的上下文窗口装不下几百个Skill的完整内容。Skill系统的存在本质上是在这个矛盾之上做资源调度。