上周五晚上,Abyss 模式第四题,我盯着倒计时卡了二十多秒,最后交了个自认为很冷的答案。结算出来,排名倒数。
在这个模式里倒数就等于白给——它要的是最少见的那个答案,你答得越主流,分越低。
一组七道题,满分 700。那天四组打完,我 Abyss 那组 380 出头,Sky 那组 660。落差有点扎眼。
不服气,我干了件很符合我性格的事:把题目抄下来,扔给 Claude,让它帮我想 Abyss 的候选。
结果更差。
它答得不慢,还又快又整齐,一口气给我十条,每条都像模像样。问题在于这十条里有七条是“第二主流”——你排除掉最常见答案之后,脑子里第二个蹦出来的那种。Abyss 不吃这个。
画外音:今年最值的一笔 API 费,用来验证一个我早就知道、但一直没当真的结论。
Sky 是它的主场。这个模式要最常见答案,越大众排名越靠前。对模型来说不算作弊,算回家——它的训练目标就是把“给定上下文之后最可能的下一段”找出来,Sky 问的是“给定这个问题,最多人会说什么”,同一个数学问题的两种问法。我甚至不用做任何 prompt 工程,把题面原样贴过去就行。这在别的任务里几乎不可能,本身就是一个信号。
Abyss 是反过来的,要分布最末端那一小撮。这里模型有两个问题,一个在训练,一个在对齐。
训练那边,长尾在语料里本来就稀,权重低到几乎不参与采样。对齐那边更致命——RLHF 把“安全、常见、不容易被挑刺”这一档整体抬了一截,等于把尾巴又削平一次。所以你去问它要冷门答案,它给你的往往是“它觉得还算少见、实际上只是次高频”的东西。它不是不知道那些真正的长尾,是不倾向输出。
我改到第三版的 prompt 长这样:
你是一个反向共识采样器。
输入:一个问题 + 已知主流答案列表
输出:20 个候选,按"预计在 10 万人样本中的出现频次"从低到高排序
禁止:输出训练语料中高频出现的答案
禁止:输出任何解释
第一版我写的是“请给出最冷门的答案”。它给我的全是网络梗——冷是冷,但没有区分度,所有人都在拿同一个梗当冷门。第二版加了“不要用网络流行语”,它开始生造词。第三版加上“按预计频次排序”这个约束,好一点,也就好一点:它排序的依据仍然是内部那点频率直觉,而语料里的“少见”跟真实人群里的“少见”根本不是一个东西。
举个实际输出,题目是“一种早餐”:
燕麦粥
饭团
贝果
希腊酸奶配蜂蜜
四条里有三条是“我如果去健身房会吃的东西”。这也怪不得它,健身房食谱在语料里的曝光量,就是比某个县城的早饭高两个数量级。
磨了几天我想明白一件事:真正的冷门不是“奇怪”。奇怪的东西模型反而擅长给,因为它见过太多人写“奇怪的东西”。
真正的冷门是——在完全正确的方向上,偏了一点点。还是早餐那个例子:主流是包子豆浆油条,次主流是三明治麦片,Abyss 要的可能是某个县的人早上吃的一碗什么粉。这东西模型“知道”,它在语料里出现过,但只出现过那么几次,权重低到采样时根本轮不上。
所以 Abyss 考的不是见多识广,是你能不能在脑子里模拟一个你完全不了解的人群,然后从那个模拟里捞东西。这跟做需求是一回事——你得能想象一个你从没见过的用户在你的页面上乱点的样子。
插一句,我前面想写“Abyss 天然抗 AI”,写完回头看觉得话说太满。它抗的是查表式的作弊,抗不了真在长尾里泡过二十年的人。这个区别对后面的结论没影响,先放着。
扯回来,我真正想说的不是游戏。
让 AI 写代码,它给你的永远是 Sky 模式的答案——最主流、最通用、在最多项目里能跑通的那一版。这一版在九成场景里是对的,剩下那一成就是它翻车的地方,而那一成恰好是你拿工资的理由。
上个月一个导入脚本。需求简单到不像话:把用户上传的 CSV 解析进库。
# AI 给的第一版,我简化过
def load(path):
rows = []
with open(path, encoding="utf-8") as f:
for line in f:
try:
rows.append(parse(line))
except Exception:
pass
bulk_insert(rows)
看着没问题对吧。能跑,不报错,本地测了两次都过。
它不报错——这才是最要命的。真实文件里有一行带 BOM 的表头,还有一列用户手填的日期,格式五花八门。这个 try 包在循环里、except 里什么都不干,意味着这两类行会被静默丢掉。三千行的文件进去,两千九百多行落库,剩下几十行没人知道去哪了。
这个 bug 是在我毫无防备的情况下被我自己对行数对出来的。当时我盯着 2903 和 3000 这两个数字看了很久,脑子里第一个念头是“我是不是看错了”,第二个念头才是“哦,是那个 pass”。
Sky 模式的答案在这里是“能跑”。Abyss 模式的答案是“跑完之后少了 97 行,三天后会有人来问为什么他填的东西不见了”。
后来我改成逐行记失败原因、跑完甩一份 rejects 文件:
rejects = []
for lineno, line in enumerate(f, 1):
try:
rows.append(parse(line))
except Exception as e:
rejects.append((lineno, repr(e), line[:80]))
if rejects:
print(f"{len(rows)} 行导入成功,{len(rejects)} 行失败")
跑出来是这样,比我预想的还难看:
$ python load.py orders.csv
2903 行导入成功,97 行失败
$ head -3 rejects.log
14 ValueError: time data '2026/1/3上午' does not match format
88 UnicodeDecodeError: 'utf-8' codec can't decode byte 0xef
...
粗暴,但有效。多写的这十几行,没有一行是“最主流”的写法,全是从具体事故里长出来的。这种代码 AI 也能给你——前提是你先把“边界在哪”告诉它。它不知道你的文件里有什么,这不是它的错,是我没把这段历史交代清楚。
现在这游戏我每天还是四组,先打 Abyss,趁脑子还清醒。Sky 照打,但目的变了——它现在是我的自查表:如果哪天 Sky 我拿不到 620 以上,说明我对“普通人会怎么答”的直觉已经钝了。这在我这行是危险信号,写报错提示、写 README 这类活,全是 Sky 模式。
Hop Pass 我没买。真不是嫌贵,是我一天就玩那四组,无限次游玩这个权益对我是零;最近 30 天的存档回看有点意思,但我真正想要的是“我历史上所有 Abyss 答案的排名分布”,它不给。按投入产出算,不值。
顺便说一句,这游戏两个人在蒙特利尔做的,九种语言,不用注册、没有广告。这种克制现在挺少见。而且它整个立身之本就是“人类答案的真实分布”——真到了所有人都拿 AI 代打 Sky 那天,Sky 那组的数据就先废了。Abyss 反倒撑得久一点,因为你没法通过查表获得“没人会说什么”。
这句我删过一次又加回来了。它有点绝对,但我目前还是这么想的。
最后说一条我打算以后照做的规则。凡是让 AI 生成“选项列表”的活——候选函数名、候选方案、候选测试用例——我必须在它的列表之外,自己补一条,而且必须是里面最不像的那一条。
补不出来,说明我对这个问题的理解还停留在它的分布里。
这条规则目前还没帮我拿到过什么像样的高分,倒是上周 Abyss 靠它蒙对了一题。够了。
