当前位置: 首页 > news >正文

Codex Harness工程 Skills架构设计原理

在正文之间,提前说明下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-skill(skill目录名称)/ ├── 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了解"我能做什么",然后按需从工具里取用具体的脚本、文档、资源。

(1)SKILL.md

只是一个Skill的说明书,是一个Markdown文档,也是大模型识别技能的唯一入口文件,主要分两段:

name: "技能名称"description: "技能描述"

name 和 description 是必选的。description 是触发判据——模型根据它决定"这个任务该不该用这个技能"。所以 description里要包含"什么时候用"的信息,写在正文里没用,因为正文是触发后才读的。正文是给模型看的操作手册。Codex 官方建议控制在 500 行以内,详细内容放到 references/ 里,正文只引用路径和触发条件。

这里不过多说明一个Skill内容如何设定,这里我们需要深刻的认识到name 和 description重要性,因为他们是大模型优先读取到的,且用来识别是否使用该技能的关键说明。

(2)openai.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 configs"

(3)scripts/

该目录下是用于存储可执行代码脚本文件,例如python脚本,模型在对话中可以决定调用这些脚本——通过Shell工具执行它们。设计意图是:模型可以直接运行脚本而不把脚本内容读进 context。比如一个 rotate_pdf.py 脚本,模型只需要 python scripts/rotate_pdf.py直接运行,而不需要先花几百 token 读一遍脚本源码。这节省了大量 context。

(4)references/

SKILL.md是精简的说明书,references是完整的技术手册。模型不会自动读取这些文件,只有在SKILL.md中明确引导时才会去读。比如数据库 schema 文件、API 文档、公司政策。SKILL.md 正文中写清楚"什么时候该去读哪个 references文件",模型根据任务自行决定是否深入。如果文件超过 100 行,建议在文件开头加目录索引。

(5)assets/

该目录下是存储输出资源(模板、图标、字体)。这些文件不读进 context,模型只是把它们复制或修改后用在最终输出里。比如一个前端模板assets/frontend-template/,模型不需要理解模板内容,只需要复制到目标目录,通俗的说,就是一个资源文件。

了解了单个Skill的内部结构后,自然产生下一个问题:这些Skill目录放在哪里?Codex怎么知道去哪个路径找它们?这涉及到Skill的分层来源设计。

四个理论层级

Codex在架构上定义了四个Skill来源层级,按权威度从高到低,需要注意的是,当发生上下文预算不足时,会按照优先级进行裁剪,通常来说system级别skill不会被裁剪,user级别的skill优先被裁剪:

(1)System层级

在Windows系统下,位于C:\Users\你的用户名\.codex\skills\.system\,里面包含Codex自带的5个系统Skill,这些是codex自身提供的Skill,这些技能赋予 Codex 自我扩展能力,例如用户说"帮我创建一个技能",模型就会读到 skill-creator 的 SKILL.md,知道怎么用 init_skill.py 手架创建新技能;说"安装某个GitHub 上的技能",模型就会读到 skill-installer 的指令。

(2)User层级

用户自己创建的Skill就在这里,通常位于C:\Users\你的用户名\.agents\skills\。比如创建了一个叫my-helper的Skill,完整路径就是C:\Users\你的用户名\.agents\skills\my-helper\SKILL.md

(3)Admin层级

在Unix/Linux系统上,管理员可以把Skill放在/etc/codex/skills/,让整台机器的所有用户都能使用。但Windows没有/etc这个路径概念,Codex在Windows上不实现Admin层级。所以不会看到这个层级。

(4)Repo层级

这个层级不是全局生效的,而是取决于当前打开的项目。只有当一个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工程化能力的设计。

(1)SkillsManager如何加载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对象,包括:

  • name:Skill名称(如"imagegen")

  • description:完整描述

  • short_description:简短描述(用于目录展示)

  • path_to_skills_md:SKILL.md文件的绝对路径

  • scope:层级标记(System/Admin/Repo/User)

  • plugin_id:如果是Plugin提供的Skill,标记来源Plugin

