iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
AI 自動化

我以為我保留了五道閘門系列 第 13

Day 13|15 條檢查,看得出哪幾條是被打出來的

  • 分享至 

  • xImage
  •  

模組三|腳本與確定性驗證器(Day 12–16)

昨天講那支 64 行 bash 的設計。今天講它檢查什麼,以及怎麼從一份檢查清單,反推出這個專案踩過哪些坑。

七組,15 條

組 0  前置條件
      ├ 目錄存在
      ├ 口播稿 HTML 存在
      └ 檔案 ≥ 8000 bytes            ← 註解:likely a stub

組 1  骨架
      ├ 含資料陣列宣告
      ├ 含「片頭」
      ├ 含「收尾」
      └ 含「附錄」

組 2  立場
      └ 含「立場守則」guard block

組 3  節目慣例
      ├ 含收尾標記
      └ 含「過場音效」

組 4  敘事節拍
      ├ 含「一句話事件」
      ├ 含拆解段標記
      └ 含在地對照段標記

組 5  查核表
      ├ 含事實表宣告
      ├ 事實表非空(至少一列)
      └ 含信源層級標籤之一

組 6  視角
      └ 含某個議題視角的關鍵詞之一

組 7  禁止字串
      └ 不得出現 `undefined`         ← 註解:likely a render bug

三條一看就是事故逼出來的

檢查清單不會憑空長出來。每一條都在回答「這是為了防哪一次」,而有三條的註解直接指向了原因。

先講清楚證據強度:下面這三條的「事故」是我從註解與程式碼形狀推出來的,不是我找到了當時的事故紀錄。 我沒有那些 commit。它們發生在我開始寫剪輯記錄之前。所以這一節是推論,只是形狀證據很強。

第一條:不得出現 undefined

註解寫「likely a render bug」。

我想不出第二個解釋:某次產出的成品 HTML 裡,應該是真的印出了 undefined。 前端模板讀了一個不存在的欄位,JavaScript 老實地把它 render 成字串,然後那份稿子被存下來了。

而它是 grep 得到的,所以它變成一條檢查。

第二條:檔案必須大於 8000 bytes。

註解寫「likely a stub」。

這條防的是:產出的東西看起來完成了,但其實是半截。 一份完整的口播稿至少有幾十 KB,一份只有標題跟骨架的空殼幾 KB。

這條檢查很土。它不知道內容對不對,只知道「太小就一定不對」。而它擋的正是 AI 最典型的失效模式:產出一個結構正確但內容空洞的東西,然後宣稱完成

第三條:必須含信源層級標籤之一。

這條對應的是 Day 8 那 125 次「根本沒做分層」。查核表裡如果一個層級標籤都沒有,代表這一集的主張沒有被分級,而那正是「未定事實被講成定論」的前置條件。

一條把編輯立場寫成了程式斷言

我把一整頁編輯立場搖進軋輥機,另一頭只吐出一條薄紙帶,上面剩一個詞。

組 6 只有一條:稿子裡必須出現某個議題視角的關鍵詞之一。

註解寫著:這是節目不可協商的視角。

我第一次重讀到這行的時候愣了一下。這不是技術檢查,這是編輯方針。

而它被寫成了 grep -q

這件事可以有兩種讀法。

負面的讀法:把編輯立場降維成關鍵詞比對,是很粗暴的。稿子裡塞一個詞就能通過,但那不代表這個視角真的有被納入。它驗的是形式,不是實質。

正面的讀法:它擋住的是遺忘,不是敷衍。而遺忘才是常見的失效模式:趕稿的時候整集寫完,才發現某個角度完全沒碰到,這時候要補進去成本很高。有這條檢查,我在產出當下就會知道。

我傾向後者,但要加一句限制:這類檢查只能用在「不做就一定錯」的必要條件上,不能用來判斷品質。

一個關鍵詞的存在無法保證視角有被好好處理,但它的缺席可以保證沒有。必要條件的價值在於它的否命題。

對照組:34 行的 Python,事故寫在 docstring 裡

同一條產線的另一頭,有一支更短的驗證器,它的 docstring 是我看過最誠實的技術文件:

驗證資料檔可被瀏覽器載入:合法 JSON + id 不重複 + 必要欄位齊全。
建議掛 pre-commit / CI,防再把合併衝突標記 commit 進去。

「防再」兩個字。

