# 量一量 fast mode 到底快不快

这份文档给的是**方法**，不是结论。同一份代码在不同机器、不同任务上的结果会不一样，所以这里
只提供一件东西：一个能在你自己的机器、你自己的任务上跑出数字的工具，以及读这些数字的规则。
工具是 [`audit/run.mjs`](../audit/run.mjs)，样例清单是
[`audit/tasks.example.jsonl`](../audit/tasks.example.jsonl)。

## 它改的是什么（也是它为什么不总是有用）

fast mode 只在**写给模型的那段话**上动手：先给结论、别复述任务、少讲过程、别把工具输出再抄一遍。
它不改模型、不改推理档位、不改请求体，也不改工具的执行方式。所以可能省下来的只有一类东西——
**模型自己产出的 token**；而这些 token 又串行地决定解码时间。链条是：

    提示词更短 → 输出更短 → 解码更少 → 这一次的墙钟更少

链条上任何一环不成立，收益就不出现：

- 任务的大头是读文件、跑测试、装依赖、等网络——这些时间里模型不在说话，fast mode 碰不到。
- 任务本来就需要完整输出（导出数据、生成代码、长清单）——省不了，甚至因为"别复述"的约束而返工。
- 模型侧排队、限流、缓存冷热——这些噪声比效应大，单次比较读不出任何东西。

所以这个工具默认就假定**你不该相信单次结果**，它只给配对比值和区间。

## 怎么跑

先看它到底会执行什么（什么都不跑，只展开命令行）：

    node audit/run.mjs --dry-run --cwd . --tasks audit/tasks.example.jsonl

真跑（默认每个任务每臂 3 轮，共 5 个任务 → 30 次调用）：

    node audit/run.mjs --cwd . --tasks audit/tasks.example.jsonl

产物落在 `audit-out/<时间戳>/`：`pairs.jsonl`（每次调用的原始读数，一行一对）和
`report.md`（人看的表格 + 结论）。

`--cwd` 必须是 `check` 命令能跑的那个仓库；`check` 是每次调用**之后**执行的一条 shell 命令，
退出码 0 记为通过。它守的是"这次跑没跑成/仓库有没有被写坏"，不是给答案打分——模型答得好不好
不在这份工量具的判据里。

真实 PR 这类任务会被模型改坏仓库：在单条任务上加 `"setup": "git reset -q --hard HEAD && git clean -qfd"`，
每轮调用前先跑一遍，把仓库放回它一开始的样子；`setup` 自己失败的那一轮记失败、不进统计，免得第二轮起
在「已经被修好」的树上量出假的快棋。

一张清单要混多个仓库时，在单条任务上加 `"cwd": "/绝对/路径"`，那条任务的调用和 `check` 就都在
这个目录里跑；不写的任务仍用 `--cwd`。这样真实 PR 任务（每个任务一个 checkout）和自造任务可以
放进同一张清单，报告里的配对数按任务自然对齐。

常用开关：

    --rounds 5              每臂轮数（配对不足 6 对时报告会明写"样本不足，不做结论"）
    --profile bench         用哪个 profile（必须装了 dsh-fast-mode；见下）
    --dsh-bin <path>        换 launcher；默认是探测到的那一个
    --arm-style override    强制覆盖形式（auto 会先 --dump-config 判断）
    --arm-style none        两臂同命令：量的是噪声底，不是加速
    --on-config k=v         给 on 臂多开一个配置项，可重复（见下）
    --metrics-file <path>   换 metrics jsonl（默认 ~/.dsh/fast-mode-metrics.jsonl）
    --timeout-ms 900000     单次调用上限

## 两个臂怎么造出来

两个臂加载的是**同一个**插件包，只差 `config.enabled`。每次调用都带一份 `--patch` 覆盖：

    # arm-off.patch.yml            # arm-on.patch.yml
    - id: fast-mode                - id: fast-mode
      config:                        config:
        enabled: false                 enabled: true
        gear: auto                     gear: auto

`--on-config` 只写进 on 臂，所以一次跑一个开关组合：

    # 只量压缩：on = 开启 + compactResults
    node audit/run.mjs --tasks tasks.jsonl --cwd . --profile bench \
      --on-config compactResults=true --on-config compactMaxChars=4000

    # 量"省 token"和"改齿轮"一起开：两个都没动 off 臂，比值仍是同一基线
    node audit/run.mjs --tasks tasks.jsonl --cwd . --profile bench \
      --on-config compactResults=true --on-config gear=fast

