DeepSeek Harness (dsh):我安装了 DeepSeek 的开源编码代理,将其接入我的聊天应用,并进行了基准测试

发布日期
2026年8月18日
作者
Jacob Lloyd —— 项目完成后,在 AI 协助下撰写
阅读时长
约 17 分钟阅读

简单来说: DeepSeek 发布了一个免费、开源的“编码代理”——一个能读取你的项目、为你编辑文件并运行命令的程序,类似 Claude Code。本文展示了我是如何将它安装到我的 Linux 电脑上、让它开机自启、接入我家的聊天应用,并用 DeepSeek 的云端模型和我自己硬件上运行的模型在一些小型真实编程任务上测试了它。它运行良好、成本低廉,但也有一些小坑。

2026年8月13日,DeepSeek 发布了 DeepSeek Harness —— dsh —— 一个采用 MIT 许可的编码代理,其核心理念是 万物皆插件:模型适配器、工具、沙箱,甚至代理循环本身。上线两天就获得了 95,000 个 GitHub star。同一周我把它装在家里那台机器上,让它与 Reasonix(我现有的编码代理)并排坐在 DisPatch 里,同时指向 DeepSeek 的云模型和我自己 GPU 机器上的一个模型,并用一小套任务进行了测试。这就是测试过程、成本,以及踩坑的地方。

阅读前快速测试:鼠标悬停,然后点击它。 dsh 根据一段文字简报构建了这个站点标志的像素画版本 —— 纯 vanilla JS,零依赖 —— 完整过程见下文。

tl;dr

  • 它是什么:一个 Claude Code 级别的代理(可读/改文件、运行 shell、维护计划、派生子代理),以 npm 包形式发布。两种模式:运行在 127.0.0.1:3080Web UI,以及一种 无头 一次性模式 —— 打印一个答案后退出 —— 专为脚本和其他代理设计。
  • 花费:软件免费。我的全部 5 个任务基准测试在高峰期费率下 V4-Flash 约 3 美分V4-Pro 约 7 美分;本地模型为 0。
  • 需要什么:Node.js 22.19+/24、一个 DeepSeek API 密钥 任意兼容 OpenAI 的服务器(我两者都用了)。
  • 最终得到什么:在任意项目目录下运行 dsh --profile headless "fix the failing test",一个作为后台服务的浏览器 UI,通过编辑一个 YAML 文件切换模型,以及 —— 以我的情况为例 —— 一个带“运行”按钮、位于我聊天应用里的“DeepSeek Harness”机器人。
  • 结果:V4-Flash、V4-Pro 和本地 Gemma-4-26B 上 15/15 全部通过,每个任务 2–18 秒。开发者预览版 —— README 用大写字母警告说版本之间会有破坏性变更,而我也确实踩到了下面列出的几个尖锐边缘。

路线:从基础到进阶

步骤 1–3 让你在十分钟内得到一个可用的代理。步骤 4–5 是我在此基础上做的补充,让其他软件——以及其他代理——也能调用它。

你最终会得到什么

我实际使用的是无头模式。在项目目录内:

$ cd ~/Projects/dsh-playground
$ dsh --profile headless "Create fizz.py that prints FizzBuzz for 1..15 and run it; reply with the program output only."

1
2
Fizz
4
Buzz
…
FizzBuzz
$ echo $? 0

六秒钟,文件创建,程序运行,答案打印,退出码 0。它不会在你启动它的文件夹之外写入任何内容(默认权限模式 workspace-write),只在标准输出上打印最终消息,并持久化每次运行,因此你之后可以在 Web UI 中打开它,准确查看它做了什么——每一次工具调用,每一次 token 计数。

浏览器 UI 是熟悉的 2026 布局——左侧是会话,中间是聊天,还有一个用于模型的设置页面——它有一个好习惯:API 密钥粘贴在设置页面中,绝不放入配置文件。我的实例以 systemd 用户服务方式运行,并嵌入到我的聊天应用中,紧挨着 Reasonix:

DisPatch 聊天应用选中了“DeepSeek Harness”机器人:主面板中嵌入的 dsh Web UI 显示了一个已完成的会话“Create and run hello.py”,包含其 Think、Write 和 Bash 步骤以及答案;下方状态栏显示 Running,Model 下拉框设置为 DeepSeek-V4-Flash,以及 Start/Restart/Stop 按钮
DisPatch 中的 dsh Web UI,显示了一个已完成的会话及其步骤轨迹(Think → Write → Bash)和每轮统计行。底部的栏控制 systemd 单元和默认模型。

第一步:安装(两分钟)

一个 npm 包。我为 agent 工具保留了一个私有的 Node 前缀,这样就不会有任何东西落入系统目录——在像 Bluefin 这样的不可变发行版上,这无论如何都是唯一合理的位置——但普通的全局安装也是一样的命令:

npm install -g @deepseek-ai/dsh
dsh --version          # 0.1.0-rc.7 at the time of writing
dsh web                # starts the UI, prints http://127.0.0.1:3080

打开该 URL,进入 设置 → 模型,粘贴你的 DeepSeek 密钥,保存。这会写入 ~/.dsh/.credentials.yaml(权限 0600),模型路由立即生效,无需重启。如果要脚本化,这个文件就是一个普通的 YAML 映射——DEEPSEEK_API_KEY: sk-…——我是从其他服务已经在读取的 env 文件中写入它的,因此这个秘密多存在于一个地方,但绝不会出现在 shell 历史或 unit 文件中。

在继续之前,有两件事值得了解:

  • 遥测默认关闭(未设置 DSH_TELEMETRY_MODE = 禁用)。我检查的是随附的配置,而不是营销文案;OTLP 导出器确实存在,只是没有启用。
  • 沙箱是真实存在的,但范围很窄。 workspace-write写入限制在你启动它的目录内。读取不受限制——文档里说得很清楚——所以不要把真实任务从你的主目录启动,也不要指向包含秘密的文件夹。

步骤 2:将其作为服务运行

Web UI 是一个长期运行的 Node 进程;我希望它在登录时启动,不需要终端,仅限回环地址。systemd 的 systemd --user 单元可以实现这一点:

# ~/.config/systemd/user/dsh-web.service
[Unit]
Description=DeepSeek Harness web UI (dsh web) on 127.0.0.1:3080
After=network.target

[Service]
WorkingDirectory=%h
Environment=DSH_HOME=%h/.dsh
Environment=DSH_PERMISSION_MODE=workspace-write
ExecStart=/usr/local/bin/dsh web --host 127.0.0.1 --port 3080   # `which dsh`
Restart=on-failure
RestartSec=3

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now dsh-web.service
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3080/   # 200

WorkingDirectory 是 Web UI 的默认工作区根目录,但 UI 仍然会先让你明确选择一个工作区,之后才允许输入。这是个不错的默认设置。CLI 还拒绝 --host 0.0.0.0——作者称所有接口绑定“有意暂不支持”,而且由于 UI 完全没有认证,我同意这一点。想从另一台机器访问?在前面加一个带认证的反向代理,或者通过远程桌面方式访问宿主机自己的浏览器(我就是这么做的)。

第 3 步:模型——一个 YAML 文件,热重载

~/.dsh/settings.yaml 保存默认模型和任何额外的提供商,dsh 会在处理下一条请求时重新读取它——无需重启,也无需重新登录。我的配置如下:

agent-default-model:
  provider: deepseek-official
  model: deepseek-v4-flash          # or deepseek-v4-pro

llm-deepseek:
  reasoningEffort: high             # off | low | high | max

# A local OpenAI-compatible server (mine is llama.cpp-based on a GPU rig).
llm-pi-ai:
  providers:
    buildpc:
      displayName: StudioForge (GPU rig)
      apiKeyEnv: STUDIOFORGE_PLACEHOLDER_KEY   # a reference, not a value
      api: openai-completions
      baseURL: http://my-gpu-rig:1234/v1      # your server; mine sits on the tailnet
      defaultContextWindow: 65536
      models:
        - id: unsloth/gemma-4-26B-A4B-it-qat-GGUF/gemma-4-26B-A4B-it-qat-UD-Q4_K_XL
          name: Gemma 4 26B-A4B (rig)

