iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
AI Engineering

一天一個 PR,讓我告訴你嵌入式開發的殘酷:如何為 AI Agent 建立可信的工程閉環系列 第 12

Day 12 - Agent 不能拿另一份 YAML 冒充已審核版本:不要作弊

  • 分享至 

  • xImage
  •  

系列說明 >> 本系列大部分 PR 來自私人或公司專案,部分程式碼、環境設定與實作細節不便公開。

我只能保證文中提到的 PR 與問題皆為真實案例,但會經過必要的匿名化與內容調整。

本系列主要分享問題如何被發現、背後的思考方式,以及解法如何形成,不會深入討論完整實作與部署細節,敬請見諒。

Today’s Change

  • Change Ref: PR #131-feat(audit): P2 gate 補強

  • Issue: Audit 已經有 verify-edit → record → decide → apply → PR,但各階段仍可能拿到不同版本的 YAML;Source Citation 也沒有被可靠綁定到同一份 Source Tree。

  • Root Cause: Gate 有檢查「步驟做過沒」,卻沒有把 Before、Proposed、Source Evidence 與最後 Apply 的 Artifact 綁成同一條 Identity Chain。

  • Solution: verify-edit 的 Before 改由 RID+Case 自動解析;Proposed 固定 Snapshot 並留下 SHA;apply 同時檢查 Proposed SHA 與 Target Drift;Citation 綁定指定 Source Root 的 Git Identity。

  • Evidence: Audit Tests 214 passed,包含 Before 調包、跳過 verify-edit、Target Drift、空 Citation 等負向測試;Policy Check 無 Fail,另以 Dry Run 驗證 Source-root Git Identity。本 PR 的驗證不需要操作硬體。


上一篇省掉硬體時間,這一篇開始查戶口

上一篇,我們終於在碰 DUT 以前先讀 Workbook。

Not SupportedSkipTo Be Tested 先分掉,不要先 Upgrade FW、占 DUT、占 STA、占 Serial,忙了一輪才發現:

喔,這題不用測。

省下來的時間很多。

剩下真的需要 Audit 的 Case,才讓 Agent 去查 Source、跑 Live Probe、提出 Case YAML 修改。

到這裡流程其實已經滿像回事了:

Official Case
    ↓
Proposed YAML
    ↓
verify-edit
    ↓
record
    ↓
decide
    ↓
apply
    ↓
PR

每一站都有 Gate。

每一站看起來也都找得到人背鍋。

很有制度。

然後問題來了。

Agent 說:

「這份 YAML 我已經驗過了。」

我看了一眼路徑。

「你驗哪一份?」

它指到一份暫存的 Proposed YAML。

「那等等 apply 用哪一份?」

又是另一個位置。

欸。

事情突然開始有點抽象。

明明只是改一份 YAML,聊著聊著已經快變成:

你是誰?
你從哪裡來?
剛才被驗的是你嗎?
等等被 Apply 的還是你嗎?

老Go在旁邊補了一句:

「內容一樣不就好了?」

對。

如果真的一樣。

問題是,當時沒有任何 Gate 能一翻兩瞪眼地證明:最後要 Apply 的,就是剛才驗過的那一份。

我們前面花了一堆力氣防 Agent 亂改正式 Case,最後最重要的保護機制居然是:

大家記得不要換檔案喔。

這不是 Gate。

這比較像拜拜。

Target


Gate 很嚴格,只是原稿可以自己帶

原本的 verify-edit 大致長這樣:

testpilot audit verify-edit <RID> <case> \
  --yaml <before.yaml> \
  --proposed <after.yaml>

設計本身很合理。

before.yaml 是原始 Case,after.yaml 是 Agent 提出的修改版。

Gate 比較兩份內容,只允許動白名單欄位。像 Criteria、Expected 這類 Audit 本來就允許修的地方可以動,Case Identity 或其他禁區亂碰就擋。

很直白。

問題只在一個小地方:

--yaml <before.yaml>

這份 Before 是 Caller 自己指定的。

也就是說,正式 Case 明明在 Repo 裡,我卻可以另外準備一份「號稱是 Before」的 YAML 給 Gate。

例如正式 Case 是:

A B C D E

其中 C 是不能改的。

那我先做一份假的 Before:

A B X D E

再做 Proposed:

A B X D F

Gate 比較的其實是:

假的 Before → Proposed

E → F

白名單內。

PASS。

真正不該動的:

C → X

早就在「我自己帶 Before 進考場」的時候處理掉了。

Gate 沒壞。

Diff 沒壞。

白名單也沒壞。

大家都正常上班。

只是整群人很有秩序地驗錯東西。

老Go說:

「那規定 Agent 一定要傳正式 Case 不就好了?」

當然可以。

README 寫一次。

Agent Guide 再寫一次。

Skill 裡補個大大的:

IMPORTANT!!!
DO NOT USE THE WRONG BEFORE YAML!!!

最好三個驚嘆號,誠意比較夠。

