iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
AI Engineering

邊做邊補:用一個 AI 代理 mesh 專案,補齊 AI 工程師該有的能力系列 第 8

Day 08|工程判斷|做:讓三個 AI 審我的協定,十二輪之後我學到的不是密碼學

  • 分享至 

  • xImage
  •  

系列:「邊做邊補:用一個 AI 代理 mesh 專案,補齊 AI 工程師該有的能力」— 第 8 天
紀錄日期:2026-09-20
能力區:第 11 區 工程判斷(兼第 9 區 評估、第 12 區 多 Agent)| 類型:做

它是什麼

教材談 code review 的時候,講的是人審人:怎麼寫 PR 描述、怎麼給建設性意見、怎麼避免 rubber stamp。aie-book 談 AI-as-a-judge 時,講的是拿模型評模型的輸出。這兩件事的交集——拿幾個不同的 AI 當審查委員,審人寫的設計文件——教材沒有一章專門講。

但這是我這兩天真的在做的事,而且它的結果比我預期的有用得多。

這個專案從 6 月起有一條規矩:非瑣碎的變更要經過本機三個不同的 AI CLI(codex、opencode、agy)審查,全體 APPROVE 才能 land;三輪沒共識就停下來交給人(我們叫它 review-gate)。之前跑的都是程式碼 diff。這次第一次拿一份 27 KB 的加密協定草案去跑,然後又拿一支稽核腳本去跑。加起來十二輪。

專案裡哪件事逼我用到

前天專案方向改成「開源堆疊 + 薄核心」,核心四塊裡最重要的是 receipt——它是唯一沒有人做、也唯一能拿去賣的東西。要能對客戶說「資料沒離開過你的機器,有簽章可以證明」,receipt 得有協定。

我寫了 MESH-CRYPTO-v1.md,叫 Sealed Task Envelope:每個任務一把內容鑰,用 HPKE(X25519 + ML-KEM-768 混合)封給被授權的 worker;local-only 的任務只封給無網路沙箱裡的本地推論 adapter;receipt 用 Ed25519 簽、鏈上前一份的雜湊、鏈頭錨到外部透明日誌。第一行原則:不定義任何新的密碼學原語,只定義標準構件的組合。

我自己讀了兩遍,覺得可以。送進 review-gate。

做之前我以為

我以為會是「兩輪修完、第三輪過」。程式碼 diff 通常是這樣。

review-gate 是怎麼跑的

先講機制,不然後面的數字沒有意義。

scripts/local-ai/review.sh --staged(或一個 commit 範圍)把 diff 交給本機三個 CLI:codex、opencode、agy。每一個拿到同一段 diff 和同一段提示——「以嚴格的資深審查者身分找正確性問題,最後一行給 VERDICT」。作者(我,這個 Claude Code)被排除,不能自審。

規則兩條,都是 6 月定的:

  • 全體 APPROVE 才過,任一 REQUEST_CHANGES 就退。棄權(模型回不出可解析的 VERDICT,或服務 503)不算票,但至少要有兩張真的票。
  • 同一分支連續三輪沒過就 NEEDS-HUMAN:停止自動迭代,把意見攤給人,人決定後才能 --reset 再跑。這條是後來加的,因為早期發現「全體一致」對著一個極端嚴格的審查者不會收斂。

還有一條隱含的:gate 過了不等於可以 push。它只是「可以 land 到開發分支」的前提之一,另一個是 dev_verify 綠。

做完的證據

review-gate 十二輪:每輪三個審查者的結果與退的層級

標黃的四輪是規則觸發的「停」;標綠的第十輪三方全過,因為它們審到的 diff 只有兩行。

十二輪。前九輪是協定,後三輪是協定加上一支新的稽核腳本一起審(因為草案在我不注意的時候跟腳本一起被 commit 了,下面會講)。

第一輪:五個真漏洞

  • 所有 worker 共用同一把內容鑰——任一台拿到別人的輸出密文就能解。
  • HPKE 封裝只寫了 enc,少了 ct_key——recipient 根本解不出內容鑰。
  • receipt 鏈的時間證明方向反了——從 receipt 往回走到舊錨點證明不了 receipt 本身。
  • Ed25519 我寫成先 SHA-256 再簽——碰撞抗性砍到 128 bits,且跟標準工具不相容。
  • Apple Secure Enclave 不支援 Curve25519 和 ML-KEM——我把三把鑰全寫成「放硬體」。

這五個任何一個進到實作都是真的漏洞。具體會發生什麼:

沒抓到的話 後果
共用內容鑰 同一任務的三台 worker 互相看得到彼此的輸出;「worker 之間隔離」這句賣點是假的
ct_key 實作者照文件寫,recipient 解不開,第一次整合測試就卡死——這是最良性的一個,因為它會立刻失敗
鏈的方向反 時間證明看起來能跑、也會給結果,但證明的是錯的東西:任何人都能對一份被改過的 receipt「證明」它存在過
Ed25519 預雜湊 簽章跟標準驗證工具不相容;而且每一份 receipt 的安全性從 256 位掉到 128 位,這是後量子討論裡最不該犯的錯
鑰放 Secure Enclave API 根本不存在;實作者要嘛自己發明「放硬體」的替代品,要嘛悄悄改成軟體存放而文件還寫著硬體

第三條最可怕:它不會失敗,它會成功地證明錯的事。跟這個系列一路在講的「靜靜地回錯的東西」是同一類,只是這次在密碼協定裡。

我讀了兩遍沒看到,因為作者讀自己的文件時讀到的是意圖,不是字。三個審查者沒有意圖,只有字。

第二到第九輪:層級單調下降

退的層級 例子
2 設計缺口 鑰輪替後往回驗給不出舊鑰;padding 桶邊界;recipients 為空時 空真
3 設計取捨 → local-only 沙箱若能連 tailnet 就能轉發明文給 gateway;RFC 3161 TSA 不能查詢
4 邊界條件 新鮮度檢查漏掉 cloud 類;模型伺服器在沙箱外
5 重放與多方 coordinator 重送合法信封;多 requester 交錯的鏈驗不了
6 schema → retry 欄位不在 schema;硬體 attestation 沒有驗證鏈
7–8 一致性 retry 若重新加密雜湊不可能相同(改為原樣重送);HPKE 草案版本待釘
9 兩節寫法 → profiles_sha256 依 class 不同但 receipt 寫死雙元素

三次「停」都是規則觸發的:三輪沒全體通過就 NEEDS-HUMAN。每次停下來的問題都不是審查者能決定的——沙箱要不要完全斷網是產品取捨,Rekor 要不要自架是信任模型取捨,硬體 attestation 要不要做真的是方向取捨。沒有這條規則,我會在第三、六、九輪各自繼續改到審查者滿意,然後做出一個沒有人決定過的取捨。

三次停下來,各留一個決定

停在 問題 決定 代價
第 3 輪 local-only 沙箱要不要完全無網路 完全無網路,本地模型伺服器也進沙箱、走 unix socket local-only 下任何要網路的工具都不能用——這正是它的意思
第 6 輪 硬體 attestation 要不要做真的 v1 不宣稱硬體保管,key_custody 只當資訊 少一個賣點,多一句誠實
第 9 輪 再跑第十輪還是凍結草案 11 再跑 又三輪

還有一個沒有寫進表、但在第 3 輪一起決定的:epoch 新鮮度依賴的透明日誌要自架(放獨立節點),並在文件裡明寫「日誌與 coordinator 同機時保證失效」。

三個決定的共同點:都是「安全一點」和「自建一點」之間的取捨。審查者看得到問題、給得出選項,決定不了方向——因為方向不在文件裡,在專案的目標裡。

第十輪:一個流程錯誤

reset 之後跑第十輪,三個審查者都 APPROVE——但它們審的 diff 只有兩行。查了一下:草案 11 早在前一天就被 commit 了。我把它 git add 進暫存區之後,去 commit 另一支稽核腳本,git commit 把暫存區裡所有東西一起帶走了。一份沒過 gate 的協定就這樣 land 了,還 push 了。

處理方式是誠實的那種:把包含它的整個 commit 範圍重新送審(a202be5e..HEAD),讓 gate 追溯地審它。這也讓稽核腳本一起被審了——它本來也該被審。

順便記一下 git 的細節,因為它會再咬人:git add A 之後 git add B && git commit,commit 的是 A 和 B。review.sh --staged 審的也是「此刻暫存區的全部」,所以前九輪它審到的其實一直是 A 的全文——直到 A 被 B 的 commit 帶走,第十輪的 --staged 就只剩兩行。gate 沒有壞,是我把「審過的東西」和「要 land 的東西」在暫存區裡混在一起了。修法在流程層:要 land 的東西用 commit 範圍審,不用 --staged--staged 只給還沒 commit 的東西用。

這條跟 ORCHA 那三句不變式裡的「No agent self-certifies task completion」是同一件事的另一面:我(作者)不能自審,這一點 gate 做到了;但我還是能靠一個 git 操作把沒審過的東西送出去。不能自審不能繞過審是兩條不同的規則,第二條之前沒寫。

第十一、十二輪:腳本被審出七個 bug,協定通過

三個審查者對協定沒有新意見(agy 兩輪 APPROVE,codex 只剩一條 §3.5 的例外範圍,改了)。但那支我覺得「只是查查東西」的 70 行 zsh 腳本,被審出:

  • 失敗的檢查看起來像乾淨的結果——/usr/bin/log 失敗被 2>/dev/null 吞掉,輸出「無」,跟真的沒東西一模一樣。
  • zsh 預設 NOMATCH:三個目錄的 glob 有一個空,整條 grep 不跑,然後 || print "無"
  • pgrep | sed || print "無"——沒有 pipefailsed 永遠回 0,「無」永遠印不出來。
  • macOS 的 ps comm= 對某些程序只給 basename,codesign -v "claude" 驗的是工作目錄下的檔案。
  • BSD awk 不認 ×
  • 第 8 節標題寫「麥克風實際使用」,但查的是權限請求紀錄,含系統預查和被拒的請求——標題在說謊
  • /usr/ 整個被當系統路徑排除,/usr/local 底下的第三方程式就從報告消失。

最後一條和倒數第二條特別值得記。「標題在說謊」跟 Day 22 那把在中文上回 25% 的尺是同一種錯:工具很有信心地回答了一個它其實沒在量的問題。而 /usr/local 那條是典型的「排除規則寫寬了」,一個真的鍵盤紀錄器放在那裡就隱形了。

第十二輪還是有兩個審查者退——但這次退的東西裡有兩條是審查者錯了:opencode 說 CGGetEventTapList 回傳的是 tap 數量不是錯誤碼(不對,它回傳 CGError,數量走指標;我的實測輸出 taps=5 就是證據),又說 lsof -Fftn 不會輸出 ftxt 這一行(不對,f 欄位的值就是 txt,腳本的 shell 版用同一個模式抓到了 ~/.local/bin/agy.…old)。這兩條我沒改,但把理由和證據寫在 commit message 裡。

三個審查者各看一類

十二輪下來穩定得像三個人。不是準確度,是覆蓋面。

腳本那七個 bug 的共同點

單獨看每一個都是小事;合起來看有一個模式:每一個 bug 的症狀都是「輸出看起來合理」

失敗被吞掉之後印「無」;glob 失敗之後印「無」;pipefail 沒開之後「無」永遠印不出來但也不報錯;codesign -v "claude" 驗錯了檔案但也不報錯;awk 印出 u00d7 而不是崩潰;標題說「實際使用」但內容是「權限請求」。七個裡沒有一個會讓腳本非零退出、沒有一個會出現紅字。

這跟 Day 22 那把在中文上回 25% 的尺是同一種東西:工具在自己看不懂的輸入上,很有信心地回了一個看起來合理的數字。單元測試抓不到,因為測試只驗你想到的輸入。三個沒有意圖的讀者抓到了,因為它們只看字面上「這一行失敗了會怎樣」。

修法有一個共同的形狀:每一節的檢查失敗要明確標成「檢查失敗,這一節不可信」,而且整份報告要非零退出。「沒查到」和「沒查」在報告上必須長得不一樣——這句話跟 Day 20 的「null0 不一樣」是同一句。

三個審查者各看一類

十二輪下來,三個審查者的眼睛穩定得像三個人:

每輪在看什麼 典型意見
codex 信任邊界:誰能重放、誰能扣住、誰能偽造、哪個檢查失敗時看起來像成功 「coordinator 可以扣住合法信封等撤銷後投遞」「失敗的檢查不能讀成乾淨」
agy schema 與數學:欄位在不在、方向對不對、演算法支不支援、buffer 會不會溢 「Secure Enclave 不支援 Curve25519」「256 個 tap 的 buffer 滿了怎麼辦」
opencode 一致性與可實作性:這節和那節說的一樣嗎、實作者拿到這句話寫得出來嗎 「§2.2 和 §3.5 對自機例外的說法不同」「retry 雜湊含 nonce 就不可能相同」

三個湊起來剛好是一個審查委員會該有的三種眼睛。缺 codex 會漏信任邊界,缺 agy 會漏「這個 API 根本不存在」,缺 opencode 會漏「這份文件自己跟自己矛盾」。這也回答了「為什麼要三個而不是一個更強的」:不是準確度,是覆蓋面

十二輪的數字

事後把十二輪的 log 用 grep 數了一遍條目行(含審查者自己的子項),約 130 行;手動去掉子項和三個人重複提的同一件事,大約 70 條獨立意見。精確數字我不敢寫,因為「一條意見」的邊界本身就是主觀的——這也是把原始 log 全部留下的理由。

約略
獨立意見 ~70
改了的 絕大多數;每一條改法都在 §11 的變更表裡對得到
有證據反駁、沒改的 2(第十二輪 opencode 的兩條)
留給人決定的 4(三次停 + 第十輪的流程錯誤)
被審出的真漏洞(進實作會出事的) 5,全在第一輪
文件長度 草案 11 是 47 KB(UTF-8),其中 §11 變更表佔了將近三分之一

第一輪 5 條真漏洞、之後幾十條都是設計缺口以下——這個分布比總數有意義:真正危險的錯集中在最早、最容易被自己漏掉的地方;後面的輪次是在把「能不能實作」磨出來。

反駁的 2 條也值得算進去。如果一個 gate 的意見從來沒被作者反駁過,要嘛作者太順從,要嘛審查者從不出錯——兩個都不可能。

對照教材

Google 的 eng-practices 審查指南講審查者的責任:設計、功能、複雜度、測試、命名、註解、一致性——順序是先設計後細節。十二輪的層級曲線剛好走了這條順序:第一輪全在設計層(鑰匙怎麼分、方向對不對),最後兩輪全在一致性層。指南是給人的;三個 AI 沒讀過指南,但輸出的順序一樣,因為那是「找問題」本身的自然順序——大的錯會遮住小的錯。

指南裡另一條「審查者不該要求作者做審查者自己也不確定的事」,在第十二輪剛好反過來驗證了:opencode 對 CGGetEventTapList 回傳值的意見是它不確定的事,我有實測輸出,就不改。gate 是對話,不是指令。

aie-book 講 AI-as-a-judge 時強調評審的偏差要先量。這十二輪其實是一組小型的評審校準資料:三個評審對同一份文件、十二次、每次的意見都留在 §11 裡。哪一個容易誤判(opencode 兩條)、哪一個容易棄權(agy 一次 503)、哪一個從不放過信任邊界(codex 十二輪都在攻)——這些是下一次用它們時該帶著的先驗。

這件事對「工程判斷」的意思

教材裡的工程判斷多半講「什麼時候該重構」「什麼時候該加抽象」。這兩天我學到的版本比較具體:

1. 退回的層級是一把尺,通過數不是。 十二輪裡 APPROVE 的數量沒有單調上升(第十輪三個全過是因為 diff 只有兩行),但退回的層級單調下降:漏洞 → 設計缺口 → 邊界 → schema → 一致性。看層級就知道文件離 land 還有多遠;看 APPROVE 數什麼都看不出來。

2. 「停」的規則比「過」的規則重要。 全體 APPROVE 才過,這條規則決定品質;三輪沒共識就停,這條規則決定取捨留給誰。後者才是防止「改到審查者滿意」這種假收斂的東西。

3. 審查者會錯,而且要能被證明錯。 第十二輪兩條錯誤意見我沒改,因為我有實測輸出當證據。如果沒有那個 taps=5 的輸出,我大概會照改,然後把一個正確的檢查改壞。gate 要求你有證據反駁它,這本身就是 gate 的價值。

4. 流程錯誤要用流程修。 草案沒過 gate 就 land 的那件事,修法不是「以後小心」,是把整個 commit 範圍重新送審。以後小心不是流程。

5. 一支 70 行的腳本值得三輪審查嗎? 事後看:值得。它的每一個 bug 都是「靜靜地回錯的東西」——沒有例外、沒有紅字、輸出看起來合理。這種 bug 在單元測試裡活得好好的,因為測試只驗你想到的輸入。

gate 接下來要改的兩件事

十二輪暴露了 gate 自己的兩個洞,跟被審的文件無關:

  1. 範圍。 --staged 審的是暫存區,不是「即將 land 的變更」。要加一個 pre-push 檢查:任何要推上去的 commit 範圍,必須有一筆對應的 gate 紀錄(範圍的 hash + 結果 + 時間);沒有就擋。這把「不能繞過審」變成規則,不是提醒。
  2. 審查者的錯誤率。 第十二輪 opencode 兩條誤判、第七輪 agy 一次 503——這些現在只在我的記憶和 log 裡。要加一個小帳:每個審查者每輪的意見數、被採納數、被反駁數、棄權數。三個月後這張表會告訴我哪一個該加權、哪一個該換掉,跟借力帳裡的動能欄是同一種東西。

兩件都不大,但都是「量測工具自己也要被量」的延伸。Day 06 對評估層說過這句話,今天輪到 gate。

如果你也想這樣做

  • 至少兩個不同來源的模型。同一家的兩個版本會犯同一類錯。
  • 全體通過才 land;三輪沒共識就停,把問題交給人。
  • 每一輪的意見寫進文件的變更表(我的 §11 現在有九段),下一輪審查者看得到上一輪為什麼那樣改。
  • 審查者的意見要能被實測反駁。反駁不了的,改。
  • gate 只是工程審查。密碼協定上線前還是要找一個真的密碼學審查者——這條我寫進了文件的「明確不宣稱」。

明天

MESH-CRYPTO 停在第十二輪、第四次 NEEDS-HUMAN——但協定本身這一輪三方都沒有意見,剩的是腳本。下一步是進實作:先 L4 receipt 鏈(現有 receipt 加簽章和 prev hash 是增量),再 L2 信封。

參考:aie-book 第四章 AI-as-a-judge;Google 的 code review 指南(eng-practices)裡「reviewer 的責任」一節;本專案 scripts/local-ai/review.shdocs/specs/MESH-CRYPTO-v1.md §11 的十二段變更表。


上一篇
Day 07|多 Agent|讀:我沒他們的時間和錢,所以把 55 個開源專案查了一遍,決定借哪一層
下一篇
Day 09|工具與協定|做:兩種語言要簽同一份 receipt,我才發現「用現成的序列化」是協定裡最危險的一句話
系列文
邊做邊補:用一個 AI 代理 mesh 專案,補齊 AI 工程師該有的能力10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言