大家好,我是小木头。在之前的视频里,咱们已经提到过很多次 Skills 在 Agent 中是怎么工作的。以 Codex 采用的渐进式披露为例,它会先加载 Skill 的 name 和 description,等到真正需要使用某个 Skill 的时候,再去读取它的说明。那么是不是所有的 Agent 都是这样做的呢?只要把所有的 Skill 的名字和描述放进上下文就可以了吗?

在每一轮对话里,当我们临时提到一个 Skill 的时候,它是怎样进入当前会话的呢?完整的 SKILL.md 会放在哪里?到了下一轮它还在不在呢?还有一个更实际的问题,如果 Agent 正在运行,我们又安装了一个新的 Skill,或者修改了原来的 Skill,它什么时候才能被发现呢?又会怎样进入后续的上下文呢?

这背后还有不少值得关注的技术细节,包括 Skill 的发现、目录更新、当轮加载,以及它在会话历史中的保存和恢复。如果你对 Agent 开发感兴趣,也想进一步了解 Skills 在真实系统里是怎么运转的,那就跟着我一起开始今天的分享吧。咱们先从 Skill 的发现开始。

在 Codex 里,Skill 并不只来自于一个固定的目录。它可以来自当前仓库,可以来自用户目录,也可以由管理员或 Codex 自己提供。安装插件以后,插件也可以带来新的 Skill。除此以外,如果当前会话连接到了其他的执行环境,那个环境还可以提供自己的 Skill。

所以 Codex 面对的不是一个简单的 Skills 目录,而是多个来源共同组成的 Skill 目录。对于本地 Skill,Codex 可以记录它的 SKILL.md 路径;对于远程执行环境或者 orchestrator 提供的 Skill,本地并不一定存在这份文件。这时记录的就不再是普通的文件路径了,而是一个 package 或者是一个资源定位器(resource locator),后续再通过对应的读取的接口来获得正文。

这也是第一个容易忽略的细节:Skill 的名字只是给人和模型看的显示信息,真正确定它身份的是它来自哪里,以及怎样定位到它的正文。Codex 发现这些 Skills 以后,会把它们组织成一段 developer context,外层使用的是标签 skills_instructions。里面包含了 Skill 的名字、描述以及定位正文所需要的路径或者是资源定位器,也就是刚才我们提到的 resource locator。

大家已经知道了,这里并不会把所有 Skill 的 SKILL.md 正文全部展开,它只是给模型一张能力的目录,让模型知道现在有哪些工作流可以使用。不过这张目录也不是完全不受限制的。按照当前 Codex 代码的实现来讲,如果模型的上下文窗口已知,Skill 元数据默认最多使用到整个窗口的 2%;如果窗口大小未知,就使用 8,000 个字符作为退路。

用户也可以自己设置上限,但当前配置最多是一万个 token。如果 Skill 很多,Codex 会先缩短 description;仍然放不下时,可以把描述都移除掉,尽量留下名字和定位信息。再继续超出预算,就只能省略部分 Skill 并向用户发出 warning。那大家在使用中有收到过这类警告吗?

这里有两个不同的预算不能混在一起:2% 限制的是 Skill 目录,不是被选中以后 SKILL.md 正文只能使用百分之二。目录负责让模型完成选择,正文进入的是后面的加载阶段。当 Skill 来源越来越多时,一定会出现同名问题。比如仓库里有一份 deploy Skill,用户目录里也有一份 deploy,某个插件又提供了第三份 deploy。

Codex 不会把它们自动合并,也不会简单规定后发现的覆盖前面的。如果用户只在普通文本里写 $deploy(注意,$ 符号是在 Codex 中典型的引用 Skill 的一种接口或者方式),当前实现只有在这个名字能够唯一的对应一份已经启用的 Skill 时,才会完成选择。如果同名 Skill 不止一份,或者还有一个同名 Connector,Codex 不会随便挑一个。

在 Codex 的界面或者命令行里选择 Skill 时,传入的其实是结构化的信息,其中同时包含了 name 和 path。name 用来显示,path 用来消除歧义。这对 Agent 能力管理很重要:随着能力越来越多,不能把显示名当成一个稳定的身份。更可靠的做法当然是给每一份能力保留明确的来源和定位信息。

接下来回答开场提到的非常关键的一个问题:当我们在某一轮对话中临时选择一个 Skill,Codex 会从当前的 Skill snapshot 里解析它。这个 snapshot(或者叫快照)可以理解为这一轮开始时,Codex 对可用 Skills 所生成的一个稳定的视图。解析成功以后,Codex 才会读取对应的 SKILL.md。

读取出来的正文会被包装成一段单独的 skill 标签片段,其中包含了 Skill 的名字、路径和工作流说明。这段内容在模型请求里使用的是 user role,它还有一个独立的内容类型叫做 skills.selected_skill_instructions。所以它不是系统提示词,也不是把原来用户问题改写掉,原始用户消息仍然存在。

