iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
ChatGPT & Codex

AI 救得了祖傳系統嗎?30 天實戰企業 Legacy System × AI 協作開發系列 第 13 篇

Day 13|祖傳系統的金額為什麼突然變成 0?Debug 時先追資料,不要先改公式

  • 分享至 

  • xImage
  •  

前言/情境導入

上一篇,我花了不少篇幅處理一個在 Legacy System 裡很現實的問題:

沒有完整的 Unit Test,也沒有自動化的 Regression Test,AI 改完程式之後,我到底要怎麼知道它沒有改壞?

我的做法不是先相信 AI,而是先建立 Baseline,再用 Row Count、Aggregate、Sample Row 與 Production 歷史資料的形狀,一層一層建立 Evidence。

但真的進入 Debug 現場之後,還有另一個很容易踩進去的陷阱。

最近我處理一個工程資料轉入問題。

使用者反映:

某些資料轉入之後,金額會變成 0。

看到「金額變成 0」,第一個直覺通常是什麼?

很容易先想到公式。

金額 = 數量 × 單價

是不是數量算錯?

是不是單價沒有帶到?

是不是原本應該加總,結果被覆蓋?

甚至可以直接把相關函式丟給 AI:

「這段 Delphi 金額計算有問題,請幫我找出為什麼會變成 0。」

AI 很快就能開始分析公式。

但這次我真正學到的事情反而是:

畫面上的金額是 0,不代表算式把它算成了 0。

它也可能在進入公式以前,就已經是 NULL、空值或 0。

更麻煩的是,在 Delphi Dataset 裡,某些讀值方式還可能讓原本的 NULL 語意在後續計算中只剩下數值 0。

所以這次 Debug,我沒有先改公式。

我先問了一個更基本的問題:

這個 0,到底是從哪裡開始出現的?


核心技術解析

看到錯誤結果,不代表錯誤發生在最後一步

Legacy System 很容易讓人產生一種錯覺:

畫面金額錯誤
    ↓
金額計算錯誤

但真正的資料流通常比較像:

來源資料
    ↓
SQL / Dataset
    ↓
Delphi 讀取欄位
    ↓
暫存變數
    ↓
商業邏輯計算
    ↓
寫入 Dataset
    ↓
Post / ApplyUpdates
    ↓
Database
    ↓
重新查詢
    ↓
畫面顯示

畫面只是最後一站。

如果最後看到:

Amount = 0

至少有幾種完全不同的可能:

來源資料本來就是 0
來源資料其實是 NULL
Dataset 讀值後只留下數值 0
中間變數沒有正確取得來源值
計算過程真的算出 0
計算正確,但寫入時被其他值覆蓋
資料庫正確,但重新查詢時抓錯資料

這些問題最後長得都一樣:

畫面上是 0。

可是 Root Cause 完全不同。

如果沒有先確認資料在哪一層開始變化,就直接修改公式,很可能只是修改了第一個看起來可疑的地方。


這次最容易誤導我的,是一條非常正常的公式

把實際系統欄位簡化之後,原本的程式概念大致像這樣:

// 簡化後的示意程式碼
Qty :=
  dtSource.FieldByName('CHANGE_QTY').AsFloat;

Price :=
  dtSource.FieldByName('CHANGE_PRICE').AsFloat;

Amount := Qty * Price;

如果最後:

Amount = 0

直覺上當然會開始檢查:

Qty * Price

甚至 AI 也很容易沿著這個方向繼續推論:

  • 是否應該使用另一個數量欄位?
  • 單價是否取錯?
  • 是否應該改用來源金額?
  • 是否有四捨五入問題?
  • 是否需要重新計算?

這些假設都不是完全沒有道理。

問題是,在還不知道 Qty 與 Price 進入公式以前到底是多少時,討論這些事情都太早了。

真正應該先確認的是:

Qty = ?
Price = ?
Amount = ?

而不是:

Amount 的公式應該怎麼改?

這兩種 Debug 思路看起來只差一點點,實際上差很多。


第一層:先確認資料庫裡到底存了什麼

我先暫時不看 Delphi。

直接回到來源資料。

例如:

SELECT
    ItemNo,
    ChangeQty,
    ChangePrice,
    ChangeAmount
FROM SourceDetail
WHERE ProjectNo = @ProjectNo;

先確認最基本的事情。

假設查到:

ChangeQty    = NULL
ChangePrice  = 350
ChangeAmount = 0

