iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
AI Engineering

AI 寫的測試,誰來測?系列 第 17

【Day15-番外】六項不合格,沒有一項是 AI 的錯

  • 分享至 

  • xImage
  •  

TL;DR

  • 正文用三個字帶過的「基底有了」,實際上跑了兩輪
  • 第一輪 6 項不合格:5 項是我的量尺寫錯,1 項是我的規格沒寫
  • 系列問「AI 寫的測試,誰來測?」,這篇問的是那個「誰」自己準不準

https://ithelp.ithome.com.tw/upload/images/20260917/20103826DMxTF8ZMk6.jpg


前言

正文〈第一個問題:對著誰跑〉用一句話帶過了整個過程:

過程比結果曲折得多:第一輪沒過、我的量尺自己出錯、規格被逼出第十二條。

這篇寫那個過程。不是實驗結果,是踩坑紀錄。

這個系列問了十五天「AI 寫的測試,誰來測?」——而這個問句預設了有一個「誰」。
今天那個誰是我和我的三道驗收器。六項不合格報出來的時候,每一項看起來都像「AI 出錯了」。

第一輪:6 項不合格,只有 1 項是產物的

規格(PRD v1.1)與介面契約貼進隔離環境,模型吐出 calc_fixed.py,三道驗收器跑下去:

❌ 驗收器 1 diff 逐項分解:5 項不合格
❌ 驗收器 2 四條缺陷測試全紅:1 項不合格
✅ 驗收器 3 非法參數拋 ValueError:通過
共 6 項不合格,calc_fixed.py 不得進 repo

看起來是一場失敗。逐項追下去,6 項裡只有 1 項真的在產物身上。

兩個洞,都在量尺

我只數了兩條,實際上改了四條

驗收器 1 的規則是我事前寫死的:差值必須等於「只改 PRD-01」與「只改 PRD-02」兩個分支算子各自貢獻之和。

不合格的五組是 inc_labor_65inc_labor_70inc_bothdef_c_ghostdef_c_ghost_far——三組跟退休後收入有關,兩組跟大筆支出有關。

而 PRD v1.1 相對 legacy 改動的條款有四條:除了 PRD-01(複利頻率)與 PRD-02(提領首年),還有 PRD-05(大筆支出上界)與 PRD-10(收入通膨基準年)。我在寫規則的時候把「改了哪幾條」數錯了。

補上另外兩個分支算子之後:

案例 實際差 2 條分解 4 條分解 殘差
inc_labor_70 3,003,631 567,684 3,003,631 0.00
def_c_ghost −1,193,639 567,684 −1,193,639 0.00
def_c_ghost_far −3,428,202 567,684 −3,428,202 0.00
inc_labor_65 3,006,735 113,537 2,840,588 166,147
inc_both 1,409,607 0 1,328,658 80,949

三組歸零,兩組還是對不上。

「條款貢獻可加」這個假設是錯的

inc_labor_70 閉合而 inc_labor_65 不閉合,兩者只差在勞保的請領年齡。這就是線索。

PRD-02 多算的那一年是 65 歲,而 65 歲那年剛好有勞保收入,那筆收入又受 PRD-10 影響——兩條款在同一個格子上相遇。inc_labor_70 之所以閉合,是因為勞保 70 歲才起領,新增的那一年沒有收入,就沒有交互項。

案例 單獨相加 四條同時開 產物 產物 − 同時開
inc_labor_65 2,840,588 3,006,735 3,006,735 0.00
inc_both 1,328,658 1,409,607 1,409,607 0.00

單獨相加 ≠ 同時開啟,而產物與「同時開啟」逐位元相符。產物從頭到尾都算對了,是我的判準寫了一個不成立的假設。

我在 manifest.yamlexpectations 裡還寫著:

gate_1: 會過。PRD-01 與 PRD-02 在算式上互不相干,疊加應該成立

這句話是跑之前寫的,而它兩個部分都錯了——不只數錯條款數,連「互不相干」都是假的。

