iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
AI Engineering

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

【Day8】規格考古:十個沒有人做過決定的地方

  • 分享至 

  • xImage
  •  

TL;DR

  • 這支工具沒有 PRD,但它每一行都在替使用者做決定——我把那些決定挖出來,數了十個
  • 十個裡面只有三個對應到已知缺陷,其餘七個現在「沒有壞」,但也沒有人能說它是對的
  • 「叫 AI 讀程式碼把規格補出來」是循環論證:程式碼怎麼寫,規格就怎麼寫

https://ithelp.ithome.com.tw/upload/images/20260908/20103826niiYGL5o6S.jpg


前言

第一幕結束在一句話:我知道它現在怎麼動了,但還不知道它應該怎麼動。

今天開始處理後面那半句。

(規格考古,spec recovery:面對一個沒有需求文件的既有系統,從程式碼、UI 文案、預設值、歷史版本裡逆向反推出「它當初被期待做什麼」的過程。名字聽起來很酷,實際做起來像在整理別人的抽屜——而那個別人是去年的我。)

那份 PRD 不存在

這支工具 2025 年 9 月上線,跑了一年。它沒有需求文件、沒有規格書、沒有驗收條件。有的只是一段我丟給 AI 的對話,和一份 416 行的 HTML。

沒有任何一句話寫過「退休那一年算不算支出」,但程式碼寫了。

第 306 行的迴圈從 i = 1 開始,age = retirementAge + i,所以第一個被計算的年份是 66 歲,也就是退休那年不計支出,壽命那年計入。這是一個決定,它已經生效一年了,只是從來沒有人做過它。

這就是第二幕要處理的東西。不是找 bug,是找那些沒有人做過、卻已經在生效的決定

為什麼不能叫 AI 讀程式碼補規格

最直覺的做法是:把 oracle/calc.js 丟給 AI,說「幫我從這段程式碼整理出規格文件」。它會給你一份看起來很專業的東西。而那份東西的每一條,都只是把程式碼換成中文再寫一遍。

Day 5 我們拒絕過同一件事:那時候是「沒有規格,就不能用重寫的版本當裁判」。今天是它的鏡像:沒有規格,就不能把程式碼當規格。程式碼是被告,不能兼任證人。~~(裁判、球證、旁證、技術委員、主辦、協辦,所有單位都是我的人,你們怎麼跟我鬥!)~~如果那樣做,第 306 行i = 1 會變成規格書裡的「退休當年不計支出」,語氣篤定得像有人想過。實際上沒有。

十個現場

所以我改用笨方法:把計算核心從頭到尾走一遍,每遇到一個「程式替使用者做了決定,而原始碼裡沒有任何一句話解釋為什麼」的地方,就記一筆,走完是十個。

# 現場 程式碼座標 程式默默決定了什麼 性質
RC-01 退休當年與壽命當年 306349 退休那年不計支出,壽命那年計入 沉默決定
RC-02 折現與提領的時點 321 vs 372 目標金額用期末、提領用期初 已知缺陷 A
RC-03 名目月利率 297 r/12,不是有效月利率 歧義
RC-04 同帳戶兩種複利頻率 296 vs 298 本金年複利、月存月複利 歧義
RC-05 淨支出下限截斷 320363 收入大於支出時,盈餘直接蒸發 歧義
RC-06 大筆支出沒有上界 329 只檢查下界,身後的支出也計入 已知缺陷 C
RC-07 退休收入的通膨處理 312–318 勞保、勞退全額隨通膨逐年調升 假設
RC-08 年支出的通膨基準 302–303310 月支出×12 與年度支出共用同一組通膨指數 沉默決定
RC-09 報酬率的切換點 296 vs 321372 退休當年起,全部切換為退休後報酬率 沉默決定
RC-10 輸入防呆 215275–290 金額欄位靜默補 0、年齡欄位 NaN 整筆拋棄 已知缺陷 D

數一下:十個裡面只有三個是已知缺陷。

另外七個,程式現在跑得好好的,測試全綠,使用者也沒有抱怨,但它們每一個都是一個沒被回答的問題。而缺陷 B 和 A' 不在這張表上——它們不是規格問題,是實作沒做好。壽命填相反要不要擋,跟「退休那年算不算支出」不是同一種問題:前者有標準答案,後者需要有人拍板。

挑兩個看

RC-03:那個 /12 到底差多少

第 297 行把年報酬率除以 12 當月利率。Day 3 提過它,但沒算過它值多少錢。

要先講清楚它用在哪裡:只用在月存那一段。 第 296 行的既有本金走的是 Math.pow(1 + r, 年數),正正經經的年複利;r/12 只出現在第 298 行的月存終值公式裡。