這支檔案只有一個 commit,而那個 commit 的訊息把整個事故鏈講完了:

解開 commit 進檔的合併衝突、救回被丟的 6 則、加驗證

事故的完整經過可以從這句話還原:

  1. 兩台機器同時改同一個巨型 JSON 檔(Day 6 講過它是單檔)
  2. 合併時產生衝突,而衝突標記被直接 commit 進去了
  3. 那個檔案不再是合法 JSON,前端整個載不動
  4. 解衝突的時候,6 則資料被丟掉
  5. 同一個 commit 裡:資料救回來、驗證器補上

而它的檢查順序就是優先級,而且遇到第一條失敗就 return:

1. 逐行掃衝突標記(<<<<<<< / ======= / >>>>>>>)
2. json.loads 解析
3. 頂層必須是陣列
4. id 不得重複
5. 每筆必須有必要欄位

第一條放最前面,因為它是最直接的死因。而如果檔案有衝突標記,後面四條全部沒有意義:json.loads 會直接爆,錯誤訊息會是難懂的語法錯誤,而不是「你有衝突標記沒解」。

把最常見的死因放最前面,並且在它命中時停下來,可以省掉一整串誤導性的下游錯誤。

從檢查清單反推專案史

我在牆上七個洞前面,一塊一塊釘上木板——每一塊都在補一次已經發生過的事故。

把這兩支腳本的檢查項排在一起,這個專案的傷疤就浮出來了:

檢查 它在說
不得出現 undefined 模板 render 過壞資料
檔案 ≥ 8000 bytes AI 產過空殼還宣稱完成
必須含信源層級標籤 有稿子沒做分層
事實表不得為空 有稿子的查核表是空的
掃合併衝突標記 衝突標記被 commit 進資料檔
id 不得重複 撞過 id
必要欄位齊全 有條目缺欄位

七道疤,七條檢查。

這給了一個很實用的閱讀方法:看到一份檢查清單,先問「哪幾條看起來很奇怪」,那幾條就是事故現場。

正常的檢查(欄位齊全、格式正確)到處都有;而「不得出現 undefined」「檔案要大於 8000 bytes」這種具體到不像通則的條目,一定是有人被咬過。

代價

這 15 條檢查的代價是它們全部是硬編字串。

段落標記改個名字,對應的檢查就會失敗——它會噴錯,不會靜默通過(這點昨天我一開始記錯了,現場修正過),所以不算危險。但它意味著稿子格式跟檢查器是緊耦合的:我不能自由地重新命名段落,因為每改一個都要同步改腳本。

實際的後果是:我沒有改過段落名。 不是因為現在的命名最好,是因為改的成本超過收益。這是一種很小、但確實存在的僵化。

第二個代價:這 15 條全部是「必須存在」,沒有一條是「必須不存在」——除了 undefined 那條。

也就是說它擅長抓遺漏,不擅長抓多餘。一份稿子如果多了不該有的東西(例如複製貼上殘留的上一集內容),它一條都不會響。Day 27 會講這個問題在檔案系統層級的版本。

帶走什麼

寫檢查清單的時候,每一條都要答得出「這是為了防哪一次」。

答不出來的,通常是憑想像加的——那些條目會在未來某天被人以「這條沒意義」為由拿掉,而且他大概是對的。

反過來,答得出來的條目要把答案寫進註解。「likely a render bug」這五個字,讓我幾個月後還知道那條檢查在幹嘛。沒有它,那行 grep -i undefined 看起來只是個怪癖。

第二條,關於怎麼把主觀規則變成檢查:

找必要條件,不要找充分條件。 「稿子有沒有好好處理某個視角」驗不了;「有沒有出現相關關鍵詞」驗得了,而且沒有關鍵詞就一定沒處理。

必要條件擋不住敷衍,但擋得住遺忘。而在真實工作裡,遺忘比敷衍常見得多。

第三條:把最常見的死因放檢查的第一條,並在它命中時立刻停。 否則你會得到一串由第一個錯誤衍生出來的、彼此矛盾的下游錯誤訊息。

明天講那擋不住的另一半:為什麼起草的那個模型不能自己給自己打分數。


上一篇
Day 12|64 行 bash,退出碼 0 才算完成
下一篇
Day 14|起草的那個模型,不能給自己打分數
系列文
我以為我保留了五道閘門16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言