或者另一筆資料是:

ChangeQty    = 0
ChangePrice  = 350
ChangeAmount = 0

這兩組資料最後算出來都可能是:

0

但它們的語意完全不一樣。

在這個案例裡,0 代表這個欄位已經有明確數值,而且值就是零;NULL 則代表來源值尚未建立。

這個差異非常重要。

因為:

數量 = 0

跟:

數量尚未建立

不是同一件事情。

而換到另一套系統,NULL 也可能代表未知、不適用或尚未產生。

資料真正代表什麼,仍然要回到系統的 Context 與商業規則判斷。


第二層:.AsFloat 可能讓原本的 NULL 語意消失

接著,我回到 Delphi Dataset 檢查來源欄位。

假設程式直接這樣讀:

SD_QTY :=
  dtSource.FieldByName('CHANGE_QTY').AsFloat;

如果 Dataset 欄位本身是 NULL,後續直接以 .AsFloat 取得數值時,就可能讓程式只剩下 0 這個數值結果,而原本的 NULL 語意不再出現在後續計算裡。

以這次的資料追蹤結果來看,可以簡化成:

Database
CHANGE_QTY = NULL

        ↓

Delphi Dataset
Field.IsNull = True

        ↓

.AsFloat

        ↓

SD_QTY = 0

到了後面的程式:

SD_AMOUNT := SD_QTY * SD_PRICE;

假設此時:

SD_QTY   = 0
SD_PRICE = 350

結果當然是:

SD_AMOUNT = 0

這時候如果只看最後這一行:

SD_AMOUNT := SD_QTY * SD_PRICE;

公式完全正確。

真正遺失的資訊,是更前面的:

這個 0 原本不是業務上的 0,而是來源欄位沒有值。

這裡的重點也不是宣稱所有 Delphi Dataset、所有 Field 類型都一定會有完全相同的行為,而是:

Debug 時不能只看 .AsFloat 之後的結果,還要確認原始 Field 的 IsNull 狀態。


NULL 變成 0,不只是型別轉換問題

這也是我覺得 Legacy System Debug 很麻煩的地方。

程式沒有 Exception。

SQL 沒有報錯。

Delphi 也正常執行。

甚至:

0 × 350 = 0

數學上完全正確。

可是商業語意可能已經錯了。

程式正確執行 ≠ 資料語意正確

這種 Bug 特別容易騙過 AI。

因為如果只把最後的計算函式交給 AI:

SD_AMOUNT := SD_QTY * SD_PRICE;

它看到的世界只有:

0 × 350 = 0

它不會憑空知道:

這個 0 在資料庫裡原本其實是 NULL。

除非我把這個 Context 一起提供給它。


把前面的線索串起來:建立 Data Trace

所以後來我把 Debug 問題改寫成另一種形式。

不是問:

為什麼金額算成 0?

而是要求 AI 幫我追:

Source Field
    ↓
Dataset Field
    ↓
Local Variable
    ↓
Calculation
    ↓
Target Field

例如整理成:

階段 欄位/變數 值
Database CHANGE_QTY NULL
Dataset CHANGE_QTY.IsNull True
Delphi CHANGE_QTY.AsFloat 0
Local Variable SD_QTY 0
Calculation SD_QTY * SD_PRICE 0
Target Dataset CHANGE_AMOUNT 0

做到這裡,問題就完全不一樣了。

原本看起來是:

金額公式算錯

現在真正要調查的是:

NULL
 ↓
數值讀取
 ↓
0
 ↓
乘法
 ↓
0

公式反而是整條資料流裡最無辜的一段。

更重要的是,我已經找到:

原本的資料語意第一次消失的位置。


找到異常點之後,才決定程式怎麼處理

不要急著改公式,先把「沒有值」與「值為 0」分開

如果這個案例的商業規則要求來源數量一定要存在,那麼比較安全的處理方式,不是偷偷幫它補成 0,而是先把異常攔下來。

例如:

// 先確認來源欄位是否真的有資料
if dtSource.FieldByName('CHANGE_QTY').IsNull then
begin
  raise Exception.Create(
    '來源數量為空值,無法計算變更金額'
  );
end;

// 確認不是 NULL 之後,再讀取數值
SD_QTY :=
  dtSource.FieldByName('CHANGE_QTY').AsFloat;

接著才進入真正的計算:

SD_PRICE :=
  dtSource.FieldByName('CHANGE_PRICE').AsFloat;

