iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
AI Engineering

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

【Day5】打造影子模型:抽取,還是重寫?

  • 分享至 

  • xImage
  •  

TL;DR

  • 要驗證一支程式算得對不對,你需要第二個獨立的實作——但「獨立」到什麼程度,是一個要人來裁決的問題
  • 機械式抽取和重新實作,會得到兩種完全不同的測試:一種驗移植,一種驗規格
  • 我選了機械式抽取,而這個選擇同時決定了明天的差分測試能抓到什麼、抓不到什麼

https://ithelp.ithome.com.tw/upload/images/20260905/20103826jaBfSg0kbZ.jpg


前言

前四天都在講「它哪裡壞了」。今天開始做「怎麼證明它壞了、以及往後不再壞」。

第一件工具是影子模型:一支獨立的參照實作,專門用來回答「這個數字到底該是多少」。

(影子模型:在金融業,重要的計價模型通常會有第二套獨立實作來對照,用來檢查主模型有沒有偏掉。這裡借用同樣的精神。)

為什麼不能直接測那支 HTML

受測物是一支 416 行的單一 HTML 檔。麻煩的不是大小,是「計算邏輯的位置」,整段包在一個按鈕的事件處理器裡面:

calculateBtn.addEventListener('click', () => {
    const currentAge = parseInt(document.getElementById('currentAge').value);
    // ...一百多行的計算,中間夾著 document.getElementById、
    //    querySelectorAll、textContent 指派
});

沒有一個叫 calculate() 的函式可以呼叫。要拿到計算結果,唯一的入口是「按下按鈕」。

所以想測它,我們得開瀏覽器、填表單、按按鈕、讀畫面上的數字。跑十組可能還行,跑五百組就會開始遇到渲染延遲、非同步、DOM 查詢失敗這些跟「計算對不對」完全無關的問題。

更糟的是昨天講過的那件事:畫面上的數字已經被美化過。程式碼在第 374 行將負餘額夾成 0,我們從圖表資料取值,拿到的是被夾過的結果。因此用畫面當測試對象,等於讓遮羞布同時騙過使用者和測試。所以第一步不是寫測試,是把計算核心從畫面裡剝出來

閘門 2:抽取,還是重寫?

把計算核心從畫面裡剝出來的方式有兩種,而它們會長成完全不同的東西。

  • 做法一:機械式抽取。 打開 HTML,把計算那段逐行搬成 Python,連寫法都盡量對應。變數名照抄,迴圈邊界照抄,Math.max(0, ...) 也照抄。
  • 做法二:重新實作。 不看原始碼,只看「這個工具該做什麼」,從財務數學重新寫一份。

兩種都叫「獨立參照實作」,但它們回答的是不同的問題:

機械式抽取 重新實作
差分測試在測什麼 我有沒有搬錯 它有沒有寫對
缺陷 A 會不會被抓到 不會(兩邊都錯) 會(兩邊必然不同)
什麼時候有用 行為固定、回歸防護 驗證規格符合度
前提 不需要規格 需要規格

第二欄看起來比較好——它能直接抓到缺陷 A。但它有一個前提:你得知道「對」是什麼。

而這支工具沒有規格。沒有任何一份文件說過錢該在年初還是年底離開帳戶。如果我現在就「照財務數學重寫一份」,我寫的其實是「我自己認為」的正確版本,然後拿它去說原版錯了。那不是驗證,是用一個沒人審過的假設去否定另一個沒人審過的假設。

所以我選機械式抽取。

這個決定的代價要講清楚:明天的差分測試,抓不到四個缺陷裡的任何一個,兩邊會一起錯,而且錯得一模一樣。

那它有什麼用?

先釘死,才有資格談對錯

這支抽取出來的 oracle,回答的是一個基礎但關鍵的問題:這支程式「現在」到底是怎麼動的?

聽起來很廢,但這是所有後續工作的地基。等到後面我要修這四個缺陷的時候,我需要知道哪些行為改變是「修好了」,哪些是「順手改壞了別的」。沒有一份釘死現況的參照,改完之後你分不出來。

至於「它寫得對不對」——那個問題要等第二幕,我得先將規格挖出來,有了規格才有資格重寫,重寫的版本才有資格當裁判。

這也是為什麼這系列的順序是這樣排的:先釘行為(第一幕),再挖規格(第二幕),最後才談測試夠不夠兇(第三幕)。

https://ithelp.ithome.com.tw/upload/images/20260905/2010382649gROhmJuq.jpg

抽取的邊界劃在哪

讀完那 210 行 script,可以看出界線其實很清楚:

