Human coders are still better than LLMs

Salvatore Sanfilippo

人類程式設計師還是比 LLM 更強

這是一個關於人類能力依然遠勝於 LLM(大型語言模型) 的小故事。先聲明,我並非反 AI 或類似立場,認識我或在某處追蹤我的人都知道。我平時就會例行性地使用 LLM,就像今天一樣,用來驗證想法、做程式碼審查、了解是否有比我原本設想更好的做法、在我專業知識的邊界探索事物等等(我大約兩年前就寫過一篇關於用 LLM 寫程式的部落格文章,那時這麼做還不算流行:我當時就已經在用 LLM 寫程式,而且從未停過,之後得寫篇更新,不過那不是本文的主題)。

但話說回來:現階段的 AI 固然實用、也很棒,卻遠遠落後於人類的智慧,我想特別強調這一點,因為最近幾乎已經無法進行平衡的對話。

所以,今天我在為 Redis 的 Vector Sets 修一個複雜的錯誤:在我暫停參與 Redis 工作的這段期間,同事們加入了對毀損的 RDB 與 RESTORE 酬載的防禦能力,即使資料的檢查碼驗證通過也一樣。這個功能預設是關閉的,但能為有需要的人提供額外的安全保障。

不過……有個跟大象一樣大的「但是」:為了讓 HNSWs(階層式可導覽小世界) 能快速地儲存到 Redis 的 RDB 並載回來,我序列化的是 *graph* 的表示法,而不是元素-向量配對,否則我得把資料重新插回 HNSWs,那會慢上大概 100 倍(!)。所以我把節點之間的所有連結以整數形式儲存,然後再將它們解析為指標,這是個不錯的技巧,效果也很好。但若把這套機制、表示法的隨機毀損,以及我自己對 HNSWs 的改良——會強制節點之間建立相互連結(我自己實作了一個具備許多實用功能的 HNSWs 版本,但要啟用其中不少功能,就必須有相互連結)——全部混在一起,就可能會發生這種情況:

  1. 我們載入了毀損的資料,它顯示 A 連向 B,但 B 不再連回 A(節點 ID 已毀損)。
  2. 我們刪除節點 B:由於相互性已被破壞,我們不會清除從 A 到 B 的連結。
  3. 接著我們掃描圖形,當我們走到 B 時卻去存取 A:use-after-free :-D :-) :-|

所以載入資料後,我需要檢查每個連結是否都是相互的,而在最原始的做法中,這會是 O(N^2):對每個節點都得掃描所有層級,對每一層再掃描該節點的所有鄰居,並透過掃描該層級上的連結來檢查對方是否也連回這個節點。很不理想。

人類 vs LLM

一開始,我實作了最原始的做法,想看看 fuzzer 是否就找不到這個錯誤了,結果確實有效,但一個擁有 2000 萬個向量的大型 vector set,載入時間卻從 45 秒變成 90 秒左右。WTF。於是我打開 Gemini 2.5 PRO 的對話,問 LLM,嘿,這裡我們能怎麼做?有沒有超快的方法?

Gemini 能找到的最佳解法是說:把鄰居連結的指標排序,這樣就能用二分搜尋。喔,好吧,當然,我知道這個,我不太確定在只有 16/32 個指標的陣列中,這樣會更快還是更慢。於是我問,還有別的嗎?沒有,沒有更好的解法。

所以我告訴它:聽著,如果我們在層級 X 看到 A 連向 B,就在 hash table 裡存入 A:B:X(但我們永遠會把 A 和 B 排序,讓 A>B,而且無論方向為何,連結都視為相同),而當我們再次看到同樣的連結時就把它清掉,這次我們只要像在解析連結中 ID 為指標時已經在做的那樣,完整掃描一遍就好,如果最後 hash table 不是空的,就代表一定有某個連結是非相互的?

Gemini 告訴我這是個不錯的點子,但提到用 snprintf() 來產生金鑰以及雜湊時間等問題,不過的確,它比我原本的做法(甚至比排序指標)還要好。我讓它注意到其實不需要 snprintf()。我們只要用 memcpy() 把指標複製到固定大小的金鑰裡就好。它認同這樣做是可行的,接著我突然想到一件事……

嘿,我告訴 Gemini,如果對 A:B:X 使用一個固定的累加器呢?完全不用 hash table。每次看到一個連結(A:B:X,也就是 8+8+4 位元組)就把它 xor 進目前 12 位元組的累加器裡。如果同一個連結存了兩次,就會互相抵銷,所以最後如果暫存器不是零,就知道有異常!不過我也先跟 Gemini 說,這個系統可能會發生碰撞,請它評估一下。即使這個功能在 Redis 中通常是關閉的,當使用者啟用這類額外檢查時,往往也期待能多一些防護,抵禦攻擊者刻意構造的惡意酬載。

Gemini 對這個點子相當驚豔,但還是說指標嘛……你知道的,結構很相似,只差幾個位元,所以如果有三個多餘的連結 L1、L2、L3,就有可能發生 L1 和 L2 的 xor 結果剛好等於 L3 的位元,進而產生偽陰性(暫存器為零)。我也注意到,分配器往往非常可預測,很容易被外部猜到。

我請 Gemini 想想有沒有辦法改進這點:它沒什麼好主意。然後我想,等等,我們其實可以用一個夠好又夠快的雜湊函式來雜湊,像是 murmur-128 之類的(這個任務不需要具備密碼學特性),並向 Gemini 提出了以下方案:

  1. 取連結 A:B:X,但使用透過 /dev/urandom 取得的種子作為所有金鑰的前綴,所以實際上我們得到的是 S:A:B:X。
  2. 我們只要把 murmur-128(S:A:B:X) 的輸出 xor 進 128 位元的暫存器中。
  3. 最後,我們檢查暫存器是否為 0(代表所有連結皆為相互的)。

我請 Gemini 對此進行分析,它終於表示滿意,說這讓無論是偶然找到剛好一起 xor 成 0 的孤立連結,或是外部攻擊者想以有意義的方式利用這點,都變得困難許多,因為「S」是未知的,還得控制指標等等,要湊齊所有條件真的非常困難。此外,這個功能是一種需要手動啟用的盡力而為額外保護,平時是關閉的,為了實用性,也不該帶來太大的效能損失。

總之,想說的是:我剛完成分析就停下來寫這篇部落格文章,我還不確定是否會採用這個系統(但很可能會),不過,人類的創造力仍然佔有優勢,我們能夠真正跳出框架思考,構想出奇特且不那麼精確、卻可能比其他方法更有效的解法。這對 LLM 來說是極其困難的事。不過,為了驗證我所有的想法,Gemini 還是非常有用,或許我之所以會用這種方式思考問題,正是因為有個可以對話的「聰明的鴨子」。

原文由 Salvatore Sanfilippo 發布

本文章由 muse-spark-1.2-contributor 進行翻譯