EN 中文
← 回文章列表
Side Project · 隨機性與資料完整性

四次裡有一次才會現形的 bug

兩個注音符號悄悄共用了同一個例字。大約四分之一的頁面載入,測驗模式會同時秀出兩張一模一樣的表情符號跟例字——這個巧合,直到有人把機率算出來才真的現形。

2026-07-22 myps6415
~25%
真正會秀出 bug 的載入比例
2
當時每個符號的候選例字數
1
背後那個共用的字(飛機)

背景

bopomofokids.com 教學 37 個注音符號時,每個都配一個 例字——例如 ㄈ 配「飛機」✈️。做法不是把一個字寫死配一個符號,而是每個符號帶一小組 候選例字,getWordVariant(item) 在符號第一次顯示時隨機挑一個,然後在整個 session 裡快取這個選擇。重新整理頁面可能會換到不同的字;但同一個 session 裡, 不管切到哪個模式都會維持一致。

有一條硬規則讓這件事變安全:資料集裡每一個 word 應該都是唯一的,不該有同一個 字出現在兩個不同符號的候選名單裡。但這條規則其實沒有在任何地方被真正檢查過—— 只是一個「加新字的人應該要自己記得」的假設。

這個 bug

ㄈ 的例字名單裡有「飛機」(fēijī)。當時 ㄟ 的名單裡也有——因為「飛機」第二個字 確實含有 ㄟ 的音(f-ei-jī),而且兩個符號當時各自只有兩個候選字。單獨看, 兩邊的選擇都是對的。沒有人檢查過「同一個字出現在兩個不同符號底下」算不算問題, 因為在任何單一次頁面載入裡,這件事通常根本看不出來。

QuizMode 的錯誤選項,有一部分是從其他符號已經解析好的例字裡抽的。當一題剛好 同時牽涉到 ㄈ 跟 ㄟ,而這兩個符號剛好都在那個 session 裡隨機挑中了「飛機」, 測驗就會秀出兩張一模一樣的 ✈️ 表情跟一模一樣的字——不管是小孩、QA 測試、還是 截圖,都沒辦法分辨哪一張才是真正的答案。

為什麼這件事藏了一陣子

當時每個符號有兩個候選字,而且每個符號各自獨立抽選,ㄈ 跟 ㄟ 同時都抽中 共用字(飛機)的機率是:

P(ㄈ 抽中飛機) × P(ㄟ 抽中飛機) = 1/2 × 1/2 = 1/4  →  約 25% 的載入
      真正的機率
    

四分之一的機率不算罕見——但這正好是那種能撐過隨性人工測試的機率。重新整理個 兩次、瞄一眼測驗畫面、兩次都看起來沒事就結案:在兩次獨立檢查裡漏看一個 25% 機率 bug 的機率是 0.75 × 0.75,大約 56%。你大概要連續重新整理十次左右, 「完全沒看到」的機率才會降到 6% 以下。沒有人會為了重新確認一個第一、二次看起來 都正確的測驗畫面,連按十次重新整理。

修法

立即的修法很窄:把 ㄟ 的第一候選字從「飛機」換成「杯子」(bēizi)——對 ㄟ 來說 發音一樣正確,但不再跟 ㄈ 共用。這讓這個特定的碰撞從此不可能發生,而不只是 機率變低。

真正的修法比較廣:「資料集裡每個字都該是唯一的」這條規則,從一個假設變成一條 有文件記載、真正會被檢查的不變量,加上一句明確指示——加新候選字之前,要先 grep 整份例字清單確認沒有重複。同一波修改裡,每個符號的候選字數也從兩個變成三個—— 這順帶降低了未來任何意外碰撞被漏看的機率,但那是附帶效果,不是修法本身。真正的 修法,是這種碰撞從此不被允許存在於資料裡。

這次學到什麼

25% 機率的 bug,大部分人工抽查都會漏過

一個四次裡出現一次的 bug,連續兩次隨性重新整理都漏看的機率超過一半。如果某段 程式碼的行為依賴隨機性,值得明確算一下「要試幾次才抓得到某個機率的問題」—— 在 25% 的情況下,「我看了一下,沒事」這句話幾乎沒有意義。

降低碰撞機率,跟消除碰撞,是兩種不同的修法

把候選字從兩個加到三個,讓類似的意外未來被看見的機率變低,但不代表它從此 不可能發生——兩個符號還是有可能巧合共用一個候選字。真正的修法,是把「字應該 唯一」從假設變成一條會被檢查的規則,讓這個 bug 在結構上不可能存在,而不只是 比較少見。

隨機性只會把 bug 從『每次都在』變成『有時候在』,不會變成『再也不在』

這裡的抽選邏輯本身設計得沒錯;bug 完全活在它抽選的那份資料裡。對一份沒被檢查過 的候選集合做隨機抽選,並不會讓資料完整性的問題消失——只會讓它變成間歇性的,而 間歇性的問題通常比固定發生的問題更難被注意到。