DeepSeek Harness 的 skill 是什么格式、怎么被装载
写扩展的人第一次接触 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 必填两项:name 与 description。缺任意一个,该文件被忽略并打一条 warning(packages/skill/skill-filesystem/src/index.ts:810-815)。
可选项里,路由用的 whenToUse 是驼峰写法(packages/skill/skill-filesystem/src/index.ts:830)。
而两个调用开关是 kebab-case:disable-model-invocation 与 user-invocable,省略时都默认为 true(packages/skill/skill-filesystem/src/index.ts:996-997;docs/subsystems/skills.md:126)。归一化逻辑在 :1000-1003,写出来是 modelInvocable = disableModelInvocation !== true 与 userInvocable = userInvocable !== false——注意这两个内部字段名和 frontmatter 里的键名不是一回事,前者是解析后的结果。
★ 最值得单独记一笔的是:旧的驼峰键名不是被忽略,而是硬报错。写成 disableModelInvocation、modelInvocable 或 userInvocable 会抛出 frontmatter field "<legacy>" is unsupported; use "<canonical>"(packages/skill/skill-filesystem/src/index.ts:993-995、:1004-1008)。缺 name 只是 warning,写错开关键名却是抛错,这两种失败方式不一样,排查时要分开看。
布尔值的解析倒是很宽松:接受 true/false、1/0、'1'/'0',以及大小写不敏感的 true|yes|on 与 false|no|off;其它值抛 TypeError(packages/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:
| rank | source | 根路径 |
|---|---|---|
| 100 | project-dsh | <projectRoot>/.dsh/skills |
| 200 | project-agents | <projectRoot>/.agents/skills |
| 300 | custom | Config.customSkillDirs |
| 400 | user-dsh | <dshHome>/skills(跳过其 .system 子目录) |
| 500 | user-agents | <agentsHome>/skills |
| 600 | bundled | 配置了 Config.bundledSkillDir 时 |
这张表的读法是:数值小的在前,项目级压过用户级,用户级压过打包级。<projectRoot> 的定义是最近的含 .git 的祖先目录,找不到就退回当前 cwd(docs/subsystems/skills.md:77、:192)。文件监听用 Chokidar;根目录当时不存在时,会从最近的存在祖先起、一次跟一段缺失路径地往下逼近,直到能挂上(:81)。
表里还差一个值。源码里另有 RUNTIME_RANK = 250(packages/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-badge(packages/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 Badge,name 与 description 由代码提供。也就是说前面那套 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: true;packages/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,整数下限 3(docs/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 没被认到时,按这个顺序查
- 看它有没有被当成候选。 先确认路径落在上表六个根里的哪一个,且只有一层:
<root>/<name>/SKILL.md或<root>/<name>.md。多套一层子目录就不在发现范围内(packages/skill/skill-filesystem/README.md:71)。 - 看是 warning 还是抛错。 缺
name或description是忽略加 warning(src/index.ts:810-815);把开关写成驼峰是抛错(:993-995)。两种表现不同,指向的修法也不同。 - 看名字是否合正则。 拿
/^[a-z0-9]+(?:-[a-z0-9]+)*$/去比一遍你的name,大写字母、下划线、连续连字符都过不了(packages/skill/skill/src/index.ts:20)。 - 看是不是被更高优先级的同名条目压住了。 rank 数值小的在前,项目级的
.dsh/skills与.agents/skills会压过用户级目录。 - 验证方式:改完之后回到上面四条逐条对,看不到诊断信息也别急着改代码——
packages/skill/skill-filesystem/README.md:71-75的已知局限里明写了,格式错误的条目只留一条 warning,模型目录拿不到逐 skill 的诊断,也分不清「不存在」与「非法」。 - 什么情况说明不是这个原因:如果 skill 已经出现在模型目录里、模型也能调用,只是调用后的正文不是你刚改的版本,那就跟发现与格式无关了——同一份 README 的局限小节里写着没有正文修订协议:已加载的正文就是一条普通的历史工具结果,之后的文件编辑既不会重写旧结果,也不会通知正文变了。同样地,如果是
disable-model-invocation让模型侧不可调用,那属于策略生效,不是没装载。
两处文档与代码对不上,只说差异
写这篇时对到两处口径不一致,按规矩只陈述差异与位置:
一是 frontmatter 键名风格在同一份 frontmatter 里不统一:两个调用开关是 kebab-case,写成驼峰会硬报错(packages/skill/skill-filesystem/src/index.ts:996-997、:993-995;docs/subsystems/skills.md:126 里点名的也是这两个 kebab-case 键),而同一份 frontmatter 里的可选路由字段读的是驼峰 whenToUse(packages/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 是什么:建仓三天、13 万 star 的 Agent 框架
- 本专题共 45 篇,完整分组目录见专题页
- DeepSeek Harness 的 Agent 轮次上限:包默认 256、出厂组合配 64
- DeepSeek Harness 的六个 subagent provider:只有两个能续接
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-16,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库建立于 2026-08-13,README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。