模組二|選題與素材庫(Day 6–11)
模組二的最後一天,講一個我不太想寫的東西。
素材庫有一個前端儀表板,可以瀏覽條目、依日期跟情緒標籤篩選。上面有一顆按鈕,叫「生成腳本」:選一則條目,按下去,它應該產出一段口播稿。
它是壞的。而我知道它壞了很久,沒有修。
按下去之後,前端會打一個 /api/proxy 端點,帶著條目內容去呼叫一個 flash 級的語言模型。
而 /api/proxy 沒有人在接。
錯誤訊息裡有提示,說要先跑一支本地伺服器腳本。我去找那支腳本:
它不存在於 repo,也不在 git 歷史裡。
不是被刪掉(被刪掉的話 git log 找得到),是從來沒有被 commit 過。它可能曾經存在於我的某台機器上,也可能從頭到尾就沒寫完。
所以完整的狀態是:前端寫好了、按鈕在那裡、錯誤處理也寫了,而後端從來沒有進過版控。

這支前端檔案裡還有一組東西:一個明文的 API 金鑰,寫在 React 的 useState 預設值裡。
// 大概是這個形狀(金鑰內容不貼)
const [apiKey, setApiKey] = useState('<39 個字元的金鑰>');
這支檔案被 git 追蹤,前後 commit 過 11 次。我用 git log -S 查過:金鑰在 git 歷史裡。
意思是:現在把它從檔案裡刪掉,也還在。 要真正清除,得輪替金鑰,加上改寫 git 歷史。前者是必須做的,後者對一個有其他協作痕跡的 repo 來說很麻煩。
而這兩件事(功能不能用、金鑰外洩)是同一個決定造成的:
把語言模型的呼叫放在前端。
放在前端,就需要金鑰在瀏覽器拿得到,所以它被寫進原始碼。
放在前端,就需要一個 proxy 繞過 CORS 跟隱藏金鑰,所以有了 /api/proxy。
而那個 proxy 沒寫完,所以功能是壞的。
一個架構決定,同時製造了一個不能用的功能跟一個不可回收的洩漏。
老實的答案:因為它想解決的問題,已經被別的東西解決了。
口播稿實際上是 CLI 的一個子命令產出的:export-voice-scripts。它跑在本機、金鑰在環境變數裡、輸出是 markdown 檔案。它能用,而且我一直在用。
而它贏的地方不只是「能跑」。它產出的稿子是五段固定結構,每一段還有硬編的字元上限:
開場 ≤ 95 字元 (約 10–15 秒)
重點一 ≤ 115 字元 (約 20–30 秒)
重點二 ≤ 115 字元 (約 20–30 秒)
查核段 ≤ 110 字元 (約 20–30 秒)
收尾 ≤ 100 字元 (約 10–15 秒)
用字元數控時長。 這是一個很土的近似(中文口播速度大概每秒四到五個字,所以字元數乘一個係數就是秒數),但它有一個決定性的好處:它在產出的當下就把「這段會唸多久」變成可檢查的。
那顆前端按鈕做不到這件事。它產出的是一段自由文字,長度不受控,唸下去可能三十秒也可能三分鐘。而 rundown 上每一段是有時間預算的(Day 15 會講),一段超時,整集就要重排。
所以它不是「壞掉所以被取代」,是「即使能跑也不夠用」。 這讓修它變得更沒有動機,修好了也還是要重寫產出邏輯。
所以那顆按鈕從「主要路徑」變成了「一個沒人走的路徑」,然後就被留在那裡。
這個過程沒有任何一刻是「決定不修」。它是逐漸變得不重要,然後被遺忘。
我原本以為「壞掉但沒人用」等於沒事。列出來之後不是:
一、它會誤導人。 任何打開這個前端的人(包括未來的我)會看到一顆功能按鈕,然後花時間搞清楚為什麼它不動。
二、金鑰還在,而且會一直在。 這個成本跟按鈕壞不壞完全無關,它是獨立存在的、而且不會隨時間衰減。
三、它讓「這個前端能做什麼」變得不可信。 當一個介面上有一顆假的按鈕,其他按鈕的可信度也會下降,我下次看到別的功能,會先懷疑它是不是也是壞的。
壞掉的功能不會安靜地待著。它會消耗每一個看到它的人的注意力,而且它帶著的東西不會自己消失。

Day 3 那五道閘門的第一道是「不得自動 commit」。
而這次是我自己 commit 的。
金鑰不是 AI 推上去的,是我寫完那支前端、覺得先能跑再說、然後推上去的。閘門一條都沒有違反,因為閘門管的是 AI,不管我。
這件事讓我看清楚那五道閘門的一個假設:它們預設「風險來自 AI,人是那道防線」。
而人也會累、也會趕、也會覺得「先能跑再說」。當防線本身就是風險來源時,這個設計沒有備援。
真正該擋這件事的東西不是閘門,是一支會掃金鑰特徵的 pre-commit hook。那東西不管是誰在 commit,它都擋。
閘門分辨的是「誰在做」,而好的檢查不該關心誰在做。
這一篇的代價是我到現在還沒輪替那組金鑰。
寫這篇的時候我又確認了一次,它還在歷史裡。這不是不知道要做,是知道而沒做,跟這個系列裡其他每一件「知道但沒做」的事一樣。
而它跟其他幾件不同的地方在於:其他的是延後的收益,這一件是持續的風險。 模板 8% 合規不會有人受害,一組活著的金鑰在公開歷史裡會。
(這個 repo 是 private,所以嚴重性打了折。但「private repo 所以沒關係」是一個很脆的理由:它假設它永遠不會變 public、永遠不會被誤加協作者。)
第二個代價:我沒有把那顆按鈕拿掉。 拿掉它是五分鐘的事,而我留著它到寫這一篇的今天。
一個架構決定會同時製造多個後果,而你通常只看到第一個。
「把 LLM 呼叫放前端」的第一個後果是「需要一個 proxy」:我看到了,所以我開始寫 proxy。第二個後果是「金鑰必須在前端可得」:我沒看到,或者看到了但覺得之後再處理。
決定的時候可以多問一句:這個決定會讓什麼東西「必須存在於它不該存在的地方」?
金鑰必須在瀏覽器拿得到、密碼必須寫在設定檔、憑證必須放進映像檔,這一類「必須」通常就是第二個後果的形狀。
第二條,關於壞掉的功能:
「壞掉但沒人用」不等於零成本。 它至少有三個持續成本:誤導後來的人、帶著的東西不會消失、拉低整個介面的可信度。
處理方式很簡單:刪掉它,或者在它旁邊寫清楚它是壞的。 兩個都是幾分鐘的事,而放著不管是唯一一個會持續累積成本的選項。
第三條,也是這一天最刺的一條:
如果你的安全機制是「靠人謹慎」,那它在人累的時候就不存在。 而那正好是最容易出事的時候。
模組二到這裡結束。明天進模組三:這條產線上唯一一個真正活下來的規則,是一支 64 行的 bash。