两个都开是本机实测过的组合（见下「本机实测」），结论是分不开；`gear` 写别的值同样能跑，
但没量过就别在文档里当成结论。`enabled` 和 `gear` 这类基础键由 `--on-config` **覆盖**，
不是追加——1.7.11 之前会写出两行 `gear:`，严格解析器直接报错（on 臂每次 70 毫秒就失败，
报告里看着像"快得离谱"）。

值的写法保持类型：`true`/`false` 是布尔、纯数字是数字、其余按字符串。写错键名会在启动前报
`--on-config 的键名不合法`，不会跑完一轮才发现 on 臂其实用的默认值。

`--arm-style auto`（默认）先用 `dsh --profile <p> --dump-config` 看这个 profile 里
有没有 `- id: fast-mode`：有就走 override，没有就走 insert。这一步不能省，因为两种错法都不响：

- 该 override 却 insert：dump 里出现两条 fast-mode，后一条赢，两臂可能都在同一个开关上。
- 该 insert 却 override：覆盖没命中任何条目 → 什么都没变 → 两臂完全相同。
- 两种情况都会得到"比值接近 1.0"的漂亮报告，但那不是结论，是没做实验。

真跑遇到 profile 探不通（例如 `profile "desktop" is managed exclusively by the Electron
application`）或里面压根没有 fast-mode 条目，工具会直接停下并说明该换什么，不会继续跑出一个假报告。

## 两个指标，一种读法

**墙钟**：`process.hrtime.bigint()` 包住子进程，从 spawn 到退出。**输出 token**：优先取 CLI
`--json` 事件里 `step_end.usage.outputTokens` 的累加；没有就按本次 `session` 事件的
`sessionId` 去 metrics jsonl 里过滤求和；两者都没有就写 `N/A`，不做任何估算——估算混进测量
里，报告就失去意义。

统计口径：

- 每对的比值 = on / off，多个任务/多轮取**几何均值**（时间比值相乘取平均，不能算术平均）。
- 区间用**配对 bootstrap**：4000 次有放回重采样，取 2.5 / 97.5 分位，固定种子 → 重跑结果一致。
- 区间整体 < 1 才算变快，整体 > 1 算变慢，跨 1 一律写"无法区分"。
- 配对数 < 6 时不看区间，直接写"样本不足，不做结论"。少于 6 对时 bootstrap 区间会窄得不像话。

## 该说"没有加速"的时候

下面每一条都够格写进报告，而不是含糊过去：

- 区间跨 1，或中心值离 1 不到几个百分点（本机噪声常在 5% 上下）。
- 任务大头是工具执行，输出本来就短。
- `--arm-style none` 的噪声底跑出来，区间宽度和"加速"差不多——那说明测到的就是噪声。
- 任务清单里有任务每次都失败（检查项没通过）——失败重试的耗时是另一种东西。

说"变慢"也一样直接。fast mode 的约束（先给结论、别复述）在需要完整输出的任务上会真的让人
多花一轮，报告里出现 > 1 的区间是正常结果，不是工具坏了。

## 已知限制（含本机未验证的点）

- **凭据**：本机这个 shell 直接跑真调用会失败在 `MISSING_CREDENTIAL: llm-pi-ai: no credential
  for provider route "company"`；补上 `COMPANY_LLM_TOKEN` 后才能跑（下面「本机实测」就是这么跑
  的）。跑到凭据失败时工具会让你看见 `turn_end.reason.error.code`，不会把它算成一次测量。
- **profile**：`headless` 的 node_modules 里没有 dsh-fast-mode，在那儿做 A/B 两臂都关着；
  `desktop` 由 Electron 独占，dump 不了。默认值 `bench` 是实测能 dump、且 `--patch` 真能把
  `enabled` 从 false 翻到 true 的那个（证据写在 `DETECTED.evidence`）。
- **顺序**：必须串行跑。并行跑会让两臂互相抢 CPU 和模型侧并发额度，测出来的差异是调度差异。
- **缓存**：同一台机器上多轮之间模型侧缓存会变热，配对设计只能抵消一部分，所以样本量比单次
  精度重要。
- 墙钟包含进程启动（约几百毫秒）。要更纯的对比得看 token 那一列，或者把 `--rounds` 加大让固定
  开销摊薄。