依 Day 10 立的規矩「跑之前定、跑完不改」,我沒有回頭改第一輪的判準。缺陷記進 execution_status,修正版另立實驗編號。

還有一個:清洩漏時把該留的一起刪了

第一輪的產物,進入點叫 calculate_retirement,而契約要求的是 calculate

我一開始以為是模型沒照契約走。翻回輸入才發現是我自己把那一行刪掉的。

起因是清洩漏。第一版貼上檔被我掃出 11 個洩漏關鍵詞,最嚴重的一條是規格書的「尚未涵蓋」一節直接寫著:

缺陷 B(壽命倒掛)與 A'(負值遮蔽)——⋯⋯它們本來就不是規格問題,是實作沒做好

四個缺陷被說出兩個。驗收器 2 的整個意義是「舊缺陷不得遺傳」,而輸入裡點名了其中兩個,那道門當場失效。

我重建了輸入,掃描歸零。但重建時,介面契約的程式碼區塊少了一行 def calculate(p: Params) -> Result:,而文字部分還寫著「公開以下三個 dataclass 與一個函式」。模型看到「一個函式」卻沒看到簽名,只好自己取名。

成因是我的掃描只做了一半:它檢查「不該有的字串在不在」,沒有檢查「該有的內容還在不在」。 第二輪改成雙向——must_not 26 項全為 0、must_have 18 項全在位,兩邊都過才放行。

剩下那 1 項:規格確實沒寫

驗收器 2 的不合格項是 test_defect_a_prime_clamp_hides_deficit 在產物上仍然通過,缺陷 A'(負餘額被夾 0)被遺傳了。產物寫的是:

balances_charted = tuple(max(0.0, b) for b in balances_raw)

這正是我跑之前猜中的那一項。但這個「猜中」很不舒服,因為模型把它列進了檔頭的 GAP 清單,還寫了理由:規格未定義此欄位的具體行為,採用 max(0, balance),因為通常圖表端不預期顯示負值餘額。

Day 10 立過一把尺:罪在不標示,不在推論。 依那把尺,這是好行為——它推論了,而且說了。依驗收器 2,這是不合格。同一個行為,兩把尺相反判定。我沒有回頭改任何一把,照實記在 execution_status 裡。

而模型是對的。去翻 PRD v1.1 的 PRD-03,邊界處置欄寫的是「禁止 Math.max(0, ...) 截斷真實餘額」——「真實餘額」是 balances_rawbalances_charted 那一欄規格從頭到尾沒有提過。

這不是 AI 讀不懂規格,是規格有洞。而缺陷 A' 是 Day 3 驗屍報告認定的四個缺陷之一,規格本來就該禁止它。閘門 1 裁決新增 PRD-12:

balances_charted 必須逐項等於 balances_raw,不得下限截斷。圖表要不要顯示負值是呈現層的事,計算核心不得代為隱藏。

代價看 golden 案例 def_ap_clamp(0 元本金、0 月存、月支出 3 萬):

年度 真實餘額 v1.1 下的圖表
第 1 年 −602,199 0
第 10 年 −8,254,225 0
第 20 年 −21,232,803 0

二十年全部被夾平。使用者看到一條貼著 0 的線,實際缺口是 2,123 萬。

這條規格的來源跟前面十一條都不一樣。 RC-01 到 RC-10 是我翻程式碼翻出來的,PRD-11 是我對照介面欄位發現的。PRD-12 是一個沒看過舊程式的模型,在照規格實作時撞出來的——不是被考古挖出來的,是規格自己被用一次才露餡。

不過有個懷疑沒解決:PRD-03 的條文「禁止 Math.max(0, ...) 截斷真實餘額」,在禁止的同時也把 max(0, ...) 這個寫法示範給了模型。所以我那個「猜中」有多少是模型的傾向、有多少是我自己種的,分不開。閘門 1 裁決不修改條文(那句話從 JS 受測物考古而來,改掉會跟 RC-05 與 Day 3 的敘述脫節),改為每一輪在 manifest.yaml 明列為效度威脅 VT-1

第二輪:三道全過,而「猜中」沒有資訊量

