DeepSeek Harness 的 skill 是什么格式、怎么被装载

2026-08-16

写扩展的人第一次接触 skill 这个概念,通常都是从「我写了个 Markdown 丢进去,它没被认到」开始的。要把这件事说明白,得先分清两件不一样的事:skill 文件本身长什么样,和 dsh 从哪些目录、按什么顺序把它们捡起来。这两件事在 DeepSeek Harness 仓库里分别落在不同的包上。

先说清限定:本文全部依据 deepseek-ai/deepseek-harness 在 2026-08-16 的快照 47f9438。这个仓库建立于 2026-08-13,我们采集时前后只差三天;根 package.json 的版本是 0.1.0-rc.5,GitHub 上一个 Release 都没有,README 自述处于开发者预览阶段并明确写了未来会出现破坏兼容性的变更。下文出现的每一个字段名、默认值、目录约定,都要带着这层限定读——它们随时可能变。我们没有安装过 dsh、没有跑过任何命令,所有内容都是读文件所得。

skill 不是一个包,是四个包

docs/subsystems/skills.md:5 把 skill 拆成四个角色,对应 packages/skill/ 下的四个包:服务定义是 dsh-skill(提供 ctx.skills);本地文件系统的提供方是 dsh-skill-filesystem;另有一个可选的打包 badge 提供方 dsh-skill-badge;消费方是 dsh-tool-skill,它拥有注入给模型的目录消息和模型可见的 skill 工具。

这一拆分不是 skill 独有的。docs/user/develop/practice/index.md:9 把「需要可替换实现」的能力统一拆成 Service Definition / Service Provider / Consumer 三个角色,并在 :52-54 给了三条依赖规则:提供方依赖定义、消费方依赖定义、提供方与消费方互不依赖。skill 组就是这条规则的一个实例:skill-filesystem 负责「从磁盘找出来」,tool-skill 负责「讲给模型听」,两者之间没有直接依赖。

所以后面讲「格式」时看的是 skill-filesystem,讲「模型怎么用」时看的是 tool-skill。这两个包同属一个包组,我们采集时数出整个 packages/skill/ 组是 13 个 TypeScript 文件、6,242 行(同批统计里 packages/subagent/ 组是 89 个文件、24,466 行)。

文件形态:两种,且只认一层

本地提供方接受两种形态(docs/subsystems/skills.md:85):一种是目录 bundle,即 <name>/SKILL.md;另一种是扁平的 Markdown 文件 <name>.md。源码里对 SKILL.md 的判定在 packages/skill/skill-filesystem/src/index.ts:672:683:725

这里有一条最容易踩的限制:不支持嵌套递归的 **/SKILL.md 发现。同一句话在 packages/skill/skill-filesystem/README.md:71 里换了个说法叫「Discovery is one level deep」。也就是说,把 skill 按业务域再套一层子目录归类,第二层就不会被扫到。

名字必须是 kebab-case,源码里的正则原样是 /^[a-z0-9]+(?:-[a-z0-9]+)*$/packages/skill/skill/src/index.ts:20),docs/subsystems/skills.md:85 的转述与之一致。名字不合这条规则的文件会被直接忽略(packages/skill/skill-filesystem/src/index.ts:816-818)。

frontmatter:两个必填、一个驼峰、两个 kebab

frontmatter 必填两项:namedescription。缺任意一个,该文件被忽略并打一条 warning(packages/skill/skill-filesystem/src/index.ts:810-815)。

可选项里,路由用的 whenToUse驼峰写法(packages/skill/skill-filesystem/src/index.ts:830)。

而两个调用开关是 kebab-casedisable-model-invocationuser-invocable,省略时都默认为 truepackages/skill/skill-filesystem/src/index.ts:996-997docs/subsystems/skills.md:126)。归一化逻辑在 :1000-1003,写出来是 modelInvocable = disableModelInvocation !== trueuserInvocable = userInvocable !== false——注意这两个内部字段名和 frontmatter 里的键名不是一回事,前者是解析后的结果。

★ 最值得单独记一笔的是:旧的驼峰键名不是被忽略,而是硬报错。写成 disableModelInvocationmodelInvocableuserInvocable 会抛出 frontmatter field "<legacy>" is unsupported; use "<canonical>"packages/skill/skill-filesystem/src/index.ts:993-995:1004-1008)。缺 name 只是 warning,写错开关键名却是抛错,这两种失败方式不一样,排查时要分开看。

布尔值的解析倒是很宽松:接受 true/false1/0'1'/'0',以及大小写不敏感的 true|yes|onfalse|no|off;其它值抛 TypeErrorpackages/skill/skill-filesystem/src/index.ts:1010-1028)。

frontmatter 之后的 body 就是 skill 正文,.trim() 之后作为 content:832)。

装载:六个根,各带一个 rank

docs/subsystems/skills.md:68-75 给了一张发现根的表,我们逐条对到了 packages/skill/skill-filesystem/src/index.ts:36-40:246-258

ranksource根路径
100project-dsh<projectRoot>/.dsh/skills
200project-agents<projectRoot>/.agents/skills
300customConfig.customSkillDirs
400user-dsh<dshHome>/skills(跳过其 .system 子目录)
500user-agents<agentsHome>/skills
600bundled配置了 Config.bundledSkillDir

这张表的读法是:数值小的在前,项目级压过用户级,用户级压过打包级。<projectRoot> 的定义是最近的含 .git 的祖先目录,找不到就退回当前 cwd(docs/subsystems/skills.md:77:192)。文件监听用 Chokidar;根目录当时不存在时,会从最近的存在祖先起、一次跟一段缺失路径地往下逼近,直到能挂上(:81)。