## 本机实测（可复核的原始数字）

同一台机器、`--profile bench`、串行跑、配对 bootstrap 4000 次、种子 20261102；任务是
`audit/tasks.example.jsonl` 的 5 个（list-files / read-package / grep-geomean / run-tests /
explain-config），目标仓库就是本仓库；标了「真实 PR 任务」的那一行是另一套任务（见下一节），
目标仓库是 6 个 `pallets/click` 的真实 PR。

| 日期 | on 臂比 off 臂多了什么 | 任务×轮 | 合池墙钟 | 合池输出 token | 校验 off/on |
| --- | --- | --- | --- | --- | --- |
| 2026-11-02 | 只有 `enabled: true`（`gear: auto`） | 5×3 | **0.781×** [0.656, 0.923]，省 21.9% | **0.667×** [0.547, 0.806]，省 33.3% | 12/12 / 12/12 |
| 2026-11-03 | 只有 `compactResults=true`（默认 8000），真实 PR 任务（**仅 click 6 题集，不外推到 SWE-bench Lite**） | 6×4 | **0.805×** [0.656, 0.985]，省 19.5% | **0.768×** [0.618, 0.960]，省 23.2% | 24/24 / 24/24 |
| 2026-11-03 | 只有 `enabled: true`，样本加到 6 轮 | 5×6 | **0.787×** [0.671, 0.928]，省 21.3% | 0.901× [0.751, 1.097]，分不开 | 24/24 / 24/24 |
| 2026-11-03 | 再加 `compactResults=true, compactMaxChars=4000` | 5×3 | 0.900× [0.732, 1.152]，分不开 | 0.967× [0.766, 1.282]，分不开 | 12/12 / 12/12 |
| 2026-11-03 | 再加 `gear=fast`（两个开关一起开） | 5×3 | 0.858× [0.640, 1.140]，分不开 | 0.858× [0.643, 1.136]，分不开 | 12/12 / 12/12 |
| 2026-11-03 | 同一组合（`compactResults` + `gear=fast`），样本加到 6 轮 | 5×6 | 0.867× [0.745, 1.005]，分不开 | 0.934× [0.775, 1.138]，分不开 | 24/24 / 24/24 |
| 2026-11-03 | 再加 `gear=aggressive`（开到最猛） | 5×3 | 0.980× [0.833, 1.141]，分不开 | 1.085× [0.872, 1.309]，甚至略多 | 12/12 / 12/12 |
| 2026-11-03 | `gear=aggressive` 单独开，样本加到 6 轮（不带压缩） | 5×6 | 1.076× [0.931, 1.240]，分不开 | 1.122× [0.946, 1.328]，分不开 | 24/24 / 24/24 |
| 2026-11-03 | 只开 `workspaceMap=true`（开局附文件清单） | 5×6 | 0.943× [0.805, 1.100]，分不开 | 0.969× [0.811, 1.154]，分不开 | 24/24 / 24/24 |
| 2026-11-03 | 只开 `compactResults=true, compactMaxChars=2000`（收紧预算） | 5×6 | **0.818×** [0.675, 0.980] | 0.854× [0.690, 1.051]，分不开 | 24/24 / 24/24 |
| 2026-11-03 | 同一组合同一批次重跑（独立第二次 30 对） | 5×6 | 0.893× [0.741, 1.068]，分不开 | 0.932× [0.762, 1.145]，分不开 | 24/24 / 24/24 |
| 2026-11-03 | 两次合池（60 对，同一配置） | 5×12 | **0.855×** [0.747, 0.971]，省 14.5% | 0.892× [0.769, 1.032]，分不开 | 48/48 / 48/48 |
| 2026-11-03 | 只开 `compactResults=true`（默认预算 8000） | 5×6 | **0.756×** [0.611, 0.921]，省 24.4% | **0.786×** [0.623, 0.990]，省 21.4% | 24/24 / 24/24 |
| 2026-11-03 | `compactResults=true`（默认 8000）+ `gear=fast` | 5×6 | 0.937× [0.763, 1.151]，分不开 | 0.973× [0.815, 1.165]，分不开 | 30/30 / 30/30 |
| 2026-11-03 | 两臂完全相同（`--arm-style none`，噪声地板） | 5×3 | 0.940× [0.726, 1.192] | 0.913× [0.667, 1.229] | 12/12 / 12/12 |
| 2026-11-03 | 两臂完全相同（`--arm-style none`），样本加到 6 轮 | 5×6 | 0.945× [0.836, 1.067]，分不开 | 0.968× [0.782, 1.222]，分不开 | 24/24 / 24/24 |

