iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
AI Engineering

AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄系列 第 16

Day 16|Skill 的疤痕生命週期:規則什麼時候該拿掉?

  • 分享至 

  • xImage
  •  

簡短回顧

前兩天我們拿外部的 Benchmark 量了自己一次,跑完之後最實際的收穫不是名次,是抓到自己的算法一直在替自己加分。

到今天為止,這個系列一直在做同一件事:發現問題,加一條規則。今天想講反方向的事。

一個我一直在想的問題

我的認知是,隨著模型演化,有些觀點跟知識它應該會內化,不用特別跟它講它就知道了。而如果那個東西它其實已經會了,你還特別去講,有可能反而會帶壞它。

這不是我憑空想的。OpenAI 在 o 系列模型出來之後,官方的 Reasoning best practices 就寫得很直接:

譯文:有些 prompt engineering 的技巧,像是要求模型 "think step by step",不一定能提升表現,有時候反而會有反效果。

原文:Some prompt engineering techniques, like instructing the model to "think step by step," may not enhance performance (and can sometimes hinder it).

來源:Reasoning best practices | OpenAI

「think step by step」曾是早期常見的 prompt 建議;到了推理模型上,它可能沒有幫助,甚至造成反效果。

要講清楚的是,這段引文講的是 prompt 技巧,不是領域規則,那份文件也沒有說「不要給模型規則」。但我覺得這兩件事是同一個母題:為舊模型設計的引導,在新模型上可能會從幫助變成干擾。差別只在於 OpenAI 有能力發公告告訴大家,我只能自己想辦法量出來。

所以我需要一個 way out。規則要有辦法進來,也要有辦法出去。

疤痕與癒痕

先講名字。這兩個詞其實是 LLM 給我的,不是我自己想的。如果是我命名,大概會叫 GC 或 rule deprecation 那類很工程師的東西 😅。

不過現在回頭看,這個比喻有它的情境在。受過傷,癒合之後會留下痕跡,那個痕跡就是我們發展、演化過程的一部分,是歷史的標記。有點像人會把自己經歷過的人事物用刺青記在身上。

我把每一條「踩過坑才補上」的規則叫做疤痕。前面幾天講到的很多都是:入口枚舉閘、反蒙蔽協議、dict.get 防不了 None,每一條旁邊都釘著一個具體的事故。

先給位置:這些條文的本體都收在公開 repo 的 skills/nathan-code-review/references/ 裡,九面向的判準(含入口枚舉與 dict.get 那條)在 review-dimensions.md,反蒙蔽協議在 re-review.md。至於今天要講的底帳與癒痕,是我維護 skill 用的內部紀錄,不進 runtime 的 skill,也不在公開 repo 裡;後面提到的兩條癒痕,它們退役後的「現在式」倒是公開可驗:同一個 skill 底下的 scripts/render_report.py 就是取代 slim 流程的確定性 render,scripts/report_model.py 裡掃描命中與人工發現共用同一條編號序列。

出生證明:加規則的時候先回答三個問題

我的做法是,每次要新增一條這種失敗錨定的規則,在 commit 之前先回答三句話,寫進規則旁註或 commit message 裡。

一、這是覆蓋缺口,還是執行缺口?

上次會漏抓,是因為「沒有這條規則」,還是「規則早就在,那一輪沒有執行」?

如果是後者,加新規則會讓事情更糟。你是在一個注意力的問題上面繼續堆條文,而新的條文一樣會被跳過。這時候該做的是去修執行機制。

這條不是推論,我真的遇過。今年 7/30 我補了一個叫 rejected_leads 的欄位,起因是流程裡早就規定「要逐筆檢查前次撤回的 finding,看理由是不是還成立」,但每次審查都查不到東西,因為根本沒有人把撤回的候選記下來。消費端的規則早就在,缺的是生產端沒有產生資料。

照直覺辦事的話,這裡會再加一條「請記得記錄撤回」的規則,然後它會跟前面那條一樣被忽略。所以我沒有加規則,改成在報告的 schema 裡補一個結構化欄位,讓資料自己被生出來。

二、跟既有規則的優先序

這條新規則如果可能跟某條既有規則在同一個情境同時觸發,誰優先?答案要當下就寫死在條文裡,不要留給未來的 AI 自由心證。

我吃過這個虧。曾經一邊寫著「要實證,鼓勵實際跑跑看」,另一邊寫著「不准執行被審查的程式碼」,兩條都是硬規則,結果每個 session 的表現都不一樣,最後被迫把「程式碼執行邊界」抽成一份獨立的權威定義。

