DeepSeek Harness(DSH)深度解析:把「万物皆插件」做到底的 AI Agent 运行时
一句话概括:DSH 不是又一个功能固定的编码助手,而是一个用插件行「组合」出来的 Agent 运行时——它的配置文件根节点是空的
[],你正在用的每一个工具、每一段提示词、每一条沙箱策略,都是逐层叠加出来的。
如果你只用过「装个 CLI、给个 API Key、开聊」这类工具,DSH 的第一印象可能会有点反直觉:它几乎没有传统意义上的「配置项」。想加一个工具?加一行。想换个提示词?加一行。想让某个能力只对某一种会话生效?还是加一行。
这篇文章会从设计哲学讲到上手实战,把 DSH 的骨架拆开给你看。
一、DSH 是什么
DeepSeek Harness(简称 DSH) 是 DeepSeek AI 开源的 agent harness(智能体框架),仓库在 deepseek-ai/deepseek-harness。
官方对自己的定义是两句话:
DeepSeek Harness(
dsh)是由 DeepSeek AI 开发的开源 agent harness。 它构建于一切皆插件的架构之上,由 Cordis 驱动。
值得单独一提的是:它的设计有论文支撑——A Programming Paradigm for Spatiotemporal Composability(时空可组合性的编程范式)。Cordis 也不是 DSH 内部造的轮子,而是一个独立的开源依赖注入 / 插件框架。这也解释了为什么 DSH 的架构看起来更像「框架」而不是「产品」。
先摆一组客观数据(截至 2026 年 9 月中旬):
| 项目 | 数值 |
|---|---|
| GitHub 仓库 | deepseek-ai/deepseek-harness |
| Star | 约 22.2 万(222,590) |
| Fork | 约 2.64 万 |
| 主语言 / 许可 | TypeScript / MIT |
| npm 包 | @deepseek-ai/dsh |
| 当前版本 | 0.1.5-rc.1(latest)/ 0.1.5-rc.2(next) |
| npm 月下载量 | 约 191 万(近一周约 40 万) |
GitHub dsh-plugin 话题仓库数 | 约 8200+ |
| 首次发布 | 2026-08-10 |
| 官方文档 | deepseek-harness.github.io |
⚠️ 先说清楚:它还在「开发者预览」阶段
官方 README 里有一段加粗的提醒,我认为应该原样转达:
DeepSeek Harness 处于开发者预览阶段,正在快速迭代。未来将出现破坏兼容性的变更。
官方还有一份独立的安全说明,原文写得非常坦诚:
它尚未接受安全审计,不得视为安全或可用于生产环境的软件。 沙箱、审批提示与权限控制可以降低风险,但不保证隔离,也不能保证防止损害。 不要把 DeepSeek Harness 当作不可信工作负载唯一的安全控制措施。
所以本文的定位是:研究它的架构设计,以及在个人开发环境里使用它。生产环境和不可信工作负载,请等它走出预览期。
一个值得注意的细节:它的版本号还停在 0.1.x,但生态已经跑起来了。8000 多个 dsh-plugin 话题仓库、近两百万月下载,说明它吸引的不只是使用者,还有大量写插件的人——这恰恰是它架构设计最想达成的效果。
它和「编码助手」的差别
大多数同类产品是产品:功能边界由官方定义,你用配置去微调。
DSH 更像运行时(Runtime)+ 发行版(Distribution):
-
内核是一套依赖注入 + 插件系统(Cordis);
-
官方提供几个「表层」(profile),比如 Web GUI、headless、SDK、ACP;
-
表层能干什么,由一串组合包(bundle)patch 叠加决定;
-
具体某个会话能干什么,再由「智能体预设」(Agent Preset)决定。
所以你既可以当它是开箱即用的编码 Agent(dsh web 一句话跑起来),也可以当它是搭 Agent 产品的底座(SDK / ACP 都只是 profile)。
二、设计哲学:从空的 [] 开始
这是我第一次看到 DSH 配置树时最惊讶的地方。
一个 profile 目录里的 cordis.yml,内容是这样:
# dsh profile root — an empty entry list. The tree is composed as patches: # each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml, then any # --patch overlays. Edit cordis.patch.yml, not this file. []
根节点是空数组。
也就是说,DSH 没有任何内置能力是「写死在代码里注册」的。整个运行时是这样叠出来的:
空的配置树 [] │ ├─ bundle 1: @deepseek-ai/dsh-base 84 行插件行(宿主核心) ├─ bundle 2: @deepseek-ai/dsh-web-app 94 行插件行(Web GUI 外壳) ├─ bundle 3: 你装的社区插件 bundle 例:@1e0zj/dsh-plugin-mall │ ├─ profile 自己的 cordis.patch.yml 你的覆盖层 ├─ $DSH_HOME/cordis.patch.yml 全局覆盖层 └─ --patch <file.yml> 临时覆盖层(可重复)
叠加规则很简单,但有两个必须记住的点:
-
按
id覆盖,最后一次写入生效; -
patch 会替换目标行的整个
config,而不是深度合并——所以你的覆盖必须把想保留的设置重新写一遍。
第二条是新手最容易踩的坑,官方文档里专门用「已知限制」标了出来。
能力 = 一行 YAML
在 DSH 里,「给 Agent 加上网页搜索工具」不是去翻配置文档找 tools.web.enabled = true,而是:
- insert:
- id: tool-web
name: '@deepseek-ai/dsh-tool-web'
config:
fetch: true
searchTimeoutMs: 60000
没有独立的配置语言——改配置和改能力是同一件事。
三、五个核心概念
先把名词对齐,后面就顺了:
| 概念 | 是什么 | 粒度 | 实际载体 |
|---|---|---|---|
| Cordis | 底层的插件 + 依赖注入框架 | 框架层 | @deepseek-ai/cordis |
| Plugin | 最小能力单元,可提供 Service / 注册 Tool / 监听 Event | 一行 | 一个 npm 包 |
| Bundle(组合包) | 一组插件行的静态 patch 文档 | 一个 profile 层 | 带 dsh.bundle.patch 的 npm 包 |
| Profile(表层) | 一个可启动的运行时形态(web / headless / sdk / acp) | 一个进程 | $DSH_HOME/profiles/<name>/ |
| Preset(智能体预设) | 一个会话能用的工具与人格 | 一个会话 | .agent-presets/<id>/agent.cordis.yml |
Bundle 的判定标准
一个 npm 包算不算「插件层」,只看它的 package.json:
{
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}
有 dsh.bundle.patch → 它是一个 bundle,装上后会被叠加进配置树。 没有 → 它只是一个普通依赖,装了对运行时毫无影响。
这个判定法非常实用,后面讲插件市场时还会用到。
Plugin 的两种朝向
| 声明 | 运行位置 | 适合做什么 |
|---|---|---|
dsh.bundle.patch | 宿主(Node.js 进程) | 文件、网络、命令、子代理、宿主事件、模型工具 |
dsh.client | 浏览器页面 | 主题、布局、工具卡片、Slot UI |
两者通过 Package 私有 JSON 方法通信(客户端 → 宿主单向),只能传无损 JSON。
四、架构核心:Host 平面与 Agent 平面
这是 DSH 架构里最重要、也最容易搞错的一点:同样是「加一个插件」,写在宿主组合里和写在预设里,语义完全不同。
官方把这件事称为「平面(plane)」划分:
| Host 平面(宿主组合) | Agent 平面(Agent Preset) | |
|---|---|---|
| 实例数 | 每进程 1 个 | 每会话 1 个 |
| 放什么 | 注册表本体、跨会话设施、沙箱与审批栈、模型路由、子代理注册表 | 本会话贡献的工具、人格与提示词段、压缩策略 |
| 例子 | tools / systemPrompt / sessions / agents 注册表、持久化、llm、subagents 注册表 | tool-bash、tool-fs、tool-subagent、persona、compaction |
| 生命周期 | 进程存活期 | 随会话挂载与卸载 |
判断标准只有一条
凡是「外面还有人读」的服务,就不能搬进预设。
以 subagents 为例,官方在 standard 预设里写得很直白(我把它翻译过来):
-
subagents注册表是进程单例,宿主侧的 api-proxy 要靠它回答跨会话查询; -
一个 provider 名字只能注册一次;
-
所以预设只能贡献「委派工具」,注册表和 spawn/fork 后端必须留在宿主。
反过来,workflowEngine 服务外面没人读(只有本 Agent 的 workflow 工具用),所以它可以整体搬进预设,只要用一个 isolate 领域包起来。
inject 与 realm:两条硬规则
规则一:用 ctx.get() 读可选服务,inject 只声明硬依赖。
return {
inject: ['requiredService'], // 硬依赖:服务不在就等待
apply(ctx) {
ctx.requiredService.someMethod()
const optional = ctx.get('optionalService') // 可选:自己处理 undefined
if (optional !== undefined) optional.someMethod()
},
}
规则二:预设里发布服务的行,必须包在一个带 isolate 领域的 group 里。
否则它会把服务注册进进程全局领域,第二个会话挂载同一个预设时直接冲突,挂载会被拒绝(而不是等到运行时才炸)。
- id: delegation
name: cordis:group
group: true
isolate:
workflowEngine: true # true = 每个挂载会话一个私有领域
config:
- id: workflow-worker-thread
name: '@deepseek-ai/dsh-workflow-worker-thread'
config:
provider: spawn
- id: tool-workflow
name: '@deepseek-ai/dsh-tool-workflow'
注意 isolate: { workflowEngine: true } ——提供者和每一个消费者都必须在同一个 group 里。消费者留在外面,它会去解析宿主注册表,而预设根本没往那里注册任何东西,结果就是「这一行悄悄什么都没干」。
顺带一提:字符串标签是加入同一个共享领域,不是「实例池」。第二条
provide()依然会抛错。所以预设要的是true,不是标签。
五、真实的组合长什么样
光讲规则太抽象,看真东西。
宿主核心:dsh-base 的 84 行
@deepseek-ai/dsh-base 是每个 base 系 profile 的共享核心,它就是一个 487 行的静态 patch 文档(84 行插件行),本身不挂载任何服务、不持有任何状态。节选几行你能感受到它的密度:
- insert:
- id: timer
name: '@deepseek-ai/cordis-plugin-timer'
- id: llm
name: '@deepseek-ai/dsh-llm'
- id: session-persistence-jsonl
name: '@deepseek-ai/dsh-session-persistence-jsonl'
- id: sandbox
name: '@deepseek-ai/dsh-sandbox'
- id: approval
name: '@deepseek-ai/dsh-user-approval'
config:
policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'workspace-write') === 'danger-full-access' ? 'never' : 'ask'"
- id: permission
name: '@deepseek-ai/dsh-permission-presets'
config:
presets:
read-only:
sandbox: read-only
approval: ask
workspace-write:
sandbox: workspace-write
approval: ask
danger-full-access:
sandbox: danger-full-access
approval: never
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'
disabled: !!js process.platform === 'win32'
- id: tool-pwsh
name: '@deepseek-ai/dsh-tool-pwsh'
disabled: !!js process.platform !== 'win32'
几个值得注意的点:
-
!!js表达式:YAML 里可以直接写 JavaScript,DSH 在组合期求值。平台门控(Windows 用 pwsh、其他平台用 bash)就是这么做的——保证一台机器上恰好一套 shell 栈。 -
权限三档:
read-only/workspace-write/danger-full-access,每档是「沙箱模式 + 审批策略」的组合。 -
行序无加载语义:注释里明确写了「激活由服务可用性驱动」,顺序只是为了人读。这是依赖注入框架的典型特征。
用户层:一个真实的 cordis.patch.yml
这是我自己 profile 里的实际内容(装了一个同花顺数据插件 + 一个本地文件读取插件):
# ── dsh-tool-ths: 同花顺行情数据插件 ──────────────────────────
- insert:
- id: tool-ths
name: ./plugins/dsh-tool-ths/index.js
config:
channel: public # public 免登录 / ifind 官方 API
refreshToken: ''
maxCodes: 20
maxRows: 120
# ── dsh-file-reader: 文件读取与 GUI 预览 ──────────────────────
- insert:
- id: dsh-file-reader
name: ./plugins/dsh-file-reader/index.js
config:
workspaceRoot: /path/to/workspace
maxChars: 60000
lineLimit: 500
注意 name 既可以是 npm 包名,也可以是相对路径——本地开发插件不需要发包、不需要发布,改完重启宿主即可。
还有几个开关技巧:
-
禁用一行(不删代码就关能力):给该行加
disabled: true; -
改某行配置:按
id写一条覆盖,记得重述完整 config; -
临时实验:
dsh --profile web --patch ./extra.yml,不污染配置文件。
六、五分钟上手
环境要求
Node.js 22.5 及以上。
判断依据:DSH 的会话查询层(dsh-session-query-sqlite)使用 Node 内置的 node:sqlite 模块,该模块从 Node 22.5 开始提供。实测 Node 26 运行正常。
包内没有声明
engines字段,所以版本过低时不会在安装阶段报错,而是在启动后某个功能上失败——建议直接上 Node 22 LTS 或更高。
安装
官方推荐的方式就是 npx 直接跑——不装全局包:
# 官方推荐:免安装直接跑
npx @deepseek-ai/dsh web
# 如果打算长期用,全局装一个更省事
npm i -g @deepseek-ai/dsh
dsh web
# 国内网络可加镜像
npm i -g @deepseek-ai/dsh --registry=https://registry.npmmirror.com
启动 Web GUI
npx @deepseek-ai/dsh web
web 就是 --profile web 的别名。首次启动会自动从随附模板初始化 profile,然后:
-
默认监听
http://127.0.0.1:3080,本机启动时会自动用默认浏览器打开; -
换端口:
dsh web --port 8080; -
只跑服务不开浏览器:
--no-open; -
需要 LAN 访问:配合
--trusted-host(有可信主机白名单机制,不是无脑放开)。
SSH 场景下的细节:通过 SSH 启动时,DSH 只打印宿主机 URL、不会尝试打开浏览器——因为本地转发地址是由 SSH 客户端或编辑器持有的,它没法替你猜。
从源码运行
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
pnpm run build 准备仓库产物,pnpm dsh web 直接消费这些已构建产物(不会重新构建),适合想读源码、调插件加载行为的人。
其他入口模式
| 命令 | 用途 |
|---|---|
dsh web | 启动 Web GUI |
dsh --profile headless "跑一下测试" | 跑一个全新会话,打印最终答案后退出 |
dsh --profile sdk | 以 JSON-RPC stdio 为 SDK 客户端服务 |
dsh --profile sdk-minimal | 极简独立配置树,适合当最小示例 |
dsh --profile acp | 通过 ACP stdio 服务自动化客户端 |
注意:SDK 和 ACP 都只是 profile,不是独立的公开 bin。 这是「万物皆插件」在入口层的体现。
目录结构
$DSH_HOME(默认 ~/.dsh)
├── profiles/
│ └── web/
│ ├── package.json # dsh.profile.bundles 有序列表 + 插件依赖
│ ├── cordis.yml # 组合后的树(根节点是 [],别改它)
│ ├── cordis.patch.yml # ← 你的覆盖层,改这里
│ └── plugins/ # 本地插件
├── .agent-presets/ # 你自己写的 Agent 预设
├── sessions/ # 会话记录(JSONL)
├── storages/ # 会话查询索引、投影缓存
└── settings.yaml # 模型、UI、插件设置
诊断利器
dsh --dump-config --profile web # 打印组合后的完整配置树
dsh --dump-default-config --profile web # 只看官方默认层,不含你的覆盖
配置没生效?先 dump 一遍,看你的那一行到底叠上去了没有。
七、安全模型:为什么预设不能给自己放权
DSH 的权限体系是「沙箱 + 审批 + 权限预设」三层:
| 权限模式 | 文件沙箱 | 审批策略 |
|---|---|---|
read-only | 只读 | 询问 |
workspace-write(默认) | 只能写工作区(+ 会话自己的临时目录) | 询问 |
danger-full-access | 无限制 | 从不询问 |
默认是 workspace-write + ask。这个组合的含义是:Agent 可以自由干活,但越界前必须问人。
一条我觉得设计得很聪明的约束
预设不能放宽自己的沙箱。 沙箱、审批、权限这三行属于宿主平面,是刻意划出的边界。
理由很直白:一个 Agent 恰好和它点名的插件一样有特权。如果允许预设修改自己的围栏,那围栏就没有意义了——任何一个从市场装来的预设都能给自己开 danger-full-access。
同理还有几条「不能搬进预设」的硬约束:
-
agent-loop注册唯一的 Agent 工厂,第二次注册直接抛错; -
会话持久化必须留在宿主,否则会话列表会碎掉;
-
上面讲过的
subagents注册表。
这种「能力可组合,但边界不可自授」的设计,是 DSH 在开放生态下还能守住安全底线的原因。
但别把沙箱当保险箱
回到官方那份安全说明,有几句话值得每个使用者读三遍:
仅向本项目授予所需的最小权限和访问范围。
优先在一次性虚拟机、容器或专用环境中运行。
备份本项目可以访问的文件。
除非你接受相关风险,否则不要向其暴露敏感凭据或数据。
在允许运行前检查插件、配置和拟执行命令。
因为 DSH 会执行模型生成的代码与命令、加载第三方插件,并访问你开放给它的网络、进程、凭据和文件。沙箱能降低风险,但官方自己明确说了——它不保证隔离。
实践上的建议:
-
日常用默认的
workspace-write,别图省事常开danger-full-access; -
从市场装插件前,先看它的
package.json和仓库; -
有敏感凭据的机器,考虑放容器或虚拟机里跑。
八、和同类工具怎么选
先说清楚:下面是设计取向的对比,不是功能跑分。每个工具都在快速迭代,具体特性请以各自最新文档为准。
| 维度 | DSH | 传统编码 Agent CLI |
|---|---|---|
| 定位 | Agent 运行时 + 官方发行版 | 一个产品 |
| 能力扩展 | 加一行插件(改组合) | 主要靠 MCP / 钩子 / 配置项 |
| 配置模型 | 空根 + 多层 patch 叠加 | 一份配置文件 |
| 浏览器 UI | 是插件(dsh.client 双面) | 通常无 / 另做 |
| 跨产品复用 | SDK、ACP 都是 profile | 常需另学一套接口 |
| 多 Agent 编排 | 内建子代理、工作流、Ralph 循环 | 视产品而定 |
| 安全边界 | 沙箱/审批不可被预设自授 | 视产品而定 |
| 上手成本 | 偏高(要理解平面与 realm) | 低 |
DSH 的学习曲线确实是它的门槛。「宿主平面 vs Agent 平面」「realm 隔离」这些概念,不搞懂就会写出「文件语法全对、但一行都不生效」的预设。
但反过来说,这套概念是它可组合性的代价,也是它可组合性的来源。当你想做的不再是「用别人的 Agent」,而是「造一个自己的 Agent 产品」时,这些抽象就开始回本了。
九、总结
把 DSH 拆完之后,我觉得它最值得学的是三个设计决策:
1. 空根 + patch 叠加。 配置树从 [] 开始,任何能力都必须显式叠上去。好处是没有隐藏的魔法:--dump-config 打印出来的那棵树,就是运行时的全部真相。代价是覆盖要重述完整 config,但这是可接受的诚实交换。
2. 双平面划分。 「这个能力是每进程一个还是每会话一个」——用一个问题代替了一堆特例。凡是外面有人读的服务,就必须留在宿主;凡是只有 Agent 自己读的,就搬进预设并用 realm 隔离。规则简单,但覆盖了绝大多数情况。
3. 边界不可自授。 开放插件生态 + 严格的安全边界,这两件事通常互相拉扯。DSH 的解法是:能力随便组合,但沙箱、审批、权限三行不许进预设。生态可以野蛮生长,底线由宿主钉死。
如果你只是想找个能写代码的 AI 助手,市面上有更省心的选择。 但如果你想搞清楚「一个可组合的 Agent 运行时是怎么搭出来的」,或者想基于它做自己的 Agent 产品——DSH 目前是把这件事做得最彻底的开源实现之一。
而它现在还在 0.1.x。
附录:六个最容易踩的坑
1. 以为 patch 是「合并」,其实是「替换」 给某行改一个配置项,结果其他设置全丢了。patch 条目会替换目标的整个 config,想保留的必须重述。
2. 预设里发布服务却没包 realm 表现是「文件没报错,但工具没出现」。挂载校验会给出 row(s) published process-global service(s) [...] 或 N row(s) did not activate——看到这两个消息,先检查 realm。
3. 把消费者留在 realm 外面 和上一条是镜像错误。消费者解析的是宿主注册表,而预设没往那里注册东西,于是这一行静默失效。
4. 装了插件但没重启 插件层的生效点是 dsh 进程启动。dsh plugin add 成功 ≠ 当前进程已加载。
5. 装了个没有 dsh.bundle.patch 的包 市场工具会明确写 dsh.bundle.patch: absent。这种包装了只是多一个依赖,运行时行为零变化。
6. 把「同一件事的两个提供者」都开着 两个例子都会让 profile 直接加载失败(不是警告):
-
Windows 上同时启用
bash与pwsh两套 shell 行——切换 shell 栈必须同时禁用两个、启用两个; -
在沙箱化文件系统提供方之上再挂一个普通提供方——两者注册同一个服务。
DSH 在这类冲突上选择启动即失败,而不是「后注册的赢」。虽然第一次遇到会懵,但比运行时随机行为好得多。
更多推荐


所有评论(0)