两条 6 轮（30 对）是目前最硬的数字，但结论不同，不能混着读：

- 只有 `enabled: true` 那条，墙钟区间整体落在 1 以下（0.671–0.928），是唯一一条能说"确实变快"的。
- 两个开关一起开那条，墙钟 0.867× 的区间是 0.745–1.005，**上界擦着 1**；token 0.934× 的区间
  0.775–1.138 直接跨过去。所以组合的结论仍是"分不开"，不是"更快一点"。30 对下它离显著的
  距离比 3 对时更近，但没有跨过去，就只能这么写。

`gear=aggressive` 的 30 对值得单说：墙钟中心值 1.076×、token 1.122×，两条都**在 1 的上方**，虽然
区间仍跨 1 所以只能写"分不开"，但方向和"越猛越快"是反的。逐任务里 `list-files` 墙钟 1.310×、
token 1.483×，正好是基线里它变快（0.874×）的反面——多出来的指令让这个最短的任务反复确认。
这就是把开关收进"用户自己选"的理由：没有一条档位在这台机器上量出过额外收益。

目前唯一一条在 30 对以上量级站得住的提速是输出压缩，而且默认预算就够了：`compactResults=true`
单开（`compactMaxChars` 用默认 8000）30 对墙钟 **0.756×** [0.611, 0.921]、token **0.786×**
[0.623, 0.990]，两项都离开 1，校验 24/24 / 24/24。它快在哪能点出来：`grep-geomean` 0.416×
[0.294, 0.540]、token 0.479× [0.350, 0.652]——那个任务的工具结果是几十万字符的 grep 输出，压掉
中间之后上下文和等待一起降。

**这里有一条要撤回的说法。** 先跑的是收紧预算（`compactMaxChars=2000`）两次 30 对：0.818×
[0.675, 0.980] 和 0.893× [0.741, 1.068]，合池 60 对 0.855× [0.747, 0.971]；当时据此写过「8000
的默认预算对它几乎等于没压，压得越紧越省」。随后补跑的 8000 单独一组把这句话推翻了：默认预算
的点估计（0.756×）比两次收紧预算都低，收紧预算并没有再省一层。三次都在 1 以下，方向一致，
但「越紧越好」这个推论没有数据支撑，撤回。代价也照写：收紧到 2000 时 `run-tests` token 1.414×
[1.049, 1.939] 变多、墙钟 1.032× [0.963, 1.103] 分不开——测试输出被截短后模型会回头重读行，
省下的字符又花回去；8000 那一组 `run-tests` 墙钟 0.713× [0.378, 1.023]、token 0.717×
[0.337, 1.355]，分不开。所以结论是「打开压缩本身有用」，不是「压缩预算该调小」。

两个开关一起开这件事也补了一组：默认预算的压缩 + `gear=fast`，30 对墙钟 0.937× [0.763, 1.151]、
token 0.973× [0.815, 1.165]，都跨过 1。也就是说在已经打开的压缩之上再叠闪电，没有量到额外收益；
它既没变快也没变慢，跟噪声地板（0.945×）在同一个位置。

`workspaceMap` 的 30 对又是一次反面教材：合池分不开，可逐任务方向互相抵消——它本该帮忙的
`list-files` 反而墙钟 1.405× [1.016, 1.823]、token 1.533× [1.215, 1.915]（清单本身要进上下文，
模型还会按清单再确认一遍），而 `grep-geomean` 0.696× [0.626, 0.776]、`explain-config` 0.624×
[0.429, 0.899] 明显变快。合池把这些抹平，所以"开局附文件清单"这条既不能宣传成省时间，也不能
说不省——它对哪类任务有用是能看见的，对最短的那类反而是负担。

