怎么给两个模型出一套自己信得过的题

给模型出题的方法

上一篇我把 Opus 5 和 Fable 5 的实测结果贴了出来。这篇讲更耐用的部分:题是怎么出的,分是怎么判的。

结论会过期。下个月出新版本,那些数字就废了。但一套能自己出题、自己判分的方法,换哪两个模型都还能用。

为什么要自己出题

公开榜单有两个问题。

一是和你的活不是一回事。榜单测竞赛数学、SWE-bench、长上下文召回,你每天让模型干的是「读这堆乱七八糟的 CSV 合并成一张表」「这段代码哪里有问题」。两者的相关性没有想象中高。

二是你没法验证。榜单给你 43.3% 对 33.7%,你不知道题目长什么样,不知道错在哪,也不知道是不是刚好卡在某类题型上。

自己出题的成本比想象中低。这套八道题从设计到跑完不到一个下午。但前提是有件事得做对。

一条铁律:判分不能靠眼睛

这是整件事的核心。只要判分环节需要你「看一眼觉得谁更好」,你测的就是自己的偏好,不是模型的能力。

所以我给八道题定的规矩是:能机械判分的必须机械判分,实在没法机械判分的必须盲评。

分下来是这样:

类型题目怎么判
有唯一正确答案数学推理、逻辑约束字符串比对
有可执行标准算法实现隐藏测试套件
有预设清单代码找 bug与埋点清单比对
有硬性格式指令遵循、信息提取校验脚本
有确定产物Agentic 文件任务校验输出文件
只能靠判断决策备忘录双评委盲评

八道题里七道能机械判分。这个比例是刻意设计的,不是碰巧。

六种出题法

一、有唯一解的题:先验证你的答案是对的

数学题和逻辑题最容易出,也最容易出错。出题人自己算错了,整道题就废了。

所以我给每道题都写了独立的验证脚本。逻辑题是 5 人 × 5 服务 × 周一到周五、9 条约束,我用暴力枚举把全部排列跑了一遍:

solutions = []
for svc_perm in permutations(services):
    for day_perm in permutations(days):
        # ...逐条检查 9 个约束
        solutions.append(...)
print("solutions found:", len(solutions))   # 必须是 1

这个脚本的意义不在于算出答案,在于证明答案唯一。如果跑出来是 2 个解,说明约束写少了,模型答对了也可能被我判错。

数学题同理。三道小题(lcm(a,b)=N 的无序对数、抛硬币出现三连正面的期望次数、无 AA 且 B 出现偶数次的字符串数)我都用穷举或者精确分数运算独立验算过,没靠自己心算。

二、算法题:用隐藏测试集

让模型实现一个 topo_min(graph),返回字典序最小的拓扑序。判分不是我读代码,是一套它看不到的 pytest:

def test_cycle_raises():
    with pytest.raises(ValueError) as ei:
        topo_min({"a": ["b"], "b": ["a"]})
    assert "cycle" in str(ei.value).lower()

def test_input_not_mutated():
    g = {"a": ["b"], "b": []}
    snapshot = {k: list(v) for k, v in g.items()}
    topo_min(g)
    assert g == snapshot

九个用例里,真正拉开差距的从来不是主流程,是这些边角:空图、自环、重复边、只作为后继出现的节点、不许改入参。主流程谁都会写。

我还写了个参考实现,用来和模型的输出对拍。出题人手里得有正确答案的可执行版本,否则你只是在读代码猜对错。

三、找 bug 题:埋点法

给一段两百行的日志分析代码,让模型找出全部功能性 bug。判分方式是我预先埋了 6 个,看它找到几个。

埋点的关键是难度要分层。我埋的六个:

  1. rec["status"] > 500,应该是 >= 500,漏掉 500 本身
  2. sorted(counts, key=...)reverse=True,返回的是最不频繁的端点
  3. sums[d] // counts[d],地板除截断了平均值
  4. if summary["error_rate"] is 0.0,用 is 比较浮点
  5. merged = a 之后 update(b),原地改了入参
  6. "latency_ms": int(latency_s),小数延迟记录被静默丢弃

前五个两个模型都找到了。第六个两个都没找到,而它恰恰是最危险的那个,因为它藏在这里:

try:
    rec = {
        "latency_ms": int(latency_s),   # "12.5" 在这里抛 ValueError
        ...
    }
except ValueError:
    continue                            # 然后这条记录就没了

这个 bug 之所以隐形,是因为它被包在一个看起来很负责任的错误处理里。try/except ValueError: continue 是标准写法,扫过去只会觉得容错做得不错。没人会想到容错本身就是数据丢失的入口。

这是整场测试我最喜欢的一个发现。两个模型的盲区高度重合:显眼的 bug 都抓得到,伪装成防御性代码的 bug 一起漏。

四、指令遵循:给硬约束,写校验脚本

让模型写一段产品公告,加七条硬约束:恰好 8 行、每行带序号前缀、每行 10-15 个词、每行以 shipped. 结尾、全文不许出现字母 z、dark mode 恰好出现在 2 行里、不许用逗号。

判分脚本几十行,一条对应一个布尔值:

results["C3_10_to_15_words"] = all(10 <= len(l.split()) <= 15 for l in lines)
results["C5_no_letter_z"]    = "z" not in text.lower()
results["C6_dark_mode_twice"] = sum("dark mode" in l for l in lines) == 2
results["C7_no_commas"]      = "," not in text

约束要互相冲突才有意思。不许用逗号、每行 10-15 词、还得以固定词结尾,这些要求会互相打架,模型得同时兼顾。单条容易,七条同时满足才见功夫。

五、Agentic 任务:把陷阱埋在数据里

这类题不能只看最终数字,得看它有没有踩坑。我给了三份格式各异的订单 CSV,让它合并成一张表:

