四家的 skill 都叫 skill,装法和触发完全不是一回事

2026-08-17

先说清楚这篇的起点:手上有一个写好的 SKILL.md 目录,想让它在几个不同的 agent 里都能用。名字都叫 skill,格式看着也都是「frontmatter + 一段 Markdown 指令」,于是很容易默认「拷过去就行」。真去翻源码和文档,会发现四家在四个环节各自划了不同的边界:目录放在哪才会被发现、同名的两个技能谁赢、谁有资格触发它、触发后它以什么形态进上下文、活多久。下面按这四个环节走一遍,每处都标出可以自己去核的位置。

有两条前提要先摆出来。其一,DeepSeek Harness 的 README 在「Developer preview」一节里自述该项目处于开发者预览阶段、并用大写强调会有破坏兼容性的变更,所以下面提到的字段名、默认值、目录层级都可能随版本变动。其二,Codex 仓库里的 docs/skills.md 全文只有一句话,把读者指向一个站外文档链接;我们不联网,所以 Codex 这边只用仓库内的 codex-rs/skills 源码作依据,凡是源码里读不出来的,本文就不比。

一、放在哪才会被发现

DeepSeek Harness 把「谁提供技能」和「技能从哪来」拆成了两层。packages/skill 下我们数出 4 个包:服务定义(ctx.skillspackages/skill/skill)、本地文件系统 provider(packages/skill/skill-filesystem)、一个可选的打包 badge provider(packages/skill/skill-badgedocs/subsystems/skills.md 原文称其为 optional packaged badge provider)、以及持有模型侧 skill 工具的消费者(packages/skill/tool-skill)。真正扫盘的是本地 provider,它按固定顺序扫六个根,docs/subsystems/skills.md 里给了一张 rank 表,源码里的常量与之对得上:<projectRoot>/.dsh/skills 是 100、<projectRoot>/.agents/skills 是 200、配置里的 customSkillDirs 是 300、<dshHome>/skills 是 400、<agentsHome>/skills 是 500,配置了 bundled 目录时是 600(BUNDLED_SKILL_RANK 定义在 packages/skill/skill/src/index.ts)。这里的 projectRoot最近一个含 .git 的祖先目录,找不到就拿当前 cwd 当项目根。另外文档明写:用户侧的 DSH 根会跳过它的 .system 子目录,本地 provider 自己不合成任何内建系统技能。

Claude Code 官方文档给的是四行位置表:企业级、个人级 ~/.claude/skills/<skill-name>/SKILL.md、项目级 .claude/skills/<skill-name>/SKILL.md、插件级 <plugin>/skills/<skill-name>/SKILL.md。它多出一条别家没有的规则:嵌套目录里的技能启动时不加载,要等 Claude 第一次读或改了那个子目录里的文件才变得可用,此前既不出现在自动补全里也不能按名调用;文档同时写明,父目录方向会一路向上加载到仓库根。

Pi 的文档把位置列成了清单,还额外给了一条发现规则上的区分:在 ~/.pi/agent/skills/.pi/skills/ 里,根层直接放的 .md 文件会被当成单个技能;而在 ~/.agents/skills/ 和项目 .agents/skills/ 里,根层的 .md 文件被忽略——只有含 SKILL.md 的目录才会被递归发现。项目级位置还有一道前置:文档写明只有项目被 trust 之后才加载。Pi 也是四家里唯一在文档中直接给出「复用别家目录」写法的:在 settings 的 skills 数组里填 ~/.claude/skills~/.codex/skills

Codex 这边源码里能确认的是另一件事:codex-rs/skills/src/lib.rs 会把编译期内嵌的系统技能安装到 CODEX_HOME/skills/.system,装之前先算一个指纹写进 .codex-system-skills.marker,指纹没变就整段跳过,变了就先删掉整个 .system 目录再重写。内嵌来源是 src/assets/samples,我们数出 6 个样例目录。注意这跟 DeepSeek Harness 的做法正好指向两个方向:dsh 的本地 provider 明说不合成内建技能、并主动跳过 .system,而 Codex 把内建样例实实在在写进了那个目录。说完差异就停——这两处只是各自的公开事实。