同一批数据里也有真的变慢/变多，照写不误。基线那条：`run-tests` 1.198× [1.046, 1.421]（工具本身
占大头，被要求先给结论时反而多绕一圈）、`list-files` 输出 token 1.285× [1.026, 1.583]（不是每个
任务都省 token，合池之后才落回噪声）。组合那条方向更分明：`list-files` 墙钟 0.770×
[0.634, 0.909] 真的变快，而 `run-tests` 墙钟 1.180× [1.061, 1.317]、输出 token 1.878×
[1.309, 2.707] 双双变差——像 `run-tests` 这种输出本来就短、答案主要来自工具自身的任务，压缩和 8 路并行一起上是负担。
合池把这些抹平了，所以合池数字不能拿来当"这个组合更快"的证据。

最后两行是同条件下**什么都不改**的两个臂：区间一样跨过 1。5×3 的噪声地板墙钟是 0.940×
[0.726, 1.192]、token 0.913× [0.667, 1.229]；30 对时收窄到墙钟 0.945× [0.836, 1.067]、token
0.968× [0.782, 1.222]。也就是说这台机器上 30 对的墙钟分辨率大约是 ±11%，token 大约 ±22%——
`compactResults` + `gear=fast` 那条 0.867× 落在 ±11% 能造出来的范围里，token 0.934× 更是直接
在噪声带内。所以那些"分不开"不是样本不够到能看见真差异，而是这个测法的分辨率就这么大。

### 输出很大、结论埋在中间

`audit/tasks.output-heavy.jsonl` 是一个专项任务：脚本刷 4000 行噪声，唯一那行 `FAIL
src/renameme.test.js: expected 1 got 2` 在第 2000 行，既不在开头也不在结尾。压缩开着
（`compactMaxChars=2000`）跑一次：输出 231 KB → 1.3 KB，失败行连同行号被带进 marker
（`kept 1 matching line 2000:FAIL …`），模型答对了整行内容、文件名和期望值；这 1 对是
11.1 秒 / 1080 token，对比关掉的 20.7 秒 / 1827 token，两边校验都过。单对不构成统计结论，
它证明的是**这条路径端到端能走通、答案没被截掉**。

读法：

- 第一行是"只开主开关"的基线，也是唯一一次区间整体落在 1 以下的组合。
- 加压缩的两行区间都跨过 1：**这点样本分不开"压缩帮了忙"和"噪声"**，不能写成压缩有效。第一行
  与"只开关主开关"的差别只有 15 对样本，同一台机器上模型侧缓存冷热就能吃掉这个幅度。
- 加压缩那次 `list-files` 单任务先给出 1.927× 墙钟 / 2.245× token（3 对，CI 都不跨 1），看着像
  "截短输出以后模型多跑几轮"。拿同一个任务重跑 6 对：1.200× [0.833, 1.755] 和 1.092×
  [0.667, 1.907]，都跨过 1；同样 6 对、两臂完全没差别的噪声地板是 0.967× [0.619, 1.546]。
  所以那个"代价"是 3 对样本上的噪声，**不能写成结论**——这也是为什么单任务那一列永远只该看
  `样本不足`，结论只从合池读。
- 三个开关全开（压缩 + aggressive）既没有更快、token 还略多：**开关不是叠加的**，多开一个不等于
  多省一点。这也说明"能开就全开"是错的做法，默认把几项关着是有依据的。
- `--on-config gear=fast` 交接时踩过一次坑：1.7.10 会把 `gear:` 写两遍，on 臂 70 毫秒就失败，
  报告里显示"快得离谱"。1.7.11 修好后才有上表第三行。

复现（两个组合各跑一次，输出目录别覆盖）：

    COMPANY_LLM_TOKEN=... node audit/run.mjs --tasks audit/tasks.example.jsonl --cwd . \
      --rounds 3 --on-config compactResults=true --on-config compactMaxChars=4000 \
      --out /tmp/fm-audit-comboA

    COMPANY_LLM_TOKEN=... node audit/run.mjs --tasks audit/tasks.example.jsonl --cwd . \
      --rounds 3 --on-config compactResults=true --on-config gear=fast \
      --out /tmp/fm-audit-fast

### 真实 PR 任务：跑得通，但还量不出快慢

自造任务只能说明「这个仓库变快了」，要说「真活也变快」得用真实 PR。这一节用的是**本地重建的
真实 PR**；官方 SWE-bench Lite 的题目定义后来接上了（走代理，见下一小节），但它也只到「题目是
官方的」，不是排行榜成绩。所以这里的「真实 PR」仍是本地重建的：取 PR 的 `base_commit`，只保留它的测试改动，源码退回
base，prompt 用它自己的标题和描述，依赖按仓库自己的配置装好，校验直接跑仓库原生 `pytest` 的一条
命令。**没有跑过任何外部 benchmark**，下面这些数字不能和任何排行榜对齐。

