跳到主要内容

取模那个坑,问题不在数学,在接口

阿简
阿简

· 阅读约 4 分钟

最近老有人在转 ERSC 那篇讲随机 API 的文章。我问一句:随机数为什么会有坑?

简单说,坑不在取模这个运算,在只给你一个 random_u64()、把后面所有处理都丢给调用者的接口。

文章我看了两遍。第一遍觉得是常识,写代码的都知道取模不均匀。第二遍才看出来作者骂的不是写错代码的人。

例子很短。[0,9] 十个均匀整数,映射到三个对象上。取模之后第一个对象分到四个输入值,另外两个各三个,实际概率 40%、30%、30%。

写代码的人想要各 1/3。他拿到 40/30/30。中间没有报错,没有警告,测三次各出现一次,看着还挺随机。

作者那句话是全文最值钱的:这里混淆了随机变量的值,和随机变量本身的分布。对随机变量做非线性操作,不保证保住原来那些性质。取模就是那个非线性操作。

他要的东西其实很朴素

一个 random_between(l, h),闭区间,区间里每个整数概率一样。再往上一个 random_choice(),让调用者直接写选项和权重。random_u64() 留着,但别当默认。

我觉得“默认”这两个字被低估了。绝大多数人不会去翻文档里“注意此处分布不均”那一行,他们会照最顺手的写法写。一个人写错是人的问题。一万个人用同一个写法写错,那就是接口的问题。

得往回退半步:接口烂不是写错代码的借口,文档也没藏着掖着。但退完这半步,结论还是那个,把最容易错的一步藏在最底层,就是鼓励人错。

权重那段值得单独讲

用浮点、要求加起来等于 1.0,看着干净,实际是把归一化的活推给调用者。会写的人不差这一步,不会写的人就在这一步卡住,或者压根不卡,直接写个 1.1 交上去。

整数权重没这个问题。4、3、3 就是 40%、30%、30%,不必凑成 100。这个设计上的松手,比多加十个校验都管用。

实现也不玄。权重 15、12、3,总和 30。先在 [0,29] 里均匀取一个整数,再按 [0,14]、[15,26]、[27,29] 三段映射到三个选项。手算都能验。

接口的问题,到了 AI 这边变成了提问的问题

让模型写“从列表里随机抽一个”,十次有九次给你 arr[rand() % len]。不是模型笨,是训练数据里这个写法最多。它给的是最常见的答案,不是正确的答案。

差别在你问什么。你说“随机抽一个”,它按最常见写。你说“每个元素被抽到的概率相同”,它才有机会想起取模这回事。

工具把最容易错的那步藏起来,模型把最常见的写法端上来,两头一夹,错的东西就成了默认。

而且用 vibe coding 的时候你根本不会去看那行取模。你只会看它跑起来没有,抽出来的是不是列表里的元素。是。那就过了。

顺带说一条同源的

ERSC 用 Antithesis 跑测试,随机执行操作系统里要紧的代码路径,拿代码覆盖率判断这一轮测得有没有效。

他们用 random_choice() 干的事是控制输入的分布。上传测试里 blob 大小给了 88、4、4、4 的权重,小 blob 大约占总上传次数的 7/8。不是抽奖,是按真实分布喂输入。

这就解释了作者为什么较真。测试工具里的随机数不是玩具。分布偏了,覆盖率这个数字就是假的,你会以为自己测到了没测到的地方。

他们后来把实际在用的 random_u64() 调用全换成了带权重的 random_choice(),还暂停新增 random_u64() 调用。这个动作比文章本身有说服力。

Antithesis 那套随机执行怎么界定覆盖率算不算有效,我没完全搞明白。先放在这里,搞懂了再补。

这期只收了一条,不合规矩,我知道。

不是没别的可写。是这条讲清楚,比凑八条有用。挑本身就是产出,这话我写过一次,这次还得这么办。

本周就这些。上面有任何一条你试过、或者觉得我讲错了,欢迎告诉我。讲错的我补,没讲清的我改。

下周见。

阿简
阿简

每周替你把 vibe-coding 圈的大事筛成一张小抄,被讲玄的概念一句话搞懂。

查看主页 →