这是《DeepSeek Harness 权威指南 》系列的第 2 篇。 本系列所有架构结论均来自官方文档、源码与本机真实运行,核验时间 2026-08-16。
dsh 没有内核。 这里说的"没有内核"不是代码层面没有主程序,而是架构层面不存在一个"特权层"——不存在一段必须打补丁才能改行为的代码。模型适配器、工具注册表、会话日志、权限策略、agent 主循环,全是插件,全在同一个框架(Cordis)里平等挂载。
这个设计直接挑战传统框架的默认假设:传统框架用"内核 + 扩展点"组织代码,dsh 用"插件 + 插件"组织代码。后者要成立,必须回答一个失控问题:插件之间靠什么约束,才不至于变成一团乱麻? 答案在 Cordis 的五个核心概念里。本篇先把这五个概念讲透,再用真实的配置树和核心服务图把整个架构钉在纸上。
一、Cordis:dsh 的插件地基
Cordis 是 dsh 以 vendor 方式引入的插件框架(上游是 cordiverse/cordis )。dsh 不是"用了 Cordis",而是整个产品就是一棵 Cordis 插件树。五个核心概念缺一不可:
1. 插件是实现服务的对象。 一个插件可以是带 inject 和 apply(ctx) 字段的函数,也可以是一个 Service 子类。它的生命周期由 Cordis 挂载到当前上下文时决定。
2. 上下文是服务的容器。 一个服务占据一个稳定的 ctx.<key>(ctx.tools、ctx.llm、ctx.sessions)。插件之间通过 key 查找服务,不 import 具体实现。这一条是"可替换性"的基石——消费方只知道"有个东西在 ctx.tools 上",不知道也不关心它是谁实现的。
3. inject 声明服务依赖。 插件声明需要的服务,Cordis 等它们全部就绪才启动插件。加载顺序通过服务依赖表达,不靠手动编排启动序列。
4. 类型化事件用于通信。 服务用 TypeScript 声明合并注册事件名,以四种模式分发:emit(观察)、waterfall(环绕中间件,可包装可短路)、parallel(并行扇出)、serial(按序执行)。事件是插件之间唯一的通信通道,而事件本身也是声明出来的。
5. 注册是可逆的副作用。 提示词片段、工具 schema、适配器、监听器——一切注册都通过 ctx.effect() 或 ctx.on() 安装,reload 和 teardown 时按预期撤销。插件卸载时,它注册的一切自动消失,不留垃圾。
五条合起来就是一个闭环:依赖就绪才启动(3)→ 通过 ctx 服务交互(2、4)→ 退出时全部撤销(5)。没有全局状态、没有隐藏依赖、没有卸载残留。 这就是"插件 + 插件"能替代"内核 + 扩展点"的原因。
二、五个核心包:产品的主干服务
packages/core/ 是构成默认控制主干的七个包(官方叫"产品 API 主干")。它们本身也是插件,只是地位特殊——所有扩展插件都构建在这些稳定接口之上:
| 包 | 职责 | ctx 键 |
|---|---|---|
core/session | 事件溯源会话日志 + 内存存储 | ctx.sessions |
core/system-prompt | 提示词片段 + 工具 schema 组装 | ctx.systemPrompt |
core/tools | 作用域工具注册表 + 执行流水线 | ctx.tools |
core/agent | Agent 接口、活跃注册表、agent/* 事件 | ctx.agents |
core/agent-loop | 实现该接口的默认驱动器 | ctx.agentLoop |
core/scope | 按 agent 划分作用域的注册原语 | 库,无 ctx 键 |
llm/llm | 消息与流式词汇表 + 适配器 seam | ctx.llm |
注意 agent 和 agent-loop 的分工:agent 只定义公开约定(接口、注册表、事件词汇),agent-loop 是它的默认实现。扩展插件依赖的是 seam(接口),不是实现——所以换个驱动器不用改任何消费方代码。这是整个架构里最值得抄的设计:接口和实现分离到两个包里,替换实现成为一等操作。

图:核心包向 Cordis 插件树贡献的服务。消费方通过 ctx.
三、五层配置树:profile 与组合包
dsh 最反直觉的部分是它的"配置即组装":运行中的 dsh 是一棵插件树,由启动时按序叠加的各层组合而成。 每一层都是一份 patch(cordis.patch.yml),patch 按条目的 id 定位,替换整条 config 或插入新条目。

图:dsh 配置树的分层组装。2026-08-16 的本机 web profile、无额外 home/CLI overlay 时,--dump-config 输出 491 行和 24 条来源层注释;版本、profile 或 patch 改变后计数也会改变。
层与层的关系:
第 1 层:dsh-base(每个 profile 的第一层)。插入模型适配器、共享的默认模型选择、工具、持久化、沙箱与审批策略、设置/凭据/遥测。它的 patch 还做了平台门控——bash-sandbox/tool-bash 带 disabled: !!js process.platform === 'win32',孪生行 pwsh-sandbox/tool-pwsh 取反,同一份 patch 每个宿主恰好挂一个 shell 栈。
第 2 层:表层组合包。dsh-web-app(浏览器表层:webserver、API 网关、workspace、前端 dist)或 dsh-headless(一次性任务运行器,无服务器)。两个都是 dsh-base 之上的同级表层,互不挂载。
第 3-5 层:用户 patch。profile 的 cordis.patch.yml → home 级 $DSH_HOME/cordis.patch.yml(后者优先级更高)→ --patch 命令行 overlay。中文架构文档对这条规则的原文是:
各层按此顺序应用在空条目列表之上:先按 profile 列出的顺序应用每个组合包,然后是 profile 的
cordis.patch.yml,然后是 home 级的那份,最后是任意--patchoverlay。一条 patch 按 id 定位某个条目并替换其整个 config,或插入新条目。
这里有一个关键区分:覆盖已有 id 时,整个 config 被替换,想保留的字段必须重述;insert 新条目时没有旧 config 可深度合并,也不需要重述字段。
换产品形态 = 换组合。 web 是 base + web-app,headless 是 base + headless,零代码切换——这是"一切皆插件"的第一个直接收益,也是理解 dsh 一切配置的钥匙:你看到的所有配置项,都只是某棵插件树某个节点的 config。
四、本地实证:491 行的配置树
上面说的不是纸面设计。我们在本机跑:
pnpm dsh --profile web --dump-config
输出 491 行;每段来源相同的连续行前有 # == 注释标明来源层。这个数字是 2026-08-16 那次本机 web profile 的观察值,不是发行版承诺的固定行数:安装包、profile、home patch 或命令行 overlay 都会改变它。
# == @deepseek-ai/dsh-base
- id: llm
name: '@deepseek-ai/dsh-llm'
- id: session
name: '@deepseek-ai/dsh-session'
- id: agent
name: '@deepseek-ai/dsh-agent'
- id: agent-default-model
name: '@deepseek-ai/dsh-agent-default-model'
config:
provider: deepseek-official
model: deepseek-v4-flash
# == @deepseek-ai/dsh-base, patched by @deepseek-ai/dsh-web-app
- id: hmr
name: '@deepseek-ai/cordis-plugin-hmr'
disabled: true
这段真实输出说明三件事:第一,默认模型选择(agent-default-model)本身是个插件,它的 config(provider + model)可以像其他已打印条目一样被 patch 覆盖;第二,web-app 对 base 的 patch 会把 hmr 的 disabled 翻成 true(浏览器表层不需要热重载服务);第三,--dump-config 中出现的条目可被自己的 patch 按 id 替换。这不等于所有实现细节都是公开配置项,能否覆盖仍取决于该能力是否以配置条目暴露。
五、这套架构的代价与边界
“没有特权内核"不是免费午餐。三个代价值得摆出来:
第一,覆盖已有条目时没有深度合并。 中文架构文档写的是“按 id 定位某个条目并替换其整个 config,或插入新条目”。因此 profile 想改已有 id 的一个字段,必须重述仍要保留的字段;新 insert 条目则不涉及覆盖旧 config。
第二,加载顺序由依赖推导,调试时需要理解 inject 图。 插件启动顺序不是脚本里写死的,是 Cordis 根据 inject 声明推导的。插件多了以后,启动失败排查要顺着服务依赖链找。
第三,开发者预览阶段存在破坏性变更。 README.zh.md 的原文是“DeepSeek Harness 目前处于 开发者预览 阶段,正在快速迭代。未来将出现破坏兼容性的变更。” 因此生产集成应锁定版本或 commit 并在升级前回归验证;该声明不保证 seam、配置格式或事件词汇稳定。
这套架构的边界: dsh 把可替换性放在插件 seam 上;想改的行为若没有以插件、服务、事件或配置条目暴露,就需要更低层的源码改造或 fork。47f9438 的 docs/subsystems/ 有 46 篇中文页面和 46 篇英文页面,提供较广的入口,但不构成“每个 seam 都完整说明”的保证。
六、决策表
| 设计问题 | 方案 A(传统框架) | 方案 B(dsh/Cordis) | 淘汰 A 的原因 |
|---|---|---|---|
| 扩展点怎么定义 | 内核预留扩展接口 | 一切皆插件,无特权层 | 预留的接口永远不够,特权层要打补丁 |
| 插件间怎么通信 | import 实现 / 事件总线 | ctx. | import 实现导致替换成本高 |
| 启动顺序 | 手动编排 | inject 依赖推导 | 手动编排在插件多时不可维护 |
| 卸载行为 | 手动清理 | 注册即副作用,自动撤销 | 手动清理必漏,泄漏难查 |
| 配置怎么组装 | 配置文件 + 代码分支 | patch 分层叠加 | 分层让"换形态"成为配置操作 |
七、系列路线
下一篇进入整个系列最深的一篇——插件树深讲:插件的三种形态、挂载与卸载生命周期、ctx.effect() 的可逆副作用、--patch 的实际用法,全部配真实代码和 dump 输出。
FAQ
Q:dsh 的"没有内核"是指没有主程序吗? 不是。指架构上不存在特权层:模型适配器、工具、日志、主循环都是平等插件,通过 ctx 服务接口互相查找,没有一段"必须打补丁才能改"的代码。
Q:Cordis 是 DeepSeek 自己写的吗? 不是。Cordis 是独立的开源插件框架(cordiverse/cordis),dsh 以 vendor 方式引入。dsh 的贡献在于把整个产品建在 Cordis 之上,并维护 40+ 篇子系统文档。
Q:我能在自己的项目里用 Cordis 吗? 可以,Cordis 是独立的开源项目。但注意 dsh 对 Cordis 的用法(profile/bundle/patch 分层)是 dsh 自己的设计,与 Cordis 上游用法不完全相同。
Q:–dump-config 和 –dump-default-config 有什么区别? 前者输出本机实际组装好的配置树(含用户 patch),后者输出纯模板的默认配置树。排查"我的配置为什么没生效"时两个对比着看。
Q:为什么 patch 不做深度合并? 设计取舍:深度合并(merge)的语义在多层叠加时难以预测(数组是替换还是追加?null 是删除还是占位?),整条替换的语义简单可预期。代价是覆盖时要重述字段。
互动模块
① 站队:你认为"无特权内核"的插件架构(dsh/Cordis)会成为 agent 框架的主流,还是"内核 + 扩展点”(传统框架)更实际?A. 无内核是终局 B. 内核还是需要的,只是要小 C. 取决于场景
② 征集:你在自己的项目里用过插件化架构吗(Cordis、VSCode 扩展、Webpack 插件等)?遇到最痛的点是什么——扩展点不够、文档缺失、还是卸载泄漏?评论区分享,我会在插件树深讲篇里结合真实案例展开。
③ 转发:如果你身边有人想深入理解 agent 框架的插件化设计,把这篇文章转给他——配置树分层图值得收藏。
