iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
AI Engineering

《矽墟》:我把一部科幻小說當成軟體專案來管系列 第 16

Day 16|「已存在就跳過」失效,20 個角色 100% 重生,而且不會報錯

  • 分享至 

  • xImage
  •  

模組三|AI 生圖與生片產線(Day 10–19)

模組三的圖產線收尾。前六天講怎麼把產線建起來,今天講它壞掉的地方:一個只有帳單會告訴你的 bug。

而且我要先說:這個 bug 我在 2026-07-23 就記下來了,標註「重跑陷阱(仍在)」。今天是 08-06,它還在。這篇的最後會講為什麼。

問題:批次腳本的省錢機制

三視圖生成腳本要跑 20 個角色,每個角色一次 API 呼叫。

跑到一半失敗是常態,API 逾時、內容審核擋下、網路斷都會。所以腳本必須能重跑,而重跑的時候不該把已經生好的再生一次。

標準做法就是那一行:

for f in docs/character-prompts/*.md; do
  name=$(basename "$f" .md)
  [ "$name" = "index" ] && continue
  out="assets/turnarounds/${name}.png"
  if [ -f "$out" ]; then echo "skip $name (exists)"; continue; fi
  ...

檔案已存在就跳過。這行看起來完全沒問題,而且它在寫下來的當天確實是對的。

然後成品改名了

第二版產線定稿之後,成品檔名做了一次統一:從中文改成英文。

assets/turnarounds_old/   01-焰刃.png    02-霜瞳.png    ...   ← 舊,中文
assets/turnarounds/       char-01-turnaround.png
                          char-02-turnaround.png       ...   ← 新,英文

改名的理由很正當。中文檔名在某些工具鏈上會出問題,統一成英文比較安全。

但那行 skip 檢查沒有跟著改。它算出來的路徑是:

out = "assets/turnarounds/" + name + ".png"

name 來自 prompt 檔的檔名,也就是 01-焰刃秤兒調律者 這些中文。

於是它每次都在找 assets/turnarounds/01-焰刃.png,而那個檔案在改名之後就不存在了。

實測:100% 失效

我為了寫這篇,把 skip 邏輯抽出來做了一次 dry-run(只判斷不呼叫 API):

  [WOULD REGENERATE] 01-焰刃  -> 找不到 assets/turnarounds/01-焰刃.png
  [WOULD REGENERATE] 02-霜瞳  -> 找不到 assets/turnarounds/02-霜瞳.png
  ...
  [WOULD REGENERATE] 調律者   -> 找不到 assets/turnarounds/調律者.png
  [WOULD REGENERATE] 靛煙     -> 找不到 assets/turnarounds/靛煙.png

會重生: 20 角色 | 會 skip: 0 角色

20/20 重生,0 skip。

不是部分失效,也不是偶爾漏掉。這個機制是完全死的,而它看起來還活著。

為什麼這種 bug 特別貴

我背後那個錶一直在跑,而我手上這疊單子每一張都印著勾

因為它沒有任何症狀。

跑下去會發生什麼:

gen  01-焰刃 ...
saved assets/turnarounds/01-焰刃.png
gen  02-霜瞳 ...
saved assets/turnarounds/02-霜瞳.png
...
ALL DONE

每一行都是成功訊息。沒有 error、沒有 warning、exit code 0。

腳本第 4 行有 set -e,那是好習慣,但 set -e 只在指令失敗時中止。這裡沒有任何指令失敗。每一次 API 呼叫都成功,每一個檔案都成功寫出。

它是成功地做了不該做的事。

而且更麻煩的是:它產出的檔案是新的中文檔名,跟現有的英文檔名並存。所以跑完之後目錄裡會有 40 個檔案,兩套命名各 20 個,你甚至不會馬上發現多出來一組。

唯一的症狀是帳單,而帳單是月結的。

這是最貴的一類 bug

我把這類 bug 的特徵整理成三條,它們同時成立的時候最危險:

  1. 會花錢或造成外部副作用(API 呼叫、寄信、寫入外部系統)
  2. 失敗路徑看起來跟成功路徑一樣
  3. 回饋延遲很長(帳單月結、額度用完才發現)

一般的 bug 你跑一次就知道。這種 bug 你跑一次會覺得很順利。

根因:檢查的預期跟實際產物不同源

拆到最底層,這個 bug 的結構是這樣:

來源
skip 檢查預期的產物路徑 從 prompt 檔名推導(中文)
實際的產物路徑 人工改名決定(英文)

同一個事實(產物叫什麼名字)有兩個來源,而沒有任何機制讓它們保持一致。

寫到這裡我發現,這正是這個專案反覆出現的同一種問題:

出現在 兩個來源 現況
Day 9 · 連載狀態表 手寫的狀態表 vs 實際章回檔案 已爛,落後 180 回
Day 12 · prompt 索引 .md vs prompts.json / .yaml 目前同步,無機制保證
Day 15 · 封面規格 日誌寫的「定稿」vs 實際檔案尺寸 已爛,日誌寫 2528×1696,實際 38/40 是 1536×1024
Day 16 · skip 檢查 推導的路徑 vs 實際檔名 已爛,100% 失效

四次。四種形式。同一個根因。

只要同一件事實有兩個獨立維護的來源,它就會不同步,差別只在什麼時候,以及你會不會發現。

前三個的代價是資訊錯誤,第四個的代價是錢。

修法(我還沒選)

三個選項,記在 開發足跡.md 裡:

選項一:在腳本裡建映射表,中文 prompt 檔名 → 英文成品檔名,out 改查表。
→ 缺點:又多一個要手動同步的來源。這是用同一個病治病。

選項二:prompt 檔改名為英文,跟成品檔名同源。
→ 一次性解決比對問題。要同步改 index.md 和任何引用處。
→ 這個才是真的消除雙來源。

選項三:加 --dry-run 旗標,預設只印「會生成哪些」,確認無誤才加 --run 真的跑。
→ 不解決根因,但它讓根因再也不會偷偷花錢。

我現在的想法是二 + 三:二消除雙來源,三防止下一個我還沒想到的類似錯誤。

因為選項三保護的不只是這個 bug。任何會花錢的批次腳本,預設就不該直接執行。

代價:知道跟修好之間差了 16 天

我在通道口貼了一條紙帶就走開了,真正的插銷還躺在地上

這才是這篇真正的重點。

開發足跡.md 的 2026-07-23 條目,最後一行寫著:

⚠️ 重跑陷阱(仍在):腳本的「已存在即 skip」比對的是中文檔名,成品全已改名,直接重跑會把所有角色全部重生(燒 API 費)。

我當天就知道了。我甚至寫下了修法方向。

然後我沒有修。今天是 08-06,16 天後,它一模一樣。

為什麼?因為那 20 張圖已經生好了,我沒有再跑那支腳本的需求。沒有痛感,就沒有動力。

而這正是它危險的地方。等到我真的需要補生一個角色的那一天,我很可能已經忘記這件事,然後直接 bash gen_turnarounds.sh

我做的唯一有效防護,是在日誌裡加了一行:

未修前禁止事項:不得直接執行 bash gen_turnarounds.sh,包含為了取得成本數字而試跑。

一行禁令,代替一個修好的機制。這是很弱的防護,因為它依賴我記得去讀日誌。

正確的做法是選項三,把禁令變成程式碼。寫在文件裡的規則會被忘記,寫在程式裡的不會。

帶走什麼

一、冪等性檢查的「預期」必須跟實際產物同源。

檢查一支腳本的 skip / cache / 去重邏輯時,問這一句:

我拿來比對的那個東西,跟實際產出的那個東西,是不是同一個地方決定的?

不是的話,它們之間有一條沒人看守的縫。今天對,是因為還沒有人動過任何一邊。

二、會花錢的腳本,預設不該執行。

任何一支會呼叫付費 API、寄信、寫入外部系統的批次腳本,都該長成這樣:

預設          印出「會做哪些事」,不做
加 --run      才真的執行

成本是十行程式。它保護的不是這個 bug,是所有你還沒想到的同類 bug。

我這次是為了寫文章才做 dry-run,然後才發現失效。如果 dry-run 是內建的預設行為,我在 16 天前就會看到那 20 行 WOULD REGENERATE

三、「不會報錯」是危險訊號。

我們習慣把「沒有 error」當成沒事。但有一整類 bug 的定義就是成功地做了不該做的事:

  • 重複扣款(每次交易都成功)
  • 快取永遠 miss(每次查詢都正確返回)
  • 重試邏輯把單次操作變成 N 次(每次都成功)
  • 這個 skip 失效(每次生成都成功)

判斷方式:這支程式如果做了兩倍的工作,我看得出來嗎? 看不出來的地方,就要主動加一個計數或摘要輸出。

一行 echo "generated: $count / skipped: $skipped" 就能讓這個 bug 在第一次跑的時候現形。

四、把禁令寫進程式,不要寫進文件。

我在日誌裡寫「未修前不得執行這支腳本」。這是誠實的,但它是最弱的一種防護,它保護不了忘記讀日誌的未來的我。

同樣一件事,寫成 --run 旗標就是一個不可能忘記的守門員。

判斷句:這條規則如果被忘記,會發生什麼?後果嚴重的話,它就不該只是一行文件。


模組三的圖產線到這裡結束。七天講的是:產線怎麼分工(Day 10)、一致性怎麼鎖(Day 11)、prompt 怎麼變成資料(Day 12)、被擋住怎麼繞(Day 13)、圖怎麼傳(Day 14)、規格怎麼統一(Day 15)、以及它哪裡會靜靜地燒錢(Day 16)。

明天 Day 17 講成本結算:這一整條產線,實際花了多少。


上一篇
Day 15|先放大再縮小的解析度補齊鏈,以及它其實沒解到問題
下一篇
Day 17|我昨天答應給成本數字,今天給不出來
系列文
《矽墟》:我把一部科幻小說當成軟體專案來管17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言