Human coders are still better than LLMs

Salvatore Sanfilippo

人类程序员依然比大模型更强

原文由 Salvatore Sanfilippo 发布,订阅该博客

这是一个关于人类能力至今仍远胜大模型的小故事。先说明一下,我不是那种反对 AI 的人,了解我或者在什么地方关注过我的人都知道。我平时经常用大模型,就像今天这样,用来检验想法、做代码审查、看看有没有比我想到的更好的方案,或是探索一些接近我能力边界的东西等等(差不多两年前,当时用大模型写代码还不算流行,我就写过一篇关于用大模型编程的博客:那时我就已经在用大模型写代码了,而且一直没停过,以后得写个更新,但这不是本文的主题)。

但话说回来:现阶段的 AI 确实有用,甚至很棒,但离人类智能还差得非常远,我想强调这一点,因为最近要进行一场客观、平衡的讨论已经变得很难。

所以,今天我在为 Redis 的 Vector Sets 修一个复杂的 bug:在我离开 Redis 的这段时间里,同事们给 RDB 和 RESTORE 载荷加入了抗损坏能力,即使数据校验和通过也能生效。这个功能默认是关闭的,但为有需要的人提供了一层额外的安全保障。

但是……这里有个大得像大象一样的“但是”:为了让 HNSW 能快速保存到 Redis 的 RDB 中并快速加载回来,我序列化的是*图*结构本身,而不是元素与向量的键值对,否则就得把数据重新插回 HNSW,那样会慢上大概 100 倍(!)。所以我把节点之间所有的连接都存成整数,加载时再解析成指针,这是个很巧妙的办法,效果也很好。但如果把这种做法、随机损坏的表示形式,以及我对 HNSW 的自有改造——强制节点间的连接是双向的(我自己实现了一版 HNSW,加了很多实用功能,而其中不少功能都依赖双向连接)——混在一起,就可能出现这种情况:

  1. 我们加载了被损坏的数据,它说 A 连向 B,但 B 已不再连回 A(节点 ID 已损坏)。
  2. 我们删除节点 B:由于双向性被破坏,我们不会清除 A 到 B 的那条连接。
  3. 然后我们扫描图,当处理到 B 时去访问 A:use-after-free :-D :-) :-|

所以加载完数据后,我需要检查每条连接都是双向的,而最朴素的做法复杂度是 O(N^2),对每个节点都要遍历所有层,对每一层再遍历该节点的所有邻居,然后通过扫描对方在同一层的连接来确认对方是否也连回了自己。这可不行。

人类 vs 大模型

一开始,我先实现了那个朴素的版本,看看 fuzzer 是否就再也找不到这个 bug 了,结果确实有效,但一个包含 2000 万个向量的大型 vector set,加载时间却从 45 秒涨到了 90 秒左右。什么鬼。所以我打开了 Gemini 2.5 PRO 的对话,问大模型:嘿,这里我们能做点什么?有没有超快的方法?

Gemini 能想到的最好办法是:把邻居连接的指针排个序,这样就能用二分查找了。哦,好吧,当然,我知道这个,我也不确定在只有 16/32 个指针的数组里这样会更快还是更慢。于是我又问,还有别的办法吗?没有,没有更好的方案了。

于是我跟它说:你看,要是我们在 X 层看到 A 连向 B,就在哈希表里存一条 A:B:X(但我们始终把 A 和 B 排好序让 A>B,这样无论方向如何,同一条连接都一样),等再次看到这条连接时就把它删掉,这次我们只要像解析连接中 ID 到指针时已经做的那样完整扫描一遍就行,如果最后哈希表不是空的,就说明肯定有某条连接不是双向的,怎么样?

Gemini 说这是个不错的主意,但要用 snprintf() 来生成 key,还有哈希耗时等等,不过确实比我最初的方案(哪怕是给指针排序)要好。我提醒它其实不需要 snprintf(),直接用 memcpy() 把指针拷到一个定长的 key 里就行。它也承认这样可行,然后我突然又想到了一点……

嘿,我跟 Gemini 说,要不直接给 A:B:X 用一个固定的累加器?完全不用哈希表。每次看到一条连接(A:B:X,也就是 8+8+4 字节),就把它异或到一个 12 字节的当前累加器里。如果同一条连接存了两次,就会互相抵消,所以最后如果寄存器不为零,就说明有问题!不过我也跟 Gemini 提到,这个方案可能会有碰撞,让它评估一下。毕竟虽然这个功能在 Redis 里默认是关闭的,但用户一旦开启这种额外检查,往往也期望它能在一定程度上抵御攻击者刻意构造的恶意载荷。

Gemini 对这个想法相当佩服,但还是指出指针嘛……你懂的,结构都很相似,只差几个比特,所以如果有三条异常连接 L1、L2、L3,就可能出现 L1 和 L2 异或的结果恰好等于 L3 的比特,从而出现假阴性(寄存器为零)的情况。我也注意到分配器的行为往往非常可预测,很容易被外部猜到。

我让 Gemini 想想怎么改进,它也没什么好主意。然后我想,等一下,我们其实可以用一个足够好、又足够快的哈希函数来哈希,比如 murmur-128 之类的(这个任务不需要它具备密码学特性),于是我向 Gemini 提出了下面这个方案:

  1. 取连接 A:B:X,但用一个通过 /dev/urandom 获取的随机种子给所有 key 加上前缀,所以实际是 S:A:B:X。
  2. 直接把 murmur-128(S:A:B:X) 的输出异或到 128 位的寄存器里。
  3. 最后检查寄存器是否为 0(所有连接都是双向的)。

我让 Gemini 分析一下这个方案,它终于满意了,说这样一来,无论是偶然碰到几条孤立连接恰好异或为 0,还是外部攻击者想以某种有用的方式利用它,都要困难得多,因为“S”是未知的,还得去控制指针等等,把这些凑在一起非常难。而且,这个功能本来就是一项需要手动开启的尽力而为的额外防护,默认是关闭的,从实用角度看,它不应该带来太大的性能损耗。

总之,想说的是:我刚做完分析就停下来写了这篇博客,我还不确定最终会不会用这个方案(但很可能会用),不过,人类的创造力依然更胜一筹,我们能够真正跳出框框去思考,构想出一些奇怪却不那么精确、但可能比其他方案更有效的解法。这对大模型来说还是极其困难的。当然,为了验证我的各种想法,Gemini 还是非常有用的,也许正因为有这么一个可以对话的“聪明鸭子”,我才开始用这种方式去思考这个问题。

本文章由 muse-spark-1.2-contributor 进行翻译

评论