Eval:
把感觉换成数字
今天接满六个模型,让它们跑同一组题,做出你自己的排行榜。eval(评测)就是用固定的题、固定的尺子给模型打分。
一局说明不了什么:同一个模型,三局能差将近十倍
| 模型 | 第 1 局 面包 | 第 2 局 地铁 | 第 3 局 钢琴 | 中位数 | 平均数 |
|---|---|---|---|---|---|
| A 模型 | 5 | 8 | 47 | 8 | 20.0 |
| B 模型 | 6 | 7 | 8 | 7 | 7.0 |
只跑第 3 局,你会说「A 模型很差」;只跑第 1 局,你会说「A 模型比 B 强」。同一个模型、同一段 prompt,两个相反的结论。所以一局跑完不叫结果,叫一个样本。
主指标看中位数:平均数会被一局 47 轮拖垮
快而贵的模型和慢而便宜的模型,光看轮数分不出高下。所以第二个数字是钱:每局花了多少美元。下一页讲这个数从哪来。
按上限 + 1 计,也就是 51。这样「第 50 轮才猜中」和「50 轮都没猜中」才分得开。参考实现在中位数旁边的括号里另给一个「只算猜中局」的中位数(medianGuessesSolvedOnly)。
主指标只能有一个。两个指标并列,你就没法排序,也就没法回答「雇谁」。
一个人也能做 benchmark:靠控制变量,不靠人多
面包、地铁、钢琴,三个词定下来就不再动。谁跑的不重要,重要的是它们答的是同一份卷子 —— 换一个词,前面的成绩全部不可比。
6 个模型是 models.json 里 optional 不是 true 的那 6 条:五个旗舰 + 一个 9B 小模型当对照组。18 局只够分档:看得出「十几轮」和「五十轮猜不中」,看不出 11 轮和 13 轮谁更强。
猜了什么、得了多少分、花了多少钱、哪一局跑失败了,全部落成 JSON。最难看的那一局也留着 —— 只留好看的那部分,就不是 benchmark 了。
人多并不会让数据更干净:不同的人目标词不一样、局数不一样、记录习惯也不一样,那些全是混淆项。所以这次作业我们换个办法控制变量 —— 同一个人、同一套目标词、同样的局数,只换 prompt。
每局多少钱不是估出来的,是模型自己报的
| 模型 | 每猜 | 每局上限 50 猜 | 3 局上限 |
|---|---|---|---|
| GPT-6 Astra | $0.0175 | $0.875 | $2.625 |
| Claude Sonnet 5 | $0.0035 | $0.175 | $0.525 |
| Qwen3.5-9B(对照组) | $0.00016 | $0.008 | $0.024 |
思考(reasoning)token 按 completion 计价。每猜多烧 500 个思考 token,18 局的上限就变成约 10.3 美元,36 局约 20.6 美元 —— 你那 10 块充值的两倍。所以跑全阵容一定带 --reasoning-effort low。
"usage": {
"promptTokens": 6100,
"completionTokens": 420,
"totalTokens": 6520,
"costUsd": 0.0131
}
请求里带上 usage: { include: true },OpenRouter 就会在响应里回一个 usage.cost,那是真实扣款。拿不到才退回按 models.json 的单价折算。排行榜用真实值,不用上面那张估算表。
四样东西固定住,改了任何一样,前面的成绩就作废
同样 3 个词,6 个模型一模一样;课后那套 prompt B 也用这 3 个词,两张榜才比得了。
50 猜。结果 JSON 里写成 maxGuesses,没猜中的局按 51 计。
一个字都不改。promptSha256 是原文指纹,6 个结果文件里这串必须一样。
每批落一个 JSON,装着 3 局:模型、目标词、历史、是否猜中、轮数、花费,六样齐了才算数。
跑到第 4 个模型,发现它老是一次吐好几个词,于是在 prompt 里给它加一句「只输出一个词」。
结果:前 3 个模型的成绩和它不可比了,18 局全部作废,得重跑。
备注写「模型 4 第 2 局起持续多词输出」。这是它的成绩,不是你的错。想改 prompt,18 局跑完之后另开一轮。
结果:这一轮的 18 局互相可比,而且你多了一条关于这个模型的真实观察。
把词表给它,它就像换了一个模型 —— 上下文决定行为
$ npm run bench -- --model qwen/qwen3.5-9b \
--show-vocab --targets 面包,地铁,钢琴 --max-guesses 15
# 结果 JSON 里这一行跟着变:
"vocabGiven": true
按「模型 × prompt × 是否给词表」三元组分行。给了词表的单独占一行,绝不和没给的混在一起 —— 否则 vocabGiven 这个字段就白记了。
同一个模型、同一段 prompt,一行给了词表、一行没给,两行摆在一起,差别就只能是词表带来的。这就叫对照实验(controlled experiment):一次只动一个变量,其余全部锁死。
5 万词塞进 system prompt,每轮约 11 万 token。一局 50 猜按 Claude Sonnet 5 的价位可达约 11 美元 —— 一局就烧掉你充值的一半。只用便宜模型(DeepSeek / Qwen3.5-9B / Haiku),并且把 --max-guesses 调小。
两件事:轮数变了多少,花费变了多少。默认设置下模型看不到词表,只能靠语感造词;给了词表,它猜的每个词都保证在表内,词表外的浪费直接归零。
模型的权重一个都没动,行为却变了。这就是第 1 节那句话的证据:程序是你放进上下文里的那段话。
把这套步骤做成 /bench 卡:跑归机器,判断归你
--- name: bench description: 跑一批 benchmark 并记录。用法 /bench <模型 id> [prompt] [结果目录] --- 参数 $ARGUMENTS:模型 id 必填;prompt 默认 prompts/promptA-frozen.md; 目录默认 results/promptA。目标词固定:面包,地铁,钢琴,36 局跑完前不许动。 每一步把命令和输出原样贴给我;不对就停,别自己换模型、改 prompt、改代码。 1. 先算钱:目录里旧 JSON 的 costUsd 最大值 × 3,超 1 美元先问我。 2. 空跑:npm run bench -- --model <id> --prompt <p> --targets 面包,地铁,钢琴 --out <目录> --reasoning-effort low --dry-run,核对目标词和 prompt。 3. 真跑:同一条命令去掉 --dry-run。报错贴原文,先说在哪一步。 4. 读六样:model、target、guesses、solved、costUsd、guessLog 里的翻车条数。 5. 填一行:追加到 docs/bench-log.md(批次|模型|目标词|三局轮数|猜中|花费|异常)。 6. 核对指纹:promptSha256 和目录里别的文件一样吗?不一样就大字报我,别跑下一批。 7. 只回一句:本批 3 局、猜中 N、花 X 美元、异常 K 处、指纹一致否。
第 5 节那 3 局,你手动读过 JSON、手动填过表。今天起同一套步骤要再来 12 遍:A 六批、B 六批。第 2 节的规矩:第三遍还在手打,就该搬进 SKILL。
跑局本来就是机器跑的。这张卡把跑前算钱、跑后读六样、填一行、核对指纹也交给它照卡做;但每一批仍是你敲一次 /bench,你看它交回的那一句。
怎么用:让 agent 把这张卡存成 .claude/skills/bench/SKILL.md,你自己打开看一眼;第一批用 /bench qwen/qwen3.5-9b 跑,看它有没有把六样和那一行交回来。交不回来,改卡,不改代码。
接满六个模型,做出你的排行榜页
- 打开 models.json,抄下 optional 不是 true 的那 6 条的 id。抄 id,不是 label —— bench 认的是 id。
- 先用最便宜的那个探路:npm run bench -- --model qwen/qwen3.5-9b --games 1 --max-guesses 5。几分钱,跑通了再往下。
- 把上一页那张卡存成 .claude/skills/bench/SKILL.md(让 agent 建,你自己打开看一眼),然后 /bench qwen/qwen3.5-9b 跑第一批:看它有没有交回六样、一行记录和那一句汇总。交不回来,改卡,不改代码。
- 剩下五个模型一个一批:/bench <id>。跑批的会话会一直忙,再开一个终端窗口起第二个 claude 做排行榜页:读 results/ 下所有 JSON,按「模型 × prompt × 是否给词表」分行,每行中位轮数、解决率、平均花费。先要它打印「读到了几个文件、各是哪个模型」,再谈页面。
一张按中位轮数升序的表,六行,每行有解决率和平均花费。下一页那张图就是它的样子 —— 但你的花费那一列不该是 $0.0000。
某个模型报 HTTP 4xx:404 是型号下架,跑 npm run pull-models 挑一个替代 id,只改 models.json 里的 id 和 label;401 检查 key;402 去 OpenRouter 充值。别动 prompt。
先保证 6 个模型各跑通 1 局(手打命令加 --games 1),把排行榜页做出来,剩下的课后用 /bench 补。今天页面比局数重要。
三个问题读完一张榜:谁最快、谁最便宜、谁没猜出来
看中位轮数那一列,越小越快。表默认就是按它升序排的。
看平均花费那一列。点一下列头改成按它排序,名次多半跟第一问不一样。
看解决率。解决率低的模型,它的中位轮数是被 51 顶上去的,不是真的「慢一点」。
用 prompt A 跑满 6 个模型 × 3 局
- 先算钱,再跑。翻第 5 节的结果 JSON,找出每局花费最高的那个数,乘 36(作业 ② 还有 18 局),再乘 2 留余量,对照 OpenRouter 余额。不够就先充。
- 冻结 prompt A。复制一份改名 promptA-frozen,接下来 18 局一个字不改。6 个结果文件(一个模型一个,各装 3 局)里的 promptSha256 必须一模一样,那就是「没改过」的证据。
- 跑满 18 局。一个模型一批,每批 /bench <id>,六批;卡里的目标词、prompt、目录都已固定,你只换 id。它每批交回的六样和那一行,你自己看一眼。某批报错就只重跑那一批,别删已经跑好的。
- 打开排行榜页截图,并用手算核对其中一个模型的中位数是否和页面一致。
- 填 docs/benchmark-sheet.md 的 A 榜那一行,备注里写清 3 个目标词和所有异常;再写 2 句局限:3 局能说明什么、不能说明什么。
- 可选加分。挑一个便宜模型加 --show-vocab 再跑 3 局,单独记录,不进 A 榜的主列。
排行榜页截图 1 张 + benchmark 表的 A 榜那一行(电子版)+ 第 5 步那 2 句局限。6 个结果 JSON(共 18 局)留在你电脑里,第 8 节带电脑来。
自检三条:6 个文件的 promptSha256 一致;6 个模型用的是同样 3 个词;花费总额你知道,且没超预算。
跑不动就把报错原文整段贴给 agent,并要求它先说清问题出在哪一步(读文件 / 调模型 / 解析 / 写结果)再改。
第 6 步是加分,跑不跑都行。下一页那份作业不一样 —— 那 18 局必须跑完,第 8 节开头和你的 Demo Day 都建在它上面。
再写一个打法完全不同的 prompt B,预跑另外 18 局
- 动手前先存档。说明写「prompt A 定稿」,B 改坏了随时能回来。
- 先说清 A 是什么打法。比如「先按领域分头探路,再朝分数最高的方向收敛」。一句话写下来,写不出来就说明你自己也没想清楚。
- B 要换打法,不是换措辞。比如「一上来就密集试同义词,不做领域探路」。把「请你认真一点」改成「请你仔细一点」不算 B,那还是 A。
- 其余一律不动。同样 6 个模型、同样 3 个目标词、同样 50 猜上限、同样 --reasoning-effort low。唯一的变量就是 prompt —— 这是第 8 节能下结论的全部前提。
- 让它自己跑 18 局:/bench <id> prompts/promptB.md results/promptB,六批;或者直接说「六个模型各来一批」,它会照卡排队跑。A 的 6 个文件留在 results/promptA/ 不要混。你去干别的,回来收 docs/bench-log.md 里的六行。
- 回来核对两件事:B 的 6 个文件里 promptSha256 是同一串;这串和 A 的那串不一样。两条都对,才说明你真的换了 prompt 而且中途没改。
- 填 B 榜那一行,再算名次差。按 docs/benchmark-sheet.md 的三步算出 6 个名次差,填进空白计算表。
benchmark 表的 B 榜那一行 + 名次差那一列(6 个数字,填在空白计算表里)+ prompt B 的原文 + 一句话写清「A 和 B 的打法差在哪」。两个目录一共 12 个结果 JSON(36 局)留在你电脑里。
第 8 节课上并排核对:名次几乎不动的是模型实力,晃来晃去的是 prompt 噪音。
完整版在 docs/homework/lesson-06.md,落盘的目录和文件名约定也在那里。
没有数据,
就只有感觉。
第 7 节:把你猜了六节课的词库翻过来看 —— 用一个你关心的社会议题当筛子,看这份词向量知道什么、不知道什么、偏向哪边。B 的 18 局让它在后台跑着,第 8 节再看。