我做了一支抓資料庫漂移的工具,然後它抓到了我自己
綠燈是最貴的謊
寫測試是常識。比較少人談的是下一層的問題:
你已經有測試了,而且是綠的,但它其實什麼都沒保護到。
我用一天做了一輪治理稽核,結果在同一個專案、同一天之內,湊齊了七種「綠燈說謊」的機制。每一種都有可以回查的證據。我把它們寫下來,因為我猜你的專案裡,現在至少躺著其中三種。
第一種:測試自己刻了一份程式,然後測它自己
有一支測試,任務是鎖住「履約要備多少克咖啡豆」這個計算。它在檔案開頭自己寫了一個計算函式,然後測那個函式。三十四個測試,全綠。
問題是,正式環境算克數的程式,從來沒呼叫過它。
那盞綠燈只證明了一件事:測試檔跟自己一致。
第二種:錨點是裝飾品
那個「唯一正確」的計算函式,全專案搜尋只出現在三個地方:它自己的定義,跟兩個測試檔。
正式程式碼裡,零個呼叫者。
真正在算克數的三個地方,各自寫了自己的版本,其中一個把一磅的商品當成半磅算,少算一半。後來我寫了支掃描器去找同類型的,發現這種「零呼叫者的裝飾錨點」,在這個專案裡有三十九個。





第三種:錨在一個永遠不會變的東西上
另一支測試,把「寫入端」錨定在某個資料庫遷移檔上。問題是那個檔案早就被後來的版本整支取代了。
歷史檔案的內容永遠不會再變。所以那段測試永遠是綠的,而且對現行的定義,零偵測力。
最諷刺的是:這支測試當初就是為了防同一型的事故而建的。
第四種:守門員自己壞了,還在發綠燈
有支腳本負責檢查資料庫遷移檔有沒有補上權限。它在 Windows 的換行格式下有個 bug:清除註解的規則遇到 CRLF 會失效,整行註解清不掉。
於是,某個遷移檔的註解裡寫了一句「這不是一張新資料表」,被它當成真的少了權限。
那道本該擋住所有推送的關卡,在主線上一直是紅的。而沒有人發現,它紅的理由是假的。
第五種:監控器弄壞了它在監控的東西
這一段是我自己。
為了堵一個「前端可以竄改訂單金額」的漏洞,我加了一個驗證器。它的設計很保守:只記錄、不擋單,先觀察一陣子再說。
它引用了一個叫做「單價」的欄位。那個欄位不存在——同名的欄位在另一張表上。
而下單流程是直接呼叫它的。驗證器一拋出例外,例外往上傳,整筆訂單建立失敗。
一個「只是要寫一行日誌」的東西,變成了擋掉所有訂單的單點故障。
雙重諷刺是:我當天稍早,才剛做完一支專門抓這種「程式以為有、資料庫其實沒有」的掃描工具。
我用自己剛磨好的顯微鏡,照到了自己臉上的髒東西。
而真正的根因不是打錯欄位。是缺少故障保護:打錯只是觸發點,「監控器有能力弄壞被監控者」才是讓它從小失誤升級成重大事故的原因。
第六種:六個「高風險」,有兩個是死碼
治理掃描標出六個高風險項目。我沒有直接相信,而是派了獨立的角色去專門推翻它們——任務設定就是「盡力證明這不是問題」。
結果四真、兩假。
其中一個看起來很嚴重:「前端直接改訂單狀態、繞過了原子交易」。但那段程式被一個旗標守著,而後端所有版本都無條件回傳真值,所以那段程式碼永遠執行不到。
沒有這一步,我會叫人去修兩段根本跑不到的死碼。





