DeepSeek Harness Hub
← 返回列表

shlouai/dsh-debate

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
未验证

别再问你的 Agent 怎么看。让它开一场庭审。

尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/3 · 已提供中文文档

别再问你的智能体怎么想了。让它开一场审判吧。

综合分
29.2
GitHub 分
29.2
用户评分
★ Stars
1
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add shlouai/dsh-debate
该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/18(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-tools@deepseek-ai/schemastery
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

dsh-debate

别再问你的 Agent 怎么看。让它开一场庭审。

一个 DeepSeek Harness 插件:面对难题,它不再独自推理,而是安排一场正式辩论——两个互为对手的子 Agent 各执一方、带着真实调研交锋,主 Agent 依据全部记录裁决。

License
Node
TypeScript
Status

English · 简体中文

dsh Web 界面中的一场辩论:确定辩题后,两个子 Agent 逐轮交锋,裁判在最后裁决

为什么

问一个 Agent「我们该迁移到 X 吗?」,你得到的是一个声音朝一个方向推理。它会去找支持自己出发点的论据,因为这个回路里没有任何一环的职责是攻击它们。

dsh-debate 把对手放进回路。正方与反方是彼此独立的一次性子 Agent:看不到你的对话,看不到裁判的倾向,各自只被要求赢。双方在发言前都可以读工作区、可以搜网,所以一个论断是辩手能摆出来的东西,而不是它记得的东西。只有在最后一轮结束后,主 Agent 才打破沉默并裁决。

你拿回的不是一段自信的结论,而是一份记录——其中反对你想法的最强论证是被刻意做出来的,做它的那一方一心想赢。

工作原理

sequenceDiagram
autonumber
actor User as 用户
participant Judge as 裁判你的 Agent
participant For as 正方辩手全新子进程
participant Against as 反方辩手全新子进程

User->>Judge: 辩论我们是否该采用 X,两轮
Judge->>Judge: debate_open —— 确定辩题与轮数

loop 每一轮 —— debate_round
Judge->>For: 辩题 + 至此的完整记录
For->>For: read / glob / grep / web_search / web_fetch
For-->>Judge: 正方发言
Judge->>Against: 辩题 + 记录,含刚才那篇发言
Against->>Against: 调研并反驳
Against-->>Judge: 反方发言
end

Judge->>Judge: debate_verdict —— 逐轮点评,一次裁决
Judge-->>User: 胜方、理由,以及完整记录

三个工具把一场辩论拆到多个模型轮次上,于是你是看着它发生,而不是等一次巨大的调用:

| 工具 | 作用 |
| --- | --- |
| debate_open | 确定辩题以及每方有几轮,返回一个 debate_id。 |
| debate_round | 只推进一轮:一个全新的正方子进程发言,然后一个已经读过它的全新反方子进程发言。返回两篇发言。 |
| debate_verdict | 裁判唯一的一次点评:逐轮评述、胜方(pro / con / draw)以及理由。并结束这场辩论。 |

一次调用只走一轮,把单次调用的开销限定在两个子进程内,让辩论边跑边流进界面,也让失败的一轮可以重试,而无需把它前面几轮重新论证一遍。

亮点

- 真正的对手,不是角色扮演。 每篇发言都出自一个全新的子 Agent,带自己的人设,且完全接触不到裁判所在的对话,因此辩手会为分到的那一方辩护,而不是渐渐靠向裁判本来就相信的东西。
- 辩手会做调研。 默认工具集是 read、glob、grep、web_search、web_fetch——足以核实一个关于你仓库或关于世界的论断,并引用得足够精确,让裁判可以自己复核。
- 只读是构造上的保证。 交到辩手手里的任何东西都不能写文件或执行命令。一个能动手的辩方,等于打着辩论的旗号替裁判干活。
- 是降级,而不是失败。 你的部署没有挂载的工具会从列表里剔除并记录一次,不会致命。联网搜索会一直不发放,直到它的凭据真的能解析出来,所以没有辩手会把自己唯一的一轮烧在发现后端未授权上。
- Web 界面里折叠成一张卡片。 整场辩论收进一张 Chat 卡片,逐轮填充,内置 zh / en 两套文案。
- 能扛住重新加载。 一场辩论记录在工具本就会写的 tool/result 事件的 meta 里,所以重新打开会话就能重建卡片。这个插件不声明任何属于自己的会话事件类型——原因见设计说明。
- 对 harness 零侵入。 它以 bundle 形式装进某个 dsh profile,不修改 harness 的安装目录,也不修改它的 checkout。

一场辩论长什么样

下面每一张都出自同一场真实的两轮辩论,辩题针对一个真实代码库,没有任何合成或摆拍。

辩题与第 1 轮——双方发言引用的都是它们真正打开过的文件,Truncated 标记的是触达 maxSpeechLength 的发言:

辩论卡片:辩题,以及第 1 轮正反双方引用文件与行号的发言

裁判在最后一轮结束前始终沉默,然后只裁决一次:

裁判的逐轮点评,以「反方胜」的最终评定收尾

环境要求

- Node ^22.19 或 >=24,以及 pnpm。
- 一个 deepseek-harness checkout 作为构建目标。所有脚本都通过 DSH_HARNESS 定位它;只有当 checkout 正好位于 ../deepseek-harness 或 ../../deepseek-harness 时才可以省略这个变量,脚本会按此顺序探测。用 npm 装的 dsh 不够——构建会读 harness 的源码(packages/client/web/src/platform.ts 以及 tsconfig.base.json 里的路径映射)。
- 那个 checkout 必须已经跑过 pnpm install。pnpm run typecheck 还额外要求它已构建(在那边跑 pnpm run build),因为叶子配置是通过 project reference 解析 harness 包的。
- 安装时需要:要么 PATH 上有 dsh,要么就是同一个 checkout——dsh 缺失时脚本会改用它自带的源码启动方式。

本仓库不硬编码任何路径:克隆下来是没有任何链接的,setup 会为该次运行解析到的那个 checkout 写入链接。指向一个不存在或不正确的目录,会在 setup 阶段失败,并列出它尝试过的路径。

快速开始

把已发布的 bundle 装进某个 profile,无需 checkout——只要 PATH 上有 dsh:

dsh plugin --profile web add @shlouai/dsh-debate

而要从本 checkout 构建并安装:

export DSH_HARNESS=/path/to/deepseek-harness   # 若它是本仓库的同级目录则无需设置

pnpm install
pnpm run setup             # 把本仓库链接到 harness checkout
pnpm run build             # 产出 lib/
pnpm run install:profile   # 构建、打包并装进 web profile

然后启动 profile,用自然语言要一场辩论:

dsh --profile web

辩论我们是否应该把 REST API 换成 gRPC。两轮。

那个 dsh 是 harness 的启动器,本仓库既不附带也不安装它。如果 PATH 上没有,就改用 checkout 自带的源码启动方式——也就是脚本回退时用的那一个,以及 install:profile 结束时打印的那一条:

pnpm --dir "$DSH_HARNESS" exec node --import tsx/esm apps/cli/src/bin.ts --profile web

其他命令

| 命令 | 效果 |
| --- | --- |
| pnpm run install:profile -- --profile headless | 装进别的 profile。卡片只在 Web 端有,工具在哪儿都能用。 |
| pnpm run install:profile -- --no-build | 不重新构建,直接装当前的 lib/。 |
| pnpm run typecheck | 对着 harness 源码类型检查两个 face。 |
| pnpm run shoot:docs | 从一场真实辩论重出 docs/ 里的每一张图。需要 ffmpeg 和一个 Chrome;它会自己启动 profile,所以改动卡片样式后跑它。 |
| pnpm run clean | 删掉 lib/ 和那些 harness 链接。.pack/ 会保留:装过这个 bundle 的 profile 把那个 tarball 记作了自己的依赖 spec。 |

不安装、直接从源码运行——在 harness checkout 里:

pnpm dsh --profile web --patch /src/debate/cordis.patch.yml

从某个 profile 里移除:

dsh plugin --profile web remove @shlouai/dsh-debate

dsh plugin 会依据实际安装情况反向核对 dsh.profile.bundles,所以这个 layer 会随依赖一起离开。PATH 上没有 dsh 时,按上面的方式前缀源码启动命令。

配置

patch layer 只设了 provider: spawn。其余每个字段都取 schema 默认值,并且可以从 profile 自己的 cordis.patch.yml 覆盖——它在本 bundle 的 layer 之后生效:

- id: debate
config:
provider: spawn
maxRounds: 8          # 一场辩论开局时允许的最大轮数
maxSpeechLength: 2000 # 每篇发言的字符数
debaterTools:         # 辩手可用的全局工具名;[] 表示全部拒绝
- read              # 工作区:读一个文件、
- glob              # 按名字找文件、
- grep              # 搜它们的内容
- web_search        # 开放网络:搜索它、
- web_fetch         # 以及完整抓取一个页面
searchTools:          # 上面这些里,受凭据把关的那些;[] 表示都不把关
- web_search
searchCredential: DEEPSEEK_API_KEY   # 启用它们的那个凭据

| 字段 | 默认值 | 含义 |
| --- | --- | --- |
| provider | (必填)* | 跑辩手的子 Agent 后端。必须支持 persona 与 toolFilter;由 dsh-base 挂载的 spawn 满足。 |
| maxRounds | 8 | 一场辩论开局轮数的上限。每一轮花两个子进程。 |
| maxSpeechLength | 2000 | 每篇发言的字符数。后面每篇发言都要读前面所有篇,所以不设上限会让下一个 prompt 以平方增长。 |
| debaterTools | 上面那套只读集合 | 在裁判 Agent 自己能看到的工具之上的一份允许列表。 |
| searchTools | [web_search] | 其中哪些在凭据解析出来之前一直不发放。 |
| searchCredential | DEEPSEEK_API_KEY | 凭据的引用名——绝不是字面上的密钥。 |
| debaterPersona | 竞技辩手人设 | 对双方辩手都覆盖掉部署自身的人设;只陈述共通的辩论技艺。 |

provider: spawn 正是让每个辩手都拿到一个看不见裁判对话的全新子进程的东西——也是让辩手为分到的一方辩护、而不是附和裁判的那个机制。

辩手能做什么

debaterTools 会作为 tools.restrict() 施加到子进程上,所以一个不在其中的名字既不会出现在辩手的 prompt 里,也会拒绝执行。默认刻意是只读的:辩手调研自己的论据,而一个能写文件或跑命令的辩方,等于打着辩论的旗号替裁判干活。debaterTools: [] 则退回到纯粹靠论证。

有两个后果值得知道:

- 本部署没挂载的名字会被跳过,而不是致命错误。 tools.restrict() 会直接拒绝未知工具名,那将导致每一轮都失败而不是某一次调用失败,所以这份列表会被收窄到裁判实际能看到的范围,被丢掉的名字记录一次。单靠 dsh-base 会带上除 web_fetch 以外的全部默认项(它把 tool-web 配成了 fetch: false);web profile 使用的 standard agent preset 会补上它。
- 每篇发言都是某个子进程的单次轮次,所以调研消耗的模型调用发生在那一轮之内:一轮花两个辩手,而每个辩手在开口前可能搜索或读取好几次。

联网搜索取决于凭据

web_search 无论其后端能否为查询完成鉴权都会被挂载,所以对它来说「可用」和「能用」是两件事。因此辩手拿到它的前提是searchCredential 能解析出一个值;否则这个名字不发放,辩论就在无需凭据的工具上进行——read、glob、grep,以及匿名抓取 URL 的 web_fetch。未发放的工具既不在辩手的 prompt 里也不在它的分发表里,所以辩手绝不会被告知一个它用不了的搜索工具,而相关提示按进程记录一次,而不是每篇发言记一次。

密钥可以设在凭据层的任意一层,读取顺序如下:

| 层 | 位置 | 生效时机 |
| --- | --- | --- |
| 继承来的进程环境 | DEEPSEEK_API_KEY=… dsh --profile web | 那一次启动 |
| provider 托管的存储 | ~/.dsh/.credentials.yaml 的 refs: 下(或 Web 界面的 Models 页) | 立即——该存储会发布外部改动,所以辩论进行中存入的密钥能赶上下一轮 |
| 调用目录的 .env | DEEPSEEK_API_KEY=… | 重启 profile 之后 |
| harness home 的 .env | ~/.dsh/.env | 重启 profile 之后 |

那两层 .env 是启动时的快照,这就是它们需要重启的原因。这里从头到尾只读取「值存在与否」:检查问的是凭据层「如果去解析会不会返回东西」,所以密钥不会进入这个插件、它的日志,或者那场辩论。

searchCredential 是一个凭据引用名,不是字面密钥——绝不要把密钥写进 cordis.patch.yml。 DEEPSEEK_API_KEY 是 dsh-base 给它的搜索后端接的那个变量;如果某个部署把 web 的 searchProvider 指向 Exa 或 Perplexity,就要相应地设成 searchCredential: EXA_API_KEY 或 PERPLEXITY_API_KEY,另外注意这两个后端只读环境那几层,而且不在一个原装 profile 的依赖闭包里。searchTools: [] 会彻底取消这道关卡,对于根本不需要密钥的搜索后端,这才是正确的设置。

仓库结构

| 路径 | 是什么 |
| --- | --- |
| src/debate/ | 编排器:debate_open、debate_round、debate_verdict 三个工具,以及它们投影到自身结果上的展示元数据。 |
| src/debate-client/ | 浏览器半边:把那些元数据折进一张 Chat 卡片的 Conversation Node、它的渲染器,以及它的 zh/en 文案。 |
| cordis.patch.yml | profile 的 patch layer,由 dsh.bundle.patch 指名。它插入那两个 host row。 |
| src/debate/cordis.patch.yml | 同一对,但是 .ts row,用于不安装、直接从源码运行。 |
| tsdown.config.ts | host 构建。浏览器 bundle 有自己的配置,在 src/debate-client/tsdown.config.ts。 |
| tools/ | setup、build、install、clean,以及文档素材抓取脚本。 |
| docs/ | 上面各节嵌入的图。每一张都是实时 Web 界面的抓取,所以是由 pnpm run shoot:docs 整体重出的,而不是哪一张被手工改过。 |

那两个源码目录是从它们被写出来的那个 harness checkout 里原样搬过来的,并保持那套布局,这样它们在原地就能编译。编排器现在在这里维护;浏览器半边保持未改动。

设计说明

下面这些是不太显然的决定。除非你要扩展这个插件,或者撞上了什么意外,否则可以跳过。

harness 影子目录 —— 为什么 setup 要写符号链接

src/debate 与 src/debate-client 位于本仓库根目录下两层,跟它们当初位于 harness 根目录下的位置一模一样,而它们未经修改的源码是通过 ../../ 触达 harness 的——tsconfig.base.json、packages/、vendor/、scripts/。pnpm run setup 会在本仓库根创建这四个名字,作为指向那个 checkout 的符号链接,于是源码在原地就能编译。这些链接属于机器状态,已被 gitignore;移动或重装 checkout 之后重新跑一次 setup。

src/debate-client/node_modules 里放着 React 的链接,原因相同:没有任何包管理器会安装那个目录,所以 setup 把它指向 harness 工作区为某个 in-tree 客户端包解析出来的那份。

构建产物布局 —— 一个包如何同时交付一个 host row 和一个浏览器 bundle

profile bundle 是被普通 Node 导入的,那里没有 tsx hook,所以 cordis.patch.yml 指名的是构建出来的 .js row,而不是 .ts 源码:

lib/debate/plugin.js               编排器 row
lib/debate-client/index.js         浏览器 row 那个空的 host 半边
lib/debate-client/package.json     原样拷贝:dsh.client + exports["./client"]
lib/debate-client/lib/client.js    浏览器 bundle

并不存在专属于浏览器的 row 种类。dsh-client-modules 扫描的是 host row,把每个解析出的模块向上走到最近的 package.json,然后为任何声明了 dsh.client 的包提供其 exports["./client"] 那个 bundle。lib/debate-client/ 把源码布局复现得足够接近,于是那份 manifest 可以原样拷过去,它的 ./client 导出依然指向紧挨着它的那个 bundle。编排器那个 row 解析到的是本包自己的 manifest,它没有声明客户端半边,于是被跳过。

为什么安装要打 tarball —— 单例类与依赖闭包

tools/install.mjs 会先打包,再调用 dsh plugin add。如果直接安装这个目录,profile 就会被链回这个 checkout,而 Node 会通过走出本仓库来解析 @deepseek-ai/dsh-tools 和 @deepseek-ai/schemastery——而它们在这里是故意缺席的。打成 tarball 之后,它会变成 profile 的 node_modules 下一个真实目录,其向上走的路径能抵达启动器在 $DSH_HOME/profiles/node_modules 的依赖闭包,从而把运行中那份安装自己的实例交给插件——来自第二份拷贝的 defineTool 或 schema,对每一个接收它的注册表来说都是一个不同的类。它们被声明为 optional peer 也是同一个原因,而 autoInstallPeers 在这里和在 profile 里都是关闭的,这样就不会有谁装出一份重复的来。

install:profile 会在添加新 tarball 之前先移除已有的安装,这正是重装能带上重新构建结果的原因:pnpm 靠一个从不变化的路径加版本号来识别这个依赖,所以把重新打包的 0.1.0 装到它自己身上会报告成功,而 profile 继续提供它第一次解压出来的那份 lib/。首次安装时,那次移除找不到东西,并会如实说明。

一场辩论记录在哪 —— 以及为什么它不是一个自定义事件类型

一场辩论只在一个地方持久化:它自己那三次工具调用本就会写的 tool/result 事件的 meta。每个工具都声明了一个 output.presentationMeta 投影,工具层把它原样持久化在结果旁边,卡片就是由它组装出来的。

这正是内置工具给自己做卡片用的机制——read、edit、grep、web_search 等等都以这种方式投影结构化的视图数据——而用它,才是让一场辩论所在的会话可重新加载的原因。另一条路——把 debate/open、debate/speech、debate/verdict 声明为会话事件类型——在已发布的 harness 上撑不过一次重新加载:KNOWN_SESSION_EVENT_TYPES 是从 harness 自己的 packages/ 生成的,一个下游插件的事件类型在构造上就在它之外,而持久化层面对含有未知类型的日志会拒绝解读,而不是悄悄丢掉其中的事件。信封上的 ignorable 标记正是为这种情况存在的,但没有任何公开的生产者 API 会设置它,所以一个记录自有事件类型的插件,会让每一个用过它的会话都变成一次性的。

从工具结果里读取这份记录,有两个后果:

- 一个投影对单次调用的参数与它的输出值是纯函数。 工具层是拿它去重放一份存储下来的日志,所以它无法查询编排器在内存里持有的那场实时辩论。这就是为什么一篇发言的截断标记是工具声明输出的一部分,而不是投影去重新算出来的东西。
- 嵌在 run_code 程序里的调用不产生元数据。 工具层对非顶层的分发会跳过 presentationMeta,所以从程序里驱动的辩论会正常进行,但不显示卡片。按工具描述的指示直接调用,则每场辩论都会有一张。

常见问题

一场辩论要花多少?
每轮两个子 Agent,外加裁判自己的那些轮次。每个辩手在它唯一的那一轮里还可能搜索或读取好几次。两到三轮是有用的区间;maxRounds 默认把它封在 8。

辩手会改我的代码吗?
不会。它们拿到的是一份只读允许列表,并以 tools.restrict() 的形式在子进程上强制执行,所以任何会写入或执行的东西,既不在辩手的 prompt 里,也会在分发时被拒绝。

辩论能扛住页面重新加载或者会话重启吗?
卡片能——它是从持久化的工具结果重建的。进行中的辩论不能:debate_open 铸出的是一份进程内的记录,所以辩论中途重启意味着重开一场。

不用 Web 界面能用吗?
可以。装进任意 profile;那三个工具在哪儿都能用。只有那张折叠卡片是 Web 专属的。

两个会话能共享一场辩论吗?
不能。一场辩论会记下开启它的那个会话,任何其他会话拿着那个 id 都会被拒绝。

项目状态

实验性质,版本号 0.1.0 也如实反映这一点。工具接口与配置 schema 都仍可能变化。它已发布到 npm(@shlouai/dsh-debate);构建本身仍是对着一个 deepseek-harness checkout 进行的。

参与贡献

欢迎 issue 与 pull request——尤其是围绕辩论形式(交叉质询、多于两方、多名裁判组成的陪审团)、更多子 Agent provider,以及卡片设计。

在开 PR 之前:

pnpm run typecheck   # 两个 face,对着 harness 源码
pnpm run build

请与周围代码保持一致:严格 TypeScript、导出符号带 JSDoc,以及解释为什么而非复述该行代码的注释。

许可

以 MIT License 发布。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群