第 6 节 · 45 分钟
Vibe Coding · 中文 Semantle

Eval:
把感觉换成数字

今天接满六个模型,让它们跑同一组题,做出你自己的排行榜。eval(评测)就是用固定的题、固定的尺子给模型打分。

开始 Guoliang · 共 13 页
第 6 节 · 样本数与方差

一局说明不了什么:同一个模型,三局能差将近十倍

模型第 1 局 面包第 2 局 地铁第 3 局 钢琴中位数平均数
A 模型5847820.0
B 模型67877.0
示例数字,用来说明 variance(方差,结果忽大忽小的程度)。两个模型用同一份 prompt、同一组目标词、同一个上限。真实数字要你今天自己跑出来。

只跑第 3 局,你会说「A 模型很差」;只跑第 1 局,你会说「A 模型比 B 强」。同一个模型、同一段 prompt,两个相反的结论。所以一局跑完不叫结果,叫一个样本。

第 6 节 · 指标

主指标看中位数:平均数会被一局 47 轮拖垮

拿上一页的 A 模型现算一次
中位数 = 排好序之后,正中间那一个
1三局的轮数从小到大排:5、8、47
2中位数 = 中间那个 = 8。第 3 局再糟也只能占一个位置
3平均数 =(5 + 8 + 47)÷ 3 = 20.0,被一局拉高了一倍多
主指标 = 猜中所需轮数的中位数(SPEC 3.4
辅指标 · 每局花费

快而贵的模型和慢而便宜的模型,光看轮数分不出高下。所以第二个数字是钱:每局花了多少美元。下一页讲这个数从哪来。

没猜中的那一局怎么算

按上限 + 1 计,也就是 51。这样「第 50 轮才猜中」和「50 轮都没猜中」才分得开。参考实现在中位数旁边的括号里另给一个「只算猜中局」的中位数(medianGuessesSolvedOnly)。

主指标只能有一个。两个指标并列,你就没法排序,也就没法回答「雇谁」。

第 6 节 · 样本数

一个人也能做 benchmark:靠控制变量,不靠人多

01 · 同一套目标词
六个模型考同一份卷子

面包、地铁、钢琴,三个词定下来就不再动。谁跑的不重要,重要的是它们答的是同一份卷子 —— 换一个词,前面的成绩全部不可比。

02 · 足够的局数
6 个模型 × 3 局 = 18 局

6 个模型是 models.jsonoptional 不是 true 的那 6 条:五个旗舰 + 一个 9B 小模型当对照组。18 局只够分档:看得出「十几轮」和「五十轮猜不中」,看不出 11 轮和 13 轮谁更强。

03 · 记录一切
每一局的每一轮都留着

猜了什么、得了多少分、花了多少钱、哪一局跑失败了,全部落成 JSON。最难看的那一局也留着 —— 只留好看的那部分,就不是 benchmark 了。

人多并不会让数据更干净:不同的人目标词不一样、局数不一样、记录习惯也不一样,那些全是混淆项。所以这次作业我们换个办法控制变量 —— 同一个人、同一套目标词、同样的局数,只换 prompt。

第 6 节 · 成本

每局多少钱不是估出来的,是模型自己报的

模型每猜每局上限 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
摘自 reference-impl/docs/models.md 第 3 节,按「每猜 1500 prompt + 50 completion token」估。必选 6 个模型跑满 18 局(prompt A)上限约 4.49 美元,其中 GPT-6 Astra 一个就占六成;加上作业里 prompt B 的 18 局,36 局约 9 美元。这是上限不是账单。

思考(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 的单价折算。排行榜用真实值,不用上面那张估算表。

第 6 节 · benchmark 规范

四样东西固定住,改了任何一样,前面的成绩就作废

01 · 目标词固定

同样 3 个词,6 个模型一模一样;课后那套 prompt B 也用这 3 个词,两张榜才比得了。

02 · 上限固定

50 猜。结果 JSON 里写成 maxGuesses,没猜中的局按 51 计。

03 · prompt 固定

一个字都不改。promptSha256 是原文指纹,6 个结果文件里这串必须一样。

04 · 记录一切

每批落一个 JSON,装着 3 局:模型、目标词、历史、是否猜中、轮数、花费,六样齐了才算数。

会翻车 · 中途给某个模型开小灶

跑到第 4 个模型,发现它老是一次吐好几个词,于是在 prompt 里给它加一句「只输出一个词」。

结果:前 3 个模型的成绩和它不可比了,18 局全部作废,得重跑。

正确做法 · 忍住,写进备注

备注写「模型 4 第 2 局起持续多词输出」。这是它的成绩,不是你的错。想改 prompt,18 局跑完之后另开一轮。

结果:这一轮的 18 局互相可比,而且你多了一条关于这个模型的真实观察。

第 6 节 · 对照实验

把词表给它,它就像换了一个模型 —— 上下文决定行为

$ 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 节那句话的证据:程序是你放进上下文里的那段话。

第 6 节 · 第一张 SKILL

把这套步骤做成 /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 跑,看它有没有把六样和那一行交回来。交不回来,改卡,不改代码。

课堂活动25分钟
第 6 节 · 动手

接满六个模型,做出你的排行榜页

  1. 打开 models.json,抄下 optional 不是 true 的那 6 条的 id。抄 id,不是 label —— bench 认的是 id。
  2. 先用最便宜的那个探路:npm run bench -- --model qwen/qwen3.5-9b --games 1 --max-guesses 5。几分钱,跑通了再往下。
  3. 把上一页那张卡存成 .claude/skills/bench/SKILL.md(让 agent 建,你自己打开看一眼),然后 /bench qwen/qwen3.5-9b 跑第一批:看它有没有交回六样、一行记录和那一句汇总。交不回来,改卡,不改代码。
  4. 剩下五个模型一个一批:/bench <id>。跑批的会话会一直忙,再开一个终端窗口起第二个 claude 做排行榜页:读 results/ 下所有 JSON,按「模型 × prompt × 是否给词表」分行,每行中位轮数、解决率、平均花费。先要它打印「读到了几个文件、各是哪个模型」,再谈页面。
你应该看到什么

一张按中位轮数升序的表,六行,每行有解决率和平均花费。下一页那张图就是它的样子 —— 但你的花费那一列不该是 $0.0000。

卡住了先做什么

某个模型报 HTTP 4xx404 是型号下架,跑 npm run pull-models 挑一个替代 id,只改 models.json 里的 id 和 label;401 检查 key;402 去 OpenRouter 充值。别动 prompt。

时间不够怎么办

先保证 6 个模型各跑通 1 局(手打命令加 --games 1),把排行榜页做出来,剩下的课后用 /bench 补。今天页面比局数重要。

第 6 节 · 读表

三个问题读完一张榜:谁最快、谁最便宜、谁没猜出来

截自参考实现的 /leaderboard 页,已裁掉导航栏与页脚。这三行是离线 mock 假模型跑出来的演示数据,不代表任何真实模型;花费全是 $0.0000,因为它不联网、不花钱。
01 · 谁最快

看中位轮数那一列,越小越快。表默认就是按它升序排的。

02 · 谁最便宜

看平均花费那一列。点一下列头改成按它排序,名次多半跟第一问不一样。

03 · 谁根本没猜出来

看解决率。解决率低的模型,它的中位轮数是被 51 顶上去的,不是真的「慢一点」。

作业 ① · 第 6 节

用 prompt A 跑满 6 个模型 × 3 局

  1. 先算钱,再跑。翻第 5 节的结果 JSON,找出每局花费最高的那个数,乘 36(作业 ② 还有 18 局),再乘 2 留余量,对照 OpenRouter 余额。不够就先充。
  2. 冻结 prompt A。复制一份改名 promptA-frozen,接下来 18 局一个字不改。6 个结果文件(一个模型一个,各装 3 局)里的 promptSha256 必须一模一样,那就是「没改过」的证据。
  3. 跑满 18 局。一个模型一批,每批 /bench <id>,六批;卡里的目标词、prompt、目录都已固定,你只换 id。它每批交回的六样和那一行,你自己看一眼。某批报错就只重跑那一批,别删已经跑好的。
  4. 打开排行榜页截图,并用手算核对其中一个模型的中位数是否和页面一致。
  5. docs/benchmark-sheet.md 的 A 榜那一行,备注里写清 3 个目标词和所有异常;再写 2 句局限:3 局能说明什么、不能说明什么。
  6. 可选加分。挑一个便宜模型加 --show-vocab 再跑 3 局,单独记录,不进 A 榜的主列。
交什么

排行榜页截图 1 张 + benchmark 表的 A 榜那一行(电子版)+ 第 5 步那 2 句局限。6 个结果 JSON(共 18 局)留在你电脑里,第 8 节带电脑来。

自检三条:6 个文件的 promptSha256 一致;6 个模型用的是同样 3 个词;花费总额你知道,且没超预算。

跑不动就把报错原文整段贴给 agent,并要求它先说清问题出在哪一步(读文件 / 调模型 / 解析 / 写结果)再改。

第 6 步是加分,跑不跑都行。下一页那份作业不一样 —— 那 18 局必须跑完,第 8 节开头和你的 Demo Day 都建在它上面。

下次上课前做完 · 预计 30–45 分钟(机器跑的时候你可以做别的)
作业 ② · 第 6 节

再写一个打法完全不同的 prompt B,预跑另外 18 局

  1. 动手前先存档。说明写「prompt A 定稿」,B 改坏了随时能回来。
  2. 先说清 A 是什么打法。比如「先按领域分头探路,再朝分数最高的方向收敛」。一句话写下来,写不出来就说明你自己也没想清楚。
  3. B 要换打法,不是换措辞。比如「一上来就密集试同义词,不做领域探路」。把「请你认真一点」改成「请你仔细一点」不算 B,那还是 A。
  4. 其余一律不动。同样 6 个模型、同样 3 个目标词、同样 50 猜上限、同样 --reasoning-effort low。唯一的变量就是 prompt —— 这是第 8 节能下结论的全部前提。
  5. 让它自己跑 18 局:/bench <id> prompts/promptB.md results/promptB,六批;或者直接说「六个模型各来一批」,它会照卡排队跑。A 的 6 个文件留在 results/promptA/ 不要混。你去干别的,回来收 docs/bench-log.md 里的六行。
  6. 回来核对两件事:B 的 6 个文件里 promptSha256 是同一串;这串和 A 的那串不一样。两条都对,才说明你真的换了 prompt 而且中途没改。
  7. 填 B 榜那一行,再算名次差。docs/benchmark-sheet.md 的三步算出 6 个名次差,填进空白计算表。
交什么

benchmark 表的 B 榜那一行 + 名次差那一列(6 个数字,填在空白计算表里)+ prompt B 的原文 + 一句话写清「A 和 B 的打法差在哪」。两个目录一共 12 个结果 JSON(36 局)留在你电脑里。

第 8 节课上并排核对:名次几乎不动的是模型实力,晃来晃去的是 prompt 噪音。

完整版在 docs/homework/lesson-06.md,落盘的目录和文件名约定也在那里。

下次上课前做完 · 你动手约 20 分钟,机器自己跑约 30–60 分钟 · 花费上限和 A 那 18 局同一量级(约 4.5 美元封顶,实际通常远低于上限)
下次见

没有数据,
就只有感觉。

第 7 节:把你猜了六节课的词库翻过来看 —— 用一个你关心的社会议题当筛子,看这份词向量知道什么、不知道什么、偏向哪边。B 的 18 局让它在后台跑着,第 8 节再看。

回到封面 Vibe Coding · 第 6 节 · 完
01 / 13