Pular para o conteúdo principal
6

dsh-tool-dispatch

66-zhimeng/dsh-tool-dispatch

Batches independent tool calls into one model round-trip: run_tools dispatches many steps concurrently (after-dependencies for ordering), run_parallel fans out sub-agents, so fewer round-trips and lower wall-clock time.

Instalar

dsh plugin --profile web add github:66-zhimeng/dsh-tool-dispatch

README

dsh-tool-dispatch

给 DeepSeek Harness 加两个工具,把「很多次模型往返」压成「一次往返」。单步往返的固定成本(本机实测 TTFT 中位 3.0s、整步中位 5.7s)是这里唯一要消灭的东西。

工具解决什么代价
run_tools一次模型往返里执行多个工具调用(读 3 个文件、跑 2 条命令、抓 4 个网页),默认并发,可用 after(下标或 label)表达依赖这一步的模型输出变长(要写 N 组参数)
run_parallel一次往返把 N 个互不依赖的子任务派给 N 个子 agent 真并行子 agent 各自烧 token

工具名固定为 run_tools / run_parallel;插件在 profile 级挂载,该 profile 下的每个会话都能用。


零、让模型自己想到用(系统提示词)

插件除了注册两个工具,还在 profile 级注册一段全局系统提示(dispatch-policy,约 8 行):

发工具调用前扫一眼:这一批里有没有两个以上互不依赖的动作?有就合并成一次 run_tools,没有就直接发。多个独立重任务 → run_parallel。只有一步、或后一步要看前一步结果 → 不要硬包。判定只看一眼,不要为此展开推理。

覆盖范围:standard / ptc / cordis(创造模式)都会带上;ptc 下额外写明「在程序里写 await tools.run_tools({steps:[...]})」。唯一例外是 minimal 预设——它的 persona 声明了 complete: true,按内核规则会覆盖其它所有段,那段提示不会出现(工具本身仍然可用)。

不想要这段提示:把 patch 里的 dsh-tool-dispatch 条目删掉,工具和提示一起消失。

一、它是怎么加速的

1. 一次往返里到底在等什么

一个 step 的墙钟 ≈ 首字延迟(TTFT) + 生成输出 token + 工具执行。

本机实测(同一台机器、同一模型、20 个会话 982 个 step):

分量数值占单步
TTFT(等到第一个 token)p50 3.0s / p90 4.3s约 36%
生成(含思考)输出 p50 611 tok ÷ 195 tok/s ≈ 3.1s约 45%
工具执行p50 0.56s约 19%

TTFT 与上下文长度几乎无关(36k 上下文 1,988ms、105k 上下文 2,346ms),所以它是一笔按次数收的固定费,不是「上下文太长拖慢的」。

2. 分开做 vs 合批:公式

分开做(N 个动作)≈ N × (TTFT + 生成 + 单个工具)
合批(一次 run_tools)≈ 1 × (TTFT + 生成(N 组参数) + max(工具))   ← 工具默认并发

省下来的是两块:

  • N−1 次 TTFT:每少一次往返就少等一次首字;
  • 工具串行改并发:sum(工具) 变成 max(工具)。

代价是这一轮模型要多写 N 组参数(输出 token 变多),以及单步超时必须够长(默认 60s,stepTimeoutMs 可调)。

3. 为什么必须由插件来做

一次模型输出里能带几个 tool_call 由模型决定,执行侧改不了;但内核原本还会把独占工具逐个串行执行(dsh 的 batch.js 对非并发安全工具分组长度为 1)。本插件在工具内部拿到 ctx.tools.execute,把 N 个调用重新派发成并发 —— 这是「一次往返里真的让多个动作同时跑」的关键,纯提示词做不到。

4. 什么时候会自动停下

dsh_browser_*、ask_user_question、todo_write 会改动共享状态(同一个页面、同一个人、同一份待办),所以它们恒定独占:等批内其他调用跑完再上,自己跑完才放行下一个。


二、什么情况下快多少

工具侧(实测,本机,pwsh 每条 Start-Sleep 1)

形状串行本应实测说明
4 条独立命令~4.0s1,315ms默认并发
8 条独立命令~8.0s1,451ms默认上限 8
8 条 + maxParallel: 4~8.0s2,608ms分两波
3 条 after 链~3.0s3,777ms依赖链不省工具时间
6 个快工具(read/grep/glob)~0.9s186ms并发后只剩最慢那个

端到端(模型往返 + 工具)

