这篇文章与其说是教程,不如说是我的踩坑记录。从认识 DeepSeek Harness到安装、到运行、到写出第一个能用的工具插件,每一步都来自真机实测。
为了防止你跟我一样绕弯,我尽量把"当时我卡住的那个点"讲出来。
全文按照我的理解顺序分三段:先弄明白它是什么,再把它用起来,最后动手真正动手写一个插件。
第一部分 · 先搞懂:DeepSeek Harness 到底是什么
1. 一个简单比喻
我发现,理解 DeepSeek Harness 只要记住一句话:
为方便,DeepSeek Harness(下面都用 dsh 简称)
Agent = Model + Harness(智能体 = 模型 + 运行框架)
拆开说是:
- Model(模型) 负责"想"------写代码、回话、推理。这是智能体的那份"智力"。
- Harness(运行框架) 负责"干"------让模型能看懂环境、能调工具、能一直干活。这是它的"身体"。
我在网页版 ChatGPT 上用模型时,其实只有"模型 ",没有"harness"。它在封闭的对话框里,既看不到本地文件,也跑不了本地命令。
dsh 多出来的就是这层"身体"。它让 AI 能在我真实的电脑上动手:读文件、执行命令、调用工具、一步步把事推完。官方英文定位是 A harness keeps agents working in real-world environments------让智能体在真实环境里持续干活。
记这条主线就够了:模型是大脑,harness 是身体,而身体由一堆插件拼起来,这就引出这一核心点。
2. 一切皆插件,不是口号
官方口号是 Everything is a plugin。头回听我以为只是宣传,后来才发现它是架构事实。
dsh 建立在 Cordis 插件系统上。在这个体系里,下面这些全部是插件:
- 模型(models)------用哪家模型、怎么调
- 工具(tools)------读文件、跑命令、搜网页这些模型能用的手脚
- 技能(skills)------可复用的能力包
- 会话(sessions)------对话状态怎么存、怎么续
- 沙箱(sandboxes)------模型能摸文件系统的哪些地方
- 存储(storage)------数据放哪
- Agent Loop------智能体循环怎么转
- 调度(scheduling)------后台任务怎么排
- UI------连图形界面本身都是
我原来以为"扩展能力"是去改 dsh 源码,其实完全不用,换模型就是换插件,加能力就是加插件,想把几个能力拼成新模式就改配置,全程不碰源码。
Cordis 内核管插件挂载、卸载和依赖。插件之间靠 services(服务) 和 events(事件) 协作。
举个具体的:我写一个工具插件,只要声明"我依赖 tools 服务",Cordis 会等 tools 就绪了再加载我的插件。这个细节后面写插件会直接用到。
3. 工具是"规范值 + 投影"
工具是模型在真实环境干活的"手"。dsh 里每个工具的定义抽象得很干净,分两层:
- schema:声明入参和返回值的具体形状(我叫它"规范值")
- render(投影):把返回值转成模型真正"看到"的内容
我一开始不太理解为什么拆两层。后来想明白了------给程序看的权威结果,和给模型看的展示内容,是两回事。
我的工具可以先返回一个结构化对象,程序拿着它精确判断;render 再把它压成一段文本给模型理解。想换个展示样式,完全不用动计算逻辑。这个两层设计贯穿 dsh 整个工具执行管线,写插件那节我会亲手碰到它。
4. 会话可追踪
另一个主张是 Every run is traceable。
模型看到的一切,都会记进一个 append-only(只追加)会话日志:系统提示词、推理过程、每次工具调用和结果、子代理调度、每次上下文注入,逐条记。
在 Trajectory(轨迹) 视图里,能按来源把这几样拆开检查。继续对话(Resume)、分叉(fork)、搜索(search)、重放(replay),全都是基于同一条事件流。这意味着我能随时回放"模型每一步为什么这么干、到底看了什么"。这套可追踪不是炫技,是我敢让它动本地文件的底气。
5. 权限和沙箱
让 AI 在我电脑上跑命令,头一个想的就是安全。dsh 用沙箱管这块。
沙箱只管文件系统效果,三档由松到严:
| 模式 | 允许什么 | 我什么时候用 |
|---|---|---|
danger-full-access |
不设限制 | 需要全局操作时 |
workspace-write |
工作区目录 + 临时区可写 | 日常干活 |
read-only |
只读,禁止写入 | 只看不动 |
沙箱之外还有审批策略:超范围的操作是问我一下,还是直接拒绝(ask / never)。
界面上的权限档位,本质就是"沙箱 + 审批"的组合。我的实践很简单:日常 workspace-write,只查不改 read-only,full access 尽量别碰。
6. 有几种跑法
dsh 给不同场景配了几套模式:
- Standard------完整工具集:文件编辑、shell、文件/网页搜索、技能、规划、目标、子代理、工作流
- Code------能力不变,但工具通过 Code Mode SDK 暴露,模型可以用一个 TypeScript 程序把多步操作编排起来
- Minimal------只有 bash + 文件编辑器,做模型基准测试用的
- Creator------检查运行时、在内存里测插件、组合新模式,是插件开发者的场地
日常用 Standard 就够;想折腾插件,就上 Creator。

