Files
HexTrails/labs/bench/README.md
T
cabinwandClaude Opus 5 47be0ad9c4 feat(bench): live observer page
Watch an agent play in real time, replayed with the main game's renderer and
animations. The board holds the stage with the live panel beside it: a
millisecond stopwatch, how long the current step has been thinking, steps,
decisions, pieces cleared, dead taps and rotations, then the agent's running
commentary. A banner over the board names who is playing, self-reported by the
agent via --agent.

The page polls unconditionally; the checkbox only decides whether the cursor
chases the newest step, and it follows the agent into its next run once this
one ends. Clicking any step in the feed replays the board at that moment, and
the arrow keys walk it — there is no scrub bar, since the feed already is one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:00:33 +08:00

13 KiB
Raw Blame History

关卡实验台(Agent Bench)

让任意 Agent 像人一样玩关卡:只看盘面、只用 tap / rotate / undo(可选 peek)与棋盘交互,每一步写理由,结束后评分。你在观察页看它怎么玩,就能判断关卡对不对;多个 Agent 跑完再看汇总。

它不是求解器,也不是真人测试。用途是:抓"体感无解"、找卡点、给关卡做相对排序。绝对难度仍要少量真人校准(见文末)。

架构:一个 JSON 文件,三种接法

Agent ──(shell)──▶ scripts/bench.mjs ──写──▶ bench/runs/<局>.json ◀──轮询── labs/bench/index.html(观察页)
Agent ──(API)───▶ scripts/bench-agent.mjs ──写──▶ 同上
  • src/bench.js 会话核心:observe() 给的是人眼在屏幕上看到的东西——每枚棋子当前的路线(依次经过哪些格、在哪里出界或闭环、路上压着谁 [tX]),加朝向和剩余数;不给求解器,不给旋转后的预测。它没有替 Agent 做的事:决定先后顺序、判断何时旋转和转哪边、发现擦碰(棋子有宽度,路线上没人也可能被邻格的棋子挡——反馈会告诉它被谁挡)。act() 经过真实的 Game 规则;metrics() 事后用求解器算指标。
  • 第一版观察只给每格的端口配对表,Haiku 只能靠坐标运算读路,结果是盲猜、连边上的棋子都不点。改成路线之后它 18 步清盘、3 次旋转全部有效,但 3 次空点全是擦碰——路线上没人,却被紧邻的棋子挡。擦碰不在格子数据里(它来自轨道曲线和棋子宽度的扫掠判定),Agent 推不出来;而人看得见线从棋子身边擦过。所以把它作为路线标记直接给出:紧贴 tX(会擦到),用真实碰撞判定算,不用邻格猜。
  • 一次只报第一个阻挡者是不够的。 最初 graze 只取扫掠碰到的第一个障碍,路上压着一枚棋子时,它身后的擦碰就被吞掉——全库 581 例「路上有阻挡」里有 113 例(约两成)在清掉挡路者之后会冒出从未提示过的擦碰。grok-4.6 玩 18 关的 4 次空点全部由此而来,它自己写的是「路线描述没提前写出这次擦碰」。现在 route.grazes 是数组,逐枚单独做扫掠判定,谁也遮不住谁,复测 0 例漏报。盘面读作 路上有 t6,另外紧贴 t4(会擦到),含义是列出来的全部清掉这枚才能走。
  • 因此 grazeDeadTaps 的语义在修复前后不同:修复前测的是「系统没告诉它」,修复后测的是「它无视了提示」。跨这条分界线比较历史数据时要注意——按 agent 字段区分批次。
  • labs/bench/AGENT.md:给 Agent 读的唯一文件——规则、读盘、命令、评分、示例都在里面。
  • scripts/bench.mjs 命令行:任何有 shell 的 Agent(Codex、Claude Code、Grok Build、人)一条命令一步。start 自动生成局名、自动起服务、打印观察页地址。
  • scripts/bench-agent.mjs 参考循环:任何只有 API 的模型(含弱模型、本地模型),纯文本进、一行 JSON 出,不需要 tool-use。
  • labs/bench/:观察页,用主游戏的渲染和动画重放每一步,可直播。
  • scripts/bench-report.mjs:按关汇总。