行數 內容 處置
274–290 從 DOM 讀十幾個欄位 改成函式參數
292–375 純計算 逐行照抄
377–402 寫回畫面、改 CSS class、畫圖表 改成回傳物件

所以抽取規則我寫成四條,放在 oracle/calc.js 的檔頭:

1. 292–375 行逐行照抄,運算順序、括號位置、變數名一律不動
2. 讀 DOM 的輸入改為函式參數
3. 寫 DOM 的輸出改為回傳物件
4. 不修正任何已知缺陷——包括 293 行的無防護減法、297 行的 /12、321 行的期末折現、329 行的缺上界、374 行的夾 0。這是 oracle,不是修正版

第 4 條最重要。抽取的時候手指頭會很癢,尤其看到 Math.max(0, remainingFund) 那行,但只要動它一下,oracle 就不再是「現況的紀錄」,而變成「我認為的正確版」(那就回到前面說的問題了)。

只有一個東西是新增的,而且我在註解裡標明「抽取時新增」:

balancesRaw.push(remainingFund);              // 抽取時新增:未夾 0 的真實餘額
assetData.push(Math.max(0, remainingFund));   // 374 遮羞布,原樣保留

原版只有後面那行。我多留一份未經處理的餘額——不是修掉遮羞布,是在遮羞布旁邊放一臺不會說謊的儀器。

Python 這邊的骨架:

@dataclass(frozen=True)
class Params:
    current_age: int
    retirement_age: int
    life_expectancy: int
    current_savings: float
    monthly_investment: float
    monthly_expense_today: float
    annual_income_after_retirement: float = 0.0   # 退休後年收入
    pre_retirement_return: float = 0.08
    post_retirement_return: float = 0.04
    inflation_rate: float = 0.02

退休後多半還會有一點兼職或被動收入,所以我憑著直覺,順手多留了 annual_income_after_retirement 欄位。

三個刻意的設計:

  • frozen=True:參數進去之後不能改。金融計算最怕的就是「算到一半有人動了某個變數」,凍結它,這種 bug 從根上消失。
  • 純函數:輸入參數,回傳結果,不碰任何全域狀態、不碰畫面、不碰時間。同樣的輸入永遠給同樣的輸出——這是能跑五百組比對的前提。
  • 回傳真實餘額,也回傳被夾過的。
balances: tuple[float, ...]          # 逐年真實餘額
balances_charted: tuple[float, ...]  # 圖表看到的(夾 0 後)

這是為了缺陷 A'。我需要能同時看到「真相」和「畫面呈現的東西」,才能量出兩者差多少。如果影子模型只回傳一種,我就等於把遮羞布也一起抄過來了。

誰來驗收這支影子模型

這支 Python 是 AI 產的,那誰來檢查它有沒有搬錯?

規則是:每個 AI 產物都要有一個在它生成之前就由人定好、而且不是它自己給的驗收器。

影子模型的驗收器就是明天要做的差分測試,用五百組隨機輸入,Python 和 JavaScript 兩邊跑,逐筆比對。驗收標準我現在就定下來,不等看到結果再定:

  • 五百組全部相符才算通過
  • 不相符的話,先假設是我的移植錯,不是受測物錯
  • 「相符」的容許誤差在明天正式定,但必須在跑之前定

最後那條是重點,跑完再定容許誤差,等於照著結果調標準。因此我先填 1e-9,明天就知道這個數字會不會打我的臉。

後記

寫這篇的時候,我一直很想在抽取的同時「順手把 Math.max(0, ...) 拿掉」。

那行就在眼前,改它只要刪七個字元,而且我知道它是錯的——昨天才花一整篇在講它怎麼把 81.6 萬藏起來。

沒改的理由不是紀律,是我想不出改了之後 oracle 還能拿來做什麼。它一旦跟受測物不一樣,明天的差分測試就會噴出一堆「不符」,而那些不符全部來自我自己動的手,不是來自任何真正的問題。

oracle 的價值完全來自它跟受測物一模一樣,包括一模一樣地錯。

這件事說起來很順,但手放在鍵盤上的時候完全不是那樣。


只帶走一件事
沒有規格的時候,你的「獨立實作」只能證明自己抄得對,不能證明原版寫得對。


免責聲明:本文提及之退休試算工具與所有數字皆為假設性試算,不構成任何投資建議。該工具目前已知存在計算偏差,修正版製作中,結果僅供參考。


上一篇
【Day4】那個 (1+r) 是怎麼來的
下一篇
【Day6】500 組差分,第一次就掛了
系列文
AI 寫的測試,誰來測?6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言