一句话概括: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
Star22.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)决定。

所以你既可以当它是开箱即用的编码 Agentdsh 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>                      临时覆盖层(可重复)

叠加规则很简单,但有两个必须记住的点:

  1. id 覆盖,最后一次写入生效

  2. 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 注册表、持久化、llmsubagents 注册表tool-bashtool-fstool-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 上同时启用 bashpwsh 两套 shell 行——切换 shell 栈必须同时禁用两个、启用两个;

  • 在沙箱化文件系统提供方之上再挂一个普通提供方——两者注册同一个服务。

DSH 在这类冲突上选择启动即失败,而不是「后注册的赢」。虽然第一次遇到会懵,但比运行时随机行为好得多。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