DSH组合模型详解:无内核架构下的插件叠加机制

DSH 以「空根+分层Patch叠加」替代传统配置语言,改能力等于改插件组合。
DeepSeek Harness(DSH)是一个「无内核」架构的 Agent 平台,彻底抛弃了独立配置语言,改用三层组合模型定义 Agent 能力:Profile 决定打底、Bundle 打包插件行、Patch 是用户覆盖层。其核心设计原则包括:空根(Coreless.iml 为空)确保升级时用户改动不被污染;Patch 采用整行替换而非字段级合并,保证语义无歧义;插件激活由服务依赖关系驱动而非文件顺序;发布服务的行必须包在 Isolate 隔离域内以防命名冲突。此外,DSH 还支持进程内动态挂载插件(Plugin/Package/PluginUnit 三级语义),无需修改任何文件即可在运行时临时扩展能力。理解这套「改能力等于改组合」的模型,是驾驭 DSH 的根本前提。
一个没有配置语言的架构
给 DeepSeek Harness(DSH)增加一项能力,你的直觉可能是:翻开某个配置文件,找到一个开关,改一个参数。但事实并非如此——DSH 根本没有独立的配置语言。
一个 Agent 能做什么,只取决于一件事:你为它组合了哪些插件行。把它拆开看,DSH 本质上是一个 Coreless(无内核)应用。所有能力都建立在同一个「空根」之上,由三层组合叠加而成:
- Profile 决定打底
- Bundle 打包一批插件行
- Patch 是你的覆盖层
这张全景图是理解 DSH 组合模型的钥匙。下面逐层拆解它们的叠加逻辑。
空根设计:Coreless.iml 为何是空的
打开 DSH 的 Profile 目录,你会看到一个叫 Coreless.iml 的文件,里面只有一行——一对方括号,一个空数组。
很多人的第一反应是:配置是不是被清空了?不是。这个空根是刻意的设计。DSH 的插件树不是写在这个文件里的,而是由一层一层的 Patch 叠出来的。
Coreless.iml 只负责证明一件事:这里什么都没有,一切从零开始。所以官方在文件里留下注释:想改能力,改 Coreless.patch.iml,别改这个文件。
为什么?因为如果你直接对空根写入自己的行,改动就会跟 Bundle 的 Patch 层纠缠在一起。下次升级或切换 Profile,它就成了没人认领的孤儿。正确的做法是:让根保持空,把你要的东西写在你自己的那一层里。这是组合模型的第一个原则——根不动,层来叠。
这种「空根+分层覆盖」的思路在软件工程中有一个更广为人知的对应概念——层叠配置(Layered Configuration),类似 Docker 镜像的分层文件系统,或 CSS 的层叠规则。每一层只记录「相对于下层的差异」,最终结果是所有层按序合并的快照。这种设计的核心优势在于变更隔离:官方升级只动底层,用户改动只在顶层,两者之间没有物理交叉,合并冲突从根源上被消除。与之形成对比的是传统「单一配置文件」模式——所有人往同一个文件写,升级时极易产生冲突,用户改动也难以追溯来源。DSH 选择空根而非预填默认值,是在刻意强迫所有有意义的配置都以显式 Patch 的形式存在,从而让每一层的职责边界在文件系统上直接可见。
Profile 与 Bundle:粗粒度的能力切片
官方文档对 Profile 有一句精确定义:Profile 是有序的插件 Bundle Patch 层栈,叠在用户自己的覆盖层之下。
翻译成人话,Profile 是启动器的粗粒度切片,它决定三件事:
- 打底用哪几个 Bundle
- 这些 Bundle 按什么顺序叠
- 你的覆盖层放在哪里
所有这些都写在 Profile 目录下的 package.json 里。以一个本机 Web Profile 为例,它现在挂着三个 Bundle:两个官方自带的 DSH-Base 和 DSH-WebApp,以及一个社区插件 DS-Market。

