dsh-bench
Run one task across several model/config variants in DeepSeek Harness and compare the trajectories side by side
安装
此插件尚未提供可验证的 bundle,或兼容性检查未通过。请先阅读仓库说明。 阅读完整 README ↗
说明文档
阅读完整 README ↗dsh-bench
English · 中文
在 DeepSeek Harness 里把同一个任务跑在多个模型/配置变体上,并排对比它们的轨迹。
插件只注册一个工具 bench_run。每次试验都是一个全新的一次性 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/ds-bench/bench.patch.yml
先验证装配,这一步不启动任何东西、零成本:
dsh --profile web --patch /path/to/ds-bench/bench.patch.yml --dump-config
路径坑。
--patch覆盖层里的相对name:是按 profile 目录($DSH_HOME/profiles/)解析的,不是按 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": "flash-capped", "model": "deepseek-v4-flash", "maxTokens": 1024 }
],
"repeat": 3,
"confirm": true
}
Ran 6/6 trials.
| variant | provider/model | runs | completed | steps | tool calls | tokens | llm ms | tool mix |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| pro | deepseek/deepseek-v4-pro | 2 | 2/2 | 7 | 8 | 24448 | 8124 | glob×6, read×4, grep×2 |
| flash | deepseek/deepseek-v4-flash | 2 | 2/2 | 4 | 4 | 11047 | 3011 | read×3, glob×2 |
没有界面:bench_run 是给模型调的工具,在输入框里用自然语言让 agent 调它即可。
汇总口径是中位数 + 完成计数,不用均值。轨迹结果的分布更接近伯努利而不是高斯,对双峰样本取均值会报出一个任何一次运行都没产生过的数。
配置
| 键 | 默认值 | 含义 |
|---|---|---|
providerName | spawn | 子 agent provider。用 fork 会继承调用者的对话历史,破坏可比性。 |
maxTrials | 24 | 单次调用 variants × repeat 的硬上限。 |
有意为之的限制(P0)
- 试验串行执行。 并发会让排队延迟污染
llmMs,使墙钟时间列失去可比性。 - 没有 preset 轴。
SubagentStartRequest不带 preset 覆盖——子 agent 会 join 父 agent 的 preset——所以这条轴得绕开 subagent seam,改用ctx.agents.create()。 - 还没有 context 消融轴(按 arm 抑制
agent/pre-step注入)和确定性判定命令。两者都是下一步;消融钩子的可行性已被$DSH_HOME/.agent-presets/anchored-standard/tool-bootstrap.mjs证明。 - 首个试验失败即致命。 provider 名字写错、模型路由不存在、递归深度超限属于装配问题,直接让整次调用失败,而不是拿同一个错误把整个矩阵烧完。之后的失败记为 note 并继续跑。
状态
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 —— 单试验超时、确定性
check判定命令、失败原因独立成列(现在传输失败和任务没做完都只表现为完成数偏低)。 - P2 —— context 消融轴:按 arm 抑制
agent/pre-step注入,量化每一段注入上下文到底值不值它占的 token。 - P3 —— 轨迹 diff、经由
ctx.agents.create()的 preset 轴、并发执行。
许可
MIT