SD_AMOUNT :=
  SD_QTY * SD_PRICE;

這裡有一個很重要的順序:

先驗證資料
    ↓
再取得數值
    ↓
最後才計算

而不是:

先全部 AsFloat
    ↓
得到一堆 0
    ↓
再猜哪個 0 有問題

NULL 與 0 不應該太早被當成同一件事

同樣的概念也適用在金額欄位。

以這次案例來說,我會先保留三種狀態的差異:

狀態 在這個案例中的處理方向
NULL 先確認來源值為什麼尚未建立
0 已經有明確數值,但是否合理仍要看商業規則
非 0 繼續後續正常流程

因此,Debug 的第一步不是把所有 NULL 都轉成 0,而是先保留這兩種狀態的差異。

如果太早把:

NULL

和:

0

都壓成同一個數值,後面就很難再判斷這個 0 原本到底代表什麼。


我後來給 AI 的 Prompt 也改了

以前我可能會直接問:

這段 Delphi 程式轉入後金額會變成 0,
請幫我修正金額計算公式。

這個 Prompt 其實已經偷偷替 AI 做了一個假設:

Bug 在公式。

於是 AI 接下來自然會努力找一條「更好的公式」。

後來我改成:

這是一段 Delphi Legacy System 的資料轉入流程。

目前症狀:
某些資料轉入後,目標金額會變成 0。

請先不要修改公式,也不要直接提供修正版。

請依照以下順序追蹤資料:

1. 來源 Dataset 的數量、單價、金額欄位。
2. 檢查來源欄位是否可能為 NULL。
3. 確認 Delphi 使用 AsFloat 後的實際值。
4. 追蹤值進入哪些 Local Variable。
5. 確認計算前的數量與單價。
6. 確認計算結果。
7. 確認最後寫入 Target Dataset 的值。

請把每一層整理成:
來源 → 轉換 → 計算 → 寫入

只有在確認數值第一次發生異常的位置後,
再提出可能的 Root Cause。

這個 Prompt 最大的差別不是比較長。

而是我把 AI 的任務從:

Fix

改成:

Trace

這次真正改變的不是 Prompt 長度,而是我把 AI 從「修程式的人」,改成了「協助建立證據鏈的人」。

這也是我現在處理 Legacy System 時,越來越重視的一件事。

AI 可以提出修改方案,但要不要進入 Fix,應該建立在足夠的 Evidence 上。


Debug 時,我現在會先找「第一個錯的值」

Data Trace 建立之後,我真正要找的其實只有一件事:

第一個跟預期不一樣的值,出現在哪裡?

例如:

Database Qty       = NULL
Dataset IsNull     = True
CHANGE_QTY.AsFloat = 0  ← 原本的 NULL 語意在這裡消失
Local Qty          = 0
Amount             = 0

找到這個位置後,Debug 範圍就已經縮小很多。

接下來才需要判斷:

這個轉換符合商業規則嗎?還是這才是真正需要修正的位置?

而不是一路從畫面上的 Amount = 0 猜回去。


Debug 時我會追五個 Checkpoint

後來我把整個流程整理成五個 Checkpoint。

Checkpoint 1:Source

先確認來源資料:

NULL?
0?
正常數值?

不要從畫面上的結果反推來源。


Checkpoint 2:Read

接著確認 Delphi 怎麼讀這個欄位。

例如:

Field.IsNull
Field.AsFloat

我要知道的不只是:

AsFloat = 0

而是:

這個 0 原本是不是 NULL?

Checkpoint 3:Calculate

接著才在 Debugger 裡確認計算前的輸入:

Qty = ?
Price = ?

例如:

// 可以在這裡下 Breakpoint
SD_QTY :=
  dtSource.FieldByName('CHANGE_QTY').AsFloat;

SD_PRICE :=
  dtSource.FieldByName('CHANGE_PRICE').AsFloat;

// 確認輸入後,再看計算結果
SD_AMOUNT := SD_QTY * SD_PRICE;

如果進入公式以前,資料的語意就已經不對了:

異常輸入
    ↓
正確公式
    ↓
異常結果

這時再怎麼修改公式,都只是在修錯地方。


Checkpoint 4:Write

就算 Debugger 顯示:

SD_QTY    = 10
SD_PRICE  = 350
SD_AMOUNT = 3500

也還不能宣布 Bug 解決。

這只能證明:

計算當下 = 3500

後面仍然可能在寫入 Dataset 時出問題。

因此還要確認:

dtTarget.Edit;

dtTarget.FieldByName('CHANGE_AMOUNT').AsFloat :=
  SD_AMOUNT;

dtTarget.Post;

也就是:

Local Variable
        ↓
Target Field
        ↓
Post

這一段是不是真的保留了正確的值。


Checkpoint 5:Reload

最後再重新從 Database 查一次:

SELECT
    ItemNo,
    ChangeQty,
    ChangePrice,
    ChangeAmount
FROM TargetDetail
WHERE ProjectNo = @ProjectNo;

我要確認的是:

Debugger = 3500
Database = 3500
重新查詢 = 3500

這才形成完整的 Evidence Chain。

整條流程最後可以收斂成:

Source
  ↓
Read
  ↓
Calculate
  ↓
Write
  ↓
Reload

只要找到哪一個 Checkpoint 第一次出現異常,Debug 範圍通常就能縮小很多。


AI 真正幫上忙的地方,不是幫我猜公式

這次我覺得 AI 最有價值的地方,反而不是直接幫我改:

Qty * Price

而是當我把 Dataset、欄位來源與中間變數一起提供之後,它可以快速協助整理:

這個欄位從哪裡來?
        ↓
在哪裡被讀取?
        ↓
存進哪個變數?
        ↓
在哪裡參與計算?
        ↓
最後寫到哪個欄位?

在幾千行甚至上萬行的 Delphi Legacy Code 裡,這件事情其實非常有價值。

因為人最容易被「症狀」吸引。

看到:

金額 = 0

就一直搜尋:

AMOUNT
PRICE
QTY
CALAMT

然後開始修改每一個看起來可疑的地方。

AI 則可以拿來協助建立 Data Flow。

但前提仍然是:

不要讓 AI 在證據不足的時候直接進入 Fix Mode。


今天真正改變的,不是一條公式

這次問題最後讓我重新調整了 Debug 的順序。

以前可能是:

看到錯誤
    ↓
找相關函式
    ↓
看公式
    ↓
改程式
    ↓
測試

現在我比較傾向:

看到錯誤
    ↓
確認 Database
    ↓
確認 Dataset
    ↓
確認 Local Variable
    ↓
確認 Calculation
    ↓
確認 Write
    ↓
重新 Query
    ↓
找到第一個異常點
    ↓
才決定要不要改程式

這個流程看起來比較慢。

但實際上常常更快。

因為它避免了一件在 Legacy System 裡很昂貴的事情:

修正一段原本根本沒有錯的程式。


今日小結

Day 12 談的是:

沒有完整自動化測試時,先建立 Baseline 與 Evidence,才知道修改前後到底有沒有改壞。

Day 13 則把同一個概念帶進真正的 Debug 現場。

當使用者告訴我:

「金額突然變成 0。」

我現在不會第一時間問:

「公式哪裡寫錯了?」

而是先問:

「這個 0,第一次是在哪裡出現的?」

因為:

畫面是 0
≠
公式算錯

公式算出 0
≠
公式本身有問題

程式沒有 Exception
≠
資料語意正確

尤其在這次案例裡,原本的:

NULL

在進入後續數值運算之後,只剩下:

0

如果沒有保留 IsNull 這一層資訊,就很容易把「沒有值」和「值就是零」當成同一件事情。

所以這次我留下來的 Debug 原則很簡單:

先追資料,再懷疑公式。

先找到第一個錯的值,再決定第一行該改的程式。

AI 可以很快提出修法,但在 Legacy System 裡,我更需要它幫我建立 Data Trace,而不是在證據不足時直接 Fix。

最後真正要回答的,仍然是:

「我有沒有證據證明,錯誤真的發生在這裡?」

下一篇,我會繼續沿著這個問題往下追。

因為當來源資料、數量、單價與金額都確認過之後,我又碰到另一個更麻煩的情況:

同樣是資料轉入,為什麼有些資料正常,有些資料卻走出了完全不同的結果?

這時候,問題就不一定還在「值」本身了。

而可能是——程式到底走了哪一條路。


上一篇
Day 12|沒有 Unit Test 的祖傳系統,AI 改完我要怎麼知道沒改壞?
下一篇
Day 14|祖傳系統同一筆資料有兩條處理路徑:你修對 Code,卻可能修錯流程
系列文
AI 救得了祖傳系統嗎?30 天實戰企業 Legacy System × AI 協作開發 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言