表里还差一个值。源码里另有 RUNTIME_RANK = 250packages/skill/skill/src/index.ts:24),用于运行时注册的 skill。这个数字没有出现在 docs/subsystems/skills.md:68-75 的表里,只在 :266-268 的生成 JSDoc 里以文字形式出现过:「Project entries outrank runtime entries, which outrank user entries」。两处口径不同,以我们实读的仓库状态为准:表里六个来源,源码里另有一个 250。

打包 skill 的样板,恰好不遵守上面的格式

skill-badge 是仓库自带的打包 skill 样板,它以 BUNDLED_SKILL_RANK(即 600)注册一个不可变的 bundled 候选 dsh-badgepackages/skill/skill-badge/src/index.ts:26:32),正文来自包内资源 ../assets/dsh-badge.md:18:47)。

有意思的地方在于:这个正文文件既不叫 SKILL.md,也没有 YAML frontmatter——packages/skill/skill-badge/assets/dsh-badge.md:1 第一行直接就是 # dsh Badgenamedescription 由代码提供。也就是说前面那套 frontmatter 规则约束的是文件系统提供方,而打包提供方是另一条路径。它提供的内容是官方 “powered by dsh” 徽章素材(本地 PNG 为 726×120,建议按 121×20 渲染,另有一个 shields.io URL,见 packages/skill/skill-badge/assets/dsh-badge.md:7-9)。

还有一点要照实说:这个插件在出厂 CLI 组合里是关闭的packages/bundle/base/cordis.patch.yml:243-245 里那一行写着 - id: skill-badge / name: '@deepseek-ai/dsh-skill-badge' / disabled: truepackages/skill/skill-badge/README.md:7 也写用户必须显式启用它的 skill-badge 行。

模型那头看到的,比你想的少

装载完之后,dsh-tool-skill 会在一个活跃会话的首个 agent/pre-step 注入一条耐久的 user 角色 <system-reminder>。这个目录里只有排序后的 name 和规范化并做过 XML 转义的 description——不含正文、不含路径、不含来源、不含 provider、不含路由提示(docs/subsystems/skills.md:231)。

描述长度上限由配置 catalogDescriptionMaxLength 决定,默认 500,整数下限 3docs/subsystems/skills.md:231;源码 packages/skill/tool-skill/src/index.ts:27:79)。该调成多少取决于你有多少 skill、描述写多长,项目没有给出通用值。

模型调用 skill({ name }) 时的顺序是(docs/subsystems/skills.md:235):校验 kebab 名 → 在中立目录里找 summary → 未通过 isModelInvocable 就在加载前拒绝 → 按调用方 cwd 重读完整定义并再查一次策略。返回结果里含 <skill_content name="..."><skill_resources><skill_instructions> 三个标签。

顺带一提子系统文档对 skill 的定性原文:「Skills are optional instructions, not session events」(docs/subsystems/skills.md:5)——它是可选指令,不是会话事件。

skill 没被认到时,按这个顺序查

  1. 看它有没有被当成候选。 先确认路径落在上表六个根里的哪一个,且只有一层<root>/<name>/SKILL.md<root>/<name>.md。多套一层子目录就不在发现范围内(packages/skill/skill-filesystem/README.md:71)。
  2. 看是 warning 还是抛错。namedescription 是忽略加 warning(src/index.ts:810-815);把开关写成驼峰是抛错(:993-995)。两种表现不同,指向的修法也不同。
  3. 看名字是否合正则。/^[a-z0-9]+(?:-[a-z0-9]+)*$/ 去比一遍你的 name,大写字母、下划线、连续连字符都过不了(packages/skill/skill/src/index.ts:20)。
  4. 看是不是被更高优先级的同名条目压住了。 rank 数值小的在前,项目级的 .dsh/skills.agents/skills 会压过用户级目录。
  5. 验证方式:改完之后回到上面四条逐条对,看不到诊断信息也别急着改代码——packages/skill/skill-filesystem/README.md:71-75 的已知局限里明写了,格式错误的条目只留一条 warning,模型目录拿不到逐 skill 的诊断,也分不清「不存在」与「非法」
  6. 什么情况说明不是这个原因:如果 skill 已经出现在模型目录里、模型也能调用,只是调用后的正文不是你刚改的版本,那就跟发现与格式无关了——同一份 README 的局限小节里写着没有正文修订协议:已加载的正文就是一条普通的历史工具结果,之后的文件编辑既不会重写旧结果,也不会通知正文变了。同样地,如果是 disable-model-invocation 让模型侧不可调用,那属于策略生效,不是没装载。

两处文档与代码对不上,只说差异

写这篇时对到两处口径不一致,按规矩只陈述差异与位置:

一是 frontmatter 键名风格在同一份 frontmatter 里不统一:两个调用开关是 kebab-case,写成驼峰会硬报错(packages/skill/skill-filesystem/src/index.ts:996-997:993-995docs/subsystems/skills.md:126 里点名的也是这两个 kebab-case 键),而同一份 frontmatter 里的可选路由字段读的是驼峰 whenToUsepackages/skill/skill-filesystem/src/index.ts:830)。

二是上面提过的 rank 表:docs/subsystems/skills.md:68-75 列六个来源,源码 packages/skill/skill/src/index.ts:24 另有 RUNTIME_RANK = 250

两处都只说到这里,不推断哪个是对的,也不据此评价什么。写扩展的时候按源码写就是了。

延伸阅读


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

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