iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Claude AI

用 AI Agent 重構一套無框架的 legacy PHP 系統系列 第 15

Day 15:數值精度——為什麼金額運算要求強制使用 bcmath

  • 分享至 

  • xImage
  •  

前言:這條規則 AI 明明知道,為什麼還是會忘記套用

「金額運算要用 bcmath,不要用浮點數」——這條規則寫在專案規範裡,隨便問哪個 AI coding agent,它都能立刻背出來:浮點數用二進位表示小數會有誤差,0.1 + 0.2 不等於 0.3,金額這種要求精確到分的場景不能冒這個險。

但「知道規則」跟「每一次都會主動套用規則」,是兩件不一樣的事。今天要講一個比前面幾篇更微妙的陷阱:當 AI 面對的不是一段全新程式碼,而是一段已經用浮點數運算、而且已經正式環境跑了很多年沒出過明顯問題的 legacy 邏輯時,它會不會主動指出這裡該改用 bcmath?

今日目標

  • 理解「AI 知道規則」跟「AI 主動套用規則」中間的落差怎麼發生
  • 看兩種相反方向的失敗模式:該用 bcmath 卻沿用浮點數、不該用卻過度套用
  • 建立判斷「這裡到底需不需要 bcmath」的具體邊界
  • 為第二部(Day 6-15)做一次簡短總結,銜接第三部主題

兩種相反方向的失敗,都源自同一個原因

先講第一種常見狀況:重構一段涉及金額計算的 legacy 邏輯時,原本的寫法就是直接用 +-* 做浮點運算。AI 讀到這段程式碼,任務是「把這段邏輯搬到新的方法/新的架構裡,行為保持一致」,它很容易把注意力全部放在「行為要不要一致」這件事上,反而忽略了「這個行為本身在數值精度上其實是有瑕疵的,搬過去的時候該不該順手修正」。AI 傾向把「維持原邏輯不變」當成最安全的選項,卻沒意識到,繼續延用浮點運算,其實是把一個已知的風險原封不動地帶進新程式碼裡。

反過來也有第二種失敗:AI 讀過一次規則之後,變得「過度熱心」——只要看到任何帶小數點的數字運算,不管是不是金額,都套用 bcmath。日誌裡記錄一個百分比、UI 上顯示一個進度條的比例,這些場景本來就不要求精確到小數點後好幾位,硬套 bcmath 只是增加程式碼複雜度,甚至讓原本一行就能寫完的運算變成好幾行字串函式呼叫。

這兩種失敗表面上方向相反,根源卻是同一件事:AI 沒有真正判斷「這個數值在這個情境下要不要求精確」,而是用一個更省力的替代判斷——「原本就是浮點數就維持浮點數」或「看到小數點就套用 bcmath」——取代真正的判斷。

一組對照:什麼時候該用、什麼時候不該用

❌ 沿用原邏輯,沒有主動指出風險:
$total = $unitPrice * $quantity * (1 - $discountRate);
→ 這是金額運算,浮點誤差在多次運算疊加後可能造成分帳對不上,
  AI 只是原樣搬過去,沒有標注這裡需要重新評估精度需求

❌ 過度套用,不需要精確度的地方也強制改寫:
$progressPercent = bcdiv(bcmul((string)$done, '100', 4), (string)$total, 2);
→ 進度條百分比不涉及金錢對帳,用浮點數完全足夠,
  硬套 bcmath 只是增加不必要的字串轉換與可讀性成本

✅ 先判斷情境,再決定要不要套用:
// 金額運算:涉及對帳、需要精確到分 → 用 bcmath,並明確指定 scale
$total = bcmul(
    bcmul($unitPrice, (string)$quantity, 4),
    bcsub('1', $discountRate, 4),
    2
);

// 非金額運算:只是顯示用途,浮點數精度足夠 → 維持浮點數,不用增加複雜度
$progressPercent = round(($done / $total) * 100, 2);

判斷標準不是「這是不是小數」,而是這個數值最終會不會被拿去對帳、加總、或者兩個系統之間互相核對——只要答案是會,就算現在看起來誤差很小,也要用 bcmath;只要答案是不會,浮點數的精度已經綽綽有餘,不需要為了規則而增加複雜度。

