iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
AI Engineering

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

【Day12】把「該有 A_d−1 / A_d / A_d+1」寫成 40 行程式

  • 分享至 

  • xImage
  •  

TL;DR

  • 昨天跳票的人工核對,今天用 40 行程式補上一部分:把「邊界該有三個點」這個 QA 直覺寫成可執行的判定
  • 掃十份案例集,PRD-05 上界的三點命中率:模型 A 中位 0%、模型 B 中位 67%
  • 而 B 缺的永遠是同一個點,界內那一步。它測了邊界上、也測了界外一步,就是不測界內

https://ithelp.ithome.com.tw/upload/images/20260912/20103826gKzJJ91Exa.jpg


前言

昨天那張 RTM 全綠。十條規格全有案例、零裸需求、抽驗 74 條零幻覺。

但綠燈只證明「這條規格有案例提到它」,沒有度量那條案例設計得夠不夠兇

PRD-05 當例子。它寫著大筆支出只有落在 A_r ≤ A_e ≤ A_d 才計入。而 run-a-3 給的兩條案例是 A_e = 70A_e = 90(該份 A_d = 85)——RTM 標綠燈,兩條都指向 PRD-05,覆蓋成立。可是一條離邊界 15 年、一條超出 5 年。壽命當年那一筆到底算不算? 那才是缺陷 C 住的地方,而它沒被碰到。

(BVA,boundary value analysis,邊界值分析:在等價類的交界處取值。標準是三個點——邊界上(on-point)、界外一步(off-above)、界內一步(off-below)。三個都取,才能確定實作把界線劃在我們以為的位置。)

這是 QA 的基本功,也是那種「看一眼案例就知道有沒有做」的直覺。今天要做的事是把這個直覺變成程式,因為 424 條案例,看不完。

有一個麻煩要先講:這支工具的邊界幾乎都不是常數。

無論是 PRD-05 的上下界、PRD-04 的大小關係,甚至勞保請領年齡,全都是使用者輸入。因此,這裡的「邊界」指的是兩個輸入的相對位置,而非寫死的數字。PRD 裡明寫的常數邊界只有兩處:PRD-01r = 0PRD-04 的「金額非負」。

檢核器得吃得下這種關係式,不然它就只能檢查這兩處。

把直覺寫成 40 行

雖然是個笨方法,但正是因為笨才驗得動:對每個案例,算出「左值 − 右值」,然後看這組差值裡有沒有出現 −1、0、+1。

Constraint = tuple[str, str, str | float]   # ("A_e", "<=", "A_d") 或 ("i", ">=", 0.0)
Case = dict[str, Any]                       # {"A_e": 85, "A_d": 85, ...}

def check_bva(constraint, cases, step=1.0, mode="3-point") -> dict:
    lhs_field, _op, rhs_spec = constraint
    required = {-1.0: "off-below", 0.0: "on-point", +1.0: "off-above"}
    # (原檔還有一個 5-point 模式的分支,此處省略)

    deltas = []
    for case in cases:
        if lhs_field not in case:
            continue
        if isinstance(rhs_spec, str):          # 關係式邊界:右手邊是參數名
            if rhs_spec not in case:
                continue
            rhs_val = float(case[rhs_spec])
        else:                                   # 常數邊界:右手邊是數字
            rhs_val = float(rhs_spec)
        deltas.append(float(case[lhs_field]) - rhs_val)

    missing = [label for offset, label in required.items()
               if not any(math.isclose(d, offset * step, abs_tol=1e-9) for d in deltas)]
    return {"score": (len(required) - len(missing)) / len(required),
            "missing": missing, "deltas": sorted(set(deltas)), "pass": not missing}

關鍵在 isinstance(rhs_spec, str) 那個分支。右手邊是字串,就當參數名去案例裡取值;是數字就直接比。一個分支,關係式和常數邊界就都吃得下。

用差值而不是絕對值還有一個好處:同一組三點,換不同的 A_d 仍然算得出來。一個案例是 A_e=70, A_d=70、另一個是 A_e=85, A_d=85,兩者的差值都是 0,都算命中 on-point。這在掃真實案例集的時候是必要的,因為各份輸出的基準情境不一樣。

由人來驗收檢核器

