DeepSeek Harness 保姆级使用教程
DeepSeek Harness保姆级教程,照做即通「装→配→跑→写插件」全流程,0.1.2-alpha.1版本官方源码实操指南。 核心内容: 1. DeepSeek Harness的定义与架构(Agent=Model+Harness,插件化设计) 2. 环境准备要求(Node.js/pnpm版本、API key、DSH_HOME配置) 3. 安装启动双路径(npm快速入门与源码运行)
这份教程的目标只有一个:让你照做一遍就走通「装 → 配 → 跑 → 写插件 → 加载 → 批处理」的完整闭环。版本快照是官方源码 0.1.2-alpha.1(开发者预览,官方已明示未来会有破坏兼容性的变更)。
下面就跟云朵君一起开始吧~
01先搞清它是什么
DeepSeek Harness(简称 dsh)是 DeepSeek 开源的 agent harness(智能体框架),核心公式一句话:Agent = Model + Harness。模型是可替换的部件,Harness 负责模型之外的几乎所有东西——工具注册、任务规划、沙箱、会话存储、agent 循环。

它不是模型,也不是又一个「成品编码工具」:它构建在 Cordis 元框架之上,slogan 是 「一切皆插件」(Everything is a Plugin)。模型适配器、会话存储、工具集、沙箱、agent 循环本身,全都是可以热插拔的插件。想给它的某个 session 换一套能力,就「挂一个插件在旁边」,而不是去 fork 源码改核心。
我们后面所有操作,都会反复碰到这几个词:插件(plugin)、profile(具名组合)、bundle(分发包)、patch(改/插一行的配置文件)。
02环境准备
^22.19.0>=24.0.0(官方 engines 声明) | |
两点提前说明:
DSH_HOME: dsh把 profile、凭据、session 等持久化内容放在 Harness home 里(默认相关目录可被环境变量DSH_HOME显式覆盖)。手动安装会用~/.dsh;用 Python SDK 时官方故意不读~/.dsh,要求你显式传dsh_home,这一点后面会说。安装体积不小:因为「一切皆插件」是真的按包拆分, @deepseek-ai/dsh及其同类插件包的依赖树比较大,受限环境(无 root / 沙箱)下npm install可能明显变慢,这是正常现象,不是装错。
03安装并启动:两种路径
路径 A:直接用 npm(最快入门)
npx @deepseek-ai/dsh web

默认会在 http://127.0.0.1:3080 启动 Web UI,本机启动时还会用默认浏览器自动打开。只想起服务、不自动开浏览器就加参数:
npx @deepseek-ai/dsh web --no-open
路径 B:从源码跑(想读源码 / 改插件时)
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 直接使用这些产物,不会再重新构建。

首次跑通
浏览器打开 http://127.0.0.1:3080 后:


打开 Settings → Models,填入 DeepSeek API key 并保存。官方文档说明 key 保存后立即生效,不用重启服务;凭据存在 $DSH_HOME/.credentials.yaml,页面只显示脱敏后的描述,不显示明文。点击 Choose workspace,加入你启动 dsh的那个项目目录并选它。没选 workspace 之前,会话输入框是不可用的。新建一个 session,发一句最简单的任务,比如「Summarize this repository and identify its main packages.」。它就能读、改 workspace 文件、跑命令、委派子任务,并在需要审批的操作前问你。


好了,到此咱们就可以初步使用了,再进一步就是插件的使用和开发了~
04认识 profile / bundle / patch
一台跑起来的 dsh,本质上是一个「插件树」,启动时按固定的层叠顺序组装:
profile 中的每个 bundle(按声明顺序)
→ profile 自己的 cordis.patch.yml
→ home 级 $DSH_HOME/cordis.patch.yml
→ 命令行 --patch 覆盖层
三个概念:
profile:一个「具名组合」,躺在 Harness home 里。列出堆叠哪些 bundle、装了哪些 out-of-tree 插件、以及你自己的 cordis.patch.yml。内置模板:web、headless、sdk、sdk-minimal、acp。bundle:一组 Cordis 配置行 + 它们挂载的代码,是「分发包」格式。比如 @deepseek-ai/dsh-base提供模型接入、完整工具集、持久化会话、沙箱与权限策略;@deepseek-ai/dsh-web-app追加浏览器应用;@deepseek-ai/dsh-headless追加无 server 的一次性 runner。patch:一个「按 id 改一行或插一行」的 YAML 文件。插件路径必须是绝对路径;对同一条配置,后应用的层优先,patch 替换的是目标行的整个 config,不是深度合并。
看效果用这条命令:
dsh --profile web --dump-config