三、可證偽的退役條件

這條規則買到了什麼?什麼情況下就可以拿掉?寫法要具體到能被驗證。

舉個實際的例子。我有一條規則是「要偵測醫療情境,並且在醫療系統裡把邏輯錯誤的嚴重度升級」,它的退役條件我是這樣寫的:如果模型能自己讀 repo 的 namespace 跟 README,在五個以上命名中性的醫療 diff 裡都主動升級嚴重度,這條就達到進一步討論退役的實證門檻;最後仍要由我確認,才能真的拿掉。

這三個問題只在寫規則的當下問一次,頻率很低,不會進到每一次審查的熱路徑。

那要怎麼判斷可以拿掉?

做法是對一條規則造一個跟它錨點相似的情境,推斷夠強的模型在沒有這條規則的情況下會不會自己抓到。會抓到才有資格成為退役候選;抓不到只代表目前不能退,還要再分辨原因。

有三個陷阱要避開:情境要真的踩到那個失敗模式,不然測了等於沒測;規則看起來沒有帶來額外增益時,要分辨是模型已經內化了(可以討論退役)、這條規則本來就沒在運作(該重寫或刪掉),還是它擋的其實是回歸跟降級(要留,只是威脅換了一個軸);最後,單一情境的推理式判斷是弱證據,只能判候選,不能直接執行。

我跑了第一輪,結果是零

六月的時候,我派了五個平行的 subagent,對當時的 16 條疤逐條跑了一次。結果是 15 條承重、1 條被判為退役候選。

而那唯一的候選,最後也沒有拿掉。

那條規則是「測試要驗行為,不是驗存在」的淺層版本,最典型的反例就是把 mock 設成回傳 X,然後 assert 結果等於 X。AI 的判斷是這個反模式現在已經被廣泛教學,Opus 等級的模型不用你講也會抓,可以退役。

看到的當下,我的第一個反應是怕怕的。

所以我沒有照做,先要它去補資料。補完之後發現三件事。第一,單一情境的推理式判斷本來就是弱證據。第二,這條規則跟同一格裡另一條承重的 Critical 規則糾纏在一起,那條講的是 mock 被設成生產環境不可能出現的值、用來觸發一個其實走不到的死分支,兩條住在同一個位置,拔前面那條很可能誤傷後面那條。第三,這條規則綁了不只那個已經內化的子命題,它還帶著 fixture 密集時的警覺、深度斷言的指引跟測試命名慣例,要退役就得從一條綁包的規則裡動手術切出一個子命題,很細、很容易切壞,換到的只是一點 token。

證據力不夠,所以留著。我在底帳(每條疤的登記簿:生於哪個事故、退役條件是什麼、動過什麼)上寫了一句提醒自己:Do not retire on itch,不要因為手癢就拔。同時記下重啟的條件,要三個都成立才准再議。

這件事後來我覺得比制度本身還重要。制度的第一個考驗不是它有沒有用,是它給出建議的時候,你會不會照單全收。

癒痕:拿掉之後要留什麼

第一次真正退役的是「slim 報告的產生跟檢查必須循序,不能平行派遣」這條規則,因為檢查的輸入就是產生的輸出。後來這整段 LLM 流程被換成確定性的 render,機制不存在了,威脅也就消失了。

要注意它的退役理由是「被守護的機制消滅了」,不是「模型學會了」。這兩種要分清楚,不然你會誤以為模型變強了。

我沒有直接刪掉它,而是留了一行癒痕,記下原本的規則、生於哪個事故、什麼時候退役、為什麼。因為「驗證者消費產生者的輸出,這種派遣有相依性、不能平行」這個教訓,對未來任何同樣形狀的流程都還成立。

這個做法其實跟 ADR 是同一個直覺。我們專案裡被取代的 ADR 也不會刪掉,它會留在原地標記著被誰取代,因為當初為什麼那樣決定、後來為什麼改掉,都是這個系統的一部分。差別只在於 ADR 記的是為什麼採用,癒痕記的是為什麼不再需要。

判準只有一句:刪掉會不會弄丟記憶?會就留癒痕,不會就直接刪。

後來底帳又多了一條癒痕。原本掃描器的發現另有一份清單,只有判為 Critical 時才會鏡射進主要發現;現在所有已確認的掃描命中與人工發現都共用同一條編號序列,舊的鏡射規則也就被取代了。它同樣不是因為模型變強而退役,而是原本需要防守的雙清單設計已經不存在。