有两个细节各花了我十分钟:apiKeyEnv 是一个从 .credentials.yaml 或环境中解析的引用,绝不会是字面值;而且即使本地服务器不需要密钥,也仍然需要引用某个凭据,因为兼容 OpenAI 的客户端坚持要求 bearer token。在 .credentials.yaml 中放一个占位值就能满足它。另外,一旦会话引用了 provider ID(buildpc),它就是永久的;要改名就新增一个。

因为这个文件就是整个接口,“把 dsh 切换到本地模型”不过是任何脚本——或任何其他 agent——都能完成的两行编辑。我的主 agent 会把 harness 重定向到免费的本地模型来处理杂活,遇到难题再切回 V4-Pro,其余什么都不用动。

第 4 步:在 DisPatch 中与 Reasonix 并列

我已经把 Reasonix 作为伪机器人放在 DisPatch 里:点击它后,聊天窗格会变成一个终端,通过 PTY 运行其 TUI。我想让 dsh 就在它旁边。问题是:官方包没有 TUI。npm 上确实有第三方“Claude-Code 风格 TUI”插件,但它们才出现了四天,还带着损坏的 workspace:* 依赖;而且一个未经审查、带 shell 权限的包不能上家庭服务器。所以 dsh 窗格是用官方包确实提供的东西搭建的:

  • 嵌入在 iframe 中的Web UI(它不发送帧阻止头);只有当浏览器能访问主机的回环地址时才显示——tailnet 上的手机会看到一段简单的说明文字;
  • 用于 systemd 单元的启动 / 重启 / 停止,外加健康状态;
  • 一个模型下拉框,用于重写那个 YAML 文件中的 agent-default-model
  • 一个无头任务(Headless jobs)标签页:输入任务,选择 home 下的一个文件夹,然后点击运行。服务器会以固定的参数列表生成 dsh --profile headless "…"(不经过 shell——任务只是 argv 中的一个元素,所以包含 ; rm -rf / 的任务也只是普通文本),一次只运行一个任务,并保留包含每条最终答案的简短历史。
DisPatch 中的无头任务标签页:一个任务输入框、一个工作目录字段,以及 ~/Projects/dsh-playground 下三张已完成的任务卡片——‘19 秒完成’(给 fizz.py 添加文档字符串并重新运行)、‘5 秒完成’(创建 primes.py,输出 2 3 5 7 11…)、‘5 秒完成’(列出文件并逐一描述)——每张卡片都显示任务内容和答案文本
来自聊天应用的无头任务。每张卡片是一次 dsh --profile headless 运行;答案就是 dsh 最后打印的内容。三次真实运行:5 秒、5 秒、19 秒。

该窗格中的所有内容都位于应用的管理员解锁之后——无头任务本质上是任意代码执行,而被锁定的家庭设备永远不知道这个窗格的存在。如果你要把 dsh 接入你自己的任何东西,请照抄这种做法:把“运行任务”当作 shell 一样对待。

dsh 与 Reasonix 对比

两者都是 Claude Code 风格的代理,在 DeepSeek 上按 token 计费。它们的区别在于制造者以及呈现给你的界面:

DeepSeek Harness (dsh)Reasonix
DeepSeek 官方,MIT 许可第三方,Claude-Code 风格
界面Web UI + 无头单次运行;没有 TUI终端 TUI(PTY 中的交互式会话)
从脚本调用dsh --profile headless "…" — 一个答案、退出码为人在回路中设计;脚本调用很别扭
切换模型编辑 settings.yaml,热重载;没有 --model 标志config.toml 分级(flash/pro)+ 按技能路由
本地模型通过 provider 块支持任意 OpenAI 兼容服务器同样三项:base URL、密钥环境变量、模型 ID
可扩展性一切都是插件(模型、工具、沙箱、循环)子代理、技能、按项目记忆
在我的 DisPatch 中iframe + 单元控制 + 无头任务标签页基于 PTY 的 xterm.js 终端
成熟度开发者预览版(rc.7),版本间有破坏性变更v1.18,自带更新器,配置稳定

实际使用中:Reasonix 是我打算和代理一起坐下来工作时打开的;dsh 则是我的其他软件调用的。正因为这种分工,两者都保留了下来。

基准测试:5 个任务、3 个模型、15/15

