DeepSeek Harness 源码解析 · 第 17 讲
DeepSeek Harness 源码解析系列 第 17 讲:Skill 系统与 Catalog 基于 DeepSeek Harness 源码 · 2026-09-01 💡 本讲一句话: Skill(技能)是"可复用的任务指令包"——模型先看到一份 只含名字+描述 的目录,需要时才用 skill 工具把正文拉进来。读完整篇你能带走四件事: 分层注册表如何用 rank + scope 链裁决重名技能 、 "摘要/候选/定义"三层形状为什么必须分开 、 会话目录靠 digest 去重而不是每步重发 ,以及 用户 /name 手势为什么是绕过 model-invocation 的唯一入口 。 一、能力缝:先定义"技能"长什么样 第 15/16 讲我们拆过 fs 策略和沙箱,都是"能力缝 + provider"的形态。Skill 是同一套范式的又一次落地: packages/skill/skill/src/index.ts 只拥有 Service Definition( ctx.skills )——它 不决定技能从哪来 ,只负责合并各 provider 的目录、裁决重名、按需加载正文。先看它对"名字"和"来源桶"下的定义: 📄 packages/skill/skill/src/index.ts(第 20-39 行) const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ const DEFAULT_COLLECT_CACHE_ENTRIES = 128 const MAX_COLLECT_ATTEMPTS = 2 const RUNTIME_PROVIDER = 'runtime' const RUNTIME_RANK = 250 /** Standard precedence rank for packaged skill providers and local bundled roots. */ export const BUNDLED_SKILL_RANK = 600 /** Return whether a string is a valid kebab-case skill name. */ export function isSkillName(name: string): boolean { return SKILL_NAME.test(name) } /** Origin bucket for a skill contribution — prompt-visible metadata, not precedence by itself. */ export type SkillSource = 'project-dsh' | 'project-agents' | 'runtime' | 'user-dsh' | 'user-agents' | 'custom' | 'bundled' | (string & {}) 为什么这样设计: 名字语法被锁死成 kebab-case( ^[a-z0-9]+(?:-[a-z0-9]+)*$ )——小写字母数字 + 连字符,不允许大写、下划线、点。这不是审美偏好:技能名会 原样出现在模型可见的目录和 /name 手势里 ,一个不含可转义字符的名字才能安全地嵌进伪 XML 标签而不破坏结构。注释特意强调 SkillSource "prompt-visible metadata, not precedence by itself"——来源桶只是给模型看的元数据, 真正的优先级由 rank 决定 (见下文)。末尾的 (string & {}) 是 TypeScript 惯用法:既保留字面量联合的类型提示,又允许 provider 塞入自定义来源字符串。 🔑 三层形状是这条缝的核心抽象 : SkillSummary (摘要,模型目录用)→ SkillCandidate (候选,provider 到注册表)→ SkillDefinition (完整定义,含正文)。三者层层包含、职责分离——目录永远不碰正文。 先看最外层的"调用策略"和摘要形状: 📄 packages/skill/skill/src/index.ts(第 47-71 行) /** Invocat…