檢核器本身也是程式,它也可能錯。而且它錯的方式很難察覺:回報 100% 命中的錯誤實作,跟真的 100% 長得一模一樣。所以它需要一組人手寫、人持有的驗收案例。這種東西有個名字叫 KAT(known-answer test,已知答案測試):輸入小到眼睛讀得完,預期結果確鑿無疑,不需要另一支程式來判斷對不對。

我寫了 7 組。其中三組是骨幹:

up = ("A_e", "<=", "A_d")

# 1) 完美三點 → 滿分
r = check_bva(up, [{"A_e": 84, "A_d": 85}, {"A_e": 85, "A_d": 85}, {"A_e": 86, "A_d": 85}])
assert r["pass"] and r["score"] == 1.0

# 2) 只給界內遠點與界外遠點 → 三點全缺,且必須指名 on-point
r = check_bva(up, [{"A_e": 75, "A_d": 85}, {"A_e": 95, "A_d": 85}])
assert not r["pass"] and r["score"] == 0.0 and "on-point" in r["missing"]

# 3) 邊界上 + 界外一步,缺界內一步 → 2/3
r = check_bva(up, [{"A_e": 85, "A_d": 85}, {"A_e": 86, "A_d": 85}])
assert r["missing"] == ["off-below"]

第 2 組就是前言那個「70 歲買車、90 歲買長照」的情境——RTM 會標綠燈,BVA 給 0 分。

另外四組管的是容易寫錯的地方:常數邊界(右手邊是數字)、同組差值不同絕對值、案例缺欄位時要跳過而不是當成命中、step 不是 1 的情況(利率以 0.01 為一步)。

這一層我不交給 AI 產。遞迴懷疑要有個底,而這裡就是底。 檔案在 tools/bva_checker.py,連 KAT 一共 128 行,跑起來只印一行「KAT 全部通過(7 組)」。

掃出來的形狀

要把案例餵進檢核器,得先把它們變成 {"A_e": 85, "A_d": 85} 這種參數字典。

這一步我卡了四次。

前三次是基準情境的寫法。十份輸出用了三種:每列一個參數(| A_c 現齡 | 40 |)、一列塞三個(| A_c / A_r / A_d | 40 / 65 / 85 |)、還有根本不設基準,每條案例自己寫滿。解析器每修一次就撞上下一種。第四次是我以為已經分析完之後才發現的。run-B-02 的案例列長這樣:

| TC-05-01 | PRD-05(下界 A_e = A_r 計入) | A_c=64, A_r=65, A_d=70;A_e=65;⋯
| TC-05-02 | PRD-05(上界 A_e = A_d 計入) | 同上,A_e=70

「同上」。第二列只寫差異,其餘沿用前一列。解析器讀不懂這兩個字,於是 TC-05-02 之後全部掉了 A_rA_d——而它們是關係式邊界的右手邊,掉了就算不出差值。

補上「同上」沿用之後,整條流程跑通了。而驗證方式是拿它跟我先前手工確認基準值算出的分數對照:

自動流程(解析器 → 檢核器) vs 人工確認基準值
10/10 完全一致

所以下面這張表是機器算的,不是我讀出來的。工具在 tools/case_extractor.py,兩條指令可以重現:

python3 tools/bva_checker.py    # 先驗收驗收者:KAT 自檢
G=generated/2026-09-09-day10-prd-to-cases
python3 tools/case_extractor.py $G/run-a-*.md $G/run-B-0[1-5].txt

範圍要先講清楚:今天只掃 PRD-05 的上下界這兩條約束,不是十條規格全掃。理由是 PRD-05 的上界正是缺陷 C 住的地方,而且它是關係式邊界(兩個輸入的相對位置),最能考驗檢核器。其餘八條規格的邊界今天沒有量。

上界 A_e ≤ A_d 缺的點 下界 A_e ≥ A_r 缺的點
run-a-1 33% off-below, on-point 0% 三點全缺
run-a-2 0% 三點全缺 0% 三點全缺
run-a-3 0% 三點全缺 0% 三點全缺
run-a-4 0% 三點全缺 0% 三點全缺
run-a-5 33% off-below, on-point 0% 三點全缺
run-B-01 67% off-below 100%
run-B-02 100% 100%
run-B-03 67% off-below 67% off-above
run-B-04 67% off-below 67% off-above
run-B-05 67% off-below 67% off-above

上界:A 中位 0%、B 中位 67%。 下界:A 中位 0%、B 中位 67%。