二、同名的两个技能谁赢

这是搬目录最容易踩的一格,因为四家的口径不一样。

DeepSeek Harness 是分层 + 层内排名两级:注册按调用上下文的 scope 落进不同层,读的时候把全局层和当前 scope 链合并,近的层直接赢,上面那张 rank 表只在同一层内部裁决重名。也就是说在同一层里,项目根下的 .dsh/skills(100)排在用户目录(400/500)前面。

Claude Code 官方文档写的顺序方向相反:跨级冲突时 enterprise 覆盖 personal,personal 覆盖 project,文档还专门举了例子——同名 deploy 同时存在于 ~/.claude/skills/ 和项目 .claude/skills/ 时,/deploy 跑的是个人的那个。插件技能走 plugin-name:skill-name 命名空间,因此不与其它级冲突;嵌套目录的同名技能会以目录限定名(文档例子是 apps/web:deploy)并存。

Pi 的处置最简单:文档写明重名会 warn,并保留先找到的那个。Codex 的 loading.rsSkillRootLoader 的文档注释把「root precedence」明确列为实现方的职责,我们没有在 codex-rs/skills 这个 crate 里找到跨根优先级的具体裁决表,所以这一点不比。

这个差异什么时候咬人:你在个人目录里留了一份旧版同名技能,然后往项目里提交了新版。在 DeepSeek Harness 的同一层里项目那份 rank 更小,在 Claude Code 的口径里个人那份会覆盖项目那份,在 Pi 那里取决于哪个先被扫到。同一次「怎么改都不生效」,三家的排查方向不一样。

三、谁能触发它

DeepSeek Harness 把可调用性归成两个独立开关,落在 frontmatter 的两个 kebab-case 键上:disable-model-invocationuser-invocable,缺省都按 true 处理,packages/skill/skill-filesystem/src/index.ts 里把它们规整成 modelInvocable / userInvocable 两个正向布尔。两处细节值得记:一是布尔值除了 true / false,还接受 yes / no / on / off / 1 / 0;二是它对驼峰写法直接抛错——disableModelInvocationmodelInvocableuserInvocable 三个键一旦出现在 frontmatter 里,就抛 frontmatter field "..." is unsupported; use "..."。搬运别家目录时这是最先炸的一处,好在它炸得很响。

Claude Code 官方文档的这两个字段同名,但默认值不在 frontmatter 表里,那张表只写字段语义;默认行为写在「Control who invokes a skill」一节的正文里:默认你和 Claude 都可以调用任意技能disable-model-invocation: true 表示只有你能调,user-invocable: false 表示只有 Claude 能调。同一节还给了一张三种组合的对照表,逐行说明「描述在不在上下文里、完整内容什么时候加载」——默认组合是描述常驻上下文、正文被调用时才加载;设了 disable-model-invocation: true 之后连描述都不进上下文,这一条在搬运时容易被忽略,因为技能仍然能用 /skill-name 跑起来,只是模型再也不会自己想起它。它的 frontmatter 表比另外三家长得多,还包含 allowed-toolsdisallowed-toolsmodeleffortcontext: forkpaths 等字段;同一页也写明,走 claude.ai 上传、Skills API 与官方打包脚本时只允许 Agent Skills 规范的六个字段(namedescriptionlicensecompatibilitymetadataallowed-tools),多写一个就是硬报错。

Pi 的 frontmatter 表里只有 disable-model-invocation 这一个调用开关,语义是设为 true 时从 system prompt 里隐藏、只能用 /skill:name 调;allowed-tools 在表里被标注为 experimental。它的校验口径也单列了一节:大多数违规只 warn 但仍然加载,唯一的例外是description 不加载

