DSH 能力接缝:一切皆插件背后的设计模式

DeepSeek Harness 如何让一切皆插件真正落地。深入拆解 Capability Seams:三角色模式让你一行配置换掉任何组件。

先说结论: “一切皆插件”说起来容易,做起来难。DeepSeek Harness 用 Capability Seams(能力接缝) 解决这个问题——每个能力拆成三个角色:Service Definition(合约)、Service Provider(实现)、Consumer(消费者,通常是面向模型的工具)。Provider 和 Consumer 互不依赖,都只依赖 Definition。换一个 Provider,整条下游链路跟着走。这就是为什么一个配置变更能同时把 bash、PTY 和 LSP 搬到远程沙箱。

“一切皆插件”的常见翻车

每个插件框架都号称可扩展。真正做到系统级替换的寥寥无几。典型的失败模式:插件能加功能,但不能替换核心行为——除非你 fork。你能加一个新工具,但能换掉所有工具的执行方式吗?能加一个模型适配器,但能换掉沙箱实现而不碰其他十个包吗?

DeepSeek Harness 用一个叫 Capability Seams 的架构模式解决这个问题。这不是营销术语——是包依赖图强制执行的结构设计。理解 seam 就理解了 DSH 的”一切皆插件”为什么经得起检验。

三角色模式

一个 capability seam 由三个角色组成,通常分在不同包里:

┌─────────────────┐       ┌──────────────────┐       ┌─────────────────┐
│ Service         │       │ Service          │       │ Consumer        │
│ Definition      │◀──────│ Provider         │       │ (工具/策略)     │
│ (合约)          │       │ (实现)           │       │                 │
└────────┬────────┘       └──────────────────┘       └────────┬────────┘
         │                                                     │
         └─────────────────────────────────────────────────────┘
                    Consumer 注入 Definition,
                    永远不直接依赖 Provider。

Service Definition — 在 ctx.<key> 上声明接口。拥有 TypeScript 类型、错误码和事件名。一旦有消费者依赖它,就很少变。

Service Provider — 实现接口。可以通过 cordis.yml 里换一行来替换。同一个 seam 可以有多个 Provider(本地、Docker、远程、E2B)。

Consumer — 使用能力,通常是把它暴露为模型可调用的工具。只依赖 Definition,永远不依赖任何具体 Provider。

关键约束:Provider 和 Consumer 互不依赖。 这是替换能力的根基——你换掉 Provider,Consumer 完全不知道也不关心。

具体例子:Bash 执行

DeepSeek Harness packages 目录展示 seam 结构 DSH 的 packages/ 目录——每个 capability seam 拆分为 definition、provider 和 consumer 包。

DSH 里的 Bash 能力由三个包组成:

角色包名做什么
Definitiondsh-shell定义 ctx.shell 服务和 bash 请求/结果类型
Providerdsh-bash-local在本地机器执行命令
Consumerdsh-tool-bash把 bash 暴露为模型可调用的 bash 工具
# cordis.yml — 换 Provider,其他不变
- name: '@deepseek-ai/dsh-bash-local'
# 换成:
# - name: '@deepseek-ai/dsh-bash-sandbox'    # 沙箱执行
# - name: '@deepseek-ai/dsh-bash-docker'     # Docker 容器
# - name: '@deepseek-ai/dsh-bash-remote'     # 远程机器

模型看到的 bash 工具 schema 完全不变。Consumer (dsh-tool-bash) 不改。只有配置里的 Provider 行变了。

为什么一次替换能搬动一切

这才是设计的威力。子进程 seam(ctx.subprocess)位于多个消费者下面:

  • bash 执行器用它跑收集式批量输出
  • LSP 用它跑到语言服务器的原始协议管道
  • PTY 后端用它跑终端会话
  • ACP subagent 后端用它跑 piped ndjson

这些共享同一个执行世界。把 ctx.subprocess 指向远程沙箱(用 subprocess-e2b 替换 subprocess-local),bash、PTY 和 LSP 就同时搬到那个沙箱了,没有任何消费者的代码要改。

这才是”能力接缝”的深层含义——不只是可插拔,是协调的可插拔。相关能力共享底层 Provider,一次基础设施切换就级联穿透整个工具面。

文件系统 Seam:四个 Provider,多个 Consumer

文件系统 seam(ctx.fs)展示了大规模应用:

角色备注
dsh-fsDefinition定义 ctx.fs,读/写/编辑/stat/glob 操作
dsh-fs-localProvider本地文件系统
dsh-fs-e2bProviderE2B 沙箱文件系统
dsh-fs-sandboxProvider受限/沙箱化文件系统
dsh-tool-fsConsumer模型工具:editreadread_imagewrite
dsh-tool-fs-searchConsumer模型工具:globgrep
dsh-fs-observation-policy策略通过 fs/* 事件强制”先读后写”

文件系统还展示了基于事件的策略注入dsh-fs-observation-policy 不实现文件系统——它监听 fs/* 事件并强制模型必须先读文件再编辑。无论哪个 Provider 支撑 ctx.fs,这个策略都生效。

完整 Seam 清单

DSH 出厂带 30+ 个 capability seams。主要家族:

SeamctxProvider 示例Consumer 示例
LLMctx.llmllm-deepseek, llm-pi-aiagent-loop, compaction-basic
Shellctx.shellbash-local, bash-sandbox, pwsh-localtool-bash, tool-pwsh
Subprocessctx.subprocesssubprocess-local, subprocess-e2bbash-local, lsp-stdio, terminal-bash
Filesystemctx.fsfs-local, fs-e2b, fs-sandboxtool-fs, tool-fs-search
Webctx.webweb-search-exa, web-search-perplexity, web-fetch-httptool-web
LSPctx.lsplsp-stdio, lsp-localtool-lsp
Subagentsctx.subagentssubagent-spawn-in-process, subagent-fork-in-process, subagent-acptool-subagent
Session 持久化ctx.sessionPersistencepersistence-jsonl, persistence-sqlitesession-persistence
Storagectx.storagestorage-json, storage-sqlitestorage-domain
Credentialsctx.credentialscredentials-local多个

在 Cordis 里怎么实现

Cordis 学术论文——时空可组合性 Cordis 论文——“时空可组合性的编程范式”——DSH seam 模式的理论基础。

DeepSeek Harness GitHub 仓库 deepseek-ai/deepseek-harness——18.5k 星、12,293 commit、MIT 协议。

底层用 Cordis 的服务依赖系统:

// Service Definition — 声明 ctx.shell
export abstract class ShellService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'shell')
  }
  abstract execute(request: BashRequest): Promise<BashResult>
}

// Provider — 实现 ctx.shell
export function apply(ctx: Context) {
  ctx.plugin(BashLocalProvider)  // 继承 ShellService
}

// Consumer — 使用 ctx.shell
export const inject = ['tools', 'shell']  // 声明依赖
export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'bash',
    execute: (args) => ctx.shell.execute(args)
  }))
}

inject 声明告诉 Cordis:“在 ctx.shell 存在之前别激活我”。加载顺序通过服务依赖表达,不是手动排序。而且注册都是可逆副作用——Provider 卸载时,Consumer 的工具自动注销。

Seam 背后的设计原则

读 DSH 架构文档能提炼出几个刻意的原则:

1. 单独一个角色不构成 seam。 只有 Definition 没有 Provider 和 Consumer 只是一个接口。新增能力意味着设计全部三个角色。

2. 封闭联合保证安全。 LSP seam 精确暴露四个操作(goToDefinitionfindReferencesgoToImplementationhover)。加第五个是跨 seam、所有 Provider 和工具的编译强制变更——你不可能”不小心”加了一个只有一个 Provider 处理的操作。

3. 事件做拦截,方法做能力。 策略(比如”先读后写”)通过事件附加;直接执行走服务方法。这让方法面保持精简,同时允许任意策略注入。

4. 注册是副作用。 每个工具 schema、prompt 段落、适配器、监听器都通过 ctx.effect() 安装。插件卸载时,它注册的一切自动撤销。没有孤儿状态。

5. Seam 负责归一化。 Consumer 永远看不到 Provider 的怪癖。Web search seam 无论 Provider 多返回了多少结果都截断到 maxResults。LSP seam 把所有响应归一化为封闭的可辨识联合。

对插件开发者意味着什么

如果你在写 DSH 插件,seam 模式给你一个决策框架:

  • 加新能力? 设计全部三个角色。如果各自独立演进就拆成独立包。
  • 为已有能力加新 Provider? 实现 Definition 的抽象方法。你的 Provider 纯粹通过配置和已有的竞争。
  • 加新 Consumer? 注入 Definition(如 inject: ['shell'])。永远不导入具体 Provider。
  • 加策略? 监听 seam 的事件。不用包 Provider 也不用改 Consumer。

横向对比

DSH(Capability Seams)LangChainOpenClawClaude Managed Agents
扩展模型三角色 seam,配置替换Runnable 接口链Fork + 适配器不可扩展
Provider 替换成本一行配置重构消费代码Fork 管线不适用
协调替换有(subprocess 带动 bash+LSP+PTY)没有没有不适用
编译时安全封闭联合、inject 声明运行时鸭子类型运行时不适用
可逆注册内置(Cordis effects)手动清理手动不适用

独特贡献不是”插件”——是通过共享能力所有权实现协调的基础设施切换。这是其他框架不 fork 做不到的事。

更多 DSH 架构信息见 DeepSeek Harness 开发者预览三方框架对比

FAQ

如果某个 seam 没加载 Provider 会怎样?

inject 那个服务的 Consumer 不会激活。它们的工具不注册,prompt 段落不出现。系统优雅降级——不崩溃,只是缺少能力。

同一个 seam 能跑多个 Provider 吗?

有些 seam 支持(比如 ctx.web 有独立的 search 和 fetch Provider)。有些设计上是单 Provider(比如 ctx.shell)。Definition 的接口决定这一点。

怎么知道有哪些 seam?

dsh --profile web --dump-config 查看活跃插件树。capability-seams 文档有完整的 mermaid 关系图。

这个模式是 DSH 独有的吗?

三角色分离在其他系统也有(比如操作系统的驱动模型)。DSH 的独特之处是把它和 Cordis 的可逆副作用、类型化事件系统、配置驱动组合结合——让替换变成用户层面的配置变更而不是代码变更。

性能开销大吗?

极小。间接性就是一次 ctx.shell.execute() 调用而不是直接函数调用。真正的成本在 Cordis 启动时的依赖解析,是一次性的 O(n) 遍历插件图。