Skill 正文是 Codex 为这一轮额外加入的一条模型可见的上下文,这一段用 Skill 标签标记的内容会进入到会话记录,而不是只在本轮临时使用。之后 Codex 组织下一轮输入、保存会话或者重新打开会话时,都知道它是一段有 Skill 注入的说明。不过正文能否完整的进入上下文呢?还要看它经过哪条加载路径。

当前代码中有一条本地加载路径,会直接使用读到的整份 SKILL.md;插件 Skill 以及统一扩展加载路径中的 Skill,则会受到 8 千字节的上限限制。超过以后,正文会被截断,同时给出提示。因此还不能够笼统地说被选中的 Skill 一定会把整份的正文完整的放进上下文。这里需要分三个问题:

第一个问题是模型还能不能看到上一轮加载过的 Skill 正文。正常情况下,这个 Skill 标签片段已经进入了会话历史,只要它还在当前模型的历史中,下轮请求就可能继续看到它。第二个问题是下一轮是否会重新激活这个 Skill?这并不是同一件事,历史里存在了旧的正文,不等于还保存着一个这个 Skill 已经激活的隐藏开关。

显式选择发生在当前的对话轮次,新一轮是否继续遵循旧的说明书,取决于模型仍能看到的上下文,以及这轮是否再次地显式或隐式地触发它。重新触发时,Codex 才会再生成一份新的 Skill 注入。第三个问题是重新提及时会不会再次读取文件?这是会的,对已经能够从当前快照解析出来的 Skill,新一轮显式提及会再次读取正文。

因此磁盘上的 SKILL.md 修改后重新选择可以读到新的正文,不过名字、描述和可见目录属于发现结果,仍可能受到缓存的影响。新增的 Skill 或者修改元数据,需要先等自动检测、force reload(也就是强制重新加载),必要时重启。所以咱们可以用一句话概括:在压缩之前,旧正文可以留在历史里面,但 Skill 的显式选择是当前对话轮次为单位的。

历史可见、当轮重新注入和重新读取是三件不同的事情。前面说 Skill 目录属于是开发者的上下文,也就是 developer context,但 Codex 并不是每轮都向历史末尾机械的追加一份完全相同的目录。当前实现把它作为一个 world state 在管理,每一轮都会根据当前的配置和环境计算有效的 Skills 状态。

如果新的状态与之前相同,就不需要再在 rollout 里追加一份重复的快照。模型请求在重建上下文时仍然会保留一份当前有效的目录,如果 Skill 发生变化或者当前执行环境发生变化,world state 才会产生一个新的更新。这解决了两个问题:一方面模型在每一轮都需要知道现在有哪些 Skills 可以使用,另一方面相同的 Skill 目录又不应该随着对话轮次不断重复占用越来越多的历史空间。

那么如果 Codex 正在运行,我们又安装了一份新的 Skill 会发生什么呢?首先写入磁盘不等于这份 Skill 进入了当前对话轮次的快照。Codex 的 Skill 发现存在缓存,skills/list 也可以复用工作目录保存的结果。产品层面支持自动检测 Skill 的变化,代码里也提供了 force reload(也就是强制重新加载),要求重新扫描磁盘。

如果更新没有被发现,官方文档给出处理方式是重新启动 Codex 刷新。完成以后新的目录会作为一个 world state 更新,进入后续上下文。它不需要重写前面所有的会话历史。这里还有个安全边界:即使用户在文本中写出了一个 Skill 的名字或路径,Codex 仍然要在已经发现、已经启用的 Skill 集合里面去解析它。

不能把任意的一个文件路径直接伪装成 Skill,绕过发现和配置层。Codex 还把安装和启用分成两个状态:用户可以在配置文件里面按照 name 或者是路径禁用某个 Skill,不需要删除它的文件。禁用之后它不应该进入可用目录,也不能通过普通的提及来加载它。

一份 Skill 还可以通过向 Agents agents/openai.yaml 配置文件设置一个选项叫 allow_implicit_invocation。默认情况下,Codex 可以在任务与描述匹配时隐式的选择 Skill;如果把这个字段设为 false,模型就不应该只因为任务匹配而自动调用。但是呢用户可以仍然显式的选择使用这个 Skill,这种设计适合有外部副作用、成本较高或者必须由用户明确确认的工作流。

因此在一个完整的 Agent 系统里,至少要区分三件事情:能力是否已经安装、当前会话是否启用,以及它是否允许隐式的调用。Skill 自己是一份工作流说明,但它可以依赖其他工具。比如一份 Skill 可以在 openai.yaml 里声明需要某个 MCP Server。在当前源码中,第一方 Codex 客户端对本轮显式选择的 Skill,会在以下功能启用时检查这个依赖。

这项功能叫 skill_mcp_dependency_install,这个功能当前默认是开启的。它不是所有 Skill 的来源,也不是所有兼容客户端必然执行的一个通用的协议。如果没有安装还要继续经过策略和权限的判断:某些情况下可以自动的批准,某些情况下需要询问用户;还有一些环境会因为 approval policy 而直接跳过。