并非严谨的科学实验——只是五个我会实际交给编码代理的小任务,每个都在全新的临时文件夹中运行,每项都经过自动验收(文件是否存在并能运行?在不动测试文件的前提下测试是否通过?重命名后是否零旧引用残留?)。墙钟时间涵盖整个过程,包括约 7,500 token 的系统提示词;token 数据来自 dsh 自己的会话日志。

任务V4-FlashV4-ProGemma-4-26B(本地,主机)
回复“PONG”(启动 + 一次调用)✅ 2.1 秒✅ 2.8 秒✅ 13.2 秒*
编写并运行 FizzBuzz✅ 5.3 秒 · 2 次工具调用✅ 8.4 秒 · 2 次工具调用✅ 4.9 秒 · 2 次工具调用
修复 2 个 bug 使单元测试通过(测试文件未改动)✅ 11.7 秒 · 8 次工具调用✅ 15.8 秒 · 7 次工具调用✅ 10.4 秒 · 8 次工具调用
总结含 6 个模块的代码库(150 词以内)✅ 8.9 秒 · 9 次工具调用✅ 10.9 秒 · 7 次工具调用✅ 12.1 秒 · 7 次工具调用
跨 3 个文件重命名函数 + 测试,证明全绿✅ 15.1 秒 · 14 次工具调用✅ 18.2 秒 · 12 次工具调用✅ 12.2 秒 · 11 次工具调用
总墙钟时间43 秒56 秒53 秒
Token(输入未命中 / 缓存读取 / 输出)42.6k / 136k / 4.5k41.4k / 107k / 3.2k40.5k / 237k / 5.6k
峰值费率成本(非峰值减半)≈ 0.027 美元≈ 0.072 美元0 美元(电费)

*首次调用是在模型冷加载到主机后进行的;后续任务显示的是预热后的速度。价格来自 DeepSeek 的定价页面,日期为 2026-08-18:Flash 每百万 token 0.014 / 0.44 / 1.32 美元(缓存命中 / 未命中 / 输出),Pro 每百万 token 0.044 / 1.32 / 3.96 美元。

表格说明了什么:

  • 提示词缓存是省钱的关键。每个任务都要支付约 7.5k token 的系统提示词,但第一步之后几乎全是缓存读取,价格仅为未命中价格的 3%。多步骤任务之所以便宜,正是因为框架保持了前缀的稳定。
  • Pro 用更少的步骤和更少的工具调用完成了同样的结果(重命名任务 12 次 vs 14 次工具调用;总结任务 3 步 vs 4 步)。在这套测试中,Flash 更快,价格只有三分之一,所以它仍然是我的默认选择。
  • 本地模型毫不逊色。Gemma-4-26B(4B 激活参数的 MoE 模型,Q4 量化,由 llama.cpp 在两块 RTX 5090 上提供服务)全部通过,虽然步骤更多、token 消耗更多,但墙钟时间同样有竞争力。这是本地模型第一次真正成为这里日常杂活的可行选项,而不只是新鲜事物——尽管五个小任务并不能说明 40 文件重构的情况。

然后是“日常任务”测试:我把它指向这个网站的仓库,让它阅读项目的 runbook,运行构建和链接检查,然后汇报——不发布、不改动。它读取了 runbook,运行了正确的命令,报告了检查器的确切输出(290 个页面、1,752 张图片、无失效链接)、构建时间,并且——在没有提示的情况下——注意到 runbook 仍写着“288 个页面”,并标记了这一偏差。五十三秒。这就是我整天交给它处理的低风险杂活。

趣味测试:给我做个小玩具

基准测试是一回事;我还想看看它在开放式创意简报下会怎么做。于是:“把这个网站的标志做成一个交互式像素画版本——原生 JS,可嵌入任何地方,悬停时有一些物理效果,点击时有一些酷炫效果,无依赖,你自己测试一下。”下面是它构建出来的成果——它是可交互的,所以请直接试试:

把鼠标移上去,然后点击它。像素会从指针处流开并弹回;点击会让标志碎裂成弹跳的像素,最终它们会找到回家的路。触摸操作也有效;它同样尊重 prefers-reduced-motion
像素标志小部件的四个帧:空闲状态、光标周围带有蓝色光晕的像素被推开、点击后一团青色像素从边缘弹开、像素逐渐飘回环形加两个 L 的形状
空闲 → 悬停斥力 → 碎裂 → 重组,由无头 Chromium 捕获。61 fps,零控制台错误,而且它确实会重组。