它会把你机器实际装配出来的插件树打印出来;--dump-default-config 打印的是默认树(不启动)。每一行都是「id + 包名 + config」,也就是「一切皆插件」最直观的现场——模型、工具、会话、agent 循环全是一排可以 patch 的插件行。

05写你的第一个插件
先建个 scratch 项目(假设你已经在仓库根目录,dsh 默认把运行目录当 workspace):
mkdir -p scratch-plugin/src
一个插件就是一个 TypeScript 模块,导出一个 apply(ctx) 函数:
// scratch-plugin/src/my-plugin.ts
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded!')
}
这就是完整的配置。dsh 加载插件时调用 apply 并把 ctx 传进来,你通过 ctx 注册能力。
插件有三种写法,功能场景不同:
// 对象形式
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) { /* ... */ },
}
// 类形式:当你要把自己的服务提供给别的插件用时,通常用这种
import { Service, type Context } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
}
}
函数形式在大多数场景够用;类形式主要用于「你提供了一个 service 给其他插件消费」。
声明依赖:inject
如果插件要用某个 service(比如 tools 或 llm),在 inject 里声明:
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// 到这里 ctx.tools 一定已就绪
ctx.tools.register(/* ... */)
}
Cordis 会等 inject 里声明的依赖全部就绪再加载你的插件,所以 apply 里不用判空。
自动清理:ctx.effect
凡是经 ctx 注册的东西——事件监听、工具、定时器——插件卸载时都会自动清理:你不用手动 removeListener 或 clearInterval。需要显式释放心智资源(比如一个网络连接)时,用 ctx.effect 返回 disposer:
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
// 插件卸载时执行
return () => clearInterval(timer)
})
}
06用 defineTool 写一个工具插件
工具是「模型能看到的插件」。一个最小工具长这样:
// scratch-plugin/src/my-plugin.ts(覆盖前面 hello 版本)
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
带异步 I/O 的经典例子是读文件(adding-a-tool),注意 exec.signal 被直接透传:
import { readFile } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'read-file-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'read_file',
description: 'Read a file from disk.',
parameters: {
path: { type: 'string', required: true, description: 'Absolute path' },
limit: { type: 'number' }, // 非必填
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args, exec) {
return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
},
}))
}
parameters 一个 schema 干三件事
推断 args的 TypeScript 类型(execute里直接拿到类型安全的参数);在 execute运行之前,校验模型生成的参数(类型、必填、字面量约束等);自动注入进 system prompt,让模型知道有这样一个工具。
execute 的执行契约(写之前必读)
官方在 tool authoring reference 里写死了五条规则:
args 已校验: execute里拿到的参数一定已按parametersschema 校验过。返回一个 canonical JSON 值:不是内容块,也不要让调用方去解析一段散文来拿 id 和字段。 抛错或返回非法值 = isError:只有基础设施故障才 throw;非理想的业务结果(比如进程非零退出)应放进返回值里表达。 尊重 exec.signal:用它取消进行中的工作(上面的readFile就是直接透传)。UI 卡片与 output.render分离:presentCall/presentResult必须是args(加结果)的纯函数,不能做 I/O。
长任务不要走前台 execute,而是走 ctx.jobs.start(...)(后台 job)。工具注册是 effect 式的:插件卸载,工具自动注销,不留「还挂在那儿但没人管」的孤儿状态。
07加载插件的三步闭环
光有 apply(ctx) 不行,dsh 得知道插件文件在哪、什么时候加载。官方教程的最小闭环是三步,每一步都能复现:
第一步:落插件文件。 把上面的 greet-tool(或 read-file-tool)存成 scratch-plugin/src/my-plugin.ts。
第二步:写挂载点 cordis.yml。 一个 patch 文件往插件树里插一行,name 必须是绝对路径:
# scratch-plugin/cordis.yml
- insert:
- id: hello
name: '/绝对路径/到/deepseek-harness/scratch-plugin/src/my-plugin.ts'
第三步:用 --patch 叠加启动。 装的是 npm 包就跑:
dsh web --patch ./scratch-plugin/cordis.yml
从源码跑就是:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
启动时终端会打印 [hello-plugin] plugin loaded!(如果你还留着 hello 版)或工具注册完成。接着在浏览器里问模型:
Use the greet tool to greet Ada.
模型调用 greet 后会拿到 Hello, Ada! 作为工具结果。
想确认插件树确实多了一行,用:
dsh --profile web --dump-config
拉到最后,会看到 id: hello(或你的插件 id)这一行。
卸载同样干净。 去掉 --patch 重启,或从 cordis.yml 删掉那一行,插件注册会随插件一起 unwind。不用手动清任何东西——这就是可逆 effect 最直观的一次兑现。
PTC mode(脚本优先模式)
DSH_TOOLS_MODE 环境变量为进程选择 native、ptc 或 both(其他值会导致启动失败)。PTC 模式下,模型先出脚本再把脚本交给执行能力跑,更贴合「确定性一步一步执行」的批处理业务。对应地,注册过的工具也能被脚本直调,参数与返回类型都从同一个 schema 推出来,走的是正常执行管线(含权限策略)。
08服务与 seam:能力怎么拆
「工具」只是插件的一种。真正的扩展单元叫 seam(能力缝),一个可替换的能力由三个角色组成:
| Service Definition | |
| Service Provider | ctx 的一个 key 上 |
| Consumer |
官方原话:只写一个角色不算 seam,加一个能力要把三个都设计全。("one role alone is not a seam; adding a capability means designing all three.")
Consumer 侧的写法值得记住,因为它体现了「硬依赖 vs 可选依赖」:
export const inject = ['tools', 'shell', 'systemPrompt', 'shellEnv']
// 已 inject 的 service,直接属性访问,缺了就起不来
const defaultMode = ctx.shell.sandboxMode
// 未 inject 的可选 service,可能返回 undefined,要自己判空
const sandboxPolicy = defaultMode === undefined ? undefined : ctx.get('sandboxPolicy')
inject 声明 + 直接属性访问 = 硬依赖;ctx.get(...) = 可选依赖。拦截与策略走事件(ctx.on / waterfall),直接能力调用走 service 方法,别混。
共享执行世界是这个设计最值钱的地方:文件系统和子进程 provider 共享同一个「执行世界」。你把沙箱 provider 指到远程环境,Bash、PTY、LSP 会一起跟着搬,不用给每个能力单独写一份 provider 分叉。
09agent loop 与四个内置入口
一个 agent 循环的基本单位:
step:一次模型请求 + 它调用的那些工具。 turn:零个或多个 step。在第一个输入被 claim 之前打开,在「不再欠任何东西」时关闭。
循环内部跑一系列事件瀑布(agent/pre-step → agent/request → llm/stream → tools/pre-execute → tools/execute → tools/post-execute → tool/result → turn/end),其中 agent/* 和 tools/* 大多是 waterfall(监听器要调 next() 委托给下一个),agent/turn-stopping 是 serial(没有 next())。
对使用者来说,更常打交道的是一套内置 profile 入口:
dsh web | --profile web |
dsh --profile headless "任务" | |
dsh --profile sdk | |
dsh --profile sdk-minimal | |
dsh --profile acp | |
dsh plugin --profile |
内置 web、headless、sdk、sdk-minimal、acp 首次使用会从随附模板自动初始化;自定义 profile 才需要 dsh plugin 创建。给某个 session 组合一套不同的能力集,就 compose 一个 agent preset——preset 里的 service 行需要一个 isolate realm,保证这套能力不污染其它 session 的作用域。
10headless:命令行一次性任务
dsh 相对交互式套装最实在的一个入口是 headless:无 GUI、无 server、一次一个任务、跑完退出。
dsh --profile headless "run the tests"
输出契约(官方文档明确写死):
每个非空推理增量写到 stderr(带 dsh: reasoning:前缀);最终答案写到 stdout; 正常完成 exit 0;中止或出错 exit 1(错误会在 stderr 打 dsh:);:任务缺失或为空,还没开始跑就 exit 1; 一次运行只有一个任务,没有交互式 follow-up;要拆开多段工作就分多次运行。
dsh --profile headless --help 只打印帮助、不运行任何任务。它很适合脚本、CI 和一次性批处理:进程不监听端口,跑完自己就退。
由于官方尚未安全审计,headless 等于把未审计能力放在无人值守模式下跑,所以请先拿它做可回滚的迁移/实验任务,别让它去执行不可逆的批量写操作。
11Python SDK:给你的代码一个 agent 入口
dsh 本体是 TypeScript/Node 栈,但官方提供了一个 Python SDK 作为批处理入口(注意:是入口,不是 dsh 本体)。
安装:
python -m pip install deepseek-harness-sdk
它会自动带上同版本的原生 runtime wheel 和 dsh 命令,正常 SDK 运行不需要系统 Node.js。最小用法:
from deepseek_harness import DeepSeekHarness
with DeepSeekHarness(
dsh_home="/absolute/path/to/isolated-dsh-home",
cwd="/absolute/path/to/workspace",
provider="deepseek-official",
model="deepseek-v4-flash",
) as harness:
result = harness.run("Inspect the repository and fix the failing tests.", session_id="example-001")
print(result.final_response)
几个关键点,缺了都会踩坑:
dsh_home必须显式传(或设DSH_HOME),SDK 故意不读~/.dsh;cwd是 agent 的 workspace;provider/model在初始化握手时发送;DeepSeekHarness是懒启动的,复用一个 runtime 直到close()或退出 with 块;profile默认sdk,极简用profile="sdk-minimal"(后者只组合持久bash+str_replace_editor,系统提示词固定为You are a helpful software engineer assistant.)。
想给 Python 用的 run 装持久插件,走 profile 管理:
export DSH_HOME=/absolute/path/to/isolated-dsh-home
dsh --profile sdk --dump-default-config >/dev/null
dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle
命令级 patches=("/path/to/foo.patch.yml",) 则用于「本次 run 生效、不持久化」的临时变更。harness.run() 返回 RunResult(session_id, final_response, finish_reason, events, notifications),final_response 是该区间最后一个根会话 assistant 文本。
12会话日志:model-visible means logged
会话日志是 dsh 里一个架构级硬约束,而不是事后补的仪表盘:模型看到的一切,必须能从会话日志重建("Model-visible means logged")。

日志是 append-only 的。每一条 prompt 片段、每一次工具调用的输入输出、原始响应都在。fork、回放、转录、telemetry、持久化,全部从这一条日志流派生。官方的运行时不变式会断言它。这意味着:当你排查一个 agent 为什么干了蠢事时,trace 不会被「没记」或「记不全」卡死——不满足这条约束,这套系统根本起不来。
13写在最后
把上面所有步骤走完,你已经有了一套能跑通 dsh 的手感。最后给你几条真实的边界,都是有出处的:
开发者预览。版本 0.1.2-alpha.1,官方明示「未来将出现破坏兼容性变更」。本文的命令与 API 都基于这个快照,投产前一定对照官方最新源码复核。官方自标未安全审计。SAFETY 文档写得很直白:尚未接受安全审计,不得视为安全或可用于生产环境的软件;沙箱、审批与权限控制不保证隔离。负责任使用的最小操作清单是:最小权限、优先一次性 VM/容器/专用环境、重要文件先备份、跑之前先审插件与命令。 没有终端 TUI。交互式使用要开浏览器(或走 headless 跑一次性任务);习惯纯终端 TUI 的人会不适应。 要自管 key 和模型。dsh 不附送模型,你要自己配 key、自己选 provider;本地 runtime(Ollama/LM Studio 这类)不在默认开箱路径里,要走「自定义 provider」配 OpenAI 兼容端点。 安装重、版本迭代快。插件按包拆分,安装体量不小;官方仍是 alpha/rc 版本、处于快速迭代期,API 可能很快变,一切以官方最新为准。 headless 一次一任务。不是多轮交互客户端代理,多段工作拆多次运行。

落到结论就是:装起来玩、读它的架构、在可回滚的环境里做实验,是对的;现在直接接生产、或对它跑不可逆批处理,时机还没到。 它并不打算替代你手里已经能用的编码工具,而是把「agent 框架应该长什么样」这件事往前推了一档——想按它这条路深挖,官方仓库的 docs/architecture.md、docs/cordis-primer.md 和 docs/cookbook/ 就是下一步最该翻的入口
立即咨询 JOTO
JOTO 提供覆盖企业智能体规划与搭建、AI 平台私有化部署、RAG 知识工程、AI 安全治理、FDE 驻场共创及持续运营优化的全周期 AI 落地服务,帮助企业把验证中的 AI 能力转化为安全、可控、可持续迭代的生产力。 联系 JOTO 获取 AI 落地咨询
想把这些做法用到你的业务里?
留下你的场景和痛点,我们帮你判断从哪一步开始。
联系我们


