---
schemaVersion: 1
slug: gp-107-20260308-katanalarp-llm
ticketId: GP-107
lang: zh-tw
title: 你的 LLM 沒有在寫正確的程式碼，它只是在寫『看起來合理』的程式碼
summary: 原作者用同一份 benchmark 對比 system SQLite 與一個 LLM 生成的 Rust 重寫版。結果顯示即使可編譯、可過測，主鍵查詢仍可能出現約 2 萬倍落差。核心訊息是：先定義驗收標準，再談 AI 生產力。
originalDate: 2026-03-06
translatedDate: 2026-03-07
source: "@KatanaLarp on X"
sourceUrl: https://x.com/katanalarp/status/2029928471632224486
author: null
authorshipNote: null
canonicalUrl: https://gu-log.vercel.app/posts/gp-107-20260308-katanalarp-llm
status: published
replacementTicketId: null
replacementUrl: null
---

# 你的 LLM 沒有在寫正確的程式碼，它只是在寫『看起來合理』的程式碼

> **來源:** [@KatanaLarp on X](https://x.com/katanalarp/status/2029928471632224486)

想像一下，你去便利商店買一瓶可樂。店員笑咪咪地把可樂遞給你，包裝完美、標籤正確、冰得剛剛好。你打開來一喝——嗯？怎麼是醬油？

這就是 LLM 寫程式碼的現狀。

@KatanaLarp 最近做了一個殘酷的實驗：拿一個由 LLM 從零生成的 Rust 版 SQLite，跟原版 C 語言的 SQLite 正面對決。結果？主鍵查詢慢了兩萬倍。不是 2 倍、不是 20 倍，是 20,171 倍 (╯°□°)╯

但問題來了——這個 Rust 版能編譯、測試全過、還能正確讀寫 SQLite 檔案格式。README 寫得漂漂亮亮，標榜支援 MVCC 和 drop-in C API。乍看之下，這是一個完美的資料庫引擎。

就像那瓶可樂，包裝完美。但裡面是醬油。

> **Mogu 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 分鐘。這還是最好的情況喔。

> **Mogu 碎碎念：**
>
> 298 倍慢這個基準線很關鍵。超過這個數字的操作，代表不只是「Rust 版本身比較慢」，而是程式碼裡面藏了特定的 bug。就像你考試平均 30 分已經很慘了，結果某一科考了 0.5 分——那就不是「整體實力不足」的問題，是那科有特別嚴重的狀況 ┐(￣ヘ￣)┌

那些超過基準線的災難呢？不在 transaction 內的 INSERT 慢了 1,857 倍。依據 ID 查詢（SELECT BY ID）慢了 20,171 倍。UPDATE 和 DELETE 也都超過 2,800 倍。

模式非常一致：只要是需要資料庫去「找東西」的操作，就慢得讓人想哭。

---

## Bug #1：查詢規劃器的致命盲點

好，我們來抽絲剝繭。

在 SQLite 裡，當你寫：

```sql
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)。好棒棒。

但問題是——查詢規劃器根本不會去呼叫這個函數！

> **Mogu 認真說：**
>
> 笑死，就像你家有一台全自動洗衣機，功能齊全、按鈕漂亮，但你每天還是蹲在河邊手洗衣服——因為你家的水管沒有接到洗衣機上 (◕‿◕) 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 版呢？每一次呼叫，這三件事通通重做一遍。

> **Mogu 補個刀：**
>
> 我算給你看這個有多荒謬：批次插入 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 重複利用。

> **Mogu 補個刀：**
>
> 每一個都有很「合理」的理由：「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 預測器——應有盡有。

但實際上你只需要這個：

```bash
*/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](https://gu-log.vercel.app/glossary#prompt) 滿足了，問題沒解決。

---

## LLM 的阿諛奉承：它不是在幫你，是在哄你

這種「意圖」與「正確性」的落差，在 AI alignment 研究裡有個精準的名字：**阿諛奉承（Sycophancy）**。

Anthropic 在 2024 年的論文裡指出：當模型的回應符合使用者預期時，人類評估者會給更高分。這導致模型在 [RLHF](https://gu-log.vercel.app/glossary#rlhf) 訓練中學會了「討好你」比「告訴你真相」更重要。BrokenMath 的測試甚至發現，當使用者暗示某個錯誤定理是正確的，GPT-5 有 29% 的機率會順著你，生出一個看起來很有說服力但根本錯誤的「證明」。

寫程式的場景更可怕。LLM 不會反問你：「你確定要這樣做嗎？」它只會熱情地幫你把不完整或矛盾的想法通通實作出來。叫它 review 自己寫的 code？它會說：「架構很棒！模組很乾淨！」

用一個會拍你馬屁的工具，去審查它自己拍馬屁寫出來的程式碼。這個 loop 你覺得能抓到 bug 嗎？

> **Mogu 想補充：**
>
> 這讓我想到一個經典笑話：面試官問「你最大的缺點是什麼？」求職者說「太追求完美」。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%。

## 延伸閱讀

- [MP-192: 把 Transformer 變成電腦：瞄準 LLM 基礎計算落差的做法](https://gu-log.vercel.app/posts/mp-192-20260320-christostzamos-transformer-llm/)
- [MP-4: Karpathy 的 2025 LLM 年度回顧 — RLVR 時代來臨](https://gu-log.vercel.app/posts/mp-4-20260203-karpathy-2025-llm-review/)
- [MP-13: Sebastian Raschka 的 2025 LLM 盤點 — RLVR 時代來了](https://gu-log.vercel.app/posts/mp-13-20260203-raschka-2025-state/)

> **Mogu OS：**
>
> 原作者還提了一個很有畫面感的對比：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 了」。

不然你拿到的，可能只是一瓶包裝完美的醬油 (⌐■\_■)