第二部分 · 怎么上手动用
概念捋过一遍,开始动手。这里我尽量把每一步和前面的概念对一下,让你知道界面上那个按钮背后到底是什么。
7. 装好跑起来
先有 Node.js(dsh 的运行底座,LTS 22 及以上),然后一条命令:
bash
npx @deepseek-ai/dsh web
第一次会下载依赖。看到下面这行就是起来了:
arduino
DeepSeek Harness
http://127.0.0.1:3080/
浏览器打开 http://127.0.0.1:3080/,主界面出来就算环境就绪。
想先装好再启动也行:
npm install -g @deepseek-ai/dsh,然后dsh web,一个意思。

8. 把模型接上
dsh 自己不产模型,得告诉它用哪家、密钥在哪。
- 去 platform.deepseek.com 注册,建一个 API 密钥(
sk-开头,只显示一次,当时我立即就复制了) - 回到 dsh,右上角设置 → 模型
- 在 DeepSeek 提供方卡片点编辑,填密钥,保存
- 回主界面,对话区顶部能看到模型名(比如 DeepSeek-V4-Flash),就成了
想用更多模型,dsh 两种接法:
- 添加提供方:从内置列表挑(OpenAI、OpenRouter、通义千问、月之暗面、智谱等二十多家),填密钥即可
- 添加自定义提供方:填一个 OpenAI 兼容的服务地址,适合本地模型(Ollama / vLLM)、第三方中转、公司内网网关
一句提醒:API 密钥就是钱袋子,别截图、别提交进 git,万一泄露了赶紧去平台吊销重建。