做法和代价：每个 PR 一个 checkout，任务行写 `"cwd"` 指过去、写 `"setup"` 每轮先
`git reset -q --hard HEAD && git clean -qfd` 把仓库放回原样——不重置的话第二轮量的是模型上一轮
已经修好的树，数字会凭空变快。只收了 3 个 `pallets/click` 的 PR（另外还试过 `psf/requests`、
`encode/httpx`、`tox-dev/filelock`、`python-humanize/humanize`、`pallets/itsdangerous`：requests 的
环境装不起来，filelock 和 humanize 的 PR 在 base 上测试本来就能过（`before == after`），
itsdangerous 那个的测试补丁引用 PR 才引入的 `_lazy_sha1`，base 上连收集都过不去，都剔掉）。
每个 PR 只验了「base 上失败、打上补丁通过」这一步，写进清单的 3 条都过。

| on 臂 | 任务×轮 | 合池墙钟 | 合池 token | 校验 off/on |
| --- | --- | --- | --- | --- |
| 只开 `compactResults=true`（默认 8000） | 6×4 | **0.805×** [0.656, 0.985]，省 19.5% | **0.768×** [0.618, 0.960]，省 23.2% | 24/24 / 24/24 |
| `compactResults=true` + `gear=fast`（早期 3 个 PR） | 3×6 | 0.796× [0.587, 1.065]，分不开 | 0.888× [0.774, 1.024]，分不开 | 17/18 / 18/18 |

第一行是这一节的主结果：6 个真实 PR、每个 4 轮、24 对，**只开发布时默认关着的那个截短开关**，墙钟和
token 的区间都在 1 以下，两边校验都全绿。它和自造任务上的 0.756× / 0.786× 方向一致、量级也接近，
但两只适用各自的集合：**这 6 个 PR 的测试输出体量大（单个任务一次 `pytest` 能有几百 KB），截短正好
打在收益点上。** 换到官方 SWE-bench Lite 的 8 条 instance 上就量不到（下一小节），所以这句话只能
读成「小仓库真实 PR 上有效」，不能读成「真实任务普遍有效」。

逐任务（第一行）：`click-3884` 0.726× [0.420, 1.136] / token 0.719×、`click-3865` 0.676× [0.410,
1.016] / 0.634× [0.379, 0.942]、`click-3860` 0.663× [0.448, 0.980] / 0.573× [0.357, 0.919]、
`click-3821` 0.845× [0.542, 1.252] / 0.666× [0.472, 0.940]、`click-3805` 1.004× [0.489, 1.731] /
1.086× [0.497, 1.965]、`click-3764`（4 对，见报告）。每个 4 对都写"样本不足"，**单任务一律不解读**；
`click-3805` 就是那个反例——真实任务上也有 token 变多的形状。

第二行是早期只有 3 个 PR 时的组合臂，18 对仍分不开；更早一次单轮 3 对里同一个任务两臂能差 3.9 倍
且方向相反，所以真实任务只认 ≥4 轮。即便 24 对，墙钟分辨率仍是 ±11% 量级。

边界：这 6 个 PR 全是 `pallets/click`，手挑、剔到只剩「base 上测试红、打补丁后绿」的，prompt 用 PR
的标题和描述（比 SWE-bench 的纯 issue 描述更详细，所以更容易，不能和它的解决率比）；每个 PR 只验了
这一步。它证明的是**真实仓库、真实测试上的方向和量级**，不是排行榜分数。

复现：

    COMPANY_LLM_TOKEN=... node audit/run.mjs --tasks /tmp/swe/tasks.real6.setup.jsonl \
      --cwd /tmp/swe --rounds 4 --on-config compactResults=true \
      --out /tmp/fm-real6pure-4r --timeout-ms 900000

### 开关矩阵：单独开、一起开

同一套自造任务（`audit/tasks.example.jsonl`，5 题 × 6 轮 = 30 对，输出偏长），一次只改一个开关：