没有服务端,没有依赖。npm run dev 的静态服务器已经能提供 bench/runs/*.json。

最短路径:给 Agent 一句话

在仓库根目录启动任意 shell agent(Claude Code / Codex / Grok Build / DeepSeek Harness…),说一句:

读 labs/bench/AGENT.md,然后按它说的玩第 1 关。

AGENT.md 是唯一要给 Agent 的文件:规则、读盘方法、命令、评分表、完整示例都在里面。它读完就能开始;start 会自动生成局名、自动起本地服务、把观察页地址打印出来让它转告你。人类这边只要打开那个地址看着就行。

想换关就说"玩第 3 关",想换风格就说"用 reader 人设",想跑正式千关就说"加 --set=levels/v0.5.json"。


完整路径

按"你手上有什么"选:

路径 需要什么 单局速度 用途
A. 规则机器人 什么都不需要 毫秒级 千关粗筛:规则能不能照着清、哪些关盲转多
B. Claude Code / Codex(shell agent) 订阅 精简协议 + 连点后约 1 分钟 看它怎么玩、评分、抓"体感无解"
C/D. 别的 harness / 便携包 对方能跑 Node 同 B Grok Build、DeepSeek Harness、任何沙箱
E. 直连 API API key 每次决策 3–10 秒 有 key 时的批量主路径

A. 规则机器人:先把 1000 关扫一遍

BENCH_PROVIDER=mock node scripts/bench-batch.mjs --set=levels/v0.5.json --levels=all --concurrency=8 --prefix=bot-v05
node scripts/bench-report.mjs

它只做协议里写的四条规则:路上没人不紧贴谁就点、否则右转。18 关 D5 原型 <1 秒跑完。它不评分,但它的盲转数是个便宜的结构探针——一路右转就能清的关和必须转回来的关,一眼分开。

B. Claude Code / Codex:看它玩

单局——对 agent 说一句:

读 labs/bench/AGENT.md,然后按它说的玩第 1 关。

多局并行——用非交互模式开多个进程,各玩一关:

for L in 1 2 3 4 5 6; do
  claude -p "读 labs/bench/AGENT.md,然后按它说的玩第 $L 关,--model 写 claude。" \
    --allowedTools "Read,Bash(node scripts/bench.mjs*)" &
done; wait
node scripts/bench-index.mjs && node scripts/bench-report.mjs --by=model

Codex 同理(codex exec "…")。观察页 刷新列表 能看到正在跑的局;?run=<局名>&live=1 直播。

提速要点(已经做进 AGENT.md 和 CLI 里):AGENT.md 一个文件读完就能开始;start --no-rules 不重复打印规则、自动生成局名、自动起服务;act 用逗号连点,一次看盘做一串。

实测(Opus 5,第 1 关,含子代理启动与读盘):逐步 15 次决策 154 s → 连点 4 次决策 93 s → 精简协议 4 次决策 82 s。claude -p 非交互模式第 2 关:8 次决策 76 s。剩余时间大头是进程启动和第一次读盘(约 20–30 s);每次决策约 8–10 s。要更快只能并行:claude -p 开 6 个进程,18 关约 4 分钟。

C. 别的 agent harness(Grok Build / DeepSeek Harness / Codex …)

Grok Build(grok)和 DeepSeek Harness(dsh)都是本机 CLI,能执行 bash、能读写本地文件,所以它们和 Claude Code 走同一条路:在仓库根目录启动,把 B 节那段提示给它。区别只是提示词里 --model= 写它自己的名字。

# Grok Build:curl -fsSL https://x.ai/cli/install.sh | bash
grok    # 然后贴 B 节的提示;注意它默认不开沙箱,需要时用 workspace profile
# DeepSeek Harness:npx @deepseek-ai/dsh web  (developer preview,接口会变)

如果对方拿不到这个仓库(另一台机器、另一个人、只能在自己沙箱里跑的 harness),发独立实验台给他(下一节)。

D. 独立实验台:发给任何人

node scripts/bench-kit.mjs --zip            # → bench/hex-bench/ 和 bench/hex-bench.zip

生成一个自带观察页的独立工程:bench.mjs(命令行)、serve.mjs(本地服务)、report.mjs(汇总)、observer/(观察页)、src/(引擎,按 import 自动追踪,不会漏)、levels.json、AGENT.md、README.md。约 1.5 MB,只要 Node 20+,不联网、不装依赖、不需要原仓库。

对方那边三步:node serve.mjs → 对 agent 说「读 AGENT.md,按它说的玩第 1 关」→ 打开 http://localhost:4173/observer/index.html 看着。包里的 README.md 就是写给这个人看的。

构建时会在生成的包里真跑一遍 list → start → act → rate → report 并检查观察页的依赖齐全,所以不会发出一个跑不起来的包。每一处路径改写都有断言:仓库里改了名字,构建会直接报错而不是悄悄发坏包。

玩完让对方把 runs/ 发回来:

node scripts/bench-import.mjs --from=<路径>/runs --tag=grok
node scripts/bench-report.mjs --by=model

--tag 会给局名加前缀,不同来源不会撞名。导入后在观察页照常回放。

(scripts/bench-pack.mjs 是它的前身——只有 7 个文件、没有观察页。已被 bench-kit 完全取代,可以删掉。)

E. 直连 API(有 key 时最快)

不需要 shell、不需要 tool-use,模型每轮输出一行 JSON 或一个数组。预设了四个端点:

BENCH_PROVIDER=anthropic ANTHROPIC_API_KEY=…  node scripts/bench-batch.mjs --levels=1-18 --personas=tapper,reader --concurrency=6
BENCH_PROVIDER=deepseek  DEEPSEEK_API_KEY=…   node scripts/bench-batch.mjs --levels=1-18 --model=deepseek-flash
BENCH_PROVIDER=xai       XAI_API_KEY=…        node scripts/bench-batch.mjs --levels=1-18 --model=grok-4.6
BENCH_PROVIDER=openai    OPENAI_API_KEY=…     node scripts/bench-batch.mjs --levels=1-18 --model=gpt-4o-mini
BENCH_PROVIDER=openai BENCH_BASE_URL=http://localhost:11434/v1 node scripts/bench-batch.mjs --levels=1-18 --model=qwen2.5:7b   # Ollama / vLLM
node scripts/bench-agent.mjs --run=x --level=1 --dry    # 只看提示,不联网
预设 base URL 环境变量 默认模型
anthropic https://api.anthropic.com ANTHROPIC_API_KEY claude-sonnet-5
deepseek https://api.deepseek.com DEEPSEEK_API_KEY deepseek-flash
xai https://api.x.ai/v1 XAI_API_KEY grok-4.6
openai https://api.openai.com/v1 OPENAI_API_KEY gpt-4o-mini

模型标识符会变,用 --model 覆盖(例如 DeepSeek 的 deepseek-chat / deepseek-reasoner 已于 2026-07-24 下线,现在是 deepseek-flash / deepseek-v4-pro)。请求体只用 model / messages / max_tokens / temperature——DeepSeek 不接受 frequency_penalty 和 presence_penalty。

这条路径的网络部分本仓库没有 key,未实测;循环、解析、连点、provider 路由、索引重建都用规则机器人跑通了。

速度:连点

一步一个 LLM 往返是最大的开销(一局 15 步约 2.5 分钟,人玩这关不到 1 分钟)。解决办法和人一样——看清一次盘面就连着点几枚:

node scripts/bench.mjs act --run=x tap t4, tap t5, tap t8, rotate right --note="三枚边上的,清完就转"

链在第一个没成功的动作和任何一次旋转之后停下并返回新盘面,所以它不会"闭眼连点"到错的地方。API 循环里对应输出一个 JSON 数组。指标里 decisions(看盘次数)和 steps(动作数)分开记:同样 15 步,连点大约 5–6 次决策。

怎么看

npm run dev
  • 回放:http://localhost:4173/labs/bench/index.html?run=<局名>——下拉选局,点右侧实况里的任意一步即可回看那一刻的棋盘,「↩ 回到实时」跟回 Agent。右侧竖排是计时板与 Agent 每一步的理由,指标和评分在页面下方。棋盘上方的横幅显示当前是谁在玩(--model / --agent)。
  • 直播:勾选「直播」或 URL 加 &live=1,页面每秒读一次文件,Agent 每走一步这里就动一步。
  • 汇总:node scripts/bench-report.mjs(写 bench/report.json)。带标记的关(有放弃 / 有人觉得无解 / 盲转 ≥2)先看。

指标(事后算,Agent 看不到)

指标 含义 对应 §08
单局耗时 / 每次决策 墙钟时间,含 agent 自己的进程启动;decisions 与 steps 分开记 —
动作 ÷ 棋子 效率;猴子玩家全库 1.67 —
最长连续空点 点了没动的最长连续次数;某关中位 ≥3 = 可读性问题 连续空点
盲转 / 转错方向 一次旋转什么都没打开 / 另一边本来能打开 卡住停留、旋转效率
进入卡住状态 动作后无棋可走的次数 —
多转 清盘用的旋转数 − 最少旋转数 旋转效率
放弃 + stuck 评分 "体感无解"的直接信号 逐关会话终止率

边界

  • Agent 不是人:推理贵、感知弱。用它做相对排序和卡点检测;不要把 clear rate 当真人通关率。
  • 观察是结构化文本,不是像素。它测不出"看不清"。要测可读性,下一步是纯截图模式(让模型看图点坐标),本仓库尚未实现。
  • 没有动画时长、没有连点队列:一步一步来,等价于一个不急的玩家。
  • 校准:同一批关让 5–10 个真人玩一次,把真人的空点、放弃、评分与 Agent 汇总对照,找出 Agent 系统性偏差,再用 Agent 去筛更多关。