AI 輔助開發

為什麼應該由另一個模型來審查 AI 撰寫的程式碼

一個模型審查自己的輸出時,也會有其自身的盲點。將差異路由至其他供應商的獨立模型,並交叉檢查結果。

本文由 AI 模型從英文原文翻譯而來,用字可能與原文有所出入。 閱讀英文原文

當一個 AI 模型撰寫修補程式,並由同一個模型來審查時,對於重要的錯誤來說,這樣的審查幾乎毫無價值。產生程式碼的模型也產生了其背後的推理邏輯,因此它會將這套推理邏輯帶入審查中,並確認自己的工作成果。如果它幻覺出一個不存在的 API,它會將該呼叫解讀為正確的。如果它誤讀了規格,它會根據同樣的誤讀來評估程式碼。這就像是機器的版本:一個人校對自己寫的文章,然後略過了他們已經看過十次的錯字。

解決方法不是用一個更好的提示。而是尋求第二意見,這個意見來自一個沒有撰寫該程式碼的模型,理想情況下,這個模型來自不同的供應商,並用不同的資料訓練而成。將同一個差異比較傳送給兩個或三個獨立的模型,詢問每一個模型有哪些錯誤和風險,然後用一個固定的規則來結合它們的結論。在它們的盲點不重疊之處,交叉檢查就能捕捉到單一審查者會錯過的東西。這篇文章是關於如何執行這個流程、它的成本是多少,以及在什麼情況下,單一審查就已經足夠。

自我審查會共享作者的盲點

程式碼審查只有在審查者能看到作者看不到的東西時才有用。兩個人類之所以能合作,是因為他們有不同的心智模型、來自過去不同錯誤的不同傷疤,以及對程式碼應該做什麼有不同的假設。審查者會標示出作者從未想像過的情況。

一個模型審查自己的輸出時,完全沒有那種距離感。它對程式碼的「意見」是產生該程式碼的相同權重、相同訓練資料,且通常是相同上下文視窗的延續。在它寫完函式後馬上問它「這裡有錯誤嗎?」,你等於是在要求作者反對作者自己。它會找到表面問題,例如遺漏的 nil 檢查、明顯的錯字,但源於錯誤假設的細微邏輯錯誤往往會存留下來,因為審查繼承了那個假設。

根據經驗,失敗之處會集中在幾個地方。一個幻覺產生出來的方法或欄位會通過自我審查,因為模型仍然相信該方法存在。邊界條件中的差一錯誤會通過,因為模型在兩次處理中對邊界的概念都是同一個錯誤概念。一個安全漏洞,比如說遺漏的授權檢查,會通過,因為模型兩次都專注於正常路徑。這些正是日後捕捉成本高昂的缺陷,也正是自我審查最不擅長發現的缺陷。

不同模型在不同地方失敗

第二個模型之所以有幫助,是因為模型們並未共享同一組盲點。每一個模型都使用不同的資料組合進行訓練,以不同的目標進行調整,並在謹慎與信心之間的權衡中處於不同的位置。某個模型擅長發現並行性風險,但在 SQL 方面較弱。另一個模型能很好地讀取資料流,但會放過競態條件。它們的錯誤並不相同,而這正是重點所在。

這與讓一個集成模型勝過單一分類器的想法相同。如果三位審閱者各自錯過了 30% 的實際缺陷,但他們錯過的是不同的 30%,那麼三位都錯過同一個特定錯誤的機率遠低於 30%。獨立的錯誤會相互抵銷。關鍵在於「獨立」這個詞,而且這是個真正的關鍵。來自同一家族的兩個模型,或同一個模型提示兩次,並不會給你獨立的錯誤。它們會更有信心地給你兩次相同的錯誤。要獲得好處,你需要真正多樣化的來源:不同的供應商、不同的模型家族,而不是同一個模型的兩種溫度設定。

因此,設計目標不是「更多審閱」。而是「其錯誤不相關的審閱」。來自一個真正不同模型的單次審閱,比作者模型重新生成五次的價值更高。

工作流程,步驟詳解

此管線很小。一個模型負責撰寫,數個獨立模型負責評判,一條規則整合評判結果,並由人類來解決歧見。

步驟 執行者 輸出
1. 實作 作者模型 diff 或 patch
2. 平行審查 N 個獨立模型,皆非作者模型 各模型的裁決:GO 或 NO-GO,外加發現
3. 套用共識規則 自動化 Pass、fail 或 escalate
4. 解決歧見 人類 對於分歧裁決的最終決定

此工作流程的可行性基於兩個特性。審查者獨立運行,因此它們不會互相錨定。而且每個審查者都收到相同的中性提示,因此一個 NO-GO 在所有模型中都代表相同的意思。應將作者模型完全排除在審查池之外。它的投票是你已經擁有且最不信任的一票。

其偽代碼的樣貌如下:

def multi_model_review(diff, author_model, reviewers, rule):
    verdicts = []
    for model in reviewers:            # reviewers excludes author_model
        v = model.review(
            diff=diff,
            prompt="List concrete bugs, security risks, and correctness "
                   "issues in this change. Then answer GO or NO-GO.",
        )
        verdicts.append(v)

    passed = rule(verdicts)            # unanimous or majority, see below
    if passed is UNDECIDED:
        return escalate_to_human(diff, verdicts)
    return passed, verdicts

review 呼叫會回傳兩樣東西:一份具體發現的清單,以及一個 GO 或 NO-GO 的裁決。當審查失敗時,人類讀取的就是這些發現。而 GO 或 NO-GO 則是規則所使用的內容。

選擇符合風險的共識規則

將多個評判轉為單一決策的規則,就是您調整嚴格度的地方。有兩種合理的預設值,以及介於兩者之間的各種選項。