9. 工作区与会话
工作区就是一个项目目录的持久化记录,agent 读文件、改代码、跑命令都以它为根。不选工作区,输入框是锁着的------因为 agent 没有地盘,没法干活。
添加工作区:
- 点对话区顶部的选择工作区 ,或侧边栏工作区面板的添加
- 在系统目录选择器里选项目根目录(别选 C 盘、用户主目录这种又大又全的)
- 确认后工作区进侧边栏,输入框解锁
会话则是一个独立对话。点左上角新会话就能开一个,每个会话有自己的上下文,互不干扰。一个工作区可以挂多个会话。
10. 第一次任务
第一跳指令我建议从"总结式"开始------只读、安全、立刻见效:
列出当前工作区目录下的文件,并简要说明这个项目是做什么的
点发送,dsh 的干活过程大致是这样:
- 读------调工具读目录、开文件
- 想------模型推理得出结论
- 干------有必要就执行命令、写文件(超出权限会先问你)
- 交差------给结果
对话区会一条条滚动展示工具调用。切到轨迹视图,能看到它每一步读了什么、做了什么,前面说的"可追踪"这时候就落地在界面上了。
跑完这条,你就算会用了。剩下的切换模型、加附件、设目标、子代理、后台任务、工作流,都是绕这个循环增强,概念没变。
第三部分 · 真正动手写第一个插件
会开只是会"开车",自己写插件等于"造零件"。既然前面懂了"一切皆插件",那就亲手往 harness 这只"身体"上加一只手。下面是从零写一个真实能用工具插件的全过程。
11. 一个 dsh 插件长什么样
在 Cordis 里,插件就是一个导出 apply 函数的模块 。dsh 加载时拿上下文对象(ctx)调它,我通过 ctx 去注册能力。最小形态:
js
// greet-tool.js ------ 最小插件骨架(纯 JS / ESM)
export const name = 'my-greet-tool' // 可选,诊断日志标注用
export function apply(ctx) { // 插件主体,挂载时调用
console.log('[my-greet-tool] apply() ran')
}
Cordis 接受三种插件形态:函数 (最常见)、对象 、类(想公开服务时用)。加工具用函数就够了。
两个关键地方:
inject:声明依赖的服务。export const inject = ['tools']等于说"等工具注册表就绪了再调我这个 apply"- 纯 JS(ESM)能零配置跑 :
.ts写法要额外参数,.js不用
插件目录里放一个 package.json:
json
{
"name": "dsh-my-tool",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "greet-tool.js",
"dsh": { "bundle": { "patch": "./bundle.patch.yml" } }
}
这里 "dsh.bundle" 是 bundle 清单,只有打算"装成 bundle"那条路才需要它。
12. 注册一个工具:defineTool
光有 apply 还不够,得把工具注册给模型用 。dsh 提供了 defineTool 来规范化工具定义,再注册进 ctx.tools。完整代码:
js
// greet-tool.js ------ 一个最小的 dsh 自定义工具插件
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-greet-tool'
export const inject = ['tools']
export function apply(ctx) {
console.log('[my-greet-tool] apply() ran, registering "greet" tool')
return ctx.tools.register(defineTool({
name: 'greet', // 工具名:模型用它发起调用
description: 'Greet someone by name.', // 模型看它决定何时调用
parameters: { // 入参 JSON Schema,execute 前会校验
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: { // 输出契约:schema 规范值 + render 投影
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) { // 真正干活
return `Hello, ${args.name}!`
},
}))
}
defineTool 替我做三件事:把参数转成 JSON Schema、execute 前校验 args、做冲突检测。注意 output 里的 schema + render 双层------这就是前面概念 3 讲的分层,现在落到代码里了。
这套
defineTool({ name, description, parameters, output, execute })+ctx.tools.register(...)我实测跑通了:在 dsh 0.1.0-rc.6 真实启动后,apply()确实执行到了注册那一步,日志见 14 节。
13. 把插件挂进 dsh:有三条路
先分清两个概念------boot profile (dsh --profile web 启动的,只有 web 和 headless)和 agent-preset (standard / code / minimal / cordis 四份 YAML)。加工具 = 往 web profile 插件树顶层插一个插件。
路 A:--patch 覆盖层
写一个 insert-greet.patch.yml:
yaml
- insert:
- id: my-greet-tool
name: file:///D:/dsh-practice/my-tool/greet-tool.js
Windows 下这里有个坑 :路径必须 file:/// 前缀且盘符小写。我一开始写裸的 D:/...,或大写 file:///D:/...,一律报 ERR_UNSUPPORTED_ESM_URL_SCHEME。
还有一个更关键的坑(模块解析) :用 --patch 挂 file:/// 路径的插件时,dsh 不会帮你解析插件里的裸包名 import。也就是说 greet-tool.js 里的 import { defineTool } from '@deepseek-ai/dsh-tools',Node 会从 my-tool/ 目录往上找 node_modules,找不到就中断报 ERR_MODULE_NOT_FOUND。全局装的 dsh 不会背这个三包名解析的锅。
两种解法的实测结论:
- 在插件目录里装一份 dsh-tools :
my-tool/下执行npm i @deepseek-ai/dsh-tools,然后得用绝对路径 import(file:///c:/...)。干净,但要在插件里配依赖。 - 改挂载方式 :走 bundle(路 C)而不用
file:///路径,插件会被装进 profile 的 node_modules,依赖自然能解析。
真 boot 后,stdout 出现下面这行标记,说明插件加载成功了。这是我验证的核心证据(实测输出,见 14 节):
arduino
[my-greet-tool] apply() ran, registering "greet" tool
dsh web: http://127.0.0.1:3080
路 B:profile 用户层 cordis.patch.yml
内容跟路 A 一样,只是写进 $DSH_HOME/profiles/web/cordis.patch.yml(初始是空的 [])。boot 行为和路 A 相同。这条路藏在 profile 目录下,官方文档只在层序图里顺带提了一句,不显眼。
路 C:dsh plugin add 装成 bundle
bash
node node_modules/@deepseek-ai/dsh/lib/bin.js plugin --profile web add D:/dsh-practice/my-tool
前提是环境里有 pnpm。注意 :没声明 dsh.bundle 的话,只会装成普通依赖、不进层栈;补上 bundle 清单再 remove + add 一次。bundle 里的插件行要按包名引用:
yaml
- insert:
- id: my-greet-tool
name: dsh-my-tool
14. 验证它到底装上没有
这里有个最容易被坑的地方:dsh --dump-config 不代表加载了。dump 是 boot-free 的,只证明"配置树里有条目",不证明"真的加载"。要看加载,得看 boot 的 stdout。
我把验证拆成三层:
--dump-config:配置树里出现插件图层(只证明"有条目")- 加载日志 (最硬):boot stdout 出现标记行
[my-greet-tool] apply() ran, registering "greet" tool - 真实调用:让模型真正触发 greet,看返回结果
验证①:--dump-config 看配置树
前提有坑:dsh 首次运行会在 home 目录写 profile 并建 symlink,Windows 上 EPERM 很可能挡路。解法是显式把 DSH_HOME 指到自己有权限的目录:
bash
# Windows PowerShell;把 DSH_HOME 指到自己可写的目录
$env:DSH_HOME="D:\dsh-practice\home"
dsh --profile web --patch D:/dsh-practice/my-tool/insert-greet.patch.yml --dump-config
输出末尾会出现 patch 图层(实测确认):
yaml
# == D:\dsh-practice\my-tool\insert-greet.patch.yml
- id: my-greet-tool
name: file:///D:/dsh-practice/my-tool/greet-tool.js
到这一步只证明配置树有条目,插件根本没执行。
验证②:真实 boot 看加载日志
这才是决定性的。boot(dsh 0.1.0-rc.6,web profile):
bash
dsh --profile web --patch D:/dsh-practice/my-tool/insert-greet.patch.yml
stdout 出现下面两行,就说明插件 apply() 真执行了、走到了 ctx.tools.register(...),Web UI 也正常起来:
arduino
[my-greet-tool] apply() ran, registering "greet" tool
dsh web: http://127.0.0.1:3080
这行 apply() ran, registering "greet" tool 就是"插件真的被加载"的实锤。
是不是能自己起个最小 Cordis 环境来验证?我试过不行。
ToolRegistry构造时依赖reflect去找父级 Context,裸new Cordis Context()拿不到ctx.tools。完整的 tools 服务是由dsh-agent通过 dsh 自身插件树装配好再注入的。所以验证只能走 dsh 本体 profile boot,另起炉灶拼 Cordis 既非官方路径,也根本跑不通。
验证③:真实调用
配好模型(API key)后,在会话里让它"用 greet 工具向某个名字打招呼",模型会调 greet,你会看到类似 Hello, ${name}! 的返回。这层要模型参与,不想配模型的话,前面验证②已经足够证明注册成功。
下面这组截图是我在本机真实跑通的记录(dsh 0.1.0-rc.6,web profile,用 OpenAI 兼容的内网网关接 Qwen3.6-35B-A3B):
为了不动我电脑上正式环境(端口 3080 那个),验证实例用的是独立的 DSH_HOME 快照 + 独立端口,配模型、挂插件全是隔离的。
启动后的首页 ------右侧已选好工作区 ai-uview-pro、标准模式、模型 Qwen3.6-35B-A3B:

输入提示词,让它调 greet:

对话结果 ------模型完整走了一遍"读提示 → Think(决定调 greet)→ 工具调用 → 读到结果 → 输出"。留意消息流里那个闪电图标行 Tool call · greet · Alice,工具确实被模型选中并执行了:

点开工具行看详情 ------这一张最能说明插件定义的效果。能看到入参 IN {"name": "Alice"}(正是我们 parameters 里声明的 schema)和返回值 OUT Hello, Alice!(正是 execute 的返回),右上角还有个 Inspect 按钮。前面概念 3 的"规范值 + 投影",在界面上就是这个样子:

隔离验证的小窍门:正式跑时用一个独立
DSH_HOME(比如dsh-test/home)配独立端口--port 3081,把模型密钥、插件都放这个"沙盒"里,截图演示完不用了直接删目录就行,不会弄脏日常用的 dsh。
15. Windows 踩坑清单
下面都是我实机踩过的,按"阻断程度"排:
| 坑 | 现象 | 解法 |
|---|---|---|
file:/// 前缀且盘符小写 |
裸 D:/... 或 file:///D:/... 报 ERR_UNSUPPORTED_ESM_URL_SCHEME |
用 file:///d:/...(小写盘符) |
| 插件裸包名解析失败 | import '@deepseek-ai/dsh-tools' 报 ERR_MODULE_NOT_FOUND |
插件目录装依赖,或用 bundle 挂载(路 C) |
Windows symlink EPERM |
首次运行写 ~/.dsh 报 EPERM: symlink |
设 $env:DSH_HOME 到有权限的目录 |
| Node 版本太老 | 报缺 createZstdDecompress / stripTypeScriptTypes |
升到 `^22.19.0 |
| pnpm 缺失 | dsh plugin add 装 bundle 失败 |
先装 pnpm |
无 dsh.bundle |
装了也不进层栈 | 补 package.json 的 dsh.bundle |
.ts 插件 |
npm 安装版直接跑报错 | 用纯 .js,或加 NODE_OPTIONS |
--dump-config 误判 |
以为 dump 有条目 = 已加载 | dump 是 boot-free,看 boot stdout |
| 模型调用无密钥 | 让它调 greet 报 401/空结果 | 自定义提供方走 apiKeyEnv 从环境变量读密钥(如 OPENAI_API_KEY),启动前先 $env:OPENAI_API_KEY=... |
16. 绕了一圈,回到开头
写到这,第三部分其实和第二部分的"概念"对上了:
- 第 2 节"一切皆插件" → 我写的 greet 工具,和 dsh 内置的 shell、文件编辑工具,在这套架构里是平级 的,都是注册进
ctx.tools的一个条目 - 第 3 节"规范值 + 投影" → 就是我代码里的
output.schema和output.render - 第 6 节"Creator 模式" → 就是给插件开发者准备的测试场
想动手的话
- 先最小实现 :把
greet-tool.js和package.json抄下来 - 看配置树 :设好
$env:DSH_HOME,跑dsh --profile web --patch ... --dump-config,确认 patch 图层进树 - 真实 boot 复现 :
dsh --profile web --patch ...,抓 stdout 里[my-greet-tool] apply() ran那行 - 稳定后再装 bundle :走路 C
dsh plugin add,依赖解析更省心
等把第一个工具跑起来,你大概就有数了------dsh 那批社区插件就是这么来的。顺着前面那条主线,读文件、用工具、写插件,都是同一件事。