第 3 層:工具層強制。
前面三層都是 advisory——AI 願意配合才有效。
這一層不一樣。這一層是 AI 物理上做不到違反。
而那個失敗的案子,這一層完全沒有設置。
講一下那是什麼案子:一個四個月的技術升級案,舊系統是 Web Forms,最後客戶要求重做。
它在「規定」這件事上其實很用力。12 月 26 日,「DB Schema 不可動」第一次被寫成方框,放在一份專門的關鍵規則文件裡。主規範文件也用方框重申了同一件事。粗體、驚嘆號、「絕對」「必須」「嚴禁」,該用的都用了。
然後測試問題單裡出現這一行:
AI 創建了新的資料表
兩份文件,擋不住一個 CreateTable。
因為那些字全部停在第 0 層——它們是講給 AI 聽的,不是擋在 AI 前面的。
先看兩種寫法,做的是同一件事。
第 0 層的寫法:
## 🔴 絕對禁止事項
⚠️【極其重要】**嚴禁**異動資料庫 Schema!
**這不是建議,是強制要求!!!**
任何 Migration 指令都必須先經過審核!
第 3 層的寫法:
#!/bin/bash
# PreToolUse hook:輸入是 stdin 的 JSON,不是環境變數
input=$(cat)
# 看「要寫進去的內容」,不是看磁碟上的檔案
body=$(jq -r '[.tool_input.content, .tool_input.new_string] | map(select(.!=null)) | .[]' <<<"$input")
grep -qE 'migrationBuilder\.(AddColumn|DropColumn|CreateTable)' <<<"$body" \
&& { echo "❌ 偵測到 Migration 結構異動指令" >&2; exit 2; }
exit 0
這裡有一個很容易寫錯的地方,我第一版就寫錯了: 我原本抓 .tool_input.file_path 然後去 grep 那個檔案。但 PreToolUse 是在寫入之前觸發的。新建一支 migration 的時候那個檔還不存在,grep 回錯誤、&& 短路、exit 0 放行。
它只擋得住「編輯一支本來就已經違規的既有檔案」,而那不是主要情境。 要看的是即將被寫進去的內容,不是磁碟上的東西。
(另外這條 pattern 只涵蓋三個動詞,它不完備。RenameColumn、AlterColumn、Sql() 都沒擋到,換行寫法也繞得過。完備的清單要你自己補,這裡只示範「第 3 層長什麼樣」。)
第二種沒有任何形容詞。沒有「極其重要」、沒有驚嘆號、沒有「嚴禁」。
但它是唯一真的擋得住的那個。
而且它有一個第一種永遠達不到的性質:腳本本身不佔 context。那五行 shell 不會被讀進 AI 的注意力,它在外面等著。(但這句話要打一個折,明天會講。它擋下來的時候,錯誤訊息還是會回餵給模型。)
實務上可用的手段,按介入時機排:
| 手段 | 介入時機 | 擋什麼 |
|---|---|---|
| PreToolUse hook | AI 寫檔之前 | 直接 return error,檔案根本不會被寫出去 |
| PostToolUse hook | AI 寫完檔案之後 | 自動跑檢查,違規立刻回報 |
| 自訂 Linter 規則 | 編輯/編譯時 | 禁止特定 API、特定寫法 |
| Git hook(pre-commit) | 提交之前 | 擋住不該進版控的東西 |
| 唯讀連線 | 執行時 | 物理層封鎖——AI 想改資料庫,連線層直接拒絕 |
| CI gate | PR 合併之前 | 不一致就 block |
最上面那個 PreToolUse 是最徹底的。它的差別在於「錯誤根本沒有機會存在」。不是寫出來再抓,是寫不出來。最下面那個唯讀連線我特別喜歡,因為它不需要任何規則、任何檢查、任何提示詞。開發環境連到唯讀副本,「不可以改資料庫」這條規則就從一句話變成了一個物理事實。
能用物理封鎖的,就不要用規則。

講理論比較虛,我拿三個不同案子的實際做法來看。
例一:結構檢查掛在寫檔之後一個進行中的專案,設定檔裡是這樣:
每次 Write / Edit 之後 → 自動跑結構檢查腳本
每次工作結束時 → 自動跑文件一致性檢查
結構檢查很便宜(毫秒級),所以每次寫檔都跑。文件一致性稍貴,結束時跑一次。關鍵在於:這件事不是 AI 決定要不要做的。 它是外部執行的,不在 AI 的自主範圍內。例二:把「貼警語」升級為「機器擋」
Day 12 提過的那條迭代紀錄。原本的做法是在文件旁邊貼一段警語(第 0 層),後來把檢查腳本的掃描範圍擴及文件內的 SQL,違反就擋。
同一條規則,從第 0 層搬到第 3 層。
例三:擋數字漂移——而且它處理的不是程式碼一個文件產出的工作區:建議書、簡報、報價。一組承重數字散在十幾份文件裡,改一個地方沒同步就是自相矛盾。做法是建一份數字母體當單一真相,配一支檢查腳本掛在 hook 上,每次改檔就跑。而最關鍵的設計是這個參數:
--quiet 無漂移時靜默,有漂移才中斷出聲
平常完全不吵,出事才叫。 如果每次改檔都印一堆綠色的 PASS,三天之後就沒有人會看了。那份說明文件開頭那句話,我認為是整個系列可以直接搬走的一句:
文件「數字漂移」= runtime bug,必須自動擋。
(後面會單獨講那支腳本實際檢查什麼。包括我後來回頭去查,發現它有幾項根本沒在跑。)
回到那個失敗的案子。這一層完全沒有設置,為什麼?
我認為有三個原因,而且每個都很常見。
一、它不像「開發工作」。 寫一支 hook 腳本不會產出功能,在進度報表上是空白的。趕進度的時候第一個被砍。
二、它需要先知道要擋什麼。 而「要擋什麼」通常要踩過才知道。所以第 3 層天然是滯後的。這也是為什麼 Day 11 講的「95% 事後補鍋」不完全是壞事,補鍋補在第 3 層是對的,補在第 0 層才是白費。
三、多數人不知道有這些工具。 hook、MCP、CI gate 這幾種掛載點。很多團隊的認知還停在「寫 prompt」。
我現在檢視任何一條 advisory 規則,都會問同一句話:
這一條,能不能用工具強制?
能的就升級。不能的,再問下一句:「那它能不能被自動檢測?」——那是第 4 層。
兩個都不行的,才留給人。
而實務上你會發現:**大部分你以為只能靠自覺的規則,其實都能用三行 grep 擋掉。**命名規範、目錄結構、禁用 API、引用路徑存在性、版本一致性、必填欄位。這些全部是確定性問題,全部可以被機器判斷。那 591 行的命名規範裡,我猜有八成可以變成一條 linter 規則。
理論講完了。明天全部是可以複製的東西。三個階梯,由零風險到真的會擋,包含我自己第一支 hook 裝完之後什麼都沒發生的那個坑。