模型 A 五份的 A_e 取值是 70/86、70/85、70/90、65/82、70/86,大多落在距離邊界十幾二十年的地方。它確實產出了 PRD-05 的案例,RTM 也標了綠燈,但它就是沒有在邊界附近取樣。

而 B 那邊則有個很整齊的形狀:四份全都不約而同地缺了 off-below。那是「界內一步」,A_e = A_d − 1,也就是死前一年的那筆大筆支出。下界也是同一件事的鏡像:三份缺了 off-above,也就是 A_e = A_r + 1,退休後的第一年。

兩個邊界缺的,剛好都是界內那一步。

我想原因是這樣:界外一步會產生看得見的差別(支出被排除、介面出現「已忽略」標註),很容易寫出斷言;而界內一步,跟區間裡的任何一個點看起來都一樣,斷言寫起來平淡無味——它沒辦法讓案例看起來更完整,模型自然沒有理由寫它。

有一件事要說清楚,免得我誇大:這個缺口不會漏掉缺陷 C。 缺陷 C 是上界完全缺席(超出壽命的支出仍被計入),要抓到它,需要 A_e > A_d 並斷言排除,那正是 off-above 的守備範圍。B 有測這個點,A 的 run-a-1run-a-5 也有。兩邊都抓得到那個已知缺陷。

我們缺的是另一個方向的鑑別力;只是那個方向,今天剛好沒有已知缺陷住在裡面而已。

標題騙人,差值不會

run-B-02 拿到上界 100%,是十份裡唯一的滿分。我去看它怎麼做到的。

它的 PRD-05 專屬案例是 A_e = 64 / 65 / 70 / 71(該份的 A_d = 70),on-point 和 off-above 都有,off-below 沒有

那個 −1 來自別的地方:TC-X-04A_d = 66A_e = 65)和 TC-X-06A_d = 67A_e = 66)。前者是為了測「大筆支出不得觸發餘額截斷」,後者是為了測「三類支出同期彙總」。兩條都不是為了邊界寫的,只是剛好落在那個差值上。所以它的 100% 是意外拿到的。

反過來說,run-B-01TC-05-02 標題寫著:

PRD-05(上界含等號 A_e = A_d

讀這個標題,你會以為上界測完了。它確實測了上界,測了其中一個點。

這就是為什麼要量,不能讀。 案例標題是模型寫的自述,跟 RTM 的「宣稱覆蓋」是同一類東西;而差值是從輸入參數算出來的,它不受標題影響。昨天我說「覆蓋是它自己宣稱的」,今天這句話有了第二個例子——連「我測了邊界」也是宣稱。

後記

今天這支檢核器只有 40 行。我原以為難的是它,結果真正難的是前置作業:把文字案例變成參數字典。檢核器與 KAT 一次過關,解析器卻修了四次。

這個比例有點刺眼:判斷邏輯 38 行,取個輸入卻改了四輪。

原因並非模型寫得差,十份輸出在語意上高度一致(都知道要設基準、標規格、單獨測邊界)。它們不一致的是編碼:基準情境有三種寫法,參數有的寫滿有的寫「同上」,案例編號與標記更是五花八門。人讀毫無障礙,機器卻得為每種特例單獨寫分支。

其中最讓我不安的是「同上」那次:前三種格式不同頂多是解析失敗噴錯,但第四種卻是解析成功,但值是錯的(而且是我手工分析完報出數字後才發現)。這讓我意識到:「AI 產出要能自動驗收」,真正的卡點或許不在驗收邏輯,而在產出格式沒有契約

不過這還只是觀察,要驗證得用沒見過的資料。因此我在解析器檔頭下了一道凍結宣告:

本檔對 Day 10 輸出調至可用後即凍結。Day 13 輸出為未知測試集,屆時本檔必須原封不動跑一次,記錄解析成功率。

若在新資料上直接可用,「無契約」一說便被削弱;若又撞上第五種寫法,那就是實證。先把預測寫下來,再去看結果,這招今天已經用第三次了。

今天量完上界命中率後,明天我想試個新方向:PRD-05 的上界子句從規格裡刪除,看模型還會不會寫出那條邊界的案例。如果它照寫,那就有意思了。


只帶走一件事
「我測了邊界」跟「我覺得我測了邊界」,差別在那一組差值裡。而差值不看標題。


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


上一篇
【Day11-番外】十份實驗資料,全部作廢
下一篇
【Day13】我刪掉一條規格,沒有人偷偷把它加回來
系列文
AI 寫的測試,誰來測?14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言