以前我也會把這種東西叫做「一翻兩瞪眼的規則」。

後來發現其實比較接近:

大家有看到的話記得一下。

換成工程語言就是:

根本沒有 Gate。


Before 不該讓 Agent 自己報戶口

Audit 已經有 RID,其實根本不用問 Agent 原稿在哪裡。

RID 知道這次 Audit 的工作範圍,Manifest 知道 Cases Directory,Case ID 又已經指定成 D123。

那就自己找。

所以 PR #131 第一刀很乾脆:

--yaml 拿掉。

testpilot audit verify-edit <RID> D123 \
  --proposed /tmp/D123-after.yaml

Before 改成由系統自己解析:

RID
 ↓
manifest
 ↓
cases_dir
 ↓
D123
 ↓
Official Case YAML

找不到,Fail。

同一個 Case 找到兩份,Ambiguous,Fail。

只有唯一的正式 Case 可以當 Before。

以前是:

Agent:這是原稿。
Gate:好喔。

現在變成:

Agent:這是我改的。
Gate:原稿我自己找,你顧好 Proposed 就好。

這樣比較合理。

考生可以交答案。

題目原本長什麼樣,不要也讓考生順便包辦。

不然那個 Audit 有點太自由行。


好,原稿是真的。那 Proposed 還是剛才那份嗎?

做到這裡,我原本也覺得差不多了。

Before 已經綁正式 Case。

Proposed 跑 Boundary Check。

Schema Check 也過。

留下 Audit Record。

收工。

然後 Adversarial Review 又把下一個洞挖出來。

假設流程是:

10:00:00  verify-edit 讀 proposed.yaml
10:00:01  Boundary Check PASS
10:00:02  Schema Check PASS
10:00:03  另一個 Process 修改 proposed.yaml
10:00:04  系統把 proposed.yaml 存進 Audit Run
10:00:05  寫下「已驗證」

請問 10:00:05 那個「已驗證」,驗的是誰?

前面驗 A。

中途檔案被換成 B。

最後留下來的是 B。

Audit Record 還是一臉正氣:

PASS

這次連 Agent 都不一定要搞事。

Editor Auto-save、另一個 Script、另一個 Agent,甚至我自己手滑,都可能在 Check 和 Use 中間動到檔案。

老問題了。

TOCTOU,Time Of Check To Time Of Use。

最新的 Agent Workflow 裡,住著一隻不知道幾歲的老 Bug。

有一種 AI 發展得很快,但 Bug 祖譜完全沒斷過的安心感。


Filename 是地址,不是身分證

所以只記:

/tmp/D123-after.yaml

其實不夠。

這只能證明:

去這裡找。

不能證明:

你現在找到的,就是剛才驗過的那一坨 Bytes。

同一個 Filename,五秒前住 A,五秒後住 B,也沒有人犯法。

所以 verify-edit 改成先把 Proposed 固定成一份 Snapshot,再用同一份內容一路做:

read proposed bytes once
        ↓
Pinned Snapshot
        ↓
Boundary Check
        ↓
Schema Check
        ↓
SHA
        ↓
Atomic Stage
        ↓
最後才寫 Verify Record

也就是 Boundary Check、Schema、Hash,最後 Stage 進 Audit Run 的內容,都必須是同一組 Bytes。

Stage 失敗,就不能留一筆「我驗過」在那邊唬爛。

如果正式 Before 在驗證途中又被別人改掉,也停。

因為你剛剛驗的明明是:

Before A → Proposed B

現在世界已經變:

Before A.5 → Proposed B

還硬說「剛才驗過了啦」,這不是自動化,這叫硬拗。

所以我很喜歡這個理解方式:

Filename 是地址,不是身分證。

final_v3_really_final.yaml 名字取得再真誠,都不能證明它現在還是剛才那一份。

工程師看這種檔名應該更有感。


SHA 對了,還是可能拿昨天的答案蓋今天的題目

到這裡已經可以確認 Proposed 沒被換掉。

但還差另外一邊。

正式 Case 也會變。

例如早上:

Official V1
   ↓
verify-edit
   ↓
Proposed V2

一切都正確。

下午另一個人先把正式 Case 改成 V1.5。

晚上我拿早上的 V2 回來 Apply:

V1
 ├─ 我的 V2
 └─ 別人的 V1.5

最後:V2 蓋回去

Proposed SHA 對不對?

對。

它從早到晚都是 V2,忠貞不二。

問題是 V2 當初驗證的基準是 V1,不是現在的 V1.5。

這份修改已經 Stale 了。

所以 apply 現在要同時看兩邊:

現在 Proposed SHA
==
verify-edit 當時的 After SHA

而且:

現在 Target SHA
==
verify-edit 當時的 Before SHA

兩條都成立,才 Apply。

第一條防你驗 A、偷換 B。

第二條防你拿昨天 A→B 的答案,跑來蓋今天已經變成 A.5 的題目。

老Go看了一眼:

「這不就 Optimistic Lock?」

對。

沒有什麼魔法。