所以拿基準情境(42 歲、65 歲退休、報酬率 8%、月存 2 萬)實跑那一段:

用 r/12(現況)        15,774,622
用有效月利率           15,142,806
差額                      631,816

**整整六十三萬。**這裡我刻意只寫絕對金額,不寫百分比。原因在 Day 4 提過:這 63 萬如果除以現況值是 4.01%,除以正確值卻是 4.17%,同一件事兩種說法,不如直接看金額。而這 63 萬的灌水,使用者在畫面上完全看不到。它唯一的功用,就是默默讓你的退休金看起來更充裕。

但它是不是錯的?我沒有明確答案。我的直覺是「銀行的定存、房貸都這樣算,這是業界通則」——但那只是直覺,我沒查過。

如果使用者心裡想的是「我的投資年化 8%」,那就高估了;如果想的是「每月 0.667%」,那就是對的。而沒有任何一份文件說過是哪一種,這才是「歧義」的意思。不是誰算錯,是問題本身沒被問完整。順帶一提,這一條跟 RC-04 是同一個現場的兩面:同一個帳戶、同一個報酬率,本金按年滾、月存按月滾。兩種頻率並存在相鄰的兩行。

RC-07:程式比勞保局大方

第 312–318 行,勞保年金、勞退月領、其他收入,全部逐年乘上通膨係數:

inflatedAnnualIncome += laborInsurancePension * 12 * Math.pow(1 + inflationRate, yearsFromNow);

也就是說,程式假設勞保年金每一年都會足額跟上通膨

現實不是這樣。依《勞工保險條例》第 65 條之 4,勞保年金要等到消費者物價指數累計成長率達 5% 才調整一次。實際運作起來是這樣:2026 年 5 月,2011、2012、2016、2017、2018、2022 這六個年度請領的人調升 6.46%,約 76 萬人受惠,而在那之前,他們已經好幾年沒調過了。程式是連續的,現實是階梯式的,中間那幾年的落差,這支工具看不到。

這一條我標成「假設」而不是「歧義」,因為它跟現實有明確的落差。只是這份落差並不是出於刻意簡化,而是因為我當初寫程式的時候,根本就沒有想到。

規格提取的範本

十個現場我不打算靠記憶管理,每一個都寫成同樣的格式,放進 spec/ 底下。以 RC-05 為例:

# RC-05:淨支出下限截斷

- **性質**:歧義
- **程式碼座標**:`sut/進階退休規劃試算.html` 320、363 行

## 現有實作

    const netExpenseForYear = Math.max(0, inflatedAnnualExpense - inflatedAnnualIncome);

## 隱含假設

某年度收入大於支出時,該年淨支出以 0 計。多出來的錢既不抵扣其他年份,
也不滾入本金——直接消失。

## 待裁決

- A. 維持現況:盈餘不滾存。保守,避免高估晚年資產
- B. 允許淨支出為負:盈餘滾入本金並產生報酬
- C. 全期平準:累積期與提領期之間動態再平衡

## 裁決

最後那一格今天是空的。

分界在最後一欄:「待裁決」的選項可以由 AI 列,「裁決」那一欄不行。

上面那三個選項確實是 AI 幫我列的——列出技術上可行的做法,它做得比我快也比我期權。但「這個工具要對使用者做出什麼承諾」是產品決定,不是程式決定。這也是為什麼閘門 1 到明天才登場。今天只負責把問題找齊,不做答。找問題和做決定分開,是為了避免一邊挖一邊順手把自己喜歡的答案塞進去。

這十條會變成什麼

明天開始,每一條都要有一個裁決,而每一個裁決都要能變成一條可執行的測試。到那時候才會知道,Day 7 那 23 組 golden set 裡面,有多少組鎖住的是「對的行為」,多少組鎖住的只是「當初剛好那樣寫」。

我現在的猜測是後者比較多。

後記

整理這十條的時候,我一直有一種很怪的感覺:我在幫一個不存在的人做會議紀錄。

那些決定確實被做出來了——程式在跑,使用者在用,數字有出來。但沒有任何一個時刻,有人坐下來說「我們決定退休當年不計支出」。它就那樣從 i = 1 裡長出來了,然後生效了一年。我去年寫那段程式碼的時候,腦袋裡在想的是「迴圈要從 1 還是從 0 開始」,一個純粹的語法問題。一個語法選擇,變成了一條沒人知道的業務規則。


只帶走一件事
沒有規格的測試,只是把程式碼的偏見換一種語法再寫一遍。


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


上一篇
【Day7】把錯的行為,正式寫進測試裡
下一篇
【Day9】閘門一:替一年前的自己做決定
系列文
AI 寫的測試,誰來測?14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言