场景分开做一次 run_tools提速
3 条各 2 秒的命令30.4s(3 次往返)4.1s(1 次)7.4×
4 条各 1 秒的命令19.9s(4 次往返,实测 5.98+4.66+4.66+4.56s)约 5.7s(1 次往返:工具 1.3s + 单步 4.4s)3.5×
4 个文件读(工具很快)13.2s(4 次往返)8.15s(1 次)1.6×

「分开做」的端到端取自会话日志的 step/start → step/end 累加;「合批」的单步模型时间取同类低输出步(同一会话、输出 <200 tok),避免拿「我恰好在写长文的那一步」当基线。

两者的差距说明一件事:工具越慢、动作越多,收益越大。读文件这类毫秒级动作,省下来的几乎全是往返;而每次往返本身还有 3s 首字 + 一段思考,所以 N=4 时只有 1.6×。

每动作的模型时间(本机 5,953 个历史 step)

每步承载的动作数单步均值折算每动作
1 个8.8s8.8s
≥2 个12.4s6.2s

即:合批把每次动作的模型开销从 8.8s 压到 6.2s(−30%),因为固定成本被摊薄了。


5 动作小任务上的实测(同一窗口,同一模型,2026-11-11)

任务:node -v、git --version、读文件前 5 行、取一个 GitHub API 字段、写文件后读回。5 个子 agent,3 个「尽量合批」、2 个「严格串行」。

组steps工具调用run_tools模型时间工具时间墙钟输出 tok
合批 ×34 / 5 / 54 / 5 / 50 / 1 / 157.3 / 34.7 / 51.8s1.0 / 1.6 / 1.3s58.8 / 37.2 / 53.8s2.5 / 2.9 / 2.9k
串行 ×27 / 107 / 90 / 036.8 / 98.4s1.0 / 1.0s38.9 / 100.8s3.0 / 2.0k

结论:步数 4.7 → 8.5(−45%),模型时间 47.9s → 67.6s(−29%),墙钟 49.9s → 69.9s(−29%);但只有 5 个样本、单步方差 ±20s——串行的 s1(38.9s)比最快的合批样本(37.2s)只慢 1.7s,所以这个差距的置信度有限。工具时间只占墙钟 2–3%,合批省的是往返次数,不是「跑工具的时间」;慢工具任务才是主场:8 条 1 秒命令,串行 ≈8s,合批 1,451ms。

PTC 模式下的实测(同一台机器,2026-11-11,全部在 PTC 会话里跑)

场景(都是 1 个 run_code 步)写法程序耗时整轮墙钟
3×pwsh 各 6sPromise.all20,303 ms40.8s
3×pwsh 各 6s逐个 await19,599 ms37.6s
3×web_fetchPromise.all966 ms17.0s
3×web_fetch逐个 await2,793 ms21.4s
3×pwsh 各 6s(standard 预设,run_tools)默认并发6,284 ms—

结论:pwsh / 浏览器是独占工具,PTC 内核让同批独占调用构成屏障,Promise.all 只能拿到「排队」,拿不到重叠;read / grep / web_fetch 这类并发安全工具才是 Promise.all 的主场(966 vs 2,793 ms,2.9×)。另外在 PTC 里 tools.run_tools 看不到原生工具(返回 UNKNOWN_TOOL: only run_code is callable directly),所以要让 PTC 里的独占工具真正并发,必须把它们包进 await tools.run_tools({steps:[...]}):v0.2.8 起 run_tools 在程序内用 parent 令牌直派到执行器的 pipeline,绕开程序内那个「独占工具排队」的调度器。实测同一段 3×pwsh(各 6s):Promise.all ≈ 19,600–20,300 ms,await tools.run_tools(...) 6,418 ms(三步分别 6,415 / 6,414 / 6,417 ms,完全重叠),与 standard 的 6,284 ms 基本一致。

三、什么时候不该用

按形状选工具,别让一种工具做所有事:

形状用什么为什么
互不依赖的多个动作一次 run_tools(默认并发)省往返,工具侧并发
线性链 / 菱形汇合(先后能提前写死)run_tools + after(下标或 label)依赖能静态表达,不必写程序
循环、条件分支、对前一步输出做计算/筛选run_codeafter 只排顺序,不做数据流与控制流
PTC 模式(顶层只有 run_code)程序里 await tools.run_tools({steps:[...]});互不依赖的用 Promise.all保留程序表达力,别把独立动作写成串行 await
  • 有依赖关系:B 需要 A 的产物时,用 after: [0] 或 after: "build"(前一步的 label)声明,别硬塞进同一个并发波次。
  • 必须看结果才知道下一步(浏览器点一下看一眼、逐步调试):合批没有收益,收益本来就为零。
  • 只有一个动作:直接调用原工具,包一层只会多一次参数序列化。
  • 单步会超过 stepTimeoutMs(默认 60s):要么调大,要么放后台。