用户允许以后,Codex 才会写入 MCP 配置,必要时呢完成 OAuth,然后刷新当前会话里的 MCP runtime。所以一份 Skill 被模型选中并不代表它依赖的能力可以使用了,完整的流程更接近于是这样的:先确认 Skill 的身份,再加载说明,然后检查依赖,经过权限判断,最后呢才进入真正的执行。

这是为什么 Skill、plugin 和 MCP 不能混成一个概念:Skill 描述工作流,MCP 提供可以调用的工具,plugin 则可以把多份 Skills、MCP 配置和其他资源打包在一起进行分发。如果系统里不只有一个 Agent,Skill 管理还要考虑到继承的问题:一个子 Agent 可能有不同的工作目录、角色配置、执行环境和权限,它需要的 Skills 也可能与父 Agent 不同。

Codex 不要求子 Agent 的有效目录与父 Agent 完全相同,新的对话轮次会使用当时有效的配置和环境取得一个 Skill 的快照。因此子 Agent 能力视图可以与父 Agent 不同,源码测试里就有这样的一个情况:父 Agent 能够使用某个 Skill,但子 Agent 使用了另一份 role 配置。这份配置把同一个 Skill 禁用了,于是它不会进入子 Agent 的有效能力视图。

这给多智能体系统一个很实用的启发:共享一套 Skill 仓库不等于每个 Agent 都应该得到完全相同的能力。Skill 目录应该根据 Agent 的职责、环境和权限重新筛选。最后再把这些内容与之前介绍过的上下文工程我们连接到一起:Codex 里的 Skill其实有两条不同的生命周期。

第一条是当前可用 Skill 的 Skill 目录,它更接近于 world state,可以根据当前的工作目录配置和执行环境重新发现并建立。第二是某一轮实际加载过的 Skill 标签的正文,它更接近于会话历史,记录的是当时模型真正看过哪一份工作流说明。进行回滚或恢复时,Codex 能够把这个 Skill 标签识别为一种 contextual user message(也就是上下文中的用户消息)。

但经过上下文压缩以后,旧的 Skill 标签片段不会作为逐字的用户历史或聊天历史保留下来。本地压缩会用包含当时历史的模型输入来生成摘要,再用真实的用户消息和摘要重建历史。远端压缩(也就是 remote compaction)返回的压缩后的历史也会过滤这种非真实用户的上下文包装。

它的含义可能会进入摘要或者服务端返回的 opaque compaction item,但客户端呢不能验证其中是否以及怎样保留了 Skill 的具体说明。但是当前有哪些 Skills 可以使用并不完全依赖这些旧消息,Skill 目录可以根据当前的状态重新提供给模型,由它来确定使用哪些 Skill。所以恢复一个 Agent 会话时不能只恢复聊天记录也不止扫描 Skill 目录,更完整的做法是同时恢复当时发生过的上下文历史并重新计算现在有效的能力状态。

到这里还没有结束,Skill 管理还必须处理失败:SKILL.md 可能缺少名字或者描述,YAML 可能无法解析,路径可能无法读取;Skill 目录可能超过预算,正文也有可能过长,依赖的 MCP 可能无法安装,OAuth 也可能失败。Codex 对这些问题会产生不同的告警:例如目录里的描述被缩短了多少,有多少 Skill 被忽略或被省略;一份显式选择的 Skill 是否成功注入,以及这次调用属于显式还是隐式。

对于 Agent 开发者来说,只统计安装了多少 Skill 没有太大的意义,更应该关心的是有多少被成功发现、多少进入了模型可见的目录、多少在选择后成功加载,又有多少因为依赖和权限无法真正执行。基于这些源码行为,咱们可以把一份 Skill 在智能体里的旅程提炼成大约 7 个阶段。注意我们是为了理解系统做的归纳,这并不是 Codex 官方命名的七层架构:

首先呢是 discover(发现),从多个来源发现 Skill,只读取最小的元数据集合。第二是 Skill 目录,在严格预算内把可选的能力或 Skill 提供给模型。第三呢解析,用稳定的路径或资源定位器来处理同名的冲突。第四呢激活,把被选中的工作流说明作为一条独立的上下文注入当前的对话轮次。第五呢 authorize,检查隐式的调用策略、工具依赖和用户权限。

第六持久化(也就是 persist),区分当前能力状态与会话历史、真正加载过的说明。第七 observe,记录发现、裁剪、注入和依赖的失败。所以 Skill 并不只是一个带有名字和描述的 Markdown 文件,对于一个真正运行的 Agent 来说它还需要一套发现机制、需要路由权限状态和恢复机制。而 Codex 当前的实现正好让我们能够从源码里看到这整套机制是怎样组合起来的。

以上就是今天的分享,如果这期内容对你理解 Agent Skills 有帮助也欢迎点赞收藏并关注我的后续视频。好了,我是小木头,咱们下期再见。