規格升到 v1.2、介面契約補回函式簽名、量尺換成四個分支算子並改比「同時開啟」。同一個模型、同一個介面,唯一該變的是規格多一條。

✅ 介面契約:相符
✅ 驗收器 1 通過
✅ 驗收器 2 通過
✅ 驗收器 3 通過

產物這次寫的是 balances_charted=balances_tuple,還自己註明 # PRD-12: 圖表餘額保真。實測兩者逐項相等,末年皆為 −22,526,423。

「全紅」我逐條查過,怕是每條都拋例外造成的假象:

缺陷測試 紅的機制
a_prime_clamp AssertionErrorassert r.balances_charted[-1] == 0.0
a_target_uses_ordinary_annuity AssertionError 於期初/期末年金比值
c_ghost AssertionErrorwith_ghost.target_fund > without.target_fund
b_inverted ValueError——輸入在任何斷言執行前就被 PRD-04 擋下

最後那條仍是正確行為,但紅的機制與另外三條不同,記下來。

但這次的「猜中」沒有資訊量。 我跑之前寫「gate 2 會過」,它過了——可是 PRD-12 是我剛寫進去的明文,模型照做是應該的。真正有資訊的是第一輪那次:規格沉默的時候,它會自己做選擇,而且會告訴你。

還有一個沒猜到的。兩輪的 GAP 清單都是 5 條,第一輪的 GAP-1balances_charted 未定義)被 PRD-12 補掉,符合預期;但第一輪那條 GAP-5「其他收入沒有對應的請領年齡參數」,第二輪它沒提,而規格在這一點上一個字都沒改。反過來,「多筆大筆支出落在同一年」第一輪沒提、第二輪提了,我兩輪都猜它會提。

temperature = 1,輸出本來就不是決定性的。同一份規格的缺口清單,跑兩次不一樣——單靠一次生成去盤點規格缺口是不夠的。

意外收穫:證據層級要修正

第一輪的 preflight 我問的是「你現在能使用哪些工具或功能?」,模型回了一篇產品功能簡介:網頁搜尋、圖像生成、程式碼執行、文件分析。

那是產品能力,不是本次對話的開關狀態。這份 preflight 不能當隔離證據。

第二輪改了問法,要求只回答這次對話的實際狀態,並附上 AI Studio 側欄截圖。證據齊了:Gemini 3.1 Pro Preview、temperature = 1、Thinking level High、六個工具全 OFF、System instructions 空白。Day 13 那兩行紀錄缺失(temperature 與 thinking 沒記錄)這次補上了。

但截圖跟模型自述對不上。截圖的 System instructions 欄位是空的,模型卻說:

是的,目前有一條系統提示正在生效,逐字內容為:"Your knowledge cutoff date is January 2025."

本系列原本的證據層級是「介面截圖 > 作者口述 > 模型自述」,照這個排序該信截圖。但兩個都沒說謊——UI 顯示的是「使用者設定的那一格」,模型看到的是「使用者設定的 + 平台注入的」。

截圖看不到平台注入的那一層,只有模型講得出來。排序要修正:截圖與模型自述不是上下級,是互補。


這篇沒有做的事

兩輪都是單次生成(runs: 1)。這是產物實驗不是分佈實驗,所以不能拿來宣稱「PRD v1.2 足以支撐實作」——那種說法要 ≥5 次獨立生成。

產物的 balances_raw 語意與 legacy 不同(它記期末餘額,legacy 記期初扣款後滾存),模型在 GAP 裡標示了。因此 golden_set_v2.jsonfinal_balance_raw 不可與 v1 直接比較。


只帶走一件事
「AI 寫的測試,誰來測?」——這個問句預設了有一個「誰」。
那個誰,誰來測?


今天的 repo:https://github.com/eyelash500/2026_ironman_test_ai/tree/day15


上一篇
【Day15】424 條案例,只有一小撮值得寫成 pytest
下一篇
【Day16】CI 全綠,但 AI 只寫了一句 assert result is not None
系列文
AI 寫的測試,誰來測?21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言