以前拿來擋 Database Concurrent Update,現在拿來擋 Agent 拿舊答案回來蓋新 Case。

技術日新月異。

工程師被同一批問題追著跑的方式也日新月異。

Target


Evidence 也不能空手來,還說自己全部驗過

YAML 的身分開始綁住之後,還有 Source Evidence。

Agent 修改 Criteria 時,Audit 本來就要求 Citation,例如 Source File、Line、Snippet,再由 record 驗證。

結果還有一種很漂亮的綠燈:

{
  "citations": []
}

最後可能得到:

citations_verified = true

不是程式算錯。

如果邏輯最後落到:

all([])

它本來就是真。

所有零個 Citation,全數驗證通過。

數學很開心。

工程上就有點北七。

所以 PR #131 直接把空 Citation 改成 Hard Fail。

要進 applied,至少得有:

verify-edit record
+
non-empty citations
+
citations_verified == true

沒有 Evidence,可以 Pending,可以 Block。

就是不要兩手空空站在門口,然後跟我說:

「我帶的證據全部都確認過了。」

你確認了個寂寞。


Source 也要先回答:你哪一版?

Citation 還有一個很實際的坑。

Case Repo 跟 DUT Firmware Source 往往不是同一棵 Tree。

Citation 如果只寫:

src/wifi/driver.c:123

這個相對路徑到底相對誰?

如果拿 TestPilot/Plugin Repo 去解,當然找不到 Firmware Source。

最省事的方式是全部塞絕對路徑。

今天能跑。

換台機器,變古蹟。

更麻煩的是 Source 更新後,檔名還是 driver.c,行號甚至可能差不多,但內容早就不是當時那一版。

所以這次 audit init 加上 --source-root,並在 Manifest 留下這棵 Source Tree 的 Git Identity:

Source Root
Git SHA
Git Describe

這樣 Citation 說的才不是:

某台機器上有一個同名檔案。

而是:

這次 Audit 綁定的那一版 DUT Source 裡,這個 Citation 存在。

做到這裡,這篇其實已經不像在改 YAML。

比較像一路在查戶口。

Before:你誰?

Proposed:你還是剛才那個誰?

Target:你中途有沒有換人?

Source:你是哪一版的誰?

PR #131 做的那些 SHA、Snapshot、Source-root,看起來東一塊西一塊,其實都在處理同一件事:

不要讓每一個 Gate 都各自 PASS,最後才發現大家講的根本不是同一份東西。


這次完全沒碰 DUT,因為 DUT 也回答不了

PR #131 最後的 Audit Test:

214 passed

裡面包含幾個我比較在意的負向情境:

Before 被調包
→ verify-edit 拒絕

跳過 verify-edit
→ apply 拒絕

verify-edit 後 Target Drift
→ apply 拒絕

Citation 為空
→ applied 拒絕

Policy Check 沒有 Fail,也用 Dry Run 確認 Source Root 的 Git Identity 會被寫進 Manifest。

這次完全沒去占 DUT。

因為 DUT 可以回答:

Runtime 現在長什麼樣。

它回答不了:

你十分鐘前 Verify 的 YAML,跟你現在 Apply 的 YAML,是不是同一份。

硬把板子拖進來,只是多一塊無辜硬體陪我們看 SHA。

所以 214 passed 也只證明這批已知 Identity Bypass 現在有機器擋。

它不代表 Criteria 語意一定正確,也不代表 Citation 的內容一定真的支持 Agent 的推論。

那幾筆帳後面還會再來。

今天先處理最基本的:

驗 A,就不要最後交 B。

Target


很好,現在每一條都要驗明正身。然後 415 條怎麼跑?

走到這裡,一個 Case 從 Before、Proposed、Source Evidence 到 Apply,總算比較像同一條 Chain。

很好。

老Go問:

「那 415 條可以開始跑了?」

「可以。」

「跑到第 287 條 Terminal 掛掉呢?」

……

「今天下班前只想先處理十條呢?」

……

「昨天已經跑兩百條,今天從 201 繼續?」

我看了一眼當時的 pass12

好消息。

它很公平。

不管你昨天跑了幾條,今天都可以重新做人。

四百多條 Case。

每一條現在都有身分證、有 Gate、有 Audit Record。

然後整趟最好一次跑完。

嗯。

這個節奏又開始不對了。

我們剛把「你到底驗了誰」這件事管住。

下一個麻煩變成:

這麼長的 Audit,憑什麼不能中途下車?

下一篇,開始做 Worklist、Case Subset,還有真正能用的 Resume。

不然這不是 Audit。

這比較像耐力賽。

Have a nice day.


上一篇
Day 11 - 54% 的 Workbook 根本不用上板測:我們卻先叫 DUT 證明「不用測」
下一篇
Day 13 - 415 個 Case 不可能每次從頭來:Audit 也要會中斷續跑
系列文
一天一個 PR,讓我告訴你嵌入式開發的殘酷:如何為 AI Agent 建立可信的工程閉環21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言