diff --git a/docs/bench-reliability.html b/docs/bench-reliability.html index 66edf90..2532ac6 100644 --- a/docs/bench-reliability.html +++ b/docs/bench-reliability.html @@ -103,7 +103,7 @@ footer{margin-top:64px;padding-top:22px;border-top:1px solid var(--rule);color:v
HEX TRAILS · 关卡实验台

Agent 评级里,哪些数字真能拿来做关卡

同一批关卡、同一个模型、同一套盘面,跑两遍。能重现的指标才配进流程——这份报告把每个指标逐个检验了一遍,结论和直觉相反。

-

生成于 2026-09-14 16:03 GMT+8 · 数据来自 bench/runs 的 130 局 · 关卡集 levels/prototypes/d5.json(18 关)

+

生成于 2026-09-14 16:04 GMT+8 · 数据来自 bench/runs 的 229 局 · 关卡集 levels/prototypes/d5.json(18 关)

01参与检验的批次

@@ -112,101 +112,101 @@ footer{margin-top:64px;padding-top:22px;border-top:1px solid var(--rule);color:v
- - - - - - - + + + + + + +
批次关数通关空点擦碰空点盲转转错多转单局均每决策
grok-4.6 · 旧盘面77/71111163 s6.8 s
grok-4.6 · 新盘面 · 第 1 次77/72111139 s4.2 s
grok-4.6 · 新盘面 · 第 2 次77/70011138 s3.4 s
codex-spark · 旧盘面33/30011116 s1.4 s
gpt-6-astra · low · 新盘面77/70011136 s7.9 s
codex-spark · xhigh · 新盘面77/70011119 s1.8 s
kimi-k3 · high · 新盘面77/70011142 s5.2 s
grok-4.6 · 旧盘面1818/18442182283 s6.8 s
grok-4.6 · 新盘面 · 第 1 次1818/18212182255 s3.9 s
grok-4.6 · 新盘面 · 第 2 次1818/180025102849 s3.9 s
codex-spark · 旧盘面66/60022215 s1.4 s
gpt-6-astra · low · 新盘面1818/18102182241 s7.0 s
codex-spark · xhigh · 新盘面1817/18002172121 s1.8 s
kimi-k3 · high · 新盘面1818/18002182248 s5.2 s
grok-4.6 · 新关卡集(终局门槛后)1818/180054641 s3.8 s
-

所有批次通关率都是 100%。这本身就是一条结论:通关率在这批内容上毫无区分度,任何流程都不该建立在通过/失败上。

+

已完成的对局通关率全部是 100%(个别批次仍有对局在进行中,未计入)。通关率在这批内容上毫无区分度,任何流程都不该建立在通过/失败上。

02信度检验:跑两遍还一样吗

-

同模型(grok-4.6 xhigh)、同 harness(pi)、同盘面(新)、同 7 关,只是又跑了一遍。两次之间唯一的变量就是运行本身,所以下面的相关系数就是这个指标的复测信度。

+

同模型(grok-4.6 xhigh)、同 harness(pi)、同盘面(新)、同 18 关,只是又跑了一遍。两次之间唯一的变量就是运行本身,所以下面的相关系数就是这个指标的复测信度。

判定规则:关间标准差 < 0.35 说明这个指标在不同关卡上几乎一样,再稳也没用(无法排序);复测 r < 0.5 说明换一次运行结果就变,不能作为依据。两条都过才算可用。

- - - - - - - - - - - - + + + + + + + + + + + +
指标关间标准差复测相关 r逐关完全一致判定
多转
清盘旋转数 − 求解器最少旋转数
0.351.007/7无区分度
盲转
转了一次,什么出口都没打开
0.351.007/7无区分度
转错方向
另一边本来能打开
0.351.007/7无区分度
步数1.600.926/7可用 · 高
决策数
看了几次盘面
1.390.843/7可用
空点
点了没动
0.70—6/7噪声
动作÷棋子0.090.866/7无区分度
评分:无解感0.490.094/7噪声
评分:难度0.351.007/7无区分度
评分:旋转意义0.490.094/7噪声
评分:清晰度0.35-0.175/7无区分度
评分:好玩0.35—6/7无区分度
多转
清盘旋转数 − 求解器最少旋转数
1.580.8717/18可用
盲转
转了一次,什么出口都没打开
1.570.9217/18可用 · 高
转错方向
另一边本来能打开
0.500.8217/18可用
步数2.240.7616/18可用
决策数
看了几次盘面
1.890.7710/18可用
空点
点了没动
0.46—17/18噪声
动作÷棋子0.150.6916/18无区分度
评分:无解感0.870.8514/18可用
评分:难度0.590.9317/18可用 · 高
评分:旋转意义0.370.3215/18噪声
评分:清晰度0.470.1912/18噪声
评分:好玩0.420.5615/18可用

「关间标准差」衡量这个指标能不能把不同关卡区分开——标准差接近 0 的指标就算完全可复现也没用,因为每关都一样。两个条件要同时满足:既能区分关卡,又能跑两遍不变。

-

多转

r = 1.00
+

多转

r = 0.87
- + 第 1 次 第 2 次
-

盲转

r = 1.00
+

盲转

r = 0.92
- + 第 1 次 第 2 次
-

转错方向

r = 1.00
+

转错方向

r = 0.82
- + 第 1 次 第 2 次
-

步数

r = 0.92
+

步数

r = 0.76
- + 第 1 次 第 2 次
-

决策数

r = 0.84
+

决策数

r = 0.77
- + 第 1 次 第 2 次
-

动作÷棋子

r = 0.86
+

动作÷棋子

r = 0.69
- + 第 1 次 第 2 次
-

评分:无解感

r = 0.09
+

评分:无解感

r = 0.85
- + 第 1 次 第 2 次
-

评分:难度

r = 1.00
+

评分:难度

r = 0.93
- + 第 1 次 第 2 次
@@ -220,18 +220,18 @@ footer{margin-top:64px;padding-top:22px;border-top:1px solid var(--rule);color:v
- - - - - - - - - - - - + + + + + + + + + + + +
指标控制变量后 r换盘面后 r差异说明
多转1.001.00两者接近,盘面变化影响很小
盲转1.001.00两者接近,盘面变化影响很小
转错方向1.001.00两者接近,盘面变化影响很小
步数0.920.86两者接近,盘面变化影响很小
决策数0.840.33控制变量后明显更高——之前被盘面变化压低了
空点—-0.17—
动作÷棋子0.860.63控制变量后明显更高——之前被盘面变化压低了
评分:无解感0.090.42控制变量后反而更低——值得留意
评分:难度1.000.47控制变量后明显更高——之前被盘面变化压低了
评分:旋转意义0.09-0.17控制变量后明显更高——之前被盘面变化压低了
评分:清晰度-0.170.65控制变量后反而更低——值得留意
评分:好玩—-0.26—
多转0.871.00两者接近,盘面变化影响很小
盲转0.921.00两者接近,盘面变化影响很小
转错方向0.821.00控制变量后反而更低——值得留意
步数0.760.95控制变量后反而更低——值得留意
决策数0.770.87两者接近,盘面变化影响很小
空点—-0.10—
动作÷棋子0.690.92控制变量后反而更低——值得留意
评分:无解感0.850.80两者接近,盘面变化影响很小
评分:难度0.930.63控制变量后明显更高——之前被盘面变化压低了
评分:旋转意义0.320.06控制变量后明显更高——之前被盘面变化压低了
评分:清晰度0.190.09两者接近,盘面变化影响很小
评分:好玩0.560.21控制变量后明显更高——之前被盘面变化压低了
@@ -239,7 +239,7 @@ footer{margin-top:64px;padding-top:22px;border-top:1px solid var(--rule);color:v

下表是同一套盘面下所有批次的 多转 / 盲转 / 转错方向。不同厂商的模型、不同 harness、各自独立跑——如果这些数字真的由关卡决定,它们就该一样。

-
逐关完全一致
7 / 7
三项数字一个不差
+
逐关完全一致
16 / 18
三项数字一个不差
参与批次
5
grok新① · grok新② · kimi · astra · spark新
@@ -252,115 +252,155 @@ footer{margin-top:64px;padding-top:22px;border-top:1px solid var(--rule);color:v + + + + + + + + + + +
L11petals0/0/00/0/00/0/00/0/00/0/0✔
L15crossing0/0/00/0/00/0/00/0/00/0/0✔
L17orbits0/0/00/0/00/0/00/0/00/0/0✔
L19petals0/0/00/0/00/0/00/0/00/0/0✔
L20petals0/0/00/0/00/0/00/0/00/0/0✔
L21switchback1/1/11/1/11/1/11/1/11/1/1✔
L22orbits4/4/110/8/34/4/14/4/14/4/1✘
L23orbits4/4/14/4/14/4/14/4/14/4/1✔
L24spiral4/4/14/4/14/4/14/4/14/4/1✔
L25spiral4/4/14/4/14/4/14/4/13/4/0✘
L26switchback1/1/11/1/11/1/11/1/11/1/1✔
L27weave1/1/11/1/11/1/11/1/11/1/1✔
L28weave0/0/00/0/00/0/00/0/00/0/0✔
L29weave2/1/02/1/02/1/02/1/02/1/0✔
-

这是全份报告里最硬的一条结论。7/7 关上,独立运行的不同模型给出了完全相同的旋转三元组。这意味着规则机器人 12 秒扫出来的这三个数,等价于大模型跑几小时的结果——这个维度完全不需要 agent。

+

这是全份报告里最硬的一条结论。16/18 关上,独立运行的不同模型给出了完全相同的旋转三元组。这意味着规则机器人 12 秒扫出来的这三个数,等价于大模型跑几小时的结果——这个维度完全不需要 agent。

+

不一致的关:不是难,是不稳定

+

下面这些关在不同运行之间给出了不同的数字。排除模型差异后,剩下的解释是同一关的体验取决于起手方向——两个玩家玩同一关,一个觉得还行,一个觉得这关有毛病。这比"稳定地难"更值得处理。

+
L22 orbits:4/4/1 / 10/8/3 / 4/4/1 / 4/4/1 / 4/4/1
L25 spiral:4/4/1 / 4/4/1 / 4/4/1 / 4/4/1 / 3/4/0

04家族结构:机械指标指向同一个地方

把「多转」按路线家族平均。这是关卡结构问题,不是玩家问题——两批 grok、以及 spark,在这一点上给出一致的结果。

grok-4.6 · 旧盘面 · 各家族平均多转

-
switchback2 关 - - 0.5
-
weave1 关 - - 0.0
-
petals2 关 +
spiral2 关 + + 4.0
+
orbits3 关 + + 2.7
+
switchback4 关 + + 0.8
+
weave4 关 + + 0.8
+
petals4 关 0.0
crossing1 关 - 0.0
-
orbits1 关 - 0.0

grok-4.6 · 新盘面 · 第 1 次 · 各家族平均多转

-
switchback2 关 - - 0.5
-
weave1 关 - - 0.0
-
petals2 关 +
spiral2 关 + + 4.0
+
orbits3 关 + + 2.7
+
switchback4 关 + + 0.8
+
weave4 关 + + 0.8
+
petals4 关 0.0
crossing1 关 - 0.0
-
orbits1 关 - 0.0

grok-4.6 · 新盘面 · 第 2 次 · 各家族平均多转

-
switchback2 关 - - 0.5
-
weave1 关 - - 0.0
-
petals2 关 +
orbits3 关 + + 4.7
+
spiral2 关 + + 4.0
+
switchback4 关 + + 0.8
+
weave4 关 + + 0.8
+
petals4 关 0.0
crossing1 关 - 0.0
-
orbits1 关 - 0.0
+

codex-spark · 旧盘面 · 各家族平均多转

+
switchback3 关 + + 0.3
+
weave3 关 + + 0.3
+

gpt-6-astra · low · 新盘面 · 各家族平均多转

-
switchback2 关 - - 0.5
-
weave1 关 - - 0.0
-
petals2 关 +
spiral2 关 + + 4.0
+
orbits3 关 + + 2.7
+
switchback4 关 + + 0.8
+
weave4 关 + + 0.8
+
petals4 关 0.0
crossing1 关 - 0.0
-
orbits1 关 - 0.0

codex-spark · xhigh · 新盘面 · 各家族平均多转

-
switchback2 关 - - 0.5
-
weave1 关 - - 0.0
-
petals2 关 +
spiral2 关 + + 3.5
+
orbits3 关 + + 2.7
+
switchback4 关 + + 0.8
+
weave4 关 + + 0.8
+
petals4 关 0.0
crossing1 关 - 0.0
-
orbits1 关 - 0.0

kimi-k3 · high · 新盘面 · 各家族平均多转

-
switchback2 关 - - 0.5
-
weave1 关 - - 0.0
-
petals2 关 +
spiral2 关 + + 4.0
+
orbits3 关 + + 2.7
+
switchback4 关 + + 0.8
+
weave4 关 + + 0.8
+
petals4 关 0.0
crossing1 关 - 0.0
-
orbits1 关 - 0.0

grok-4.6 · 新关卡集(终局门槛后) · 各家族平均多转

@@ -386,19 +426,19 @@ footer{margin-top:64px;padding-top:22px;border-top:1px solid var(--rule);color:v

05Agent 说了什么

评分是 1–5 的数字,worst 是它自己写的一句话。下面按反复出现的主题归类——价值在重复模式,不在单条内容。

-

擦碰看不出来 · 13 次提及

+

擦碰看不出来 · 30 次提及

L3 switchback(grok旧):开局 t2/t11 路上看着没人其实会擦到,差点空点;只剩 t0 和 t3 对堵时短暂以为卡住了
L6 weave(grok旧):t5走后(第9步)t2和t3都像能出界,其实互相擦碰,只能再转
L6 weave(grok新①):朝向1时 t2 和 t3 互相擦碰、t0 被 t2 压着,短暂不确定是再转还是还有能点的
L6 weave(grok新②):第9步清完后t2和t3互相擦挡,谁先走都不行,只能转
-

不知道该转哪边 · 9 次提及

+

不知道该转哪边 · 19 次提及

L1 switchback(grok新关):朝向3右转后没有新出口,t0和t3仍互相挡,短暂怀疑该不该改左转
L5 switchback(grok旧):第 13 步右转后朝向 3 没新出口,t0 和 t3 仍互相挡,不确定该不该继续同向转
L5 switchback(grok新①):第15步右转后朝向3没有新出口,最后两枚还互压,一度担心转错了方向
L5 switchback(spark旧):第 8-9 步清掉 t5,t4,t2 后只剩 t0/t3 互压,朝向2不再有出口,开始不确定该是继续走还是转哪边
-

长路线读不动 · 25 次提及

+

长路线读不动 · 32 次提及

L2 weave(grok新关):开局 t0 绕了 21 格还压着 6 枚,一眼看不出清障顺序,只能先点边上的 t4/t6
L3 switchback(kimi):第 10 步后 t0/t2/t3/t10 四枚互相压成环,盘面上看不出解法,只能靠旋转试出新朝向
L9 petals(grok新关):开局 t0/t3 各绕 19 格互相穿过,一时看不出该先剥哪层
@@ -409,7 +449,7 @@ footer{margin-top:64px;padding-top:22px;border-top:1px solid var(--rule);color:v
  1. 生成期硬门槛 · 零成本

    用已有求解器,在生成时就卡掉:最少旋转 ≥ 2、开局锁住率 ≤ 90%、首转两个方向都能推进、无死局状态。不合格的根本不进候选池。

  2. 规则机器人全量扫 · 1000 关 12 秒

    产出盲转 / 多转 / 转错方向。按 盲转 ≥ 2 或 多转 ≥ 2 打标。这条规则在 D5 上正好命中 orbits+spiral 那几关,和两个大模型跑两小时得到的结论一致。

  3. -
  4. Agent 只跑被标记的 + 5% 随机抽样

    不是 1000 关全跑。收集三样东西:difficulty(r=0.93,可用来给 1000 关排序)、stuck(r=0.85,体感无解的直接信号)、以及 worst 文字(重复模式能挖出系统性缺陷)。丢掉 rotation / clarity / fun——复测 r 只有 0.32 / 0.19 / 0.56,是模型噪声。按实测 36 s/关、4 路并行,150 关约半小时。

  5. +
  6. Agent 只跑被标记的 + 5% 随机抽样

    不是 1000 关全跑。收集三样东西:difficulty(r=0.93,可用来给 1000 关排序)、stuck(r=0.85,体感无解的直接信号)、以及 worst 文字(重复模式能挖出系统性缺陷)。丢掉 rotation / clarity / fun——复测 r 只有 0.32 / 0.19 / 0.56,是模型噪声。按实测 43 s/关、4 路并行,150 关约半小时。

  7. 真人标定锚点 · 5–10 关

    Agent 的难度排序可信(复测 r=0.93),但绝对值可能整体偏移——它觉得「刚好」的关,真人可能觉得太简单。所以真人只需要玩少数几关当锚点,把 agent 的相对排序映射到真实难度刻度上,不必逐关评。这比原本设想的省很多人力。

diff --git a/scripts/build-bench-report.mjs b/scripts/build-bench-report.mjs index 9c0326c..d6de301 100644 --- a/scripts/build-bench-report.mjs +++ b/scripts/build-bench-report.mjs @@ -18,13 +18,18 @@ const RUNS = resolve('bench/runs'); // ---------- data ---------- const set = JSON.parse(readFileSync(resolve('levels/prototypes/d5.json'), 'utf8')); const levels = Array.isArray(set) ? set : set.levels; -const meta = Object.fromEntries(levels.map((l, i) => [l.id, { - n: i + 1, +// Levels are keyed by id, never by position: regenerating the set renumbers +// everything, and keying by ordinal silently dropped every run played against +// an earlier set. Runs carry their own level, so ids seen only in the archive +// still resolve. +const describe = (l, i) => ({ + n: i == null ? null : i + 1, family: l.id.replace('proto-d5-', '').replace(/-r\d+-s\d+$/, ''), lock: l.prototype ? Math.round(l.prototype.stats.lockedShare * 100) : null, minRot: l.prototype ? l.prototype.stats.rotations : null, cells: l.cells.length, tiles: l.tiles.length, -}])); +}); +const meta = Object.fromEntries(levels.map((l, i) => [l.id, describe(l, i)])); const BATCHES = [ { tag: 'Grok Build CLI · xhigh', label: 'grok-4.6 · 旧盘面', short: 'grok旧', format: 'old' }, @@ -44,12 +49,18 @@ const NEW_TAGS = ['pi · Grok 4.6 xhigh', 'pi · Grok 4.6 xhigh · 复测', 'Kim const runs = readdirSync(RUNS).filter(f => f.endsWith('.json') && f !== 'index.json') .map(f => { try { return JSON.parse(readFileSync(resolve(RUNS, f), 'utf8')); } catch { return null; } }) - .filter(Boolean).filter(d => meta[d.level?.id]); + .filter(Boolean).filter(d => d.level?.id?.startsWith('proto-d5-')); +// Levels retired by a regeneration still have runs worth comparing. +let retired = 0; +for (const d of runs) if (!meta[d.level.id]) { meta[d.level.id] = describe(d.level, null); retired++; } +// Give every level a stable ordinal: current set first, retired ones after. +let ord = levels.length; +for (const k of Object.keys(meta)) if (meta[k].n == null) meta[k].n = ++ord; const byTag = {}; for (const b of BATCHES) { const rows = {}; - for (const d of runs) if (d.agent === b.tag) rows[meta[d.level.id].n] = { m: metrics(d), lv: meta[d.level.id], run: d.run }; + for (const d of runs) if (d.agent === b.tag) rows[d.level.id] = { m: metrics(d), lv: meta[d.level.id], run: d.run, id: d.level.id }; byTag[b.tag] = rows; } const present = BATCHES.filter(b => Object.keys(byTag[b.tag]).length > 0); @@ -82,7 +93,7 @@ const METRICS = [ function pair(tagA, tagB) { const A = byTag[tagA] || {}, B = byTag[tagB] || {}; - const shared = Object.keys(A).filter(k => B[k]).map(Number).sort((a, b) => a - b); + const shared = Object.keys(A).filter(k => B[k]).sort((x, y) => meta[x].n - meta[y].n); return { shared, A, B }; } @@ -184,9 +195,9 @@ const THEMES = [ ]; function notes() { const outp = []; - for (const b of present) for (const [L, r] of Object.entries(byTag[b.tag])) { + for (const b of present) for (const r of Object.values(byTag[b.tag])) { const w = r.m.rating?.worst; if (!w) continue; - outp.push({ batch: b.short, L: Number(L), fam: r.lv.family, text: String(w) }); + outp.push({ batch: b.short, L: r.lv.n, fam: r.lv.family, text: String(w) }); } return outp.sort((a, b) => a.L - b.L); } @@ -203,11 +214,12 @@ const clearRateNote = allCleared const liveTags = NEW_TAGS.filter(t => Object.keys(byTag[t] || {}).length); const triple = m => [m.rotations - m.minimumRotations, m.blindRotations, m.wrongWayRotations].join('/'); const crossRows = []; -for (let L = 1; L <= levels.length; L++) { - const cells = liveTags.map(t => byTag[t][L] ? triple(byTag[t][L].m) : null); - if (cells.some(c => c == null)) continue; - const lv = Object.values(meta).find(x => x.n === L); - crossRows.push({ L, family: lv.family, cells, agree: new Set(cells).size === 1 }); +const crossIds = [...new Set(liveTags.flatMap(t => Object.keys(byTag[t] || {})))] + .filter(id => liveTags.every(t => byTag[t]?.[id])) + .sort((a, b) => meta[a].n - meta[b].n); +for (const id of crossIds) { + const cells = liveTags.map(t => triple(byTag[t][id].m)); + crossRows.push({ L: meta[id].n, id, family: meta[id].family, cells, agree: new Set(cells).size === 1 }); } const agreeN = crossRows.filter(r => r.agree).length; const crossModel = crossRows.length < 3 ? '

同盘面批次不足,跨模型比较尚未成立。

' : `