這個陷阱跟「用命名猜型別」是同一種問題的不同版本

這系列前面提過,Repository 層宣告欄位型別時不能用命名前綴猜(nTotalAmt 這種命名看起來像整數,實際上資料庫欄位是 decimal),要求先查實際的資料表結構再宣告。bcmath 這條規則背後其實是同一個道理:不能用「這段程式碼看起來是什麼樣子」去判斷該怎麼處理,要回到「這個數值實際上代表什麼、會被怎麼使用」這個問題本身。

命名猜型別是靜態的線索誤導,浮點運算沿用舊邏輯則是「歷史慣性」的線索誤導——兩者都是 AI 傾向依賴表面訊號、跳過真正判斷的具體樣貌。這也再一次呼應 Day 01 的主題句:AI 給出的「已確認」結論,只在它實際查過的範圍內成立——如果 AI 只查了「這段程式碼原本長什麼樣子」,卻沒有查「這個數值在業務邏輯上要不要求精確」,那個「維持原樣應該沒問題」的結論,查證範圍本身就不夠。

原則語言無關,bcmath 是 PHP 的實現方式

金額運算要用高精度數值型別,這是語言無關的原則——Java 有 BigDecimal,Python 有 Decimal,JavaScript 生態常用 decimal.js 這類函式庫,講的都是同一件事:不要讓涉及金錢對帳的數值,暴露在二進位浮點數的表示誤差之下。bcmath 只是 PHP 標準函式庫提供的具體工具,換一個語言環境,工具會不一樣,但「這個數值需不需要精確」這個判斷本身,是完全可以遷移的。

第二部回顧:讓 AI 安全動手的核心機制

到這裡,第二部(Day 6-15)算是告一段落。這十天講的東西,表面上很分散——乾淨基準比對、獨立 subagent、Repository 分層、外部 API 介面化、bcmath——但背後其實是同一條主線:把 AI 需要憑感覺判斷的空間,一點一點收斂成可以被驗證、可以被檢查的具體邊界。 覆蓋率報告告訴 AI 這一行能不能動,Repository 介面讓 AI 不用自己組危險的 SQL 字串,PSR-17/18 讓外部依賴變得可以被 mock 掉,bcmath 讓「這個數值要不要求精確」變成一個有明確答案的問題。

這些機制單獨看都是技術細節,合起來看,才是這系列真正想講的東西:流程設計的目標,從來不是讓 AI 變得更聰明,而是讓它需要自己判斷的範圍越來越小。

今日思考題

回想你手上的專案裡,有沒有哪段數值運算是「因為原本就這樣寫,所以沒特別想過要不要改」?那段運算最終的結果,會不會被拿去跟另一個系統對帳?

今日重點回顧

  • AI 知道 bcmath 規則,不代表它每次都會主動判斷「這裡該不該套用」
  • 兩種相反的失敗:沿用有精度風險的浮點運算、或對不需要精確度的地方過度套用 bcmath
  • 判斷標準是「這個數值會不會被拿去對帳/加總/跨系統核對」,不是「看起來是不是小數」
  • 這跟「用命名猜型別」是同一種問題:依賴表面線索,跳過對數值實際用途的判斷
  • 高精度數值運算是語言無關的原則,bcmath 只是 PHP 的具體實現
  • 第二部(Day 6-15)的主線:把 AI 需要憑感覺判斷的空間,收斂成可驗證的具體邊界

明日預告

第二部到這裡結束,明天進入第三部:AI Agent 的長期記憶與團隊協作。第一篇要講的是 CLAUDE.md 跟 skill 之間怎麼分工——為什麼有些規則放在「每次都要讀」的地方,有些規則卻要設計成「只有碰到特定情境才載入」。


上一篇
Day 14:型別提示具體類別 vs 介面——DI 容器 autowire 出空殼的真實案例
下一篇
Day 16:讓 AI 記住「這個專案的規矩」——CLAUDE.md 與 skill 的分工
系列文
用 AI Agent 重構一套無框架的 legacy PHP 系統21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言