消息出来的那两天,几个技术群里的讨论有点跑偏。大家最关心的不是获奖成果本身,而是"这个东西到底能干嘛"、"跟写业务代码的人有什么关系"。说实话,这个反应我一点都不意外——计算机领域每年颁出去的那些奖,颁给系统、颁给编程语言、颁给芯片架构的时候,大家都能立刻在脑子里找到对应物;可一旦颁给计算理论,尤其是颁给"随机性与去随机化"这种名字听起来就很抽象的方向,多数人的第一反应就是"这是数学家的自娱自乐"。
我自己的看法要稍微复杂一点。2023年这个奖,颁给的是一整套关于随机性在计算中到底扮演什么角色的理论工作,核心问题是:算法里用的那些随机数,到底是必需的资源,还是只是一种方便的表达方式?如果把它们全部换成确定性的、可复现的步骤,计算能力会不会掉一档?这位获奖学者用几十年的工作给出了一个相当反直觉的答案:大概率不会掉。这篇文章我想把这个答案拆开讲讲,也顺便说说它跟我这种每天跟代码、跟线上问题打交道的人到底有什么关联,以及我在实际项目里因为随机性吃过哪些亏。适合做后端、做安全、做数据、做测试的同学看,尤其是那些觉得自己"数学不好所以理论跟我无关"的人。
1. 先把这次颁奖的事说清楚:奖励的是"把随机性从计算里请出去"
1.1 这个方向的坐标:计算复杂性理论研究的是什么
要理解这个奖,得先知道理论计算机科学里有一块地方叫计算复杂性理论。它不研究"怎么写一个更快的排序",而是研究更根本的问题:一个问题本身有多难?需要多少时间和空间才能解?哪些问题属于同一类难度?这些难度类之间是什么关系?你平时听到的 P、NP、BPP 这些符号,就是这套分类体系里的标签。P 大致是"能高效解出来的问题",NP 是"解出来容易但验证答案容易、找答案难"的问题,BPP 则是"允许算法抛硬币、并且允许小概率答错"的那一类问题。
这套东西听起来很学院派,但它其实是在给整个行业画地图。当我们说"这个问题没有多项式时间的精确算法",意思是不要在这个方向上死磕了,去做近似、去做启发式、去改问题定义。理论给出的不是答案,而是问题的边界。这位获奖者的主要贡献,就是在这个边界上啃下了一大块:随机性到底给计算带来了多少额外的能力,以及这份能力在什么条件下可以被完全消除。
1.2 随机算法好用得离谱,但"随机"本身是个麻烦
先说清楚随机算法为什么流行。很多问题用确定性方法做起来极其别扭,一旦允许抛硬币,思路立刻就通了。比如在一个巨大的数组里找一个特定元素,随机取样比顺序扫描的平均期望更快;比如素数判定,随机化的算法跑起来干净利落;比如图论里判断两个图是否同构,随机方法给出的算法漂亮得不像话;再比如海量数据里的基数估计、频繁项统计、负载均衡、抽样、去重,几乎都是随机化的天下。我做过的一个日志去重模块,用概率型数据结构把内存占用从几十 GB 压到了几百 MB,代价是结果有千分之几的误差——这个交易在业务上完全可以接受。
但硬币有它自己的成本。第一,随机源不可靠。机器上的"随机"其实是硬件噪声、系统熵池、时间和进程状态混合出来的,熵池耗尽的场景在容器化和批量启动的环境里并不罕见。第二,结果不可复现。线上偶发一个诡异结果,你想本地重现,结果每次跑都不一样,排查成本直线上升。第三,难以分析。一个随机算法的正确性证明要处理概率,测试覆盖率的思路也不太好套。第四,也是最要命的一点:你没法确信它一定对。只保证"以极高概率正确"的算法,在某些场景里是不能接受的。
所以理论界很早就有一个自然而然的追问:能不能把这些硬币全部扔掉?给定一个随机算法,能不能构造出一个确定性的算法,跑得差不多快,结果严格正确?这个问题听起来技术性很强,但它牵出的是整个领域最深的一条线。
1.3 那个反直觉的结论:难问题存在,随机性就不必要
这位获奖者最著名的一条工作,是和一个合作者一起证明的一个条件性结论,大意是:如果存在某些真正难解的问题(具体说是需要指数级电路规模才能解的问题),那么随机性就可以被完全消除。也就是说,所有 BPP 里的问题都在 P 里,P = BPP。用人话说:只要世界上真的存在"硬骨头"问题,那么抛硬币带来的那点额外能力就是免费的幻觉,一律可以用确定性的方式模拟出来,而且模拟的代价只是多项式级别的。
这个结论为什么让人意外?因为直觉上大家觉得随机是一种资源,资源总该有点用。但理论告诉你,随机性在计算里更像是一种表达上的便利,而不是能力上的增益。就像你用递归写代码感觉更自然,但任何递归都能改写成循环加栈——表达能力没变,只是写法不同。要模拟随机,需要的工具是伪随机生成器:一小段真正随机的种子,拉伸成一大段看起来随机的序列,长到任何多项式时间的算法都分辨不出来。如果这样的生成器存在,那么把随机算法的所有硬币都换成它产出的伪随机串,算法的行为几乎不变——于是随机就被"骗"过去了。
这里的关键洞察是:伪随机生成器的存在性,等价于难解问题的存在性。越难的问题,越能充当"搅拌机",把短短的种子搅成一长串无法被区分的东西。这条线索把两个看似无关的领域——密码学和复杂性——缝在了一起,也奠定了后来几十年的研究范式。
2. 伪随机的戏法:一枚硬币换一串齿轮
2.1 类比:真随机像骰子,伪随机像一组足够乱的齿轮
我习惯用这样一个类比来解释:真随机像你手里真的掷骰子,每次结果独立、不可预测;伪随机像一台设计精巧的齿轮机构,输入一个很短的初始状态,输出一长串看起来毫无规律的转动。关键在于"看起来"这三个字——它不是真的不可预测,而是任何计算能力有限的观察者都找不到规律。
这个"计算能力有限"的限定是整个理论的命门。如果你有无限算力,你可以穷举所有种子,把伪随机串和真随机串区分开。但在现实里,算力是有限的,多项式时间的观察者才是我们需要防的对象。所以伪随机的定义天然是相对的:相对于某一类计算能力不可区分。这也解释了一个常见误解——很多人以为"伪随机就是不够随机的随机",其实不是,伪随机是一个严格的定义,它说的是"在给定算力预算下无法被区分",这个"无法区分"是可以被数学证明的。
我见过不少同学把伪随机理解成"质量差",然后用"跑一遍统计检验看看分布均不均匀"来判断随机数好不好。统计检验能筛掉劣质实现,但它是必要不充分条件——一个序列可以通过所有常见的均匀性检验,却在密码学意义上完全是可预测的。这两件事不是一回事,混淆它们会带来很实际的后果,后面第 4 节会展开。
2.2 那套"如果……那么……"的结论为什么比定理本身更重要
严格来说,这位获奖者的很多结论都是条件性的:"如果存在这样的难问题,那么……"。有人会觉得这种结论不够硬——万一那个前提不成立呢?我恰恰认为,这种形式才是理论最有价值的地方。
原因是它把一个看起来无从下手的难题,转化成了一条清晰的条件链条。要证明 P = BPP,直接证太难;但可以证明"存在难解问题 ⟹ P = BPP"。这样整个领域的工作就被组织成了一张依赖图:一部分人往上追,去找难解问题的证据;一部分人往下推,看这个结论能延伸出什么应用。哪怕最终前提被推翻,这张图也不会白画,因为它精确地告诉我们"随机性到底值多少"这个问题会以什么方式被解答。
这种思维方式对工程也有借鉴意义。我们做架构评审时经常遇到"这个方案能不能扛住十倍流量"这种问题,直接回答基本靠猜。更好的做法是把它拆成条件式:如果单机 QPS 能到 X,如果缓存命中率能维持在 Y,那么扛住十倍流量需要多少台机器、瓶颈会先出现在哪。条件式结论不给出确定答案,但它给出了讨论的坐标系,让不同意见可以在同一个平面上碰撞。我从读这类理论工作里学到的最实用的一件事,就是遇到"无法直接回答"的问题时,先去找到那个决定性的前提变量。
2.3 一个能跑的小实验:把随机采样换成确定性哈希
空讲理论没意思,我给一个能直接跑的例子。假设你要在一个很大的用户 ID 集合里做均匀抽样,最直觉的做法是给每个 ID 生成一个随机数,取最小的一批。这个做法在分布式环境下有个毛病:每个节点各自随机,合并结果时不严谨。
更稳的做法是用哈希替代随机:对每个 ID 做一次哈希,把哈希值当成"随机数"来用。哈希函数是确定性的,同一个 ID 永远得到同一个值,所以结果是可复现的、可合并的、可增量计算的。
import hashlib def stable_score(user_id: str, salt: str = "sample-v1") -> int: """把任意 ID 映射到 [0, 2^64) 上的一个确定性分值""" h = hashlib.blake2b((salt + user_id).encode(), digest_size=8) return int.from_bytes(h.digest(), "big") def bottom_k_sample(user_ids, k=1000): """取分值最小的 k 个 ID,效果等价于均匀抽样""" scored = ((stable_score(uid), uid) for uid in user_ids) return [uid for _, uid in sorted(scored)[:k]]这段代码背后就是去随机化的思路:用确定性函数模拟随机行为,并且保证模拟结果对下游算法来说无法区分。它解决了三个现实问题:结果可复现,所以线上问题能本地重现;多节点可合并,所以分布式抽样不再有偏差;可增量更新,新数据进来只需重算自己的分值再和阈值比较。我用这套办法做过一个灰度抽样系统,把"随机抽 1% 用户"改成了"ID 哈希落在某个区间",之后灰度名单的争议直接归零——运营同学问"为什么这次没抽到我",你可以直接算出他的分值给他看。
注意:这里的
salt必须固定且版本化。中途改 salt 等于把整个抽样池重新洗牌,灰度名单会全部变一遍。这种"看起来无害的参数调整"是最容易引发线上事故的一类改动。
3. 基础理论值不值这个奖:我的三个判断
3.1 "几十年后才用得上"是托词还是事实
每次有理论方向获奖,都会有一种声音:"这些东西几十年后才用得上,现在颁奖是不是太早了。"我对这个说法有点意见,因为它隐含了一个假设:科学的价值必须体现在产品上,而且要尽快。
我的看法是,这句话混淆了基础研究的时间尺度和工程交付的时间尺度。工程上我们习惯以周、以月为单位衡量产出,一个季度没上线的项目就要被质疑。但基础理论的产出单位是十年、二十年,甚至更久。伪随机生成器、零知识证明这些东西,理论框架在八十年代就搭好了,真正大规模工程化是最近这些年的事——而且一旦用起来,就是嵌在底层基础设施里的,没人会特意提它。
更关键的是,这种"延迟兑现"不是效率低,而是结构决定的。理论必须走在应用前面,否则应用会撞墙。如果没有八十年代那批人把随机性和难解问题的关系想清楚,今天的很多安全协议就缺少底层假设,工程师只能凭经验去堆,堆出来的东西没人能证明它安全。这种"证明不了但大家都在用"的状态,恰恰是最危险的。
3.2 理论的价值在于它规定了"什么是不可能的"
我一直觉得,理论工作最被低估的贡献不是"提出了什么方法",而是"划出了哪些死路"。
举个贴近点的例子。你要设计一个压缩算法,理论上你会先想怎么把数据压得更小;但信息论会告诉你,对于某一类数据,压缩率有一个下界,越往下压越不可能。知道这个下界,你就不会在"再压 5%"上浪费三个月。同理,复杂性理论告诉你某些问题在多项式时间内精确求解基本无望,你就不会死磕精确解,而是转向近似、转向启发式、转向改变问题定义。
这种"负面结论"的价值在团队协作里尤其明显。一个资深工程师最有价值的能力之一,往往不是说"我们能做",而是能快速判断"这条路走不通"。理论给你的正是这种判断力的底稿。这位获奖者的工作,把"随机性带来多少额外能力"这个问题的答案压缩成了一个干净的边界:随机性在多项式时间层面几乎不带来额外能力。这句话本身就是一条非常有用的死路标记——它意味着如果你的算法只是靠随机性来加速,那这份加速大概率不是本质的,你迟早需要找到结构性的优化。
3.3 反过来讲:别把理论奖当成技术风向标
我得泼一盆冷水。虽然我在上面说了不少好话,但我不认为这次颁奖应该被解读成"明年随机性相关技术会火"。这种联想很容易误导。
一个领域获基础科学类奖项,通常说明它的理论骨架已经成型,而不是说明它的工程红利即将释放。这两件事的时间点经常差得很远。更实际的判断方式是看工程侧的成熟度:有没有标准化、有没有成熟的库、有没有踩过足够多的坑、有没有形成排查方法论。按这个标准,去随机化在工程上的直接应用场景并不算多——大多数业务系统根本到不了需要担心"随机性是不是必要资源"的复杂度,它们更需要担心的是"这行random()调用会不会把种子搞成一个固定值"。
所以我的态度是:把它当成一次很好的科普机会去了解这个领域在关心什么,但不要因为一个奖就冲去重构你的随机数调用。奖解决的是认知问题,不解决你的线上问题。这两件事分清楚,能省掉很多无谓的技术焦虑。
4. 落到工程上:随机性出错的地方都长什么样
4.1 四类典型的随机性误用
我在代码评审和问题排查里见过的随机性相关问题,基本能归到下面几类。这个分类不是为了学术完整,纯粹是排查时好用——先对号入座,再去看细节。
| 类型 | 典型表现 | 背后的原因 | 后果严重度 |
|---|---|---|---|
| 种子固定或重复 | 多个实例产生同一批"随机"结果,密钥或 ID 撞车 | 用时间戳、进程号做种子,批量启动时高度重合 | 高,安全场景直接是致命问题 |
| 用非密码学随机数做安全用途 | 生成的令牌可被预测、可被枚举 | 混淆了统计随机与密码学随机 | 高 |
| 随机性影响业务结果可复现性 | 测试偶发失败、线上问题无法重现、分片数据不一致 | 随机决策没有留痕、没有注入点 | 中,排障成本极高 |
| 熵池耗尽导致阻塞或降级 | 服务启动卡住、批量任务延迟异常 | 容器环境熵源单一,早期启动阶段熵不足 | 中 |
第一类我要多说两句,因为它太常见了。很多语言默认的随机函数在未显式设置种子时,会用当前时间做种子。单机跑没问题,一旦你在批量启动几百个进程、或者在同一毫秒内启动多个实例,种子就撞了。我见过一个场景:一批任务并行启动,每个任务需要生成一个唯一的短 ID,结果出现了大量重复,去重之后任务数少了一半,另一部分任务被重复执行。排查花了大半天,最后定位到就是种子来源太弱。
4.2 密码学场景里的随机数红线
只要随机数参与到安全决策里——生成密钥、生成会话令牌、生成盐值、生成一次性随机数——就必须用密码学安全的随机源。这条没有例外,也不存在"我这个场景不太敏感所以可以用普通随机数"。
区分方法很简单:普通随机函数的目标是"分布看起来均匀",密码学随机函数的目标是"给定之前的所有输出,无法预测下一个输出"。前者是统计属性,后者是计算属性。一个通过均匀性检验的序列仍然可能完全可预测。
# 错误示范:不要这样生成令牌 import random, string def bad_token(n=32): alphabet = string.ascii_letters + string.digits return "".join(random.choice(alphabet) for _ in range(n)) # 正确做法:使用操作系统提供的密码学安全随机源 import secrets def good_token(n=32): return secrets.token_urlsafe(n)random模块的实现是可预测的伪随机算法,适合模拟、抽样、游戏逻辑、A/B 分流;secrets背后走的是操作系统的密码学随机接口,适合任何跟安全沾边的地方。这条界限必须在团队里讲清楚,因为这两个模块的调用方式几乎一样,写错了不会有任何报错,代码评审时也极容易滑过去。
顺便说一个更隐蔽的坑:从随机源取模会引入偏差。如果你拿到一个 32 位随机整数再对 100 取模,0 到 15 出现的概率会比 96 到 99 略高。在抽样、抽奖、分片这类场景里,这个偏差会实打实地影响结果分布。标准做法是拒绝采样——落在不均匀区间内的值直接丢弃重取。多数语言的密码学随机库已经内置了这类处理(比如直接给你一个区间内的随机整数),所以优先用库函数,别自己取模。
4.3 非密码学场景:种子该固定还是不该固定
这个问题我被问过很多次,答案取决于你要的是"可复现"还是"不可预测"。
- 测试和调试:种子必须固定,而且要打印出来。一个偶发失败的测试,如果没有记录种子,你等于放弃了复现的可能性。我现在写任何带随机的测试,第一件事就是把种子打进日志。
- 数据抽样、分片、分流:用确定性哈希而不是随机数,理由在 2.3 节说过——可复现、可合并、可解释。
- 负载均衡、抖动退避、缓存过期时间打散:要真随机或者至少每次运行不同,目的是避免同步效应,让请求不要在同一时刻集中。这类场景里固定种子反而是灾难。
- 安全相关:只有一个答案,密码学安全随机源。
把这张对照表贴在代码规范里,比写十页"随机数使用注意事项"都有用,因为它直接告诉你"你现在的场景属于哪一类"。
5. 排查随机性问题的完整思路,以及我踩过的两类坑
5.1 "偶发失败"的第一嫌疑人为什么常常是随机性
我排查问题有个习惯顺序:先问"这个问题是不是每次都出现"。如果答案是"不,有时候好有时候坏",那随机性、时序、并发、外部依赖这四个方向就要优先怀疑,而随机性往往排在第一个。
理由很直接:随机性引入的不确定性是无迹可寻的,它不像空指针那样会在固定位置崩,也不像超时那样有明确的耗时特征。它表现为"大部分时候对,偶尔错",而且错的那次往往不留证据。
完整的排查链路我一般这么走:
- 确认不确定性来源的分类。是随机数、是并发调度、是网络抖动、还是数据本身分布不均?先把范围缩小。做法是固定随机种子重跑——如果固定种子后问题稳定复现,基本可以锁定随机性。
- 收集随机决策的痕迹。如果代码里所有随机决策都记录了种子和关键分支,这一步会非常快;如果没有,你只能靠加日志重跑。这就是为什么我一直坚持"随机决策必须留痕"。
- 构造最小复现。把种子、输入数据、配置一起固定下来,看能不能在本地复现。这一步失败通常意味着还有别的非确定性因素混在里面,比如并发或者时间。
- 验证修复。修完之后不能只跑一次通过就算完,要跑足够多次,或者用多个不同种子批量跑,看失败率是否归零。
我在第 2 步上吃过最大的亏。早期写的一个带随机抽样的数据清洗任务,线上跑出来的统计结果和预期差了几个百分点。当时没有任何随机痕迹记录,只能重跑对比,而每次重跑结果都不一样,等于在一个移动的靶子上找偏差。后来加了种子日志,同样的偏差十分钟就定位到了——某一段抽样逻辑在处理空值时提前返回,导致实际参与抽样的样本比预期少,概率分布整体偏移。这个 bug 跟随机数本身没关系,但没有随机痕迹你就找不到它。
5.2 另一类坑:把"随机"当成"均匀"
这是概念层面的错误,比代码写错更难发现。很多人默认"随机抽一批"就等于"均匀覆盖全部",实际上不是。纯随机会出现明显的聚集和空洞,样本量小的时候尤其明显。我用随机抽样做过一次覆盖率统计,抽了 200 个样本,结果某个占比 5% 的分组一个都没抽到——这在概率上完全正常,但如果你拿这个样本去推断整体,结论就会偏得离谱。
解决办法是换成分层抽样或者确定性轮转:先按关键维度分组,每组内部再抽,保证每个组都有代表。代价是你需要一个稳定的分组键,好处是结果可解释、可复现、不会出现"某个组完全被漏掉"的尴尬。分流的场景也是一样,纯随机分流在小流量下会严重不均衡,用哈希取模配合分层,能把各组的样本量控制在很小的波动范围内。
还有一类更隐蔽的坑:随机性被缓存或复用。比如生成了一个随机令牌,结果因为代码路径问题,在缓存里存了一份又反复取用;或者同一次请求里多次调用随机函数,本以为拿到的是不同值,实际上被某层中间件统一了。这类问题不会报错,只会在安全审计或者数据一致性检查时暴露出来。
5.3 想往这个方向深入,学习顺序怎么排
如果有人看完这篇想认真了解一下这个领域,我给一条我自己走过的顺序。先说清楚,我不是理論方向出身,这条路是站在工程视角排的,目标是"能读懂结论、能用上思路",不是"能发论文"。
第一层,概率与随机算法的基础。至少要能理解期望、方差、马尔可夫不等式、切尔诺夫界这几个工具,以及"以高概率正确"这个说法到底是什么意思。这一层不扎实,后面所有的复杂度讨论都是空中楼阁。
第二层,计算复杂性的基本框架。搞清楚 P、NP、BPP、RP、ZPP 这几个类的关系,知道"归约"是什么意思。这里不需要会做证明,但需要知道每个类对应的直觉——比如 BPP 就是"允许抛硬币、允许小概率错"。
第三层,伪随机生成器与单向函数的关系。这是整个领域的枢纽。理解了"难问题存在 ⟺ 伪随机生成器存在"这条链,你就理解了为什么密码学能建立在复杂性假设之上。
第四层,零知识证明的基本思想。这位获奖者的另一条重要工作就在这里。零知识证明的直觉其实不难:证明者能让验证者相信自己知道某个秘密,同时不泄露这个秘密的任何信息。这个思想现在在隐私计算、可验证计算里有大量应用,是理论落地最直接的例子之一。
第五层才是去看具体的工程实现和标准文档。顺序千万别反,先看文档你会被一堆缩写淹没,看不出它们之间的逻辑关系。
最后分享一个我在实际工作中反复验证的小结论:凡是涉及随机性的代码,写的时候多花五分钟把种子记下来、把决策留痕,将来排查时能省掉几个小时。这个投入产出比,在所有的工程习惯里都算是相当高的。