ByteNoteByteNote
DeepSeek Harness 技能子系统设计解析
字

字节笔记本

2026年10月6日 · 约 8 分钟读完

DeepSeek Harness 技能子系统设计解析

API中转
¥120

技能(skill)是 Agent 框架里一类特殊的扩展:它不改变运行时行为,只往模型上下文注入一段可选指令。DeepSeek Harness 是一个开源 agent 框架,它的技能子系统由几个独立的包协作完成,把技能的注册、发现、调用与目录呈现拆成了清晰的四层。本文整理自该项目技能子系统的官方设计文档,逐块拆解这套设计。

技能子系统四个包的协作架构

四个包组成的技能能力族

技能子系统由四个包协作:dsh-skill 提供服务定义,也就是 ctx.skills 注册表;dsh-skill-filesystem 是本地文件系统提供方,负责扫描磁盘上的技能;dsh-skill-badge 是可选的随包徽章提供方;dsh-tool-skill 则是消费方,拥有会话的初始目录、目录替换,以及面向模型的 skill 工具。

有一个前提值得先记住:技能是可选指令,不是会话事件。它的全部词汇都定义在子系统内部,不进入核心会话模型,这让它可以独立演化而不牵动核心协议。

宿主加作用域的分层注册表

ctx.skills 可以组合本地、内嵌、远程等多种提供方。注册表采用宿主层加按作用域分层的结构,与该框架工具注册表的形态同构:一次注册会落入调用上下文所在作用域对应的层,宿主与仓库插件落进全局层,由 agent 预设常驻组合挂载的插件则落在该预设自己的层。提供方名称只在层内唯一,不是进程级唯一。

读取时,注册表把全局层与观察作用域的链合并:最近层的条目直接赢得重名技能,rank 顺序只在同一层内裁决重名;单层内部依次比较 rank、提供方顺序与本地顺序。发现缓存以解析后的作用域链为键,因此重设作用域父级之后,下一次读取无需任何注册表变更就能看到新结构。

注册是同步的,远程初始化与发现属于 list() 内 await 的部分。每个提供方工厂会拿到一个注册作用域内的控制对象:invalidate() 只在对应注册仍存活时清除已完成目录,其 AbortSignal 在注册失败或释放时中止。提供方与运行时变更会发出 skills/change 失效事件,事件不带差异信息,消费方需要用自己的查找选项重新拉取 snapshot()。

发现还有明确的完整性语义:list() 直接返回数组即视作完整发现;提供方也可以返回显式的观测对象,一边给出仍可直接加载的候选项,一边声明这次发现不具权威性。不完整的快照永不缓存,消费方保留上一份可用目录并择机重试。发现进行中若提供方代次变化,该次发现重试一次;再次变化则返回最新候选,标记为不完整且不缓存。

六档本地发现优先级

随附的本地提供方按 rank 顺序扫描六类根目录:项目根下的 .dsh/skills(rank 100)与 .agents/skills(rank 200)排最前,其次是自定义目录(rank 300)、用户 dsh 主目录(rank 400)、用户 agents 主目录(rank 500),最后是配置的随包技能目录(rank 600)。同名技能由 rank 顺序裁决,项目级技能因此天然覆盖用户级同名技能。

本地发现优先级与 skill 工具调用链

项目根取最近的包含 .git 的祖先目录,找不到就用当前工作目录;在远程或沙箱工作区里,.git 探测经由文件系统服务完成,避免越过宿主文件系统边界。用户 dsh 根会跳过 .system 子目录;本地提供方不合成内置系统技能,随包技能由部署方通过 bundled 根目录或专用提供方给出,徽章提供方在交付的 CLI 里默认禁用,启用它的组合配置行即是显式选择。

文件监听由 Chokidar 承担:监视已有根目录中直属条目的增删与变更,缺失的根从最近的现存祖先起逐段跟随,直到能够挂上监听。技能包下的资源文件变更不算目录变化;模型侧 write 和 edit 观测在目标与目录相关时同步失效提供方,宿主监听器则覆盖 IDE、Git、shell 与外部进程的改动。监听器失败只让当次观测标记为不完整,不会隐藏仍可直接加载的候选。

技能身份与两类调用开关

技能名是 kebab-case。本地提供方接受两种形态:目录包,即名称目录下的 SKILL.md;或平铺的同名 Markdown 文件。嵌套递归的全目录扫描不受支持,这把技能发现约束成一层浅扫描,行为可预期。

每个技能都携带一组规范化的调用策略,由两个独立布尔值组成:modelInvocable 决定是否进入面向模型的目录与加载器,userInvocable 决定是否进入面向用户的命令目录。四种组合全部保留:只给模型用的技能设前者为真;只给用户用的设后者为真;两者都为假时,技能只能被受信的 ctx.skills.get() 调用方获取。本地提供方从 frontmatter 读取 disable-model-invocation 与 user-invocable 两个键,省略的字段默认为真。摘要、候选项与完整定义都携带这组策略,但任意的 frontmatter 字段不会因此混入领域模型。

目录快照与按需加载

目录快照带一个 complete 标记,用来区分确定性的空目录、提供方的瞬时失败,以及发现期间仍在变化的目录。只有所有提供方都在没有并发目录修订的情况下完成发现,快照才算完整。

注册表不缓存完整定义。每次 get() 都会拿着选中的候选项去找胜出提供方,本地提供方因此总是重读当前正文;如果读回的定义名字与候选项不再匹配,会被拒绝,并触发该提供方重新发现。运行时也可以通过 ctx.skills.register() 直接注册技能,调用策略与提供方标签两个字段均可省略,返回的 disposer 移除贡献并使发现缓存失效。

会话目录与 skill 工具契约

消费方在存活会话的第一个前置步骤注入一条持久的用户角色提醒消息,即初始技能目录。目录只含排序后的技能名和经 XML 转义的描述,描述默认上限五百字符,不含正文、路径、来源与路由提示。

此后每个模型步骤之前,消费方对完整快照中可用技能标签之间的条目计算摘要,与上一条可见目录消息中的条目比对:摘要变化就追加一条持久的全量替换,删光技能则追加显式的空替换;不完整快照保留上一份可用视图。如果上下文压缩把历史目录消息全部隐藏,下一份完整快照会重新建立目录;空视图且从未发布过目录时不发送任何内容。这些目录消息属于会话历史,不是全局状态。

模型侧的 skill 工具流程是:校验 kebab-case 名称,在目录里查摘要,先用 modelInvocable 拒绝无权访问的技能,再按调用方工作目录重读完整定义并复检策略,最后返回包含技能正文、资源说明与指令三段的工具结果。资源只按需解析显式引用的脚本与素材,加载结果不枚举技能目录。因此只改正文会影响后续工具调用,却不会产生目录消息,也不改写先前的工具结果。

写在最后

这套设计有几个可借鉴的点:同名技能用分层加 rank 两级仲裁,作用域成为一等公民;发现完整性作为显式状态贯穿缓存与目录更新,消费方永远知道一次读取可信到什么程度;目录与正文读写分离,目录保持极简,正文永远按需重读;调用策略独立于目录渲染,模型与用户两条通道各设一道闸门。对正在给 Agent 搭技能系统的团队来说,这是一份可以直接对照的参考实现。

相关文章

分享: