这两天有个需求,要给手头的 Codex 启动模板新增一个筛选项。模板加新能力,听起来是个很常规的事,但我在动手之前先干了一件别人不太会注意的事:把 skills 目录里所有已有 Skill 全部拉出来过了一遍。为什么?因为我踩过太多次“功能写好了、模板反而变难用”的坑,后来发现根源往往不是代码质量,而是没想清楚这个新能力到底该由哪个 Skill 来承接。
如果你也在维护自己的 Codex 启动模板,或者正把一堆 Skill 塞进同一个配置里,这篇文章应该对你有用。我不会教你怎么从零写一个 Skill,而是讲我怎么在已有模板里“选”一个 Skill:先看什么、怎么判断、如何低成本验证、以及选完之后哪些默认行为必须改。内容全部来自实际维护模板时的经验教训,不一定是最优解,但至少能让你少踩几个我踩过的坑。
1. 先分清新增筛选项的类型,再翻 Skill 目录
很多人一拿到“新增筛选项”这个需求就直接去搜现成 Skill,或者打开 Skill Creator 准备自己造一个。我不一样,我会先停下来问一个问题:这个筛选项在 Codex 环境里,到底属于哪一类任务?因为“筛选”这个词在现实项目里太宽泛了,不同形态的筛选对应完全不同的 Skill 选择策略。
1.1 筛选项在我这里至少分成三种:输出过滤、上下文过滤、会话分诊
第一种是输出过滤。Codex 返回了一大段内容,你需要从这段结果里挑出符合特定规则的部分。举个例子,让 Codex 扫描项目代码后,把带TODO注释的片段整理成一份待办清单,这种就是典型的输出过滤。它发生在模型推理完成之后,本质上是结果后处理。
第二种是上下文过滤。在把内容喂给模型之前,先对候选文件或信息做一轮筛选,只让相关度高的部分进入上下文窗口。最常见的使用场景是:项目里有src/、vendor/、docs/一堆目录,你不想让模型把几千个文件全部读一遍,于是先按路径或扩展名过滤出真正需要的那些。这种筛选项发生在推理之前,直接影响模型“看到什么”,这也是我自己在模板里最常用的一类。
第三种是会话分诊。它不是单纯过滤文件或结果,而是把用户发来的请求按规则路由到不同的子 Skill 或处理流程里。比如“用户提到日志分析就走日志Skill,提到代码生成就走代码Skill”,本质上也是一种筛选,只不过筛的是会话上下文里的“意图”。
这三种形态差别很大。如果上来不分类,直接打开 Skill 列表按名字找,很容易被一个名字里带“filter”的 Skill 带偏方向。比如你想做的是上下文预过滤,结果选了一个专门处理后处理输出的 Skill,那整个链路都会对不上。
1.2 筛选动作发生在模板链路的哪个环节,决定了 Skill 的挂载方式
确定了筛选项属于哪一类之后,下一步是把它放到模板的数据链路里去看。我通常把 Codex 启动模板理解成一条流水线:
用户输入 → Agent 决策 → 上下文组装 → 模型推理 → 工具调用 → 结果后处理
筛选项可能挂在这条流水线的不同位置上。上下文过滤发生在“上下文组装”阶段,输出过滤发生在“结果后处理”阶段,而会话分诊则横跨“Agent 决策”和“工具调用”之间。
这个位置判断很重要,因为 Skill 的挂载方式必须和所在的流水线环节匹配。比如一个上下文过滤类 Skill,如果它的工作方式是输出一段“建议”而不是直接返回过滤后的文件列表,那它在模板里几乎没法用,因为模板上下文组装阶段需要的是确定性的结果,而不是一段建议文本。
我习惯用一个生活化类比来想这件事:上下文过滤就像进厨房前先把冰箱里不需要的食材挑掉,输出过滤就像菜上桌前再摆个盘。两者的工作位置完全不同,前者影响的是“能做出一道什么菜”,后者影响的是“这盘菜看起来怎么样”。选 Skill 时先确认你的筛选项是在“进厨房前”还是“上桌前”,方向就不会偏。
1.3 筛选标准是固定规则还是动态规则,直接决定 Skill 的复杂度要求
还有一个前置判断:这个筛选项的筛选标准是固定不变的,还是每次使用都会随用户输入变化?
固定规则很好理解。比如“始终排除node_modules和vendor目录”,这就是一条写死的规则。对应这种需求,一个轻量、纯规则实现的 Skill 就够了,不需要有很复杂的参数接口。动态规则则相反,比如用户可能传任意 glob 模式进来:今天筛*.md,明天筛*.py,后天筛最近 24 小时修改过的文件。这时候 Skill 必须提供一个明确的参数入口,能把用户的临时输入正确解析进筛选逻辑里。
这个判断影响选型方向:固定规则重点看 Skill 的“默认配置”是否合理,动态规则重点看 Skill 的“参数设计”是否顺手。很多 Skill 失败的原因不是规则写错了,而是它只能处理固定规则,却碰上了一个需要动态参数的真实场景。
2. 我看一个 Skill 能不能用,先盯住五个技术维度
做好了前置分类,我才开始正式逛 Skill 列表。但说句实话,技术圈里的 Skill 目录少说也有几百个,如果每个都点开读一遍 SKILL.md,一下午就没了。我现在会用一个固定的评估框架,快速筛掉不合适的,只对少数候选做深度验证。
我把这套判断维度归纳成五个,按重要性排序:输入输出契约、交互节奏、决定权归属、运行时开销、维护信号。下面逐个展开。
2.1 输入输出契约:Skill 的产出能不能被模板的下游节点直接消费
这是我最先看的一条,也是翻车率最高的一条。所谓契约,就是 Skill 接收什么格式的输入、返回什么格式的输出,以及这两端是否和模板现有的数据流对齐。
举个例子。我的模板里有一个后处理节点,它解析的是 JSON Lines 格式的结果。如果我引进一个过滤类 Skill,它输出的却是 Markdown 表格,那后处理节点根本拿不到内容,还得额外包一层解析器。这个解析成本如果一开始没算进去,后面就是无穷无尽的调试痛苦。
怎么判断契约好不好?我会看 SKILL.md 里对输出的描述。如果写的是“返回过滤结果”、“返回相关文件”这种模糊描述,我会直接降低优先级。如果明确写了“输出为文件路径列表,每行一个”或者“输出为 JSON 数组”,那说明作者至少考虑过下游消费的问题,踩坑概率小很多。
这里我要多说一句:很多 Skill 作者会把输出写得“很丰富”,既给结果又给统计信息还附上一段解释。看起来贴心,但实际上对模板不友好,因为多出来的内容会被下游当成正常输出的一部分来处理。输出纪律好的 Skill,往往比功能全面的 Skill 更适合放进模板。
2.2 交互节奏:它是显式命令触发,还是自动嗅探,还是混合模式
第二个维度看交互方式。Skill 在 Codex 里被唤起的方式大致分三类:
显式命令式最理想——用户或 Agent 明确调用,比如/filter-docs,命令发出去 Skill 才开始工作,不调用就不介入。自动嗅探式则相反,它声称“当用户提到文件筛选时,我就自动参与过滤”,看起来智能,但实际上会抢占很多上下文判断权。混合模式则是在显式机制之外,同时保留一组自动触发条件。
对筛选项这个需求,我个人强烈偏向显式触发。原因很简单:筛选动作天然带有“改变信息边界”的属性,如果它自动触发,模型可能在你没要求的情况下把某些结果悄悄过滤掉,这种隐性行为在模板环境里非常难排查。而且模板中一般不止一个 Skill,一旦两个会自动触发的 Skill 对同一段输入都有反应,就会产生意图冲突,最终行为完全不可预测。
选 Skill 时候看到“auto”、“detect”、“automatic”这类描述,我会额外提高警惕,除非它有非常清楚的触发边界,否则宁可放弃。
2.3 决定权放在哪里:到底是谁判断“要不要筛”
这一条是我后来才总结出来的,但我觉得它最关键。一个 Skill 好不好,不仅要看它能做什么,还要看它把自己的权力边界放在哪里。
好的 Skill 会把决定权交给调用者。它提供enabled、dry_run、on_miss这一类的开关,允许模板决定“什么时候启用、什么时候跳过、筛不到时怎么办”。它只是一个执行工具,不做越权的判断。
糟糕的 Skill 恰恰相反,它在上下文里看到两个候选文件,就自动帮你把“看起来不相关”的删掉。单独用的时候你可能觉得这很智能,但放进启动模板之后,你完全猜不到它在什么条件下会自作主张。模板里跑多个 Skill 的时候,最怕遇到这种“控制欲很强”的角色。
说实话,这个维度不像输入输出契约那么好量化,但它其实是所有维度里最有预测价值的。Skill 对自己权力边界的描述越清楚,它在复杂环境里的表现就越稳定。
2.4 运行时开销:加载体积、Token 消耗、外部依赖都要算进去
第四个维度我一般放在选型的后半段,但绝不跳过。Skill 不是免费的,它进入模板之后会带来至少三方面的成本。
第一是加载开销。Skill 本质上是 SKILL.md 正文加一组脚本,而 SKILL.md 会占用上下文窗口。如果模板里挂了十个 Skill,每个 1500 token,一次会话光 Skill 定义就吃掉了 15000 token,还没开始干活,预算已经烧了一截。所以筛选项这种低频能力,没必要让它常驻,尽可能选支持懒加载或轻量定义的。
第二是执行开销。有的过滤类 Skill 会在内部调用大模型来“理解”被过滤内容,这在小样本场景下可用,放到批量筛选上百个文件时就完全不现实。对筛选类任务,我优先选那些基于确定性规则实现的候选,比如 glob、正则、文件名后缀判断,而不是“AI 判断相关度”。
第三是外部依赖。有些 Skill 每次调用都要访问在线接口,这在本地单测环境下可能正常,但放进模板跑通后,一旦外部服务不可用,整个筛选动作就静默失败。后面章节我会详细讲我踩过的这个坑,这里先给结论:外部依赖越少,Skill 在模板里的可预测性越强。
2.5 维护信号:文档质量、更新频率、示例完备度一眼判断
最后一个维度是低成本判断一个 Skill 是否值得信任。我不可能对每个 Skill 都做完整测试,所以会先看几个外在信号。
第一看有没有示例输入输出。好的 Skill 至少会带一个 sample 或者 usage 说明,带样例说明作者真的跑通过。第二看版本兼容描述。它是否标注了适配的 Codex 版本范围?如果完全没提版本,说明它可能只是在某个特定环境下跑通过一次,兼容性无从谈起。第三看最近更新频率。Github 仓库三个月没动过不代表不能用了,但至少说明已经有段时间没人在实际环境中打磨它。
如果五个维度都过关,这个 Skill 才会进入我的候选池。否则哪怕它有再漂亮的功能演示,我也不会让它进启动模板,因为模板的稳定性比单点功能的惊艳重要得多。
3. 用两条 Skill 实际走一遍“看它怎么选”的决策路径
上面的框架有点抽象,我用一个近期实际场景带大家走一遍完整路径。
场景是这样的:我需要在启动模板新增一个筛选项,要求是“只把当前任务相关的 Markdown 文档放进上下文”。具体说,项目根目录下一堆.md文件,但只有本次任务涉及的若干篇需要被模型看到,其他文件要过滤掉。候选来了两个:FilterDocs 和 PathScope,名字看着都挺合适,我分别做了评估。
3.1 候选 A:FilterDocs——轻量过滤型 Skill,优先考虑
FilterDocs 是一个专门做文档列表过滤的 Skill,核心逻辑是基于 glob 规则的包含/排除,不依赖外部 API。我看到的 SKILL.md 大概是这样的结构:
--- name: FilterDocs when_to_use: 需要按文件路径或扩展名过滤文档列表时 input: 文件路径列表 + 过滤参数 output: 过滤后的文件路径列表 --- 使用 glob 规则对输入文件列表进行过滤,支持 include 和 exclude 参数。先过一轮五个维度。输入输出契约很干净,输入是文件列表加参数,输出是过滤后的文件路径列表,正好能被我模板里的上下文组装节点消费。交互节奏是显式命令式,调用方式明确。决定权也交到了调用者手里,它有--dry-run参数,可以先看效果不实际执行。运行时开销很低,就是本地 glob 匹配,没有 token 消耗。维护信号算是中等偏好,文档里有示例,目录结构也比较规范。
评估下来,这个 Skill 可以直接进入冒烟测试阶段。唯一要注意的是它的默认参数,后面章节再说怎么改。
3.2 候选 B:PathScope——会话路由型 Skill,功能更强但冲突风险高
PathScope 看起来更强大,它的定位是做多轮会话中的范围感知,能自动判断当前工作区,并根据对话历史调整后续上下文策略。单看描述,它不仅能筛文件,还能管理整个会话的信息边界。
但放进我的评估框架里,问题出现了。输入输出契约这一关就过不去:它输出的不是文件列表,而是一段“范围变更指令”,需要模板有额外的解析层才能消费,而我的模板并没有这一层。交互节奏是自动嗅探式,它会根据对话内容自行决定是否调整范围,这和我模板里已有的 Agent 调度逻辑会产生重叠。决定权归属也偏向它自己,它内置了比较强的自动决策逻辑,一旦启用,后续上下文策略会被它接管。
运行时开销也高一些,因为它需要维护会话状态的记录和更新,不是一次简单的过滤,而是持续的上下文管理。维护信号倒是不错,文档和示例都很完整,但完整不代表兼容。
结论很明确:PathScope 不是坏 Skill,它只是不适合承接这一次“新增筛选项”的需求。我要的是一个能被模板紧密控制的过滤能力,而不是一个会反过来影响 Agent 行为的范围管理器。
| 维度 | FilterDocs | PathScope |
|---|---|---|
| 输入输出契约 | 输出文件路径列表,直接可消费 | 输出范围指令,需要额外解析层 |
| 交互节奏 | 显式命令式调用 | 自动嗅探式介入 |
| 决定权归属 | 提供 dry-run 和开关,调用者主导 | 内置自动决策,Skill 自身主导 |
| 运行时开销 | 极低,本地 glob 匹配 | 较高,需要维护会话状态 |
| 维护信号 | 有示例,文档规范 | 文档完整,但设计方向不同 |
3.3 用最小样本做冒烟测试,比读一百页文档都有效
选型评估做得再细,也比不上一次真实的实测。在把 FilterDocs 接入模板之前,我会先做一个十分钟级别的冒烟测试。
第一步,备份当前模板配置。改模板前备份是基本操作,尤其当你的模板里已经有十几个 Skill 的时候,回滚能力是底线。第二步,把 FilterDocs 放进 skills 目录,但先不启用它的自动模式,保持显式调用。第三步,构造一个小样本输入:项目里放三个文件,一个README.md、一个docs/guide.md、一个lib/utils.py,然后下发过滤指令“只保留*.md文件”。预期结果是过滤后只剩前两个文件。
第四步最关键,我会在“不启用 Skill”和“启用 Skill”两种状态下各跑一次,对比输出差异。这样能看出 Skill 是否真的在起作用,以及它对正常输出有没有副作用。第五步,检查模板的日志输出,看被过滤掉的文件是否有记录。如果一个筛选项工作完不留任何日志,那它日后出问题时会非常难定位。
整个测试下来就五六分钟,但它能覆盖选型判断里最核心的三个问题:输入输出是否匹配、是否引入额外开销、失败时是否能被感知。只要这三个问题没有明显问题,这个 Skill 就可以进入下一步的模板定制阶段。
4. 选中 Skill 之后我不会直接焊进模板,至少要改这些默认行为
很多人觉得选好 Skill 就能直接用了,其实大错特错。任何一个第三方 Skill 都带着作者自己的假设,这些假设未必适配你的启动模板。我每次接入新 Skill,都会按下面的清单做一轮定制,核心原则是:把 Skill 变成模板里的一个可配置零件,而不是一个黑盒。
4.1 把硬编码阈值和规则列表改造成模板变量
FilterDocs 默认的排除路径里带了一组硬编码规则,比如assets/**、node_modules/**。这些规则在我的一些项目里是对的,但在另一些项目里会误伤正常文件。我不能每次使用都改一遍 Skill 源码,所以我会把它改造为读取模板的变量配置。
# config/template.yaml filter: enabled: true max_candidates: 200 include: ${FILTER_INCLUDE_GLOBS:-["*.md"]} exclude: ${FILTER_EXCLUDE_GLOBS:-["vendor/**", "node_modules/**"]}这样改完之后,不同项目可以通过环境变量或者项目级配置覆盖默认值,模板的复用性会明显提升。启动模板的意义就在于可复用,如果每个 Skill 都带一套改死的规则,模板就退化成一次性脚手架了。
4.2 为筛选失败设置显式的降级路线
筛选类 Skill 最容易出现两类异常。一类是筛完之后结果为空,比如排除规则写过头了,把所有文件都过滤掉了;另一类是筛完之后结果仍然巨大,比如 include 规则太宽,几千个文件都命中,超出上下文承载能力。
针对这两类情况,我会在模板层设置降级策略。空结果时执行on_empty: pass_through,也就是把原始文件列表原样返回,同时在日志里标记“filter produced empty set, fallback to raw”;结果过大时执行on_overflow: truncate,截断到前 N 个文件,并提示调用者手动指定更精确的范围。
很多人不做这层处理,结果就是 Skill 第一次用挺好,第二次换了个项目路径就返回空列表,模型拿到的上下文全是空的,生成的回答自然莫名其妙。降级策略是模板工程和单纯写个 Skill 之间的关键分水岭。
4.3 修改触发条件描述,避免抢其他 Skill 的戏
第三个我一定会改的地方是 SKILL.md 里的when_to_use。第三方 Skill 作者通常会把触发条件写得比较宽,好让 Skill 在更多场景被唤醒。但模板里同时存在多个 Skill 时,过宽的触发条件会让模型陷入“选择困难”,在几个候选 Skill 之间犹豫不决,甚至选错。
我一般会把 FilterDocs 的触发条件改成更窄更明确的版本。比如原来写的是“当用户提到文件筛选时使用”,我会改成“仅当用户明确请求过滤文档内容,且当前 Agent 未指定使用其他 Skill 时,才考虑启用”。同时我会在模板的全局提示里加一句约束:除非用户明确要求,否则不要自动进行上下文过滤。
这一步其实是在治理 Skill 之间的边界。技术圈里经常讨论 Skill 和 Agent 的区别,我的理解是,Skill 是能力,Agent 是决策者,而模板要做的事情就是确保决策者不会在下层能力面前失去控制权。
4.4 给新 Skill 建立一套最小回归样本
最后一步不是改代码,而是建测试资产。我会在模板仓库里维护一个samples/filter/目录,里面至少放三段测试输入。
第一段是正常过滤场景,输入里既有应保留的文件也有应排除的文件,验证基本过滤逻辑。第二段是边界场景,故意提供一个匹配不到任何文件的过滤规则,验证空结果降级策略是否生效。第三段是长会话场景,在第二条消息才触发过滤,看看 Skill 是否因为前一轮上下文残留导致误判。
每次我给模板新增 Skill 或者调整过滤规则,都会先跑这三段样本。模板类的项目最怕的不是功能不够,而是改一处挂三处,回归测试能在五分钟内把这种风险降到最低。
5. 三次选型翻车实录:从现象到根因的完整排查链路
讲了这么多方法论,最后分享三个我真实踩过的坑。这三个案例都没有按上面的框架走,所以都付出了实打实的调试时间。把它们写出来是想说明一件事:选型失误的代价往往不是一开始爆出来的,而是藏在后面的某次诡异故障里。
5.1 表面能筛,真到下游全乱套:输出格式的隐性污染
第一次翻车是选了一个名字叫 SummaryFilter 的 Skill。当时我看它功能很契合需求,能筛代码片段,还能附带统计信息,觉得“多给点信息挺好”。放进模板之后,最开始几天没出问题,直到某次生成结果的末尾多出了一段 Markdown 表格,下游的编译脚本开始不定期报错。
排查时我先怀疑是模型生成不稳定,但复现概率不高,很难抓到现场。后来打开模板日志,发现每次异常都有一个summary_tool的调用记录,再翻 SummaryFilter 的源码,才看到它每次做完过滤都会额外输出一段统计信息,而这些统计信息被模板的下游节点当成了正常结果的一部分。
这个坑的根因就是我在 2.1 里说的输出契约问题。那个 Skill 的 SKILL.md 里写着“返回过滤结果及统计”,作者本意是好的,但模板环境没有“统计信息走旁路”的概念,所有输出都被当成主链路内容。修复方式很简单,改掉它的输出逻辑,让统计信息写日志而不是写响应。但这次排查让我明白,选 Skill 时看输出的“纯度”比看功能列表重要得多。
5.2 在线服务一旦不可达,整个筛选动作全军覆没
第二个坑是选了一个依赖在线分类接口的 Skill,用来做“相关度筛选”。在本地单测环境里效果惊艳,它能把不相关的文件自动归到“低相关”类别,我也没想太多就接进了模板。
结果模板在大批量场景下频繁返回空结果。一开始我还以为是数据路径配置错了,清缓存、反复切换输入都无济于事。最后打开 debug 日志,只看到一行不起眼的request failed, skip。
真正的问题暴露了:这个 Skill 每次做筛选前都要调用一个外部分类接口,而模板运行环境为了降低延迟,配置的是本地模型,那个在线地址根本不可达。更要命的是,Skill 源码里把请求异常直接吞掉了,返回了一个空列表,导致下游还以为筛选结果是准确的。
从那以后我给自己定了一条规则:进入启动模板的 Skill,优先选能够在纯本地、确定性规则下完成任务的。那些依赖外部接口的 Skill 不是说完全不能用,但必须要有清晰的降级策略,并且失败时要么大声报错,要么保留原始内容,绝不能静默返回空集。
5.3 权限配置太严,筛选动作在部分会话里直接不触发
第三个坑很有隐蔽性。当时模板升级了一版,把所有 Skill 的权限分级管理。大多数 Skill 正常工作,但 FilterDocs 在部分用户消息上就是不触发。
一开始我以为 SKILL.md 写错了,检查了一遍触发条件,发现它的when_to_use被我改得太窄,里面充斥着“如果用户提到文件”“并且最近上下文包含文档路径”这种带多个条件的表述。模型在短指令场景下根本没有足够信息命中这些条件。
再往下查,又发现模板的全局策略把 FilterDocs 的默认权限降级成了“需要显式许可才执行”,而我在部分消息上没有加显式授权标记。两件事叠在一起,导致这个 Skill 成了半瘫痪状态。
这个案例给我的教训是:触发条件不是越窄越好,而是要在“精准”和“可命中”之间找平衡。权限控制更是要在模板层面统一治理,不能让使用者每次都要去猜某个 Skill 当前是什么权限等级。现在我的处理方式是:筛选类 Skill 用显式命令触发,权限固定为“允许执行”,但结果必须经过降级策略检查后才进入主流程。
5.4 从三次翻车里沉淀下来的选型教训
把这三件事放到一起看,能提炼出几条对后续选型很有帮助的教训。失败模式永远比功能清单重要,判断一个 Skill 适不适合模板,先看它出问题时是什么表现;输出格式越简单越不容易被下游误读,任何“额外信息”都可能变成污染;优先选能在本地用纯规则验证的 Skill,外部依赖在长链路里就是隐患;Skill 的默认行为必须可关闭、可降级,不能让它拥有超出预期的自主权;最后,所有 Skill 进模板前都要有三段以上的回归样例,不测试不接入。
现在我在模板里长期维护着一张候选 Skill 速查表,每考察一个 Skill 就把五个维度的评估结果填进去,标上验证日期和测试结论。有了这张表以后,再遇到“新增筛选项”这种需求,我的响应速度比原来快了不少。大部分时间其实不是花在写代码上,而是花在判断“该选谁”,这个判断框架一旦建立起来,后续所有类似需求都能套着走。