四、安装

从插件市场装(DSH 里添加索引 https://raw.githubusercontent.com/66-zhimeng/dsh-plugins/main/market/index.json,搜 Dispatch),或手工装:

Copy-Item "D:\work\aigc-media\modules\dsh-tool-dispatch" "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-tool-dispatch" -Recurse
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- insert:
    - id: tool-dispatch          # 换代码时必须改这个 id
      name: dsh-tool-dispatch
      config:
        telemetryPath: C:\Users\<you>\.dsh\dsh-tool-dispatch\telemetry.jsonl

保存即热挂载,不需要重启。


五、用法

{
  "steps": [
    { "tool": "pwsh", "arguments": { "command": "pnpm build" } },
    { "tool": "read", "arguments": { "file_path": "dist/index.js" }, "after": [0] },
    { "tool": "grep", "arguments": { "pattern": "TODO", "path": "src" } }
  ]
}

第 1、3 步同时跑,第 2 步等第 1 步完成。

参数默认说明
steps[].after—数字/字符串或数组(下标或前序 label),指向更早的步;声明了才等待
maxParallel8同时最多几个步骤在飞
stopOnErrortrue失败后不再启动新步骤(已在飞的不打断)
stepTimeoutMs60000单步超时
totalTimeoutMs180000整批超时
perStepChars800每步结果回给模型的字符上限
exitCodeIsErrortrue文本里出现非零 [exit code: N] 时判失败

run_parallel:tasks: [{description, prompt}],最多 8 个,maxParallel 默认 8。


六、稳定性

PTC 下 60–100 路并发子调用的实测(2026-11-12):前三轮 220/220 成功、0 超时、0 UNKNOWN_TOOL;第四轮故障注入精确命中 6/6([exit code: 3]),其余 54 步照常完成。总耗时 ≈ 单步最大耗时,说明是真并行;规模上限来自进程启动开销,不是调度器。完整数据见 docs/stability-2026-11-12.md。

七、限制

run_tools 的结果里有一个 via 字段:nested 表示走执行器 pipeline(PTC 下能并发原生工具),execute 表示回退到 ctx.tools.execute()。插件配置项 forceFallback: true 可强制走回退,只用于诊断——PTC 下回退会让原生工具报 UNKNOWN_TOOL(内核规则,安全失败);standard 下回退正常可用(实测 3 条 1 s 命令并发完成,1.26 s)。

  • 只减少往返次数,不减少工具本身的耗时;单个慢工具(例如 60s 的构建)不会因为合批变快。
  • 并发上限受模型侧影响:一次派太多次,上游可能排队(实测 8 路子 agent 中有一个首字等了 34.8s,是上游延迟不是插件排队)。
  • 加速不是自动的:模型得真的把多个动作塞进一次 run_tools。一次只发一个调用时,和没有这个插件完全一样。
  • 只作用于本 profile 的会话。

开发

node test/smoke.mjs        # 19 项:调度、超时、截断、保护区独占
node test/fast.mjs         # 9 项:退出码、after、maxParallel 封顶
npm run test:kernel        # 两道真实内核探针(并发派发、after 两波)

装好之后想改代码:改完必须改 patch 里的 id 才会重新挂载(只改 name 会导致旧实例卸载、新实例不注册)。

部分执行:一步写错,不再整批作废

0.2.15 起,静态校验按步生效。以前只要有一个 step 写错(漏 tool、把 arguments 写成 args、after 指错), 整批 0/N 直接拒绝,本来正确的步骤一起没跑;现在同一个错误只让那一步失败,其余步骤照常执行, 返回里仍带原始下标与原因:

{"index":1,"tool":"(invalid)","ok":false,"skipped":false,
 "summary":"ERROR: step[1]: tool must be a non-empty string"}
  • 依赖它的步骤标 skipped: step N did not succeed,不会带着坏前提继续跑。
  • 静态错误不触发 stopOnError——那个开关只管运行期失败。
  • 批次级问题仍是整批拒绝:steps 为空、超过 maxSteps。
  • 输出结构不变(ok / totalMs / via / steps[]),旧调用方无需改。

Plugins relacionados