第七種:滿分的那個,正在吃掉別人
這是我覺得最有意思的一種。
第八種:綠燈是真的紅過的,只是紅錯了對象
這一種是後來補的,因為我又踩了一次,而且踩得比前七種都值得寫。
我有一條紀律,寫得很清楚:新寫的驗收指令,必須先證明它現在是紅的。 一個功能還沒做就已經通過的檢查,等於沒有檢查。
那天我在做一個 Telegram 的「紅鈕」——一個按下去就能停掉所有自動發文的遙控開關。 它的做法是把一段程式碼注入到自動化平台的節點裡。我寫了一支驗證,跑起來全綠,部署上去, 按下按鈕:五次全部失敗。
回頭看的時候我發現一件讓我坐直的事:我引用過那條紀律,而且那支驗證確實紅過。 它不是空頭檢查。程式碼語法寫壞的話,它真的會噴紅。
紅的是「語法」。承重的是「這段程式碼裡的名字,跑起來的時候找不找得到」。
語法合法跟名字解得到,是兩件事。一段程式碼可以完全合法,然後一跑就說「這個東西我不認識」—— 因為它引用了幾個住在外面的變數,而注入的時候只搬了函式本體、沒搬那幾個變數。 那種錯只有在真的跑起來的那一刻才會現形。 而我的檢查沒有跑,它只是「看過」。
同一天,同一個病又發作了兩次:
- 第二次:我驗了「產生出來的程式碼是對的」,但我是用文字截取的方式把它撈出來的,
結果把最後那行「✅ 全部通過」也一起截進了程式碼裡。我驗了源頭,沒驗管道。
- 第三次:我把東西送上伺服器,然後就宣布成功了。
我驗了我手上的那份字串,沒驗伺服器上真正存著的那份。
三次的共同形狀不是「忘了驗」。是每一次都很認真地驗了「再往上一層」的那個東西。
所以那條紀律少了半句
「新寫的驗收必須先證明它是紅的」——不夠。正確的版本是:
必須對「最終真正承重的那個東西」先紅。
因為「先紅」這個要求,上面三次全部合格。
而這半句之所以重要,是因為它指出一個我原本沒想通的事: 「有叫」和「叫對對象」是兩件事,而後者你自己分辨不出來。 你不會覺得自己在偷懶——三次我都很認真。認真跟正確在這裡完全脫鉤。
我把它做成了機器,然後機器又抓到我五次
既然自己分辨不出來,就不能靠自己記得。我寫了一支會在「我剛寫完一個會宣布通過的檔案」那一刻 自動出聲的東西,它只問兩件事:
- 你用來「驗」的那個動作,結果有被真的跑起來嗎?還是只是看了一眼?
- 你把東西送出去之後,有沒有再讀回來、跟送出去的那份對過?
然後最有意思的部分來了。我在做這支防線的過程裡,又驗錯對象了五次:
- 我搜「已發布 / 已建立」這些字,結果搜到的是檔案開頭的說明文字,不是程式真的會做的事。誤報 24 次。
- 我宣稱「12 個命中全部都是真的」——那句是拿工具自己的輸出當事實,我根本沒逐個打開來看。
真的打開之後,有一個是誤報,而且它罰的是做對的人:那支檔案明明有把東西讀回來對過 (它甚至驗到了圖片檔的位元組頭尾,比我要求的還嚴),只因為它沒用我腦袋裡那幾個詞來描述,就被判違規。
- 我改用「送出之後有沒有再讀」這個結構性的判斷,結果又把兩種東西搞混:
一段描述要送什麼的設定資料,被我當成「正在送」;一個送出的動作,被我當成「讀回來」。 三個真的問題被靜默放走。
我沒有把這一段藏起來,因為它就是這篇文章的證據。 我在替「驗錯對象」造防線的時候,連續五次驗錯對象。 「你自己分辨不出來」這句話,不是我在謙虛,是那天的實測值。
這一種怎麼自救
不要問「我驗過了嗎」。問這一句:
這盞綠燈,正面觀測到了哪一個最終承重的、真實存在的東西? 把答案寫成一句話,而且句子裡不准出現任何只有你的工具認得的詞。
寫不出來,就是驗錯對象了——那時候唯一的動作是降一層,直接去看原始的那個東西。
還有一個我覺得比什麼都實用的出口:如果你真的只驗得到上一層,就明寫出來。 我最後那支驗證裡有一段離線真的跑不動,我沒有假裝,我寫了「只驗語法(離線無法真跑)」。 誠實標示範圍不是認輸,它本身就是正解——因為下一個讀到的人(很可能是三個月後的你) 會知道那裡還沒被守住。假裝驗過的那一行,才是真正的債。
觸發磁鐵:滿分不代表健康
我有八十七支「技能」——給 AI 的專門手冊,靠一段描述文字讓它自動判斷什麼時候該啟用。
我寫了測試去量一件事:使用者講這句話,會不會叫醒對的那一支。
跑完二十支,通過率七成四。三支慘敗,只有四分之一。
直覺會說:去修那三支。
但失敗紀錄裡有個更值錢的東西——它會告訴你,觸發被誰搶走了。
搶走它們的,是三支自己考滿分的技能。
一支技能拿滿分,可能不是它健康,是它貪吃。
被吃掉的那幾支有個共同點:它們的描述講的是症狀(欄位命名不一致、同一份資料散落多處);而磁鐵講的是動作(找根因、做完備性檢查、探索程式碼)。
使用者說的話,通常是「症狀加上想要的動作」。於是動作型的贏了。
所以修法不是修被吃的那幾支,是在磁鐵的描述裡,加一句「這種情況讓位給誰」。
改完,兩支各從四分之一跳到四分之三。
(這一段我後來發現有個很重要的補充,請往下看「後記」。)
當三次修法都失敗,該懷疑的是切分
有一支怎麼修都沒用。我試了三次:
補中文觸發語,還是四分之一。在搶它的磁鐵那邊加讓位,還是四分之一。補英文觸發詞,還是四分之一。
第三次失敗之後我停手了。因為三個合理的修法都無效時,該懷疑的不是修法,是前提。
回頭看一眼才發現:這支涵蓋某條規則的第一到第四層,而搶它的那支,涵蓋同一條規則的第五到第六層。
它們是同一條規則的兩半。
模型分不出來,是因為它們本來就不該分開。
合併之後,那道以前永遠失敗的題目:滿分。
不是每一支不及格的東西都該被修好。有些該被合併,有些該被刪掉。
而合併完,我的治理閘門立刻擋下我——因為合併後的檔案超過了三百行的上限,規則要求我把細節搬到附錄。
它擋得對。這是整場稽核裡,防線唯一一次真的擋住我,而且它是對的。