那模型愈來愈強,skill 會不會消失?

這題我到現在還在拉扯。

一方面我覺得模型會愈來愈往「一大包全部內化」走,尤其是 coding 這個領域。另一方面通才模型在每個細分領域又總是缺一點,而這也正是 skill 這種東西會被發展出來的原因。我不確定我是在往那個目標的路上,還是我需要的東西本來就不會消失。

我後來是這樣分的。規則其實有兩種。

一種是能力型的,像 IDOR 要檢查、SQL 要參數化、mock 不要自己驗自己。這類規則有可能隨模型能力演化而逐漸失去必要性。

另一種是事實型的。Day 6Day 7 那份清單裡掛 ★ 的那些條全部是這一種:像我們那把唯讀的 package token 是評估過後接受的風險、批次處理的原子性在我們這裡要判 Critical 因為牽涉病人安全、URL 一律用 dash 是團隊的硬慣例。這種沒有內化的路徑,因為它不是知識,是設定值。如果這些決策沒有被寫進模型能取得的脈絡,模型再強也無從知道我們接受哪一個風險。

當然這不是一刀切,中間有很多灰色地帶。前面那條醫療情境的退役條件就是灰色的例子,如果哪天模型真的能從 repo 的脈絡自己推出「這是醫療系統」,那事實型的東西也開始有內化的路徑了。

所以我現在押的是:skill 不會消失,但可能變薄,成分也會改變。教學型的條文一條一條退役,留下來的是組織事實、我們願意接受哪些風險、以及那些說好的例外。

回頭看那個 15 比 1 的結果,我不再把它解讀成退役機制沒用。它真正告訴我的是:在當時的模型與檢驗方式下,大部分規則仍然承重。有些記錄的是組織事實,有些保護的是執行流程與曾經發生過的失敗模式;不能因為模型已經知道某個通則,就推定整條規則都可以拿掉。

代價是誰在付

前面都是我自己對自己的盤點。這一段是外面來的。

系列寫到一半的時候,一位同仁跟我回饋了這些:新版的審查抓出來的東西變多了,有些檔案只動了一點點,AI 卻會往外延伸,順帶要求處理旁邊的事,而那些要求看起來也不算講錯。幾輪下來累積的項目越來越多,很難判斷做到哪裡才算做完,本來只是要合進去的一支分支就這樣一直合不進去。被點到的地方有些是舊有的實作,當初為什麼那樣寫已經查不到了,一條一條去確認太花時間,於是最後的做法變成照著 AI 的建議改,再去驗證執行的成果正確性。

我覺得他講的很有道理,若換作我自己也一定會有這樣的想法與感受。

不過,這邊確實有兩件事是我當時設(ㄗㄠˋ)計(ㄔㄥˊ)的。

第一件是量。我盤點完十六條,一條都不能拔,但「不能拔」不等於「沒有代價」。代價一直都在,只是付的人不是我。我跑審查的時候看到的是一份報告,作者面對的是一份工作清單。而且我回頭翻自己的一份長報告才發現,那個量有一部分是假的:同一筆掃描命中被列了兩次,嚴重度的措辭還不一樣。

第二件是 Day 4 的反蒙蔽協議。它要求每一輪都當作沒審過、重新盲審,換來的是不被上一輪定錨;另一面是同一段沒有動過的程式碼,這一輪可能長出上一輪沒講過的問題,而那一條一樣會被算進結論、一樣擋著合併。我買到了獨立性,付錢的是作者。

我最後把判準收斂成這幾件事:

  • 嚴重度直接對應合併結論。 Critical 阻擋合併;只有 Suggestion 時可以合併,但要把改善建議說清楚;只有 Nit 時不阻擋。不要再加一套「必改/下個 MR/不用改」跟嚴重度互相打架。
  • 先驗證線索,再談嚴重度。 可達性與實際傷害用來判斷一條線索是不是真的、嚴重到哪裡。追完仍無法驗證的項目,不先塞一個 Critical,而是放進 open_questions,同時寫清楚還缺哪一份證據。
  • 確認成立後,再判斷是否屬於這次改動。 問題如果就在 diff 裡,或是這次改動讓 diff 外的舊問題變得可達、變得更危險,就算本 MR 的發現。否則把它彙整成既有債揭露,不算到作者頭上,也不讓它假裝不存在。
  • 每一輪仍可補上自己的疏漏。 這一輪才看到的問題,如果屬於本 MR,前幾輪沒抓到不構成消音的理由;如果是既有債,就回到上一條處理。
  • 一條發現只出現一次。 掃描器命中與人工發現共用同一條編號序列,不再各自維護一份清單。

