前面提過一句話,每個事故都該變成一條永久留著的檢查,避免同一類問題下次再重演一次。這句話講起來很輕鬆,真正要落地,得回答幾個很具體的問題,這條檢查該長什麼樣子、放在哪裡、什麼時候該寫、什麼時候不值得寫。今天想把這幾個問題全部攤開來講,這是我自己實際每天在用的一套做法,不是紙上談兵。
最近讀到兩篇官方公開的文章,一篇是講一次品質下滑的事後檢討,另一篇是講一次大幅度砍掉系統提示詞的改版,兩篇乍看講的是完全不同的事,讀完卻讓我更確定一件事,一套檢查清單真正的價值,從來不是列了多少項,是關鍵時刻敢不敢真的拿它做決定。這個觀察,就是今天這篇想展開講的核心。
這兩篇文章我不打算逐段重述內容,重點想放在讀完之後真正能帶回自己日常工作裡用的東西。畢竟看別人踩過的坑、看別人做出的大膽決定,如果只是當成故事看過就算了,沒有轉換成自己接下來實際會用的方法,這種閱讀等於白費,今天想把力氣放在後面這一段轉換上。
這種把別人的經驗轉換成自己方法的能力,我覺得才是讀這類文章、乃至於讀任何一篇別人分享的經驗,真正該花心思練的能力。很多人讀完一篇分析詳盡的事後檢討,會停在「原來是這樣」的感嘆,就結束了,沒有再往下問一句,這件事換成自己的規模,該怎麼落地、該改掉自己哪個習慣。少了這一步,讀再多篇也只是累積一堆別人的故事,沒有真正變成自己能用的東西,讀完只留下一種「原來大公司也會犯錯」的安慰感,卻沒有帶走任何真正能改變自己做事方式的東西。
我自己觀察到一件事,很多人講「把 bug 變成測試」,重點放在補齊涵蓋率,好像測試越多越安心。實際用了一段時間之後才發現,一份測試清單真正的價值,不在於它列了多少項,在於關鍵時刻,你敢不敢真的照著它的結果做決定。
這個觀點聽起來簡單,實際上顛覆了我一開始對「寫測試」這件事的理解。剛開始寫的時候,會把重點放在數量,覺得寫得越多,代表自己做得越仔細,越有交代。後來發現,一堆寫得模糊、沒人真的信任的檢查,堆再多也沒用,反而是那幾條寫得精準、關鍵時刻真的敢拿來擋改動的檢查,才是真正在發揮作用的部分。數量從此不再是我衡量這件事做得好不好的標準,可信度才是。
這個轉變也讓我重新認真想了一次,為什麼過去會習慣用數量當標準。大概是因為數量比較容易衡量,寫了幾條檢查、涵蓋了幾個檔案,這些都能直接數出來,一份具體的數字,看起來比一句模糊的「這幾條檢查我信得過」更有說服力。但可信度這種東西沒辦法直接量化,得靠實際遇到關鍵時刻,真的照著結果做了決定,才算真正驗證過,這也是為什麼它容易被忽略,卻才是真正重要的那一個標準。
這個決定有兩個方向。一個方向是擋,這條檢查沒過,這個改動就不能放行,這是大家比較直覺想到的用法。另一個方向是給信心,想砍掉一段自己覺得多餘的邏輯、一條寫得很囉唆的規則,猶豫的時候,跑一次這套檢查,過了就敢動手,這個方向比較少被提起,卻同樣重要。沒有一套敢真的拿來用的檢查,遇到想精簡的念頭,通常會因為不確定砍了會不會出事,選擇保守地不動,東西只會越疊越多,沒有人敢動。
那篇講品質下滑的事後檢討,講的正是第一個方向失靈的樣子,連續好幾週使用者陸續反映感覺變笨了,查到最後是三個各自獨立的問題,全部繞過了人工跟自動化的程式碼審查、單元測試、端到端測試,才被使用者發現。另一篇講大幅砍掉系統提示詞的文章,講的則是第二個方向被用到極致的樣子,敢一口氣砍掉八成的內容,靠的就是有一套評測結果可以拿來確認,砍完之後品質沒有量得出來的下降。同一套機制,一邊沒攔住不該放行的東西,一邊給了勇氣做別人不敢做的精簡,兩種用法擺在一起看,才看得出這件事真正的重量。
判斷一套檢查夠不夠格被信任,我會問自己一個很直接的問題,這條檢查沒過,我真的會因此擋下這次改動嗎,還是心裡會想「應該沒差吧」就直接放行。答案如果是後者,這條檢查形同虛設,寫著好看,實際上沒有發揮任何作用。
我自己抓的一個簡單自測法是,寫完一條檢查,就先假裝它失敗了,問自己接下來真的會停手嗎。如果連自己都說服不了自己要停手,代表這條檢查寫得不夠有分量,要嘛是內容太模糊,要嘛是判準訂得太寬鬆,這種檢查留著不會有人尊重它,久了就會變成一條大家心照不宣直接忽略的擺設,比完全沒有這條檢查還糟糕,因為它會製造一種虛假的安全感。
不是每一個踩過的坑,都值得升格成一條永久留著的檢查,這件事跟前面講過規則該不該升格是同一個判準,先問自己,這個問題以後究竟還會不會再發生。真的會反覆出現的問題,才值得花力氣寫成檢查,只是這一次剛好運氣不好撞上的特殊狀況,記下來當參考案例就好,不用每個都寫成正式檢查,不然清單很快就會膨脹到誰都懶得維護。
實戰上我會用一個很粗略的門檻,同一類狀況出現第二次,就開始認真考慮要不要寫成檢查,出現第三次,幾乎一定會動手寫。這個門檻訂得低一點,代價是偶爾會把一個其實只是巧合連續發生兩次的狀況也寫成檢查,多花一點力氣維護,但比起放任同一類問題反覆發生、每次都要重新排查一次,這個代價我覺得划算得多。
還有一種情況值得特別提出來講,也是我自己實際會用的判準,就是那種第一次遇到就已經花了很多力氣才查出來的問題,這種問題就算只發生過一次,我也會直接寫成檢查,不等它出現第二次。理由很簡單,既然這一次已經花了大把力氣才真正搞清楚成因,把這份得來不易的理解直接寫成一條檢查,成本相對很低,卻能確保萬一以後真的又發生,不用再從頭花一次同樣的力氣重新查一遍。反覆發生的頻率跟查出成因的難度,是兩個各自獨立的判準,值得分開來看,不要只用其中一個就下判斷,兩個判準只要有任何一個成立,就值得認真考慮動手寫一條檢查下來。
第二個判準,是抓出這個問題實際上的成本高不高。如果這個問題出現的時候,一眼就能明顯看出來,那寫成檢查的邊際效益就不大。真正該優先寫成檢查的,是那種表面上看起來一切正常、其實已經悄悄出錯的問題,這種問題最怕的就是完全沒有人發現,寫成一條會被固定重跑的檢查,等於是幫自己提前買了一份保險。
這種問題最典型的樣子,是整個介面看起來完全正常,實際上某個很少被直接拿來顯示的欄位,資料格式已經悄悄變了,得過一陣子才會被發現。這類問題如果只是查清楚成因就結束,下次類似的改動照樣會踩到同一個坑,因為完全沒有任何機制會在改動的當下主動提醒。這種欄位格式的檢查,我會寫成一條固定會跑的項目,輸入固定的一組資料,比對輸出的欄位型別跟結構,這種檢查比起功能性的邏輯測試更容易被忽略,卻正好是最值得認真寫下來的那一種。
這類容易被悄悄破壞、卻很少被直接測試到的角落,我會刻意花時間盤點一輪,把它們列成一份清單,而不是等到真的出事才想起某個角落原來這麼脆弱。這種盤點做起來有點枯燥無聊,價值卻不小,因為這些角落往往就是整個系統裡最容易被人遺忘、卻一出事就影響範圍不小的地方,平常越沒人注意,出事的時候往往也越難第一時間查到根源。
寫一條檢查之前,我會逼自己先回答三件事,缺一不可。第一,要怎麼重現這個問題,用什麼樣的輸入、什麼樣的操作,能穩定讓這個異常再次發生一次。第二,正確的結果究竟長什麼樣子,這件事要寫得非常具體,不能只寫模糊的「應該要正常」,要寫出具體的數值、具體的輸出內容,讓人一看就能立刻判斷過還是沒過,不需要再動用判斷力去反覆揣摩。第三,這條檢查如果失敗,代表什麼,是完全不能接受、必須立刻擋下不放,還是只是需要留意、之後再處理,這件事要在寫下去的當下就明確講清楚,不要留給日後讀的人自己去猜。
這三件事按照這個順序寫下來,其實也是一種強迫自己的紀律。先想重現方法,會逼著自己真的把問題摸透,而不是憑猜測就動手改。再想正確結果,會逼著自己把模糊的直覺,轉換成可以被檢驗的具體標準。最後想失敗的代表意義,會逼著自己先想清楚這條檢查在整個系統裡的分量,而不是等真的失敗了才臨時決定該怎麼反應。這三個步驟合起來,其實把整個判斷的過程都提前做掉了,之後每次執行,只剩下照著結果做決定這件輕鬆的事。
順序顛倒過來寫,效果會差很多。如果先想失敗代表什麼,很容易還沒真的搞懂問題,就先入為主地定調這件事很嚴重或不嚴重,之後不管重現方法或正確結果,都會不自覺地往那個先入為主的方向去湊,反而失去了客觀判斷的機會。照著重現、結果、意義這個順序走,才能確保每一步的判斷,都建立在前一步真正查清楚的事實上,不是建立在一開始的直覺印象上。
這三件事裡最容易被偷懶跳過的,是第二件,把正確結果寫具體。很多人寫檢查的時候,會用「看起來對就好」這種模糊的標準,這種檢查跑起來永遠需要人工介入判斷,等於沒有真正做到自動化,每次都得重新花一次判斷力去看結果,這件事完全違背了寫檢查的初衷。
我自己會逼自己做一個小測驗,把正確結果寫完之後,讀一遍,問自己一個完全不熟悉這個問題的人,能不能只靠這段文字,判斷這次跑出來的結果算過還是不算過。答不出來,代表寫得還不夠具體,得繼續補,補到連一個陌生人都能照著判斷為止。這個小測驗看起來麻煩,實際上省下來的,是未來每一次執行這條檢查時,不用再重新花力氣回想當初到底想驗證什麼,也不用擔心過陣子連自己都看不懂自己當初寫的東西。
寫具體到什麼程度算夠,我自己抓的標準是,能不能用一句話講完「輸入是什麼、應該得到什麼」,講不完、還要加一堆但書跟例外的,通常代表這條檢查背後藏著好幾個不同的問題,硬湊成一條,反而讓每一個都驗證得不夠精準,這種時候我會拆成好幾條各自單純的檢查,而不是硬留一條又長又複雜的規則,讓自己以後看到都懶得細讀,最後乾脆整條跳過不看。
這件事是我自己吃過虧才學到的,寫檢查的時候,很容易圖方便,用一個簡化過的環境去跑,覺得反正邏輯是一樣的,簡化一點沒關係。實際上很多問題只在特定狀態下才會浮現,像是特定的資料量、特定的閒置時間、特定的併發情況,環境簡化得太過頭,這些條件根本不會被觸發,檢查跑起來全部通過,卻完全沒有測到真正會出問題的那條路徑。
那篇講品質下滑的檢討裡提到,其中一個問題只在對話閒置過一段時間之後才會被觸發,一般的驗證流程如果沒有刻意模擬這種閒置情境,永遠踩不到那個條件,測試看起來全部通過,只是因為根本沒有走到會出錯的那條路徑上。這件事給我的提醒是,寫檢查之前,要先誠實列出這個問題實際發生時,環境裡有哪些看起來不重要、其實缺一不可的條件,寧可多花一點力氣把這些條件也模擬進去,也不要為了圖方便而漏掉,漏掉的那個條件,往往正是下次問題捲土重來的破口。
我現在寫檢查會刻意問自己,這個環境跟真正會用到的情境,差在哪裡,這些差異會不會剛好把問題觸發的條件給拿掉。寧可讓檢查的設定麻煩一點,也要盡量貼近真實情境,不然檢查跑得再多、涵蓋率數字算得再漂亮,都只是自己騙自己安心而已。
這件事也讓我對「涵蓋率」這三個字多了一層戒心,不再照單全收。涵蓋率高,代表的只是程式碼有多少比例被這些檢查碰過,不代表碰到的時候,環境條件是不是真的貼近實際會出問題的那種狀態。一份涵蓋率百分之九十、卻全部在簡化環境下跑的檢查,可靠程度可能還比不上一份涵蓋率只有百分之五十、但每一條都貼著真實情境去設計的檢查。這件事提醒我,看到涵蓋率數字漂亮,不該直接當成安心的理由,還是得回頭確認,這些檢查測的到底是不是真正會出問題的那個地方。
我現在只要遇到有人拿涵蓋率的數字當成品質保證的唯一依據,都會多問一句,這些檢查跑的環境,跟正式使用的環境,差距有多大。這個問題常常能讓人愣一下,因為很多時候根本沒有認真比對過這件事,涵蓋率數字算出來就直接拿去用了,沒有人回頭確認過,那個數字底下的檢查,到底測得夠不夠真實,這種愣住的反應,往往就是最好的線索。
檢查寫多了,一定會遇到跟規則檔一樣的問題,清單越疊越厚,每次要跑要花的時間越來越長,久了會沒有人想認真維護。我會用跟規則檔一樣的分類邏輯去處理這件事,分成每次都該跑的核心檢查,跟只在特定情境才需要跑的延伸檢查,不要把所有東西都塞進同一份、每次都要全部跑一遍的清單裡。
實務上我會用觸發的頻率來分類,這條檢查對應的問題,只要稍微動到某一小塊範圍的程式碼,就有可能被觸發的,放進核心清單,每次改動都跑。只有在特定、比較少見的操作情境下才會被觸發的,放進延伸清單,只在真的碰到那個情境的時候才跑。這樣分下來,核心清單能保持在一個每次執行都不會太耗時的長度,延伸清單再怎麼累積,也不會拖累日常最頻繁的那個檢查流程。
這個分類方式也讓我在決定要不要動手寫一條新檢查之前,多了一個實用的判斷點,先想清楚這條檢查該進核心清單還是延伸清單,如果連自己都說不出該放哪一邊,通常代表這個問題本身描述得還不夠清楚,範圍到底多廣、多常會被觸發都還沒想明白,這種時候與其急著動手寫,不如先把問題本身想得更透徹一點再說。
這個簡單的歸類動作,也順帶逼著自己對每一個問題的實際影響範圍,有一個更清楚的認識,不會只停留在「這裡出過問題」這種籠統的層次。想清楚它該放進核心還是延伸,等於是想清楚了這個問題的輻射範圍到底有多大,這件事本身,就已經是排查跟預防之間,很有價值的一步,也讓自己對整個系統的風險分布有更立體的掌握。
定期回頭看這份清單,也很重要。有些檢查對應的問題,可能背後的邏輯早就已經整個換掉了,這種檢查留著不但沒用,還會拖慢每次執行的速度,甚至誤導人以為那段舊邏輯還在運作。我會固定找時間重新掃一遍,問自己這條檢查現在還測得到東西嗎,測不到的,該退休就讓它退休,不要捨不得砍。
判斷一條檢查究竟該不該退休,我會用一個具體的動作去實際驗證,故意把它原本對應的那段邏輯改成明顯錯誤的版本,看這條檢查會不會真的失敗。如果刻意改成明顯錯的版本,檢查還是顯示通過,代表這條檢查早就已經測不到任何東西了,留著純粹是心理安慰,該直接刪掉。這個驗證動作跟前面講過的自我檢驗方法是同一個精神,一份沒被驗證過的檢查清單,本身也可能是一份假的安全感。
這個動作我會排進定期整理的固定流程裡,不會只靠臨時想到才做。畢竟一份清單放久了,裡面哪些檢查已經測不到東西,光憑印象很難判斷準確,得真的動手驗證一次,才知道哪些是還在發揮作用的、哪些其實早就名存實亡了。這種定期整理花的時間不多,換來的是整份清單能一直保持在一個誠實反映現況的狀態,而不是一份越堆越厚、卻誰也不確定裡面還有多少真的管用的舊文件,讓人越看越不想打開。
這是我覺得最值得分享的一個實戰技巧,遇到一段自己覺得可能多餘、想砍掉的邏輯或規則,我不會憑感覺直接動手,會先確認,砍掉這段之後,手上這套檢查清單裡,有沒有任何一條會因此開始失敗。跑一次,全部過,才敢真的動手砍,跑完發現有檢查失敗,代表這段東西沒有想像中多餘,還是先留著,或者回頭想清楚為什麼會失敗再決定。
那篇講砍掉八成系統提示詞的文章,讓我特別注意到一件事,這種規模的精簡,靠的從來不是一時的勇氣,是背後有一整套可以拿來確認的評測結果撐著。換成自己規模小很多的場景,道理完全一樣,想做一次大幅度的精簡,光靠信心不夠,得先確認手上有一套自己信得過的檢查,跑過一遍沒有異常,才有底氣真的動手,而不是砍完只能祈禱不要出事。
這件事也讓我重新徹底理解了「大膽」這兩個字真正的意思。表面上看,敢一口氣砍掉一大段東西,像是一種膽識,實際上背後靠的往往不是膽識,是準備工作做得夠扎實,扎實到出事的機率已經被壓得很低,這種時候動手才叫真正的大膽,不是靠盲目的勇氣硬闖,那種硬闖出來的膽子,運氣好沒事,運氣不好就是一次代價不小的教訓,而且往往要事後很久才會發現代價有多大。
這個習慣讓我在面對「這段東西還要不要留」這種猶豫不決的時刻,多了一個可以依靠的具體依據,不用單純憑印象或者憑膽子去賭。這件事也回頭印證了規則檔精簡那天講過的道理,判斷要不要精簡,靠的不該是感覺,是有沒有辦法驗證精簡完之後,行為還是對的,這句話落地到實戰,就是這裡講的這個動作。
反過來,如果跑過一遍發現有檢查失敗,我也不會馬上放棄精簡的念頭,會先弄清楚,失敗的原因是這段東西真的不能砍,還是這條檢查本身寫得不夠精準、其實跟這次改動沒有真正的關聯。分清楚這兩種情況很重要,前者代表這次的判斷錯了,該立刻收手,後者代表檢查本身該重寫,不該把責任都推給那段原本想砍的東西。這個分辨的動作,能避免自己因為一次失敗的檢查,就錯失了一次原本值得做的精簡。
分辨這兩種情況的具體做法,我會先看失敗的那條檢查,內容講的到底是不是跟這次砍掉的東西直接相關,如果講的是完全不相干的另一件事,多半是檢查本身寫得太寬鬆,牽連到了不該牽連的地方,這種時候該做的是把那條檢查重新寫得更精準,而不是因此就否定了這次精簡的判斷。這個小小的追問,常常能救回一次差點被誤判打掉的好決定,也順便讓那條原本寫得太寬鬆的檢查,變得更精準、更值得信任。
寫到這裡,我想老實講清楚一件很重要的事,這整套做法從來沒有一個可以宣告完成的終點。不管清單累積得多完整,隨著系統本身持續變化,一定會持續冒出新的破洞,需要有人持續去發現、去補上。
那篇事後檢討裡也提到,補強之後的做法,是拉寬驗證的範圍、對高風險的變更加上一段觀察期再逐步推送,而不是宣稱這次補完就萬無一失。這句話我讀了特別有感觸,連規模那麼大、資源那麼充足的團隊,都不會說自己補完了,只會說補強了偵測的方式,讓下一次的破洞更容易被提早發現。這種誠實面對極限的態度,我覺得比補了什麼具體內容更值得學,也是這整篇文章裡最值得反覆咀嚼的一句話。
自己規模小很多,做不到觀察期跟逐步推送這種需要一定基礎建設才撐得起來的完整機制,但精神可以借過來用,遇到高風險的改動,我會刻意給自己多留一點觀察的時間,不會改完立刻就當成定案,而是先實際用過一陣子,確認沒有任何意外的狀況出現,才真的放心下來。這個簡化版的做法,雖然整體規模差很多,核心的謹慎程度,我覺得還是可以做到差不多,只是換了一套跟自己實際規模相稱的執行方式。
這件事聽起來有點令人沮喪,但換個角度重新仔細想過一次,我覺得反而踏實,正因為沒有終點,就不需要焦慮於現在做得夠不夠完整。真正該養成的,是一個持續的習慣,每次真的踩到新的坑,就老實把它變成下一次能被拿出來驗證的東西,而不是等到哪天湊出一份自己覺得完美的清單才開始動手。這個持續累積、持續精簡的習慣本身,比清單當下究竟有多少條,重要得多,也持久得多。
回頭看今天講的這幾件事,判斷值不值得寫、怎麼把結果寫具體、環境要貼近真實情境、清單要定期精簡、用檢查給自己精簡的底氣,這五件事拆開來看都不複雜,湊在一起,其實都在回答同一個問題,怎麼讓一份檢查清單,從一份寫著好看的文件,變成一個真的敢拿來做決定的工具。這個轉變,才是整件事真正的重點,不是清單本身,也不是清單裡到底列了多少條。
如果整篇只能留下一句話,我會選這句,寫檢查之前,先老實問自己,這條檢查失敗的時候,我真的會停手嗎。答得出來、也真的會停手,這條檢查才值得認真寫,答不出來,寫了也只是自我安慰,不如誠實承認這裡還沒想清楚,先擱著,等真的想清楚了再動手,這比硬湊一條沒人信任的檢查更誠實,也更有用得多。