| on 臂的配置 | 配对数 | 墙钟 | 输出 token | 校验 |
| --- | --- | --- | --- | --- |
| `compactResults=true`（8000 字） | 30 | 0.756× [0.611, 0.921] 变快 | 0.786× [0.623, 0.990] 变快 | 24/24 · 24/24 |
| `gear=fast` | 30 | 0.867× [0.745, 1.005] 跨 1 | 0.934× [0.775, 1.138] 跨 1 | 24/24 · 24/24 |
| 上面两个一起 | 30 | 0.937× [0.763, 1.151] 跨 1 | 0.973× [0.815, 1.165] 跨 1 | 24/24 · 24/24 |
| `compactMaxChars=2000`（两轮合池） | 60 | 0.855× [0.745, 0.970] 变快 | 0.892× [0.766, 1.033] 跨 1 | — |
| 只开 `enabled: true`、不加任何覆盖 | 30 | 0.945× [0.836, 1.067] 跨 1 | 0.968× [0.782, 1.222] 跨 1 | 24/24 · 24/24 |
| `workspaceMap=true` | 30 | 0.943× [0.805, 1.100] 跨 1 | 0.969× [0.811, 1.154] 跨 1 | 24/24 · 24/24 |
| `gear=aggressive`（不建议开） | 30 | 1.076× [0.931, 1.240] 跨 1 | 1.122× [0.946, 1.328] 跨 1 | — |

四个要说清的地方：

- **一起开不等于叠加**。截短开关单独开是 0.756×，加上 `gear=fast` 变成 0.937×（区间跨 1）。
  不是 `gear` 在拖慢，是这 30 对里 `gear` 省的（0.867×，本身就在 1 边上）和截短省的重叠，
  再叠一层采样噪声，读出来就什么都不是了。想省就只开截短，别叠。
- **2000 字那档省墙钟但费 token**：合池 token 读不出来（0.892× [0.766, 1.033]），
  拆到 `run-tests` 这类长输出任务是 **1.414× [1.049, 1.939]（两轮合池 60 对），明显更差**——截得太狠，
  模型要重跑命令拿被截掉的细节。所以默认 8000，不把 2000 设成默认（这条早期我写错过，已撤回）。
- **"只开开关、不动别的"这行是关键对照**：`enabled: true` 本身不改任何行为（0.945× / 0.968×，
  都跨 1），所以上面那些 >1 或 <1 的读数不能赖在"开开关有成本"上，要赖就赖各自的覆盖项。
  `gear=aggressive` 那一行方向反了但不显著，加上它砍的上下文多、更容易出错，**不建议开**。
**再加一个开关**，把「多个一起开」补全（同一套 30 对自造任务，都在 `compactResults=true` 基础上叠加）：

| on 臂的配置 | 墙钟 | 输出 token | 校验 |
| --- | --- | --- | --- |
| 只 `compactResults=true` | 0.756× [0.611, 0.921] 变快 | 0.786× [0.623, 0.990] 变快 | 24/24 · 24/24 |
| `+ gear=fast` | 0.937× [0.763, 1.151] 跨 1 | 0.973× [0.815, 1.165] 跨 1 | 24/24 · 24/24 |
| `+ workspaceMap=true` | 0.862× [0.719, 1.033] 跨 1 | 0.827× [0.680, 1.014] 跨 1 | 24/24 · 24/24 |
| `+ gear=fast + workspaceMap=true` | 0.966× [0.819, 1.151] 跨 1 | 1.138× [0.938, 1.409] 跨 1（方向变差） | 24/24 · 24/24 |

规律很干脆：**每往截短上叠一个开关，读数就更差一点**，叠到第三个连方向都反了。
所以「多开几个更快」在这套实现里不成立——想省时间只开 `compactResults` 一个。

**每类命中保留几行**（`compactKeepMatches`）也扫了一遍，30 对自造任务，其余默认：

| `compactKeepMatches` | 墙钟 | 输出 token | 校验 |
| --- | --- | --- | --- |
| 4 | 0.785× [0.626, 0.973] 变快 | 0.820× [0.636, 1.058] 跨 1 | 24/24 · 24/24 |
| 12（默认） | 0.756× [0.611, 0.921] 变快 | 0.786× [0.623, 0.990] 变快 | 24/24 · 24/24 |
| 30 | 0.875× [0.773, 0.988] 变快 | 0.950× [0.786, 1.162] 跨 1 | 24/24 · 24/24 |

方向是对的——保留越多省得越少（4 ≈ 12 好于 30），但 4 和 12 差得在噪声里，
**没有理由为这点差别把默认 12 改小**；改小还会更容易让模型看不到它想看的那几行。