注意「有序」这个词,顺序在这里不是摆设。它决定了每一层 Patch 谁先谁后,也就决定了最终的叠加结果。
那 Bundle 又是什么?一句话:Bundle 是一组插件的打包 Patch 层。一个 Bundle 就是一个 NPM 包,包里自带一个 Coreless.patch.iml,里面是一行行带注释的插件行和它们的默认配置。
每个 Bundle 自带的 Patch 文件是最好的学习材料——它逐行解释每一行的作用,比任何公开文档都权威,因为它跟着版本走。DSH-Base 管基础运行时能力,DSH-WebApp 管浏览器界面层。
Bundle 的解析顺序也有讲究:先从 DSH 安装位置找内建 Bundle,再从 Profile 自己的 node_modules 里找。后者就是第三方和自写插件的接入点。想接入任意 NPM 包,用 dsh plugin 命令,本质是转发 pnpm,把包装进 Profile 的 node_modules,再把名字加进 Bundles 列表。
Patch 叠加机制:后写覆盖先写
这是 DSH 组合模型最核心的机制。完整的叠加顺序是:
- 每个 Bundle 各自的 Patch(按 Bundles 列表顺序)
- 你的 Profile 层
Coreless.patch.iml - Home 级的 Patch
- 命令行里的 Patch 覆盖
规则只有一条:后写覆盖先写。越往后的层,越能覆盖前面层写的同一行。

但「覆盖」这个词最容易让人误解。DSH 的 Patch 不是深合并,不是说你想改一个字段就只改那个字段、其他保持不变。它是整块换掉。所以跨多个 Bundle 共享同一条语义的行,你只会在一个 Bundle 层和用户层各写一次,而不是层层都写。
这是组合模型里最反直觉、也最重要的一条:改能力,改的是整行,不是某个字段。
「整块替换」而非「字段级合并」这一设计选择,与主流配置系统(如 Kubernetes 的 Strategic Merge Patch 或 JSON Merge Patch)形成鲜明对比。JSON Merge Patch(RFC 7396)允许只传递需要修改的字段,未提及的字段保持原值;而 DSH 的行级 Patch 是原子替换——你拿走整行,换上新行。这么设计的好处是语义清晰、无歧义:一行的最终状态永远只由「最后写它的那一层」决定,不会出现「字段 A 来自 Bundle1、字段 B 来自 Bundle2、字段 C 来自用户层」这种难以调试的混合态。代价是:如果你只想微调某行的一个参数,也必须把整行完整抄写一遍再修改,冗余度更高。理解这个权衡,能帮助你在写 Patch 时避免「以为自己只改了一个字段,实际上把其他字段全部清空」的典型错误。
惰性激活:行顺序不携带加载语义
很多人改完配置没生效,问题就出在这里。打开 DSH-Base 的 Patch 文件,第一行注释就写着:「行顺序不携带加载语义」。
什么意思?插件不是按你在文件里写的顺序加载的。Coreless 的激活由服务的可用性驱动:
- A 行声明「我需要某个服务」
- B 行提供这个服务
那么 B 先被激活,A 跟在后面。谁依赖谁,谁就先就位。所以文件里的顺序只是清单,不是执行顺序。
这个机制下还藏着一条铁律,也是很多人写 Preset 翻车的地方:凡是发布服务的行,必须包进一个叫 Isolate(隔离)的领域。
为什么?如果不隔离,服务就被挂在了进程全局。第一次挂载没问题,第二次同名服务就撞车了。所以「提供服务的行」和「消费它的行」必须待在同一个领域里;而那些只消费全局服务的行(比如调用工具箱、任务系统),就必须留在领域外面。
理解了惰性激活,你就理解了 DSH 一半的架构。
「服务可用性驱动激活」本质上是一种依赖注入(Dependency Injection)与控制反转(IoC)的运行时实现。传统脚本按行顺序执行,调用方必须确保被依赖的模块已经在前面加载;而 DSH 的 Coreless 运行时承担了依赖解析的职责——它在激活任何行之前先扫描整个插件树的服务声明与消费关系,构建出一张有向依赖图,再按拓扑序驱动激活。这与 systemd 的 Wants= / After= 单元依赖、或 Spring IoC 容器的 Bean 初始化顺序在概念上高度相似。「Isolate 隔离领域」的要求则对应了进程内的命名空间隔离:同一服务名在不同 Isolate 里是不同实例,解决了多个插件单元并发运行时的服务名冲突问题,类似于微服务架构中每个服务实例独占自己的端口绑定。
三个核心文件的实战解读
回到真实的三个文件,逐行过一遍。

