1105623876/dsh-bench
The plugin registers two tools: bench run to measure, bench history to compare against past runs. Each trial is a fresh one-shot spawn subagent that sees only the task text — no parent history — so the arms are comparable.
이것은 DeepSeek Harness(DSH) 플러그인입니다. 이 사이트는 GitHub README, 설치 정보, 유지보수 상태, 공개 보안 시그널을 모아 보여줍니다.
dsh-bench
English · 中文
把同一个任务跑在不同模型、不同工具面、不同上下文配置上,得到一张对照实验表:哪个 arm 真的做对了、各自花了多少、慢的那个慢在哪一层。 当任务设计/结果有问题时,它还会直接提醒你的 agent——比如所有 arm 走出相同轨迹的任务,测的是打字速度,不是能力。
为 DeepSeek Harness 而写。
插件注册两个工具:bench_run 负责测量,bench_history 负责和历史运行对比。每次试验都是一个全新的一次性 spawn 子 agent,它只看得到任务文本、看不到父会话历史——这是各个 arm 可比的前提。
为什么做这个
插件生态里观察单条会话的工具很多——用量面板、上下文构成、轨迹诊断——但没有一个能对比两条。没有对比,就没法回答"换个便宜模型、砍掉一半工具、压低输出预算,结果到底会不会变差",只能凭感觉。
这里的每一个数字 harness 本来就有,插件做的只是驱动矩阵、折叠结果:
| 指标 | 来源 |
|---|---|
| 步数、模型墙钟时间 | sessionStats session-projection 单元 |
| token | ctx.tokenMeter.measure(session) |
| 工具调用次数与构成 | 子会话持久日志里的 tool/call 事件 |
| 终止原因 | SubagentResult.stopReason |
挂载
不用改 $DSH_HOME 里的任何东西——插件自带覆盖层。复制模板,把路径指向你 clone 的位置:
cp bench.patch.yml.example bench.patch.yml
# 然后把 `name:` 那行改成你自己的绝对路径
dsh --profile web --patch /path/to/dsh-bench/bench.patch.yml
先验证装配,这一步不启动任何东西、零成本:
dsh --profile web --patch /path/to/dsh-bench/bench.patch.yml --dump-config
路径坑。
--patch覆盖层里的相对name:是按 profile 目录($DSH_HOME/profiles/<name>)解析的,不是按 patch 文件所在目录——这和 agent preset 的行为相反(preset 里是按 preset 目录解析)。所以 profile 之外的插件必须写绝对路径,而 Windows 上 Node 的 ESM loader 只接受file://URL 形式的绝对路径。
插件从 profile 之外加载,因此不在 HMR 的监视根目录内:改动需要重启,热重载不会生效。
使用
bench_run 默认是 dry run,只返回计划、不发送任何请求。传 confirm: true 才会真正消耗 token。
{
"task": "读一下 package.json,列出所有 script",
"variants": [
{ "label": "pro" }, // 继承调用者的路由
{ "label": "flash", "model": "deepseek-v4-flash" },
{ "label": "no-shell", "model": "deepseek-v4-flash", "deny": ["bash", "pwsh"] }
],
"repeat": 3,
"check": ["node", "verify-scripts.mjs"],
"confirm": true
}
Ran 9/9 trials.
**Outcome**
| variant | provider/model | runs | pass | completed | outcome |
| --- | --- | ---: | ---: | ---: | --- |
| pro | deepseek/deepseek-v4-pro | 3 | 3/3 | 3/3 | ok |
| flash | deepseek/deepseek-v4-flash | 3 | 3/3 | 3/3 | ok |
| no-shell | deepseek/deepseek-v4-flash | 3 | 1/3 | 3/3 | ok |
**Cost & latency** — medians across runs
| variant | steps | tool calls | tokens | llm ms | ttft ms | tok/s | tool ms | tool mix /run |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| pro | 7 | 8 | 24448 | 8124 | 620 | 96 | 1180 | glob×3, read×2, grep×1 |
| flash | 4 | 4 | 11047 | 3011 | 450 | 198 | 640 | read×3, glob×1 |
| no-shell | 6 | 7 | 15220 | 5340 | 470 | 191 | 910 | read×5, glob×2 |
没有界面:bench_run 是给模型调的工具,在输入框里用自然语言让 agent 调它即可。
不只是"慢",而是慢在哪
llm ms 分不清"在等"和"在生成",而这两者的解法完全相反。所以它被拆开了,用的还是 harness 自己的数:
ttft ms—— 每步首 token 的平均延迟。token 量相当的前提下这里有差距,就是 provider 排队:换个网关、换个时段可能就没了。tok/s—— 解码吞吐。这里的差距主要来自模型——但共享 batching 在高负载下同样会拉低吞吐,所以它是上界而不是常量(相隔一小时的两次运行里,一个 arm 的吞吐掉了 17%,另一个涨了 19%)。tool ms—— 花在工具内部的墙钟时间,完全在模型之外。
测量值为 0 时渲染成 —:真实试验不可能花 0 token、不可能等 0 毫秒出首 token,所以字面的 0 只意味着"没测到",绝不是"瞬间完成"。
报告接着会把每个较慢 arm 相对最快 arm 多花的时间做归因,因为"首 token 慢 2.6 倍、吞吐低三分之一"这句话并不告诉你该动哪一半:
**Where the extra time went**
- **mimo** spent 11427ms more than `flash`: 48% waiting (+5432ms), 52% generating (+5995ms)
这个差距里大约一半可能换个网关、换个时段就没了,另一半则永远不会。两个分量之和不等于 llm ms 的原始差值——框架开销和每步的其他成本不在这两者之内——所以百分比是"已归因部分"的占比。
历史台账
每一次确认执行的运行都会通过 storage seam 记录下来(json backend 下的 $DSH_HOME/storages/bench_runs.json),bench_history 负责读回:
{ "limit": 10, "task": "package.json" } // 两个参数都可选
它按时间倒序列出历史运行,并对跑过不止一次的任务报告每个 arm 的漂移:
**Drift since the previous run of the same task**
- `mimo` on "find the bigger package.json" — ttft -58%, tok/s = (08-16 23:40 → 08-17 09:15)
这一行就是台账存在的全部理由。 单次测量分不清"端点本来就慢"和"那一分钟正好挤"——两次就能分清。吞吐稳住而延迟变动,是 provider 排队;两者一起动,说明负载已经压到解码路径,或者路由换了。而当两个 arm 在同一时间窗内朝相反方向漂移时,原因就不在你的网络——是某一个端点自己的负载。
dry run 不写台账。storage seam 缺失时运行照常完成,并明确告知没有被记录。
消融轴
每个变体都支持 suppress:一个 agent/pre-step 上下文来源的列表,这些注入会在该 arm 的试验里被剥掉。它回答的是每个 harness 用户天天在盲判的问题——这段注入的上下文,值不值它占的 token?
"variants": [
{ "label": "full" },
{ "label": "no-skills", "suppress": ["skill-catalog"] },
{ "label": "no-agents", "suppress": ["agent-instructions"] },
{ "label": "bare", "suppress": ["skill-catalog", "agent-instructions"] }
]
**Context ablation**
- `full` — full context
- `no-skills` — suppressed skill-catalog: 1 message(s) stripped per run
- `bare` — suppressed skill-catalog, agent-instructions: 2 message(s) stripped per run
常见的来源类型是 skill-catalog(可用技能提醒)和 agent-instructions(AGENTS.md / CLAUDE.md 摘要);具体装配可能还会注入别的。
有两条性质比这个功能本身更重要:
- 匹配不到任何东西时会大声报错。 消融的失败模式是静默的——不报错,只是某个 arm 悄悄带着完整上下文跑完,然后被当成证据。所以一旦请求的来源类型剥掉了 0 条消息,报告会明说,并列出它实际见到的类型,避免一个拼写错误伪装成"没有差异"的结论。
- 只影响本次运行的子 agent。 过滤依据是子会话持久化的
parentSession,不是一个全局开关,所以一次 bench 绝不会剥掉另一个工作区并发会话的上下文。
要配合 check 一起用,否则这些数字只能说明"上下文越少越便宜"——而这件事从来没人怀疑过。
当问题出在任务本身
如果所有 arm 走出完全相同的轨迹——步数相同、工具调用相同——报告会直接说出来。那样一张表很容易被读成"这几个模型能力相当",但它通常只意味着任务没给它们分化的余地:把文件路径写死,任何模型都退化成一次 read,这一轮测的只剩打字速度。歧义才是让策略显形的东西;一个没有歧义的任务不具备判别力,无论它的表看起来多好看。
成本不等于质量
completed 只表示子 agent 把这一轮跑完了,不表示它把任务做对了。check 命令是这里唯一的质量信号:一个 argv(不经 shell 解释,需要 shell 就自己传),每次试验结束后在工作区执行,退出码 0 记为通过。它能从 $DSH_BENCH_OUTPUT 拿到该次试验的最终文本,另有 $DSH_BENCH_LABEL、$DSH_BENCH_RUN、$DSH_BENCH_STOP_REASON。
不传它,这一轮就只测了成本,报告里会明说。一个便宜三倍但 check 不过的 arm 不是更便宜的那个,是错的那个。
工具面也是一条轴
每个变体都支持 allow(只保留这些)和 deny(去掉这些),于是"这个模型没有 shell 还能不能做成"变成表里的一行,而不是靠猜。
收窄的实现方式是过滤已装配的工具目录,外加一道 guard 兜底拒绝执行。这里刻意没有用 SubagentStartRequest.toolFilter:它会变成 tools.restrict(),而后者只遮蔽全局工具、遇到 scope-local 名字直接失败——而在标准装配里,所有有意思的工具(bash、pwsh、read、glob、todo_write、web_search、ask_user_question)都是由 agent preset 注册的,不是全局注册的。基于 restrict 的过滤器根本碰不到它们。
默认在所有试验里禁用三个工具(denyTools 可配):
ask_user_question—— 一次性的 bench 子 agent 背后没有人,提问只会把试验挂住、把墙钟时间灌水;而且一个跑去提问的 arm 根本没尝试任务,却会和一个老老实实做完的 arm 并排出现在同样的列里。bench_run和bench_history—— 试验本身就是一个普通 agent,能看到完整目录,包括这两个。子 agent 自己再开一个矩阵,花的是maxTrials管不到的钱——那个上限管的是单次调用,不是一棵调用树。
这是护栏,不是禁令。"比较两个模型驾驭 benchmark 的能力"是个正当实验,所以只要在某个 arm 的 allow 里点名,就能把该工具从默认 deny 里捞出来——明确表达的意图压过安全默认,没表达的则不然。同一个 arm 里既 allow 又 deny 同一个名字会直接报错,而不是悄悄替你决定。
某个名字如果从没被提供给试验,会逐名报出来,并附上完整的可用目录——正是这条检查抓住了"在 shell 工具叫 pwsh 的机器上 deny: [\"bash\"]"这种情况。
汇总口径是中位数 + 计数,不用均值。轨迹结果的分布更接近伯努利而不是高斯,对双峰样本取均值会报出一个任何一次运行都没产生过的数。repeat 小于 3 时,报告会明确提示那几列"中位数"只是单个样本、不构成趋势。
配置
| 键 | 默认值 | 含义 |
|---|---|---|
providerName | spawn | 子 agent provider。用 fork 会继承调用者的对话历史,破坏可比性。 |
maxTrials | 24 | 单次调用 variants × repeat 的硬上限。 |
trialTimeoutMs | 300000 | 单次试验的截止时间。没有它,一条卡死的路由能把整个矩阵一直挂着——曾经有一条坏路由,在总共 6 分钟的运行里独占了 5 分钟的重试退避。 |
checkTimeoutMs | 60000 | 单次 check 的截止时间。 |
denyTools | ["ask_user_question"] | 无视变体设置,在所有试验里一律禁用。 |
有意为之的限制
- 试验串行执行。 并发会让排队延迟污染
llmMs,使墙钟时间列失去可比性。 - 没有 preset 轴。
SubagentStartRequest不带 preset 覆盖——子 agent 会 join 父 agent 的 preset——所以这条轴得绕开 subagent seam,改用ctx.agents.create()。 - 首个试验失败即致命。 provider 名字写错、模型路由不存在、递归深度超限属于装配问题,直接让整次调用失败,而不是拿同一个错误把整个矩阵烧完。之后的失败记为 note 并继续跑。
- 超时的试验照样计入成本。 token 已经花掉了,只有
completed和outcome会把它标成没做完。
状态
P0,已于 2026-08-15 用一次真实的 4 试验运行(2 个 arm × repeat 2)验证。
那次运行确认了:
- 装配、模块加载、工具注册、prompt assembly 的 JSON Schema 校验、output 校验、报告渲染,全链路通。
- 没有审批死锁。 子 agent 执行了 12 次工具调用(
glob、read、grep),没有任何审批门卡住父会话——委派策略确实像child-agent.ts暗示的那样往下传递。 - 一个真 bug,已修: cordis 门禁管的是服务访问,不只是插件激活。没声明依赖就读
ctx.tokenMeter会抛cannot get property "tokenMeter" without inject,于是防御性 try/catch 把所有指标列降级成了0。现在可选 seam 统一走ctx.inject(…)获取;缺失时仍然能报出工具构成和完成计数,并明确写出缺了什么。 - 路由一旦损坏,重试退避会主导墙钟时间。 一个失败的 arm 吃掉了整轮 6 分钟里的约 5 分钟:2 次 repeat × 每次 2 轮
llm/retry,中间还有退避。单试验超时应该进 P1。
那次运行同时产出了这个工具的第一个真实发现,也正是它存在的意义:某个 arm 拿到 0/2 完成、零工具调用,其子会话全部以 turn/end → error, "Stream ended without finish_reason" (TRANSPORT) 结束。坏掉的 provider 路由在日常聊天里是隐形的——只让人觉得"今天有点慢"——但摆在一个正常 arm 旁边就无所遁形。
P1 补上的,正是第二次真实运行暴露出来的缺口:
- 默认禁用提问类工具。 那次两个 arm 都调了
ask_user_question——一次性子 agent 背后没有人,这纯粹是墙钟时间的污染,也让 5.8× 的延迟差距变得无法归因。(第一版实现走的是toolFilter,后来的运行证明它什么也没禁掉,见消融那一节。) check命令。 那次运行只能得出"flash 更便宜",永远得不出"flash 更好",因为没有任何东西验证过两边的答案。- 单试验超时,起因是更早那次 6 分钟的运行里有 5 分钟耗在一条坏路由的重试退避上。
- 报表口径修正。 工具混用那列原本是多次运行的总和,却和旁边单次的工具调用中位数并排——一张表里两种单位。现在统一成每次运行。
repeat小于 3 时,报告也会声明那个"中位数"只是单个样本。
再后来一轮对比换成了带真实定位环节的任务。两个模型都 3/3 通过,步数和工具调用完全相同、token 差距在 6% 以内,只有墙钟时间有差别——这提出了 llmMs 回答不了的问题,于是有了上面的 ttft/吞吐拆分和判别力警告。那一轮同时也淘汰了一个假设:最早那次运行里某个 arm 的大量 shell 探索始终没有复现,它是一个未经重复验证的单样本,而不是一种策略。
P2 加上了消融轴,而它的第一次真机运行立刻暴露了两个构造上就是静默的失败:
- 工具收窄从来就没生效过。
SubagentStartRequest.toolFilter会变成tools.restrict(),后者只遮蔽全局工具,而标准装配里所有有意思的工具都是由 agent preset 注册的。那次运行报出ask_user_question is not registered,可子 agent 一直在调用它——之前"P1 的 deny 生效了"这个判断是错的:它不再出现是因为任务换了,与功能无关。现在收窄改为过滤已装配的目录,后面还有一道 guard 拒绝执行。 - 归因在分量异号时算崩了。 它拿两个分量之和当分母,于是给一个"等得更久但生成更快"的 arm 印出了
292% waiting (+-852ms)。现在异号时不给百分比,直接报带符号的毫秒。
那次运行同时说明了,"什么都没匹配上"的诊断比两条轴本身更重要:skill-catalog 根本不是本装配会注入的来源类型,于是一个标着 no-skills 的 arm 悄悄带着完整上下文跑完了——没有那条警告,它会被读成"技能目录没用"的证据。同一轮里还把 bench_run 和 bench_history 设为试验默认禁用,因为可用工具清单显示一次试验能自己再开一个矩阵。
验证到哪一步了
每个功能都是对着真实运行写出来的,但不是每条路径都在真机上跑过。失败路径目前只有 mock 覆盖,这里如实标注而不含糊带过:
| 能力 | 状态 |
|---|---|
| 子 agent 试验、指标折叠、报告渲染 | 真机 |
check 通过/失败、台账写入、bench_history 漂移 | 真机 |
| ttft / 吞吐 / 工具耗时拆分、归因 | 真机,含异号那种情况 |
上下文消融(suppress) | 真机——每次运行剥掉 3 条消息 |
| 两条轴的"什么都没匹配上"诊断 | 真机——抓到了本装配根本不注入的 skill-catalog,以及在 shell 是 pwsh 的机器上 deny: ["bash"] |
工具收窄(allow / deny) | 真机执行过(pwsh 在过滤前的目录里并被剥掉),但还没有任何一次运行显示它改变了行为:那道题两个 arm 本来就不用 shell。换一道非用 shell 不可的题才能坐实。 |
| 单试验超时、check 失败、seam 缺失、provider 不支持该能力 | 仅 mock——真机还没撞上过 |
路线图
- P3 —— 轨迹 diff、经由
ctx.agents.create()的 preset 轴、并发执行。
许可
MIT
보안 및 설치 증거
이 점수는 공개 저장소 메타데이터와 이 사이트에 등록된 설치 증거에만 기반하며, 코드 보안 감사와 다릅니다.
공개 플러그인 카탈로그에서 왔으며, 공개 GitHub 저장소로 연결됩니다.
저장소가 MIT 라이선스를 선언했습니다.
최근 180일 내 코드 업데이트가 있습니다.
재현 가능한 정확한 설치 메타데이터가 아직 등록되지 않았습니다. 저장소 설명에 따라 직접 확인하세요.
검사한 패키지 메타데이터에 설치 라이프사이클 스크립트가 선언되지 않았습니다.