核心实现在loader.rs:623 的 parse_skill_file() 解析每个找到的 SKILL.md:

loader.rs:623 的 parse_skill_file() 解析每个找到的 SKILL.md: // loader.rs:623 async fn parse_skill_file(fs, path, scope, ...) -> Result<SkillMetadata> { 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) -> Result<Arc<SkillLoadOutcome>, ...> { // 1. 检查缓存——如果配置没变,直接返回缓存结果 // 2. 配置变了,重新扫描:调用load_skills_from_roots() // 3. 过滤、禁用检查、构建隐式索引 // 4. 缓存新结果 }

(2)如何选出本次需要注入的完整Skill.md

SkillsManager产出了所有可用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的描述长度

  • 超了连裁剪描述都不够 → 按层级优先级省略Skill(System优先保留 > 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> <name>imagegen</name> <path>C:\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系统的存在,本质上是在这个矛盾之上做资源调度。

http://www.rkmt.cn/news/1519852.html

相关文章:

  • PP-OCRv6_medium_det源码深度解析:理解文本检测模型的实现原理
  • 2026甄选:合肥黄金回收服务部——专业评估与高价变现的诚信品牌机构 - 品牌发掘
  • GARbro:解密视觉小说游戏资源的瑞士军刀
  • Laurel与容器环境集成:Docker/Kubernetes审计日志采集最佳实践
  • 3个步骤解锁电脑新玩法:如何在Windows上轻松安装安卓应用
  • 手把手教你用MPU6050和STM32做个简易计步器(附防误判技巧)
  • 抖音无水印下载实战指南:3步掌握专业级内容获取技巧
  • TTS-Backup:Tabletop Simulator完整数据备份终极指南
  • portaudio流处理高级技巧:回调与阻塞模式对比分析
  • 2026年东莞石龙二手手机选购全攻略,这家为何稳居专业榜首? - 资讯速览
  • 实战指南:构建高效的Python量化分析系统与策略回测框架
  • 终极学术自由指南:如何用Unpaywall一键解锁付费论文墙
  • NXP SEC硬件安全引擎:IPsec与TLS协议卸载与性能优化实战
  • 三类GEO服务商如何选?深圳本土企业全意图优化实战指南 - GEO优化
  • 2026苏州学历提升红黑榜|这几家机构口碑最好,别再乱报了 - 学历提升信息早知道
  • 3分钟搞定网易云音乐歌词下载:LrcHelper让你的音乐体验更完美
  • i.MX23 PXP引擎寄存器配置实战:从图像处理到多层合成
  • 终极防撤回解决方案:3分钟学会保护微信QQ聊天记录不被撤回
  • 2026年东莞石龙二手手机市场大盘点,这家店为何脱颖而出? - 资讯速览
  • 2026年遵义市CPPM考试最新全攻略:科目题型、通过率、备考重点及官方双认证报考机构推荐 - 众智商学院课程中心
  • 在职读EMBA哪家机构靠谱?优质正规机构全面推荐盘点 - 品牌测评鉴赏家
  • 企业管理培训班怎么选?三大主流办学机构深度对比测评 - 品牌测评鉴赏家
  • 别再手动拼接了!Spring Boot + weixin-java-cp 5分钟搞定企业微信网页授权登录
  • Chrony NTP 时间同步服务器部署教程:替代 ntpd,搭建内网 NTP 服务
  • 智能体记忆系统设计
  • 2026效率榜!好用的降AI率工具全盘点,AI痕迹清零无压力!
  • 避坑指南:在Vivado里用Block Memory ROM做DDS信号源,这些细节千万别忽略
  • 从硬件到固件:OpenDeck支持的30+开发板兼容性清单与选择指南
  • 26届四川高三同学为啥扎堆走单招?单招十大硬核优势摆明白! - 锦成星火菁英单招
  • Linux 触发用户态到内核态切换的是:系统调用、中断与异常