**截多少字**也量了一遍（同一套 30 对自造任务，只改 `compactMaxChars`）：

| `compactMaxChars` | 配对数 | 墙钟 | 输出 token | 校验 |
| --- | --- | --- | --- | --- |
| 2000（两轮合池） | 60 | 0.855× [0.745, 0.970] 变快 | 0.892× [0.766, 1.033] 跨 1 | — |
| 4000 | 30 | 0.785× [0.677, 0.915] 变快 | 0.782× [0.646, 0.953] 变快 | 24/24 · 24/24 |
| 8000（默认） | 30 | 0.756× [0.611, 0.921] 变快 | 0.786× [0.623, 0.990] 变快 | 24/24 · 24/24 |

4000 和 8000 基本一样（墙钟差 3 个百分点，区间大幅重叠），2000 反而开始费 token——
砍太狠，模型要重跑命令把被截掉的细节拿回来。所以**默认保持 8000**，没有理由往下调。

同一套结论在**六个真实 PR 任务**（`/tmp/swe/tasks.real6.setup.jsonl`，4 轮 = 24 对）上又走了一遍：

| on 臂的配置 | 配对数 | 墙钟 | 输出 token | 校验 |
| --- | --- | --- | --- | --- |
| `compactResults=true` | 24 | 0.805× [0.656, 0.985] 变快 | 0.768× [0.618, 0.960] 变快 | 24/24 · 24/24 |
| `gear=fast` | 24 | 0.918× [0.766, 1.095] 跨 1 | 0.861× [0.711, 1.040] 跨 1 | 24/24 · 24/24 |

真 PR 上也是**截短开关一个人撑起来的**：它两次独立测量（自造 30 对、真 PR 24 对）都量到了；
`gear=fast` 两次都没量到（0.867×、0.918×，都在 1 边上）。要开就只开 `compactResults`。

- 这 30 对是自造任务、输出偏长，所以截短类开关在这里更容易出效果；换到官方 SWE-bench Lite
  那种短输出任务上就量不出来（下一小节）。**表里的数字只能读成"输出长的时候有用"**。

### 官方 SWE-bench Lite：跑得通，但量不出加速

换了第三种证据：官方 `princeton-nlp/SWE-bench_Lite` 的 8 条题目定义、本机 `uv` 环境、按官方
`FAIL_TO_PASS`/`PASS_TO_PASS` 差分判分。先按其中 5 条跑了三轮两臂（共 30 对），两臂校验全绿，
但**合池墙钟 1.016× [0.823, 1.261]、输出 token 1.055× [0.915, 1.229]，都跨 1**；只开
`enabled: true` 的第三轮同样分不开（0.881× / 1.081×）。中间还发现**顺序偏差比效应大**：
每轮固定 off 先跑时读出来是 token 1.208×（更差），逐轮换序后变成 0.922×——所以
`audit/run.mjs` 现在单数轮 off→on、双数轮 on→off，报告头写明了。
最后又把池子从 6 条补到 8 条（加 `sympy-24909`、`pytest-8906`），8×3 跑两次、合池 48 对：
墙钟 **1.007× [0.808, 1.255]**、token 0.905× [0.789, 1.040]，还是跨 1。再独立跑第三次后
合池 **72 对**：墙钟 **0.933× [0.785, 1.107]**、token **0.903× [0.815, 1.003]**——墙钟仍跨 1，
token 上界贴到 1.003。三次单跑各自是 1.109× / 0.914× / 0.800×，**同一配置单跑之间就差三成**，
所以单跑读数不能当结论。**加样本既没推翻也没翻案：这类任务上收益小于噪声。**

方法和逐题数字、以及"为什么这里没有收益（输出本来就短，没有可截的东西）"都在
[`SWEBENCH.md`](./SWEBENCH.md)。

## 一次完整的复核

    node audit/run.mjs --dry-run --cwd . --tasks my-tasks.jsonl   # 先确认命令行
    node audit/run.mjs --cwd . --tasks my-tasks.jsonl --rounds 4  # 真跑
    cat audit-out/<时间戳>/report.md                              # 读表

报告里每一行都是"某任务/合池 + 几何均值比 + 区间 + 结论"，配对不够的行会自己写"样本不足"。
把这些行原样贴出来，比任何形容词都可靠。