Codex 的 parser.rs 只反序列化四项:namedescriptionmodel、以及 metadata 下的 short-descriptionname 长度上限常量 MAX_NAME_LEN 是 64,description 为空直接返回 MissingField("description")。我们在这个 parser 里没有看到上面两个调用开关,所以「谁能触发」这一维度上不与 Codex 比。但 Codex 有两处别家没有的触发通道值得单列:mentions.rs$ 作为 sigil 识别 $tool-name 形式的提及,并把 skill:// 前缀和文件名 SKILL.md(大小写不敏感)判为 skill 类型;invocation.rs 则做隐式调用识别——从一条 shell 命令里认出「读了某个技能文档」或「跑了某个技能 scripts 目录下的脚本」,其 runner 白名单我们数出 10 项(pythonpython3bashzshshnodedenorubyperlpwsh),脚本扩展名 7 项(.py.sh.js.ts.rb.pl.ps1);Windows 约定下走的是 PowerShell 分词那条分支。

四、进上下文之后是什么形态、活多久

DeepSeek Harness 的目录消息只放两样东西:排序后的 name 和经过 XML 转义的 description不含正文、路径、来源和 provider,描述长度由消费者配置 catalogDescriptionMaxLength 限制,默认 500、整数下限 3。首次注入发生在会话第一个 agent/pre-step,之后每步比对 <available_skills> 标签之间渲染出来的条目摘要,变了就追加一条完整替换,全删光则追加一条显式的空替换。模型侧的 skill({ name }) 工具在加载前先按策略拒绝,通过后按调用方 cwd 重读完整定义并再检查一次策略,返回的结果里是 <skill_content name="..."><skill_resources><skill_instructions> 三段。还有一条容易被忽略的语义:registry 不缓存完整定义,每次 get() 都让 provider 重读,因此只改正文不改 frontmatter 时,下一次工具调用就拿到新内容,且不会产生新的目录消息。

Claude Code 官方文档写的是另一套账:技能被调用后,渲染出的内容作为一条消息进入会话并留到会话结束,Claude Code 不会在后续轮次重读技能文件——所以文档建议把「整个任务期间都成立」的要求写成常驻指令而不是一次性步骤。allowed-tools 的授权则只覆盖调用它的那一轮,你发下一条消息就清掉。自动压缩时,每个被调用过的技能的最近一次调用会重新挂到摘要之后,各自保留前 5,000 tokens,重挂的技能共享 25,000 tokens 预算,从最近调用的那个开始填。另外,descriptionwhen_to_use 合起来在技能清单里会被截断到 1,536 字符。

Pi 的路径最朴素,也最该提前知道:启动时扫出 name 和 description,system prompt 按规范以 XML 形式列出可用技能,真要用的时候是让模型自己用 read 工具去读完整的 SKILL.md——文档自述「模型并不总会这么做」,并建议用提示词或 /skill:name 强制。它没有一个专门的 skill 工具挡在中间。Codex 侧我们能从源码确认的是标签常量 <skills_instructions>codex-rs/protocol/src/protocol.rs),以及配置里有 skills.include_instructions 决定要不要注入这个块、[skills.bundled] enabled 默认为 true、skills.config 可按 pathname 选择器逐条开关且后面的规则覆盖前面的(codex-rs/config/src/skills_config.rs)。

落到你手上是四个检查点

搬一个共享 skills 目录时,按上面四节各查一遍就够了:目录层级对不对得上各家的根(尤其是根层扁平 .md 在 Pi 的不同目录下待遇不同,而 DeepSeek Harness 明说不支持递归的 **/SKILL.md);同名在你这套环境里到底谁赢,方向可能和你的直觉相反;frontmatter 键是否越界(DeepSeek Harness 对驼峰键抛错、Claude Code 的规范六字段之外在上传路径上硬报错、Pi 忽略未知字段只 warn、Codex 的 parser 只取四项);上下文形态是常驻一条消息、还是每次重读、还是靠模型自己去 read。这四条决定的不是谁更好,而是同一份技能在哪一家会以你没预期的方式沉默失效。


本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的 架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。 本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目, 因此不涉及界面外观、操作手感与运行速度的任何描述。 该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更, 文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。

文中涉及的 Claude Code 内容依据其官方文档(code.claude.com/docs)整理,该产品闭源,本文不推断其实现; Codex 依据 github.com/openai/codex 快照 c6058cc、Pi 依据 github.com/earendil-works/pi 快照 027a5847 整理。 本文只对照各方公开写明的机制,不对三者做优劣排名。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。