第一个:package.json。它干两件事——声明包外的插件依赖(dependencies 目前为空),以及 Profile Manifest 里的 bundles 有序数组(三个 Bundle:DSH-Base、DSH-WebApp、DS-Market)。这就是整个 Profile 的配方,要调整打底层,就改这个数组。
第二个:Coreless.iml(空根)。什么都不写。
第三个:Coreless.patch.iml。这才是你的主场。它是顶层数组,每一条是一个 Patch 条目:可以按 ID 定向覆盖某行配置,可以禁用某一行,可以插入新行,甚至允许写 JS 表达式。想给 Agent 开个能力,就在这里加一行。记住规矩:动这个文件,别动 Coreless.iml。
动态插件:进程内的临时挂载
最后演示一次动态流程,开一个 Coreless Preset 会话。官方标注写着:把会话当做 Shell 权限来对待。

流程是一个最短闭环:
coreless.define定义一个插件(一个 apply 对象),用 harness 注册一个叫haloEcho的工具,返回 PluginID 和 PackageIDcoreless.run激活它,返回 PluginUnit,状态 Active- 直接调用
haloEcho传入消息,原样回显 coreless.undefine清理掉
这里的三级语义要记牢:Plugin 是稳定身份,Package 是不可变的代码版本,PluginUnit 是每一次激活。
全程没有动任何文件。动态插件是进程内的临时挂载,会话结束、重启进程就消失。这跟静态组合是两条路:静态行写进 Profile 文件,动态插件活在运行时内存里。
动态插件的三级语义(Plugin / Package / PluginUnit)与软件工程中的类、不可变版本快照、实例三者形成精确对应:Plugin 是类的稳定标识符,Package 是某次编译/打包产生的不可变制品,PluginUnit 是运行时分配的具体实例。这种分层设计使得同一个 Plugin 可以同时存在多个活跃版本的 Package(灰度/热更新场景),也可以在同一 Package 下运行多个并发 PluginUnit(多租户场景)。动态挂载的「进程内临时性」特征意味着它天然具备故障隔离属性:一个动态插件崩溃或表现异常,直接 coreless.undefine 即可清理,不影响写入文件的静态配置层,下次重启进程恢复如初。这使得动态插件成为在不重启 Agent 的情况下快速试验新能力的理想机制。
总结:改能力就是改插件组合
回到开头那张全景图,DSH 的一切都是在空根之上叠加出来的:
- Profile 定打底,Bundle 打包插件行,Patch 是覆盖层
- 后写覆盖先写,一个 Patch 替换整行
- 行顺序不携带加载语义,激活由服务驱动
- 静态组合写文件,动态插件活在内存
所以,记住这句话:在 DSH 里,改能力等于改组合。
这种「无内核 + 组合叠加」的设计,把 Agent 的能力配置从「改参数」升维到了「改组合」,既保证了升级时用户改动不被污染,也让第三方插件的接入变得干净。理解它,是驾驭 DSH 的第一步。
相关推荐

@ai-sdk/zai@3.0.10 发布:依赖更新的补丁版本解析
Vercel AI SDK 发布 @ai-sdk/zai@3.0.10 补丁版本,同步更新 provider、provider-utils 与 openai-compatible 等底层依赖。本文解析该版本变更内容及 AI SDK provider 体系的设计意义。

Vercel AI SDK 更新:@ai-sdk/workflow 2.0.29 修复工具结果保留问题
Vercel AI SDK 发布 @ai-sdk/workflow 2.0.29 补丁版本,核心修复工作流在终止、延迟、暂停三种响应状态下 provider 工具执行结果的保留问题,并同步升级 ai@7.0.98 等核心依赖。

Vercel AI SDK 更新:@ai-sdk/xai 4.0.58 批处理与图像生成改进
Vercel AI SDK 发布 @ai-sdk/xai 4.0.58 版本更新,新增批处理图像生成支持,修复批处理请求类型校验及 DeepSeek 推理流问题,并同步升级 provider 相关依赖。