這幾條合起來,是把嚴重度、證據與歸屬拆開。掃到舊程式裡一組寫死的密碼時,我不會只憑作者一句「早就換掉了」就結案,也不會因為查不到就直接判這次 MR Critical。審查端要說清楚已經查過什麼、還缺什麼證據;若它與這次改動無關,就列為既有債揭露。這樣既不把舊帳全算在作者身上,也不讓真正還活著的秘密躲進「這本來就有」。

不列為本 MR 的發現,不等於它會自動有人處理。報告裡寫一句「下個 MR」並不會真的生出一張 issue;沒有人追的話,那不叫延後,叫消失。這一格我目前只有紀律,沒有機制。

另外兩件也一樣。

一是這套機制本身。出生證明問的是這條規則買到什麼,退役審查問的是拿掉會不會漏抓,兩個問題都只從審查者這邊算帳。底帳上沒有一格記著:它每次觸發,寫 code 的人要動到什麼範圍。

二是工單。我有一條硬規則寫著「只點問題不給出路的 finding 不合格」,每一條發現都必須附上修法;另一條紅旗寫著不准因為「問題太多」就只挑重要的講,因為決定什麼不重要是審查者在替作者做主。兩條分開看都對,疊在一起就是一份工單。當一份報告每一條都附了修法,它讀起來就不是「發現」,是「工單」。 至少對這位同仁而言,也更容易讓人進入照做模式,而不是先判斷這項修改是否合理。

走到這裡,我怎麼理解 AI Engineering

前面這些代價也讓我看清楚:治理不只是在 AI 身上加規則。規則如何出生、怎麼驗證、什麼時候退場,以及它把多少工作推回人身上,都是同一個工程問題。

第三天我把這件事叫做「萃取一個我」。當時做的,是把我審查程式碼時會問的問題、判斷嚴重度的方式,以及團隊才知道的設定值寫進 skill。走到第十三天,我開始替它做行為回歸測試,卻在全綠之後先懷疑那盞綠燈;第十四十五天拿外部 benchmark 量,最後又發現自己設計的算法一直替自己加分。今天再往回走一步,問那些規則是否仍然承重。

我在這裡工程化的,不只是 agent 的輸出,也包括我加在它身上的要求:skill 把要求寫成規格,benchmark 量它是否有效,退役審查則判斷它是否仍有存在的理由。

這也是這個系列選擇的切面:一個會讀取程式碼、呼叫工具、作出判斷,並產出會影響合併決策的 AI agent。接下來畫面會換成容器、網路與代理,但治理只是從「它怎麼判斷」,延伸到「它住在哪裡、拿得到什麼,以及做錯之後會撞上哪一道牆」。

你今天可以做的一件事

如果你手上已經有一份 skill,今天不需要做任何盤點。你只要在下一次加規則的時候,多寫一行退役條件就好。

如果願意再多做一步,也記下它每次觸發時,作者通常要改多大的範圍。這不是拿掉規則的理由,但會讓原本只算漏抓風險的底帳,也看得見規則推回人身上的成本。

之後有空再把現有的規則逐條問一句:這條是在教它一個通則,還是在告訴它一個只有我們家才知道的事實?兩種都要寫退役條件,只是判準不同。前者看模型是不是已經能穩定做到;後者看組織的政策、系統環境或風險決策是否已經改變。模型不會自行內化我們家的設定值,但設定值本身也不是永遠不變。

本日小結

規則只進不出,skill 會愈來愈肥,而且你會誤以為它愈來愈強。

出生證明是加規則時的成本,只問三句話;退役審查是拿掉規則時的門檻,而且門檻要高,因為拔錯一條承重牆,可能會讓同一類失敗重新有路可走。底帳也不能只記審查者避免了什麼,還要看作者為這條規則付出多少工作。

我第一輪退役盤點跑完,一條都沒退。這個零不算失敗,它是這套制度第一次給我的答案。沒有退役條件的時候,會連「該不該拔」都無從問起,只會一直加。

從明天開始換場景,我們來講讓這一切跑起來的環境:dev-container

先說這條線會走到哪裡:接下來幾天要蓋的東西,最後會收在一道我親手立起來、卻被關在裡面的 agent 反過來逼著我重畫邊界的防火牆上。


上一篇
Day 15|Skill 的評測(下):盲測放榜,等等!標準答案好像不對勁 🤔
系列文
AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言