iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
AI Security

一個 Agent 我查不動,一個我改得動:內部威脅偵測的 30 天系列 第 9

當基準只是代理指標:模型「錯」了六題,但我說不出它錯在哪

  • 分享至 

  • xImage
  •  

在 Day 8 中,我們拆解了門檻的直覺陷阱,並釐清了機率作為「Routing 訊號」而非「證據」的本質。今天,我們進入第二組實驗——改動邏輯偵測(n=64)

這組實驗帶出了一個非常有趣的哲學與技術問題:當你用來當成「標準答案」的基準,本身就只是個代理指標(Proxy Metric)時,你該怎麼判定模型「答錯了」?


實驗二:執行邏輯。它「錯」了六題,但我說不出它錯在哪

這組實驗有 64 筆改動。我們的「基準」是改動前後的 ast.dump() 比對——只要語法樹的節點型別組成變了,基準就判定「動到執行邏輯」(正例 61 筆,負例 3 筆)。

在 0.50 的門檻下,Jev 與基準有 55 筆一致、6 筆不一致。而且這六筆全是假陰——基準判定動到了邏輯,模型說沒有。

我本來想在報告裡寫「模型答錯了六題」。但當我把這六筆的詳細改動攤開來時,這句話我寫不出去:

檔名 機率 基準判定 實際的改動內容
test_billing.py 0.30 動到邏輯 新增 Import × 1
test_billing.py 0.33 動到邏輯 移除 Import × 1
daily_watch.py 0.26 動到邏輯 節點組成相同,只有值不同(字串、常數、名稱)
jev_vs_ast.py 0.28 動到邏輯 同上
jev_vs_ast.py 0.35 動到邏輯 新增 Assign×1、Call×2、If×1
md2html.py 0.49 動到邏輯 新增兩個函式,含 If×2、Return×3、Call×7

看前兩筆。改動內容是加一行 import、刪一行 import

因為 Import 節點的數量變了,ast.dump() 的樹當然變了,所以基準判定「動到執行邏輯」。但模型給了 0.30 和 0.33,判定「沒有動到邏輯」。

這兩筆到底是誰錯?

我認為是基準錯了。多一行或少一行沒用到的 import,根本不會改變這支程式執行時的行為。在這個案例中,模型的語意判讀,反而比我們寫死的 AST 節點比對更接近真實的「邏輯未變」

而最後兩筆(jev_vs_ast.pymd2html.py)則是模型真的漏了。特別是最後一筆 md2html.py——它新增了整整兩個函式,包含 IfReturn 與多個呼叫,這絕對是實質的邏輯改動。但模型給了 0.49,判定為假。

這筆離 0.50 門檻只差一格(0.01),而我們量過同一個輸入重跑會抖一格。所以這一筆,正好卡在硬幣立起來的邊緣。

這給了我們一個極其深刻的教訓:不要盲目相信你的「地標(Ground Truth)」。

在軟體工程中,我們很難拿到絕對的真相,往往只能用 ast.dump() 或測試綠燈當代理指標。當你發現模型與基準不一致時,不要急著怪模型,先去讀 diff——不一致的地方,往往就是代理指標的極限。


今天的成果,你可以直接拿去用:升級判定

既然確定性層(AST)和模型(Jev)各有各的瞎眼之處,那我們該怎麼把它們結合,形成一個既省錢、又不會在熱路徑上造成延遲的 Reliable Hook

答案是:不要每筆都問模型,只問確定性層判不出來的。

我們在 hook 裡跑完三層確定性判斷(regex、路徑角色、AST)後,用下面這個判斷式決定要不要「升級(Escalate)」送去問模型:

def escalate_reason(rec):
    """回 None 代表確定性層有答案了,不用升級。這個 None 就是省下來的錢和延遲。"""
    kind = rec.get("ast_kind")
    l1_hit = sum((rec.get("l1_regex") or {}).values()) > 0

    if kind is None:                       # 狀況一:解析不動(Cursor 常常只給片段)
        return "unparsable"
    if l1_hit and kind in QUIET_KINDS:     # 狀況二:兩層不同意(L1 喊了但 AST 說沒變)
        return "l1_only"
    if not l1_hit and kind in LOUD_KINDS:  # 狀況三:反向不同意(AST 看到實質改動但 L1 沒喊)
        return "l1_silent"
    return None

請特別注意 l1_only 的命名。我原本把它命名為「L1 的誤報」,直到我踩到一個反例:subprocess.run(cmd, shell=True)

這個改動,L1 regex 抓到了(shell=True),但 AST 的節點數量和型別完全沒變(一樣是一個 Call),所以 AST 判定為「無結構變化」。

那一次,是 AST 瞎了,L1 是對的

所以,我們升級的理由是**「各層意見不一致」,而不是「哪一層寫錯了」。升級也不等於「這筆該給人看」——兩層都大聲說危險的改動(例如新增 eval()),我們根本不需要升級,因為答案早就有了。升級,處理的是沒有答案**的那些。


誠實欄

  • 64 筆數據的正負例極度不平衡(61 比 3)。 所以「假陽 0」幾乎沒有資訊量,整組資料只有三次機會產生假陽。
  • 基準是代理指標。 ast.dump() 不等於「程式行為變了」,多一行 import 就是最好的例子。
  • 樣本集中在少數檔案。 64 筆只涵蓋 22 個檔,其中一個檔佔 11 筆、另一個佔 10 筆。不是隨機抽樣。

明天,我們來看一件上線時最致命的硬天花板:hook 在熱路徑上。

Cursor 每次寫檔都在等它回來。如果我們把模型呼叫搬進 hook 裡,每一次存檔要多等多久?而我們又該怎麼用 deferredinlinesample 三種模式來解這題?


今天對應的威脅: T3(藉 Agent 之手規避控制)。我們用 AST 基準與模型的對照,看清了「代理指標」的極限,並寫出了只在意見不一致時才升級的 Reliable Hook 核心邏輯。

實驗與程式: 2026-09-19 跑、2026-09-20 重算。原始數據 data/day06-jev-vs-ast.json(64 筆)。模型為 jev-1.13.0。本篇沒有引用外部來源。


上一篇
門檻不是越高越嚴謹:0.90 如何讓模型退化回 Regex
系列文
一個 Agent 我查不動,一個我改得動:內部威脅偵測的 30 天9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言