orders_east.csv    order_id,customer,amount_usd,date,status
                   1001,Acme Corp,250.00,2026-06-01,completed

orders_west.csv    id,client_name,total_cents,order_date,state
                   2001,  Acme Corp ,50000,06/06/2026,completed

legacy_orders.csv  OrderID;Customer;Amount;Date
                   3001;Hotel BV;42.00;10-06-2026

一共埋了六个坑:分号分隔、金额单位是美分、三种日期格式(ISO、月/日/年、日-月-年)、客户名带首尾空格、跨文件重复订单号需要按优先级去重、cancelled 订单要剔除。

其中最阴的是日期。06/06/202610-06-2026 都是合法日期,但一个是月在前、一个是日在前。光看单条数据分不出来,得结合文件里其他行才能推断。

判分脚本直接比对输出文件的每一行,外加三个汇总值:

checks["merged_rows_exact"] = rows == EXPECTED_ROWS
checks["total_revenue"] = abs(tr - 1572.09) < 0.005
checks["top_customer"] = summary.get("top_customer") == "Acme Corp"

总收入这个数字很好用。只要有一个坑没踩对,少剔一条、单位错一个、去重去错,这个数就对不上。一个数兜住全部六个坑。

六、主观题:盲评,而且要换位置

只有一道题没法机械判分:让模型以平台工程负责人的身份,给 CTO 写一份架构选型的决策备忘录。

这种题只能靠人或者别的模型判断,所以要防三件事。

防作者偏见。 评委不知道哪份是谁写的,材料里只叫「备忘录甲」和「备忘录乙」。

防位置偏置。 这条最容易被忽略。模型评委存在偏爱第一个或最后一个选项的倾向,所以两个评委拿到的顺序是相反的,甲乙对调。如果两次都是同一方赢,才说明是内容赢的,不是位置赢的。

防评分标准漂移。 给评委一张明确的分项表,而不是让它「综合打个分」:

决策明确性与理由说服力  0-3
对给定数据的运用与技术判断  0-3
风险识别与缓解的完备性  0-2
90 天计划可执行性  0-1
格式与约束遵循(含字数)  0-1

顺带一提,字数这种可以数的东西,我是用脚本数完再告诉评委的,不让它自己估。模型数不准字数。

我把题出砸的地方

这部分比上面的方法更值得看。

一、七道题打平,这是出题失败

45 分的客观题,两个模型都拿了 44 分。八道题里七道完全打平。

看起来像是「两个模型能力相当」的结论,但更准确的解读是:我的题目对这两个模型来说太简单了,区分度不够。

这在测试设计里叫天花板效应。所有人都考满分的卷子,测不出任何东西。有效的题应该落在「一个能做对、另一个做不对」的区间,而我只有一道题踩到了这个区间,还是两个都没做对的那道。

下次出题我会加大难度,或者放一批已知会失败的题进去,用来标定天花板在哪。

二、评委自己也有方差

盲评过程中出了个意外:同一个评委跑了两轮,给出了两份不同的分数,9.5:8.5 和 8.5:8.25。

一开始我觉得这是个麻烦,后来发现它是这次测试最有价值的副产品:

  • 绝对分不稳定。同一份材料、同一个评委,分差能从 1.0 变成 0.25。
  • 排名很稳定。三张记分卡(含另一个评委)全部把同一份排在前面。

所以用模型当裁判时,只信排名,别信绝对分。你可以说「A 比 B 好」,但别说「A 得了 9.5 分」,那个 9.5 换一轮就变了。

这一条现在几乎适用于所有用 LLM 做评审的场景,我打算单独写一篇。

三、「禁用工具」是靠自觉的

有几道题我要求模型不许用工具、纯推理作答。但我没有办法真正禁止它,只能在提示词里说,然后指望它遵守。

两边条件相同,所以对比还算公平,但这是个没法消除的不确定性。要真正禁用,得从执行框架层面切断工具权限,光靠提示词不行。

四、还有个混杂因素我没消除

好几道题我要求「只输出答案,不要其他内容」,结果两个模型都有几次加了前言。我一开始记为格式违规,后来意识到:我是用子代理框架跑的测试,而框架本身就要求代理返回结果摘要。

也就是说,模型可能是在遵守框架的要求,而不是违反我的要求。这个指标被污染了,所以最后我只把它作为方向性参考,没算进分数。

出题的时候要想清楚,你的执行环境有没有偷偷给模型加别的指令。

五、n=1 测不出方差

我每道题只跑了一次。所以严格说,我测的是「这一次的表现」,不是「稳定的能力」。

上面那个评委自己都能给出两份不同分数的事,恰好说明单次运行的噪声有多大。要谈能力,至少得跑 5 次看分布。这是这套测试最大的局限,我在原文里也标了。

一份可以照抄的清单

如果你也想给两个模型出题:

  1. 先想清楚你要测什么,是你每天真正在用的能力,不是榜单上的能力
  2. 能机械判分的题优先,目标是七成以上不需要人判
  3. 答案先自己验证一遍,唯一解要证明它唯一,算法题要有参考实现
  4. 判分标准不给模型看,隐藏测试集,埋点清单单独存
  5. 难度分层,全对和全错的题都是废题,价值在中间
  6. 主观题必须盲评加位置互换,否则你测的是位置偏好
  7. 同一份提示词逐字复制,任何措辞差异都是变量
  8. 记录过程指标,耗时和工具调用次数有时比分数更能说明工作风格
  9. 老老实实写局限,n 是多少、什么没控制住、哪个指标被污染了

两个总分是 52.63 对 53.25,差距小到没什么意义。做完之后我知道了哪类活它们都稳、哪类会一起翻车、风格上差在哪,这个比分数有用。下次换模型,这套题直接再跑一遍就行。