順手學到的成本工程
那天我手上只有五美元的額度。這個限制逼出了幾個之後會一直用的習慣。
先做空跑。 印出「這樣會打幾次呼叫」但不真的發送。零成本,避免手滑跑了全量。
先跑兩支校準。 拿到真實單價再推算全量,不要用猜的。我原本估全量要四美元,實測完發現推算準確——但如果一開始就全量跑下去,估錯就沒有第二次機會。
便宜模型當篩子,貴模型只驗失敗。 這裡有個不對稱性值得記住:便宜模型通過是強訊號,代表連它都懂、句子夠清楚;但失敗是弱訊號,可能只是它不夠力。所以只有失敗的才值得用貴模型複驗。
這個設計當場救了我一次:有一支在便宜模型只有四分之一,換強模型跑到四分之三——它根本沒壞。如果我只信第一輪,就會白修一支好的。
整套三百八十四次呼叫,總共花了一塊四美金。
把實測用量印出來。 不要在報告裡寫「估計花費」。API 會回傳真實用量,抓下來加總就好。
可以直接拿去用的五條
一、對鏈路上每一盞會發綠燈的關卡,問一句:它正面觀測到了什麼? 三種綠燈預設有罪:沒看到錯就發綠的、驗的是自報代理指標而不是最終產物的、以及多個檢查其實同源的。
二、監控器第一原則。 任何加在關鍵流程上的輔助邏輯——日誌、指標、只記錄不阻擋的驗證——都必須包上例外保護。它壞掉要安靜退場,不能拖累它在監控的東西。
三、對抗式複驗。 派一個獨立的角色,任務是「盡力推翻」,而且預設傾向「這不是問題」。真的活下來的,才是真的。
四、結構斷言防繞過。 測「函式正確」不夠。要直接讀正式程式碼,斷言「它真的有被呼叫」。我把這條寫成測試之後,那三十九個裝飾錨點才浮出來。
五、刪除測試。 懷疑某段是廢話,就刪掉跑一遍,看指標會不會掉。我用這招從技能描述裡刪掉將近一千個位元組,準確度零變化——那些字每一次請求都在收我的錢,但對判斷完全沒有貢獻。
後記:我重新發明了一個有名字的東西,而且我的版本有瑕疵
寫完上面那些之後,我做了一件早該先做的事:去查社群。
結果有兩個,都不太好看,但都得寫出來。
第一,「觸發磁鐵」不是我的發現。 它有名字,叫 skill collision,是被正式研究過的現象——有實證論文、有做過數百次試驗的分析、也早有人把「每支技能的描述要明確列出其他技能的地盤當排除項」寫成建議。那正是我以為自己想出來的修法。
更完整的是,官方的技能產生器已經內建了整套:自動生測試題、自動調校觸發描述、自動處理衝突。它會生大約二十道題,其中一半是「故意共用關鍵字、但其實需要別的技能」的刁鑽反例,然後跑最多五輪優化,把最好的版本寫回檔案。
第二,也是更難堪的:我的做法有方法論瑕疵。
官方那套有一個我完全沒有的東西——訓練集與測試集的切分,而且明講是為了防止過度擬合。
而我做了什麼?我拿那四道題測,改描述,然後用同樣那四道題再測一次,宣稱從四分之一進步到四分之三。
這就是在對著測試集調參。
那些漂亮的進步數字,有相當一部分可能不是真的變好,只是我把描述改成剛好能通過那四道題。
哪些結論還站得住:合併那個(從四分之一到滿分)比較穩,因為它是結構改變,不是對著題目調字;刪除測試也比較穩,因為它測的是「有沒有變化」而不是「有沒有變好」——而結果是沒有變化。
至於描述調校的那些戰果,得用新題目重驗才算數。
所以我補做了。
我為那兩支各寫了一組全新的題目,紀律是:寫真實使用者會打的話,刻意避開我調進描述裡的那些關鍵字。然後拿新題目重跑。
兩支都是四分之三。跟調參時的分數一模一樣。
改善是真的,不是過度擬合。
但我不打算把這段寫成翻案。因為結果對,不代表過程對——
我當時沒有留驗收題。我是事後補驗,才知道自己修對了。在我提出「進步了兩倍」的那一刻,那句話確實沒有證據撐著。
差別在哪?差別在下一次可能就不對,而我不會知道。
先有結論、後有證據,就算結論碰巧正確,順序錯了就是錯了。
最後
這篇最強的一段,不是任何一個發現。
是第五種——我用自己當天剛做好的工具,抓到了自己犯下的、正是那個工具要抓的錯。
而寫到後記我才意識到,那件事又發生了一次,只是這次的尺度更大:整篇文章在講「不要相信沒被外部檢驗過的綠燈」,而我自己的方法論,直到我去查社群那一刻,都還沒被外部檢驗過。
我量了一整天的東西。我沒有量的,是我拿來量東西的那把尺。
工具沒有偏袒作者。這大概就是它值得存在的理由——只要你記得,也要有人來檢查你的工具。