「全數同意 GO」是嚴格的規則:變更只有在每位審核者都說 GO 的情況下才會通過,而單一一個 NO-GO 就會阻擋它。這會最大化錯誤的召回率,您可以捕捉到任何模型能看見的任何問題,代價是更多的誤報,因為一個過度謹慎的審核者就會中止變更。將其用於難以復原的程式碼。

「多數同意 GO」是寬鬆的規則:如果大多數審核者說 GO,變更就會通過。來自一個容易「喊狼來了」的模型的單一 NO-GO 不會阻擋合併,但三個中有兩個同意有問題時就會。這犧牲了一點點的錯誤捕捉,以換取遠為更少的阻力。將其用於一般的變更,在這類變更中,遺漏的問題可以在下一個 commit 中輕易修復。

def unanimous(verdicts):
    if all(v.decision == "GO" for v in verdicts):
        return PASS
    if all(v.decision == "NO_GO" for v in verdicts):
        return FAIL
    return UNDECIDED          # split -> human decides

def majority(verdicts):
    gos = sum(v.decision == "GO" for v in verdicts)
    if gos > len(verdicts) / 2:
        return PASS
    return FAIL

請注意「全數同意」規則在出現分歧時的作法:它不會默默地選邊站,而是會將問題升級。有能力的獨立審核者之間的意見不合,是一個強烈的信號,表示該變更確實存在模糊地帶,而這正是值得花費人力時間處理的情況。將分歧視為「嗯,多數獲勝」會拋棄整個練習中最有價值的產出。

對抗性提示勝過「這樣看起來對嗎?」

你怎麼問,跟你問誰一樣重要。預設的審查提示「這樣看起來正確嗎?」,會引導模型表示同意,因為同意是簡單的完成方式。你得到的是確認,而非仔細的審查。兩種調整方式可以反制這種情況。

第一種是讓審查者反駁這段程式碼。不要說「審查這個」,而是要求「你的工作是找出這個變更為什麼是錯的。給我最有力的論證來證明它有錯誤。」將任務設定為反駁,可以減少趨向同意的拉力。現在,模型因找到瑕疵而獲得獎勵,而不是為變更背書,它會揭示在一般提示下會被忽略的疑慮。你不是要求它對瑕疵的判斷必須正確,而是要求它努力尋找,之後再由人類過濾掉誤報。

第二種是給予每個審查者不同的視角。與其進行三次一般性的審查,不如指派角色:一位審查者根據規格檢查正確性,一位檢查安全性與輸入處理,一位檢查效能與資源使用。同一個變更,三個角度。這能刻意擴大涵蓋範圍,並避免兩位審查者將所有精力花在同一個明顯的問題上,而導致第三個領域被忽略。

Reviewer A: "Find correctness bugs. Where does this violate the stated behavior?"
Reviewer B: "Find security holes. Assume the input is hostile."
Reviewer C: "Find performance and resource problems under load."

反駁與角色多樣性可以疊加使用。一個使用與作者不同模型的對抗性安全審查者,大概是單靠自動化所能達到、離自我確認最遠的狀態了。

成本為何,以及何時該跳過

這一切都不是免費的。每次變更,你都會額外執行模型呼叫二到三次,因此你會支付二到三倍的 token 費用,並且增加延遲,因為即使它們並行執行,最慢的審查者也會決定整體速度。對於小型或可逆的變更,這種額外開銷幾乎沒有任何價值。修復日誌訊息中的一個錯字不需要三堂會審。

其價值體現在那些重要且難以還原的變更上:

  • 就地重寫資料的資料庫遷移。
  • 部署和基礎設施程式碼,其中一個錯誤會導致生產環境中斷。
  • 授權或計費路徑,其中一個細微的錯誤會造成安全性或金錢上的問題。
  • 單向變更,任何一旦發布就無法乾淨地還原的東西。

對於這些情況,三次模型呼叫的成本與錯誤的成本相比微不足道,而嚴格的一致同意規則也使其誤報變得值得。對於一個在功能旗標後面的常規變更,你可以在幾秒鐘內關閉它,單一審查,甚至不審查,都是正確的選擇。讓流程的正式程度與其影響範圍相稱。

分層策略可以在每次都無需思考的情況下捕捉到這一點:

變更類型 審查者 規則
瑣碎或可逆 1 位(或作者自我檢查) 僅供參考
一般功能開發 2 位獨立 多數同意即可
遷移、部署、授權、計費 3 位獨立 一致同意才可

失敗模式:意見一致的模型也可能全都錯了

這項技術的真實極限在於相互關聯的盲點。只有當錯誤確實是獨立的情況下,獨立的錯誤才會相互抵銷。如果您模型池中的每個模型,都從公開網路上那些相同、流行但有錯誤的範例中學到了同樣的錯誤用法,那麼它們全都會重複這個錯誤,並且一致且自信地批准它。共識是證據,而非證明。三個模型亮綠燈,意味著這三個模型沒有發現問題,但這不等於沒有問題。

這點對於最新類型的錯誤,以及最領域特定的錯誤來說,最為重要。一個全新框架中的微小瑕疵,或是一條只存在於貴公司內部而從未出現在任何訓練集中的商業規則,對所有模型來說都是同時看不見的。再多的交叉檢查,也無法無中生有地產生出任何審閱者都不具備的知識。多模型審查減少了漏網的錯誤類型,但並不能完全消除。

所以,在風險高到值得這麼做時,應讓人力保留在流程中,並閱讀實際的審查結果,而不只是看「通過 (GO)」或「不通過 (NO-GO)」的計數。這些結論是一個過濾器,能讓人類的注意力發揮更大作用,而非其替代品。使用一個不同的模型來審查 AI 寫的程式碼,因為有第二組盲點總比只有一組好。只是別把機器之間的共識誤認為是真理。