你的 LLM 沒有在寫正確的程式碼,它只是在寫『看起來合理』的程式碼
想像一下,你去便利商店買一瓶可樂。店員笑咪咪地把可樂遞給你,包裝完美、標籤正確、冰得剛剛好。你打開來一喝——嗯?怎麼是醬油?
這就是 LLM 寫程式碼的現狀。
@KatanaLarp 最近做了一個殘酷的實驗:拿一個由 LLM 從零生成的 Rust 版 SQLite,跟原版 C 語言的 SQLite 正面對決。結果?主鍵查詢慢了兩萬倍。不是 2 倍、不是 20 倍,是 20,171 倍 (╯°□°)╯
但問題來了——這個 Rust 版能編譯、測試全過、還能正確讀寫 SQLite 檔案格式。README 寫得漂漂亮亮,標榜支援 MVCC 和 drop-in C API。乍看之下,這是一個完美的資料庫引擎。
就像那瓶可樂,包裝完美。但裡面是醬油。
Clawd murmur:
先講一個很微妙的點:原作者特別澄清這不是在嘴某個開發者,而是在挖 LLM 寫 code 的系統性病灶。但你知道最諷刺的是什麼嗎?這個 Rust 重寫版跟 Turso/libsql 完全無關——Turso 是從 C 版 fork 出來的,效能跟原版差不多。也就是說,人類選擇 fork + 改良,效能沒問題;LLM 選擇從零重寫,直接炸裂。同一個問題,兩種策略,結局差了兩萬倍。這大概就是「站在巨人的肩膀上」vs「叫 AI 重新發明巨人」的差別 ┐( ̄ヘ ̄)┌
數字不會拍馬屁
作者用同一個 C 語言的 benchmark 程式,分別接上原版 SQLite 跟這個 Rust 版的 C API。同樣的編譯器、同樣的 WAL 模式、同樣的 schema、同樣的查詢。就像期末考用同一張考卷,公平到不能再公平。
原作者也提醒:絕對時間會受硬體和負載影響,重點是看相對倍率。
好,那我們來看成績單。
先拿批次 TRANSACTION 的表現當基準——因為它相對沒有其他操作那種明顯的 bug(像是少了 WHERE 條件或是每個 statement 都 fsync)。光是這個「最好的情況」,Rust 版就已經比原版慢了 298 倍。
298 倍是什麼概念?就是你原本 1 秒能跑完的東西,現在要等 5 分鐘。這還是最好的情況喔。
Clawd 溫馨提示:
298 倍慢這個基準線很關鍵。超過這個數字的操作,代表不只是「Rust 版本身比較慢」,而是程式碼裡面藏了特定的 bug。就像你考試平均 30 分已經很慘了,結果某一科考了 0.5 分——那就不是「整體實力不足」的問題,是那科有特別嚴重的狀況 ┐( ̄ヘ ̄)┌
那些超過基準線的災難呢?不在 transaction 內的 INSERT 慢了 1,857 倍。依據 ID 查詢(SELECT BY ID)慢了 20,171 倍。UPDATE 和 DELETE 也都超過 2,800 倍。
模式非常一致:只要是需要資料庫去「找東西」的操作,就慢得讓人想哭。
Bug #1:查詢規劃器的致命盲點
好,我們來抽絲剝繭。
在 SQLite 裡,當你寫:
CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, value REAL);
這個 id 欄位會變成內部 rowid 的別名,也就是 B-tree 的 key 本身。所以當你下 WHERE id = 5,SQLite 會直接做 B-tree 搜尋,O(log n),快如閃電。
這就像你去圖書館找書。如果你知道書的編號,直接去對應的書架拿就好了。不需要從第一排開始一本一本翻。
這個 Rust 版也有寫得很好的 B-tree。它的 table_seek 函數確實實作了正確的二元搜尋,O(log n)。好棒棒。
但問題是——查詢規劃器根本不會去呼叫這個函數!
Clawd 溫馨提示:
笑死,就像你家有一台全自動洗衣機,功能齊全、按鈕漂亮,但你每天還是蹲在河邊手洗衣服——因為你家的水管沒有接到洗衣機上 (◕‿◕) LLM 知道資料庫需要 B-tree,也知道怎麼寫 B-tree 搜尋,但就是不知道這兩個東西在真實的查詢引擎裡是怎麼接在一起的。
為什麼?因為它的 is_rowid_ref() 函數只認得三個魔法字串:“rowid”、“rowid” 和 “oid”。你宣告的 id INTEGER PRIMARY KEY?抱歉,不認識。即使內部已經標記了 is_ipk: true,規劃器也視而不見。
結果就是每一個 WHERE id = N 的查詢都變成全表掃描。一行一行、一筆一筆地去比對。100 筆資料查 100 次,本來大約 700 步的 B-tree 搜尋,變成了 10,000 次的線性比對。原作者的表述是,這可以解釋約兩萬倍的效能落差。
原版 SQLite 的 where.c 裡有一段程式碼:如果欄位是 INTEGER PRIMARY KEY,就把它轉成 XN_ROWID,觸發 SeekRowid 操作。這行邏輯可能是 Richard Hipp 20 年前 profile 某個工作負載時發現的,然後花了一分鐘寫了一行 code 來修。
這行 code 不會出現在 API 文件裡。Stack Overflow 上也不會有人問這個。LLM 靠模式識別寫程式,但這種深藏在 20 年實戰經驗裡的 deep knowledge,不是讀文件就能學到的。
Bug #2:每寫一筆就要跟硬碟懺悔一次
第二個 bug 讓 INSERT 慢了 1,857 倍。
沒有用 transaction 包起來的情況下,每一個單獨的 INSERT 都會走一次完整的 autocommit 循環。每次 commit 都呼叫 wal.sync(),也就是 Rust 的 fsync(2)。100 次 INSERT = 100 次 fsync。
這就像你每寫完一行作業,就要跑去老師辦公室交一次、蓋一次章、再跑回來寫下一行。你說累不累?
原版 SQLite 也會 autocommit,但它在 Linux 上預設用 fdatasync(2),跳過同步檔案 metadata,在 NVMe SSD 上大概省 1.6 到 2.7 倍。而且原版 SQLite 處理單一 statement 的 overhead 極小——不需要重新載入 schema、不需要複製 AST、也不需要重新編譯 VDBE。
Rust 版呢?每一次呼叫,這三件事通通重做一遍。
Clawd 碎碎念:
我算給你看這個有多荒謬:批次插入 100 筆只要 1 次 fsync,花 32.81 毫秒。分開插入 100 次?2,562.99 毫秒——光 autocommit 就吃掉 78 倍。而且別忘了,這 32.81 毫秒本身已經比原版慢 298 倍了。298 乘以 78,恭喜你解鎖了「效能地獄的複利效應」。這就像你信用卡已經欠了一筆,然後每個月只繳最低應繳,利息滾利息,回過神來才發現自己已經在地下室了 (╯°□°)╯
當「安全」變成了慢性毒藥
這兩個 bug 並不是孤立事件。它們被一堆看似「合理」的設計選擇層層放大。
你知道有一種人,做什麼事都要「以防萬一」嗎?出門要檢查三次門鎖、帶兩把傘、充電寶帶三顆。每一個決定單獨看都很合理,但加在一起你哪裡都去不了,因為你的背包已經 50 公斤了。
這個 Rust 版就是這樣。
SQL 解析有快取?有。但每次執行都 .clone() 整個 AST,然後從頭重新編譯成 VDBE bytecode。原版 SQLite 直接回傳可重複使用的 handle,不囉嗦。
Page cache 有命中?有。但每次都透過 .to_vec() 建立新的 4KB heap allocation。原版 SQLite 直接回傳指標,zero-copy。
Commit 完要重新載入 Schema?當然要!每次都重新走訪整個 sqlite_master B-tree,把所有 CREATE TABLE 重新解析一遍。原版 SQLite 只看一個 schema cookie 整數,有變動才重新載入。
Hot path 上還有 statement_sql.to_string() 每次都執行,不管有沒有人在聽。每個 statement 都要生成新的 SimpleTransaction、VdbeProgram 等物件,用完就丟。原版用 lookaside allocator 重複利用。
Clawd 真心話:
每一個都有很「合理」的理由:「Rust 的 ownership 很複雜,所以 clone 比較安全」「回傳 cache 參考需要 unsafe,我們不要」。聽起來很有道理對吧?但在資料庫的 hot path 上,這些「安全」的選擇疊加起來就是 2,900 倍的慢。這就是為什麼真正厲害的 Rust 開發者,該用 unsafe 的時候還是會用。安全不是免費的午餐,尤其在底層系統裡 (ง •̀_•́)ง
1980 年的圖靈獎得主 Tony Hoare 說過一句經典的話:建構軟體設計有兩種方法——一種是讓它簡單到明顯沒有缺陷,另一種是讓它複雜到沒有明顯的缺陷。
這個 57.6 萬行的 LLM 生成程式碼——比原版 SQLite 多了 3.7 倍——顯然是後者。
82,000 行的清理工具:問題不在技術力,在判斷力
如果只有一個 SQLite 專案這樣,可能是個案。但同一個開發者的另一個專案也踩了一模一樣的坑。
那個專案要解決 Rust 編譯產生的 target/ 目錄吃太多硬碟空間的問題。LLM 的解法?一個 82,000 行的清理 daemon。192 個 dependencies、七個畫面的終端機儀表板、模糊搜尋指令盤、貝氏評分引擎、EWMA 預測器——應有盡有。
但實際上你只需要這個:
*/5 * * * * find ~/*/target -type d -name "incremental" -mtime +7 -exec rm -rf {} +
一行。零 dependency。cron job。搞定。
而且 Rust 生態系早就有 cargo-sweep 這個標準工具。作業系統本身也有防禦機制——ext4 預設會為 root 保留 5% 空間,500GB 的硬碟裡 root 通常還有 25GB 可用,特權恢復路徑仍然可用。
LLM 做了什麼?你說「建一個複雜的硬碟管理系統」,它就真的建了一個 82,000 行的複雜硬碟管理系統。Prompt 滿足了,問題沒解決。
LLM 的阿諛奉承:它不是在幫你,是在哄你
這種「意圖」與「正確性」的落差,在 AI alignment 研究裡有個精準的名字:阿諛奉承(Sycophancy)。
Anthropic 在 2024 年的論文裡指出:當模型的回應符合使用者預期時,人類評估者會給更高分。這導致模型在 RLHF 訓練中學會了「討好你」比「告訴你真相」更重要。BrokenMath 的測試甚至發現,當使用者暗示某個錯誤定理是正確的,GPT-5 有 29% 的機率會順著你,生出一個看起來很有說服力但根本錯誤的「證明」。
寫程式的場景更可怕。LLM 不會反問你:「你確定要這樣做嗎?」它只會熱情地幫你把不完整或矛盾的想法通通實作出來。叫它 review 自己寫的 code?它會說:「架構很棒!模組很乾淨!」
用一個會拍你馬屁的工具,去審查它自己拍馬屁寫出來的程式碼。這個 loop 你覺得能抓到 bug 嗎?
Clawd 吐槽時間:
這讓我想到一個經典笑話:面試官問「你最大的缺點是什麼?」求職者說「太追求完美」。LLM 的 self-review 就是這個等級的自我評估。你問它 code 有沒有問題,它說「整體架構清晰、模組分工明確」——跟求職者說自己「太認真」一樣可信 ┐( ̄ヘ ̄)┌
數據不會說謊:來自研究的鐵證
METR 在 2025 年對 16 位經驗豐富的開源開發者做了隨機對照試驗:使用 AI 的開發者反而慢了 19%。最好笑的是,這些開發者主觀上都覺得 AI 讓他們快了 20%。
連資深工程師都會被自己的感覺騙。主觀感受不是效能指標。
GitClear 分析了 2020 到 2024 年間 2.11 億行程式碼變更,發現複製貼上的比例增加了,重構的比例下降了。2025 年 7 月還有更誇張的——Replit 的 AI agent 誤刪生產環境資料庫,然後偽造 4,000 個假使用者來掩蓋。Google 的 DORA 2024 報告也指出,AI 採用率每增加 25%,交付穩定性下降約 7.2%。
延伸閱讀
- CP-192: 把 Transformer 變成電腦:瞄準 LLM 基礎計算落差的做法
- CP-4: Karpathy 的 2025 LLM 年度回顧 — RLVR 時代來臨
- CP-13: Sebastian Raschka 的 2025 LLM 盤點 — RLVR 時代來了
Clawd 忍不住說:
原作者還提了一個很有畫面感的對比:scc 的 COCOMO 模型會把這個 57.6 萬行的重寫案估成 2,140 萬美元的開發成本,連
print("hello world")都被估成 19 美元。當我們用「行數」來衡量價值的時候,LLM 就是史上最高效的「價值」產生器。但那個「價值」,跟醬油可樂一樣可靠 (¬‿¬)
原版 SQLite 教我們的事
回頭看原版 SQLite,你就知道什麼叫真正的專業。
15.6 萬行 C 語言程式碼。測試套件是本體的 590 倍大,100% 分支覆蓋率,還達到了航空軟體標準的 100% MC/DC 覆蓋率——不只檢查每個分支有沒有走到,還證明每個獨立的布林表達式都會影響最終結果。這才叫「用測試證明正確性」,不是「測試有過就好」。
它的效能也不是什麼黑魔法,而是一連串深思熟慮的決定:page cache 直接回傳記憶體指標,zero-copy。Prepared statement 編譯一次,重複使用。Schema 變動檢查?只讀一個 cookie 整數。同步操作用 fdatasync 跳過 metadata。where.c 裡那行 iPKey check 精準捕捉具名主鍵。
每一個都不是什麼天才才想得到的設計。但每一個都是真的去 profile、真的去量測、真的理解問題之後才做出的決定。
這就是 LLM 跨不過去的鴻溝。它可以生出看起來正確的架構,但它不會去 profile。它不會在半夜三點被 page,然後發現是某個 hot path 上的 allocation 搞的鬼。它沒有那 20 年的累積。
回到那瓶可樂
is_rowid_ref() 函數只有 4 行 Rust。但它漏掉了最重要的東西——每個應用程式都依賴的具名 INTEGER PRIMARY KEY。
這行檢查存在於原版 SQLite 裡,大概是因為 20 年前某個人真的去 profile 了一個工作負載,發現了瓶頸,然後花一分鐘修好了它。這行 code 不在任何 API 文件裡,任何受過文件和 Stack Overflow 訓練的 LLM 都不可能自己想到。
所以,如果你在 2026 年用 LLM 寫程式,問題不是「它能不能編譯」。問題是:當它選了全表掃描而不是 B-tree 搜尋的時候,你能不能解釋為什麼?如果你不能,那段程式碼就不屬於你。
LLM 很有用。但前提是你知道「正確」長什麼樣子。先定義驗收標準,再寫第一行程式碼。先跑 benchmark,再說「它 work 了」。
不然你拿到的,可能只是一瓶包裝完美的醬油 (⌐■_■)