老实说,过程是这样的:

  • 尝试 1(V4-Flash):一个 10 分钟的循环,没有文件产出。简报允许手绘位图或程序化生成两种方式。它选择在推理过程中手绘一个 40×40 的位图,结果陷入了一个退化的循环——会话日志里有几百行 ################ / ....——直到我的超时机制把它终止。大约浪费了 2 美分。教训:永远不要让模型在脑子里手绘像素。
  • 尝试 2(V4-Flash,简报修改为“从几何图形光栅化,不要位图”):25 分钟,全部交付。100 个模型步骤,201 次工具调用,172k 输出 token(其中 123k 是推理 token),16.3M 缓存读取 token——≈ 按峰值费率 49 美分。它编写了一个 399 行的 ll-pixel-logo.js,带有一个全局 API 和 data- 配置项、一个演示页面、一个 README、一个用于光栅化器的 Node 单元测试,并且——在没有提示的情况下——还写了一个 Playwright 脚本,以三种尺寸截图演示页面。它在润色 README 时达到了我设定的 25 分钟上限,所以退出码是超时,但工作已经完成。退出码两头都会说谎。
  • 艺术效果:圆环和两个斜 L 一眼就能认出是那个标志;它们比真实标志更粗壮、更像 Z 形,如果真要投入使用,我会花十分钟调整它的斜切常量。但我没有调——你所看到的就是原样。
  • 嵌入到这里的代码只有一行,因为该网站的 CSP 是 script-src 'self',而这个小部件不会发出任何网络请求。简报里提到了这一点;它也遵守了。

注意事项

  • 开发者预览版,而且是用大写字母标注的。 安装时为 0.1.0-rc.7;三天前还是 rc.6。配置文件、配置键和插件布局都可能变化。在自动化内容中固定版本,并在每次升级后重新运行冒烟测试。
  • 无头模式在完成前是静默的。 不会有任何内容流式输出到 stdout——长任务看起来像卡住了。请读取会话日志(~/.dsh/sessions/…/session.jsonl.zstdzstd 压缩的 JSONL),或在 Web UI 中查看。此外,退出码为 0 表示“回合已完成”,而不是“任务已成功”。请检查结果。
  • 无头模式没有 --model 标志。 默认模型来自 settings.yaml;在那里修改(热重载)或在 UI 中修改。
  • 无密钥的本地服务器需要占位凭据,通过 apiKeyEnv 引用,而且 provider ID 是永久性的。上文已提及;你会踩到这个坑。
  • Web UI 的静态资源使用绝对路径/assets/…/api),因此如果不做重写,就无法将其挂载到你自己反向代理的子路径下;要么将其嵌入,要么给它一个独立的主机名。
  • UI 的语言环境跟随浏览器——自带的 index.html 写着 lang="zh-CN",你首先会看到一个“内部测试通知”对话框。点击“继续”;之后的内容对我来说都是英文的。
  • 安装会运行 postinstall 脚本(node-pty、koffi、protobufjs)。npm 会对此发出警告。就我所见没有恶意内容,但它是会在你的前缀目录中编译的原生代码——这也是将其放在私有前缀而非系统前缀中的另一个原因。
  • 仅支持 Node 22.19+ 或 24。 较旧的 LTS 会拒绝运行它。

这对我意味着什么

它用一个下午就站稳了脚跟。无头模式是供其他软件调用的编码代理的正确形态——一条命令、一个答案、一个退出码、一份可审计的日志——而模型由 YAML 配置意味着我的代理栈可以将它指向任何适合任务的智能体。如果你已经在运行本地模型服务器,不妨试试同样的五个任务;十美分的 API 额度和一个临时文件夹就是全部投入。

相关:Reasonix(本文中提到的另一个编码代理)、DeepSeek Everywhere(将 DeepSeek 接入 Claude Code 和代理栈)、DisPatch(该面板所在的聊天应用),以及 bench-llm(以比我这里更严谨的方式对本地模型进行基准测试)。


← 更多AI 与本地 LLM