iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
ChatGPT & Codex

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

Day 15|AI 很有自信地找到 Bug——而我差點真的相信它

  • 分享至 

  • xImage
  •  

前言/情境導入

前兩篇,我分別用 Data Trace 追金額在哪一層變成 0,再用 Branch Trace 確認程式實際走進哪個分支。這次我碰到的問題更麻煩:資料已經在系統裡待了一段時間,等使用者發現時,當初的執行現場早就不在了。

到了請款期,使用者檢查某個工程項次,才發現歷次併入與追減後,變更後數量竟然是負數。

我回查資料,看到其中一筆轉入紀錄是 2026/04/24。但這個項次並非只異動過一次,而是在一段時間內持續併入、追減。4 月有轉入紀錄,只能證明當天發生過轉入;不能證明負數就是那天第一次出現。請款期才被發現,也不等於問題是請款操作造成的。

偏偏我現在打開的程式,已經有負數防呆。如果會擋,資料當初怎麼留下來的?是某條路徑沒經過檢查、當時的 Client 還沒有這段防呆,還是後續某次異動才把數量推成負數?我帶著這些問題,把 Delphi 轉入流程交給 AI 協助檢查。

在我檢查的這份現行原始碼裡,轉入操作有一個主要入口。它很長,但和這次判斷有關的骨幹可以縮成下面幾步。**以下 Form、函式、變數與目標資料表名稱均已匿名化,保留運算關係與控制流程;示意省略了「式」、已請數量與其他分支,不能當作完整原始碼執行。**文中的 TargetDetail 代稱轉入的目標明細資料。

procedure TTransferForm.ExecuteTransferClick(Sender: TObject);
begin
  // 前段:按項次彙總歷次追減與本次變更。
  if (TotalReductionQty <> 0) and
     (Abs(OriginalQty + QtyTolerance - TotalReductionQty) > 0.000001) and
     (OriginalQty + QtyTolerance - TotalReductionQty < 0) then
    Inc(ErrorCount);
  // 其他項次與條件也可能使 ErrorCount 增加。
  if ErrorCount > 0 then Exit;

  // 後段:通過檢查後,依項次條件更新目標明細欄位。
  RemainingQty := OriginalQty - TotalReductionQty;
  RemainingAmount := RoundByRule(RemainingQty * UnitPrice);
end;

其中 OriginalQty 是原合約數量,TotalReductionQty 是此處彙總的追減數量;RoundByRule 是實際精度處理函式的匿名代稱,且「式」等分支另有算法。關鍵在於:前段先累計錯誤筆數,若 ErrorCount > 0 就結束;後段在通過後才整理要寫入的欄位。

第一輪對話:有行號的答案為什麼誘人?

我現在檢查的轉入 Form 是一支約 1.6 萬行的 .pas 原始碼。這是檔案總長度,不能拿來當作「當時一次餵給 AI 的程式碼長度」。在這樣的長程式裡,AI 能指出一個具體行號,本身就很容易讓人覺得它已經看懂整段流程。

它第一輪判斷的大意是:前面既然已檢查負數,後面約第 5796 行又重新計算,看起來有重複計算的問題;修改方向可以是整理或移除後段計算。這裡是我對當時建議的摘要,不是 AI 回覆的逐字引文;行號也只對應我檢查的那份檔案。

有行號、有看似合理的推理,還有能立即動手的建議,我一瞬間確實覺得「啊,原來兇手在這裡」。這種符合自己直覺的答案很誘人,也讓我差點陷入確證偏誤(Confirmation Bias),只找支持它的證據。我把滑鼠停下來,先問了自己一句:

如果第一次遇到負數就會被擋住,後面為什麼還要再算一次?那一次究竟在算什麼?

AI 找到的是可疑程式碼,還是這次問題的原因?

這裡其實有兩個要分開回答的問題。

第一個是現在這筆歷史資料為什麼是負數。要回答它,得知道哪一次異動讓數量首次變負、當次走了哪個入口,以及當時執行的程式和設定。

第二個是後段計算是否有錯。即使它真的值得懷疑,也必須先追清楚它讀取哪些欄位、計算哪個範圍,最後把結果寫到哪裡。找到一段看似重複的 Code,還不能把它連到這筆資料的形成過程。

目前看到的事 可以說到哪裡 不能直接推成什麼
ExecuteTransferClick 的一般項次分支檢查 OriginalQty + QtyTolerance - TotalReductionQty,錯誤累計到 ErrorCount 現行這條路徑有負數檢查,ErrorCount > 0 會 Exit 歷次異動都使用這版程式並走到同一分支
後段更新 RemainingQty、RemainingAmount 通過檢查後仍有實際欄位要計算與寫入 後段是多餘計算,或造成歷史負數
其中一筆紀錄於 2026/04/24 轉入 當天有轉入紀錄 負數就是在那次轉入形成
現在看到的數量是負數 歷次異動需要回溯 最近一次操作就是根因

AI 的問題不在於它找不到程式碼。它找到得很快;真正危險的是,把程式碼上的可疑形狀,提早升格成已確認的因果關係。

如果照 AI 建議修改,可能改壞什麼?

我差點想直接整理或刪掉後段計算。先把這個修改當成假設,反過來問:通過檢查的項次,接下來還需要哪些值?

若移除後段計算,部分項次可能失去更新 RemainingQty 與 RemainingAmount 的步驟;若改成直接沿用前段的檢查值,又可能把包含 QtyTolerance 的「放行條件」誤當成「應寫入的數量」。前者要核對欄位是否仍由其他分支賦值,後者要用容許值邊界測試兩道公式的結果。

這些是修改可能帶來的風險,不是已發生的事故。即使驗證後確定某種改法會出錯,也不能由此反推歷史負數就是這段程式造成的。這正是我暫時沒有照 AI 的建議動手的理由。

前段擋負數,後段又算一次:真的是重複嗎?

我沿著 ExecuteTransferClick 前後看,前段在一般項次的分支檢查 OriginalQty + QtyTolerance - TotalReductionQty 是否低於零;如果有錯誤,增加 ErrorCount,彙整後 ErrorCount > 0 便顯示錯誤並 Exit。後段則在檢查通過後整理 TargetDetail 的 RemainingQty、RemainingAmount 等欄位。這解釋了「為什麼後面還要算」:檢查用的預計結果,仍須在允許轉入後落到目標欄位;欄位何時進入資料庫,還要繼續追 Post 與 ApplyUpdates。

仔細看,還有一個比「算兩次」更值得追的技術差異:檢查式包含數量容許值 QtyTolerance,一般項次後段計算 RemainingQty 時,卻是 OriginalQty - TotalReductionQty;部分路徑還會依規則處理到六位精度。 因此不能只說前段會擋負數,就推論後段算出的每個中間值一定大於等於零。

以同一個一般項次分支作教學示意,假設原合約數量 OriginalQty = 10、尚未請款,並先忽略其他條件:

追減合計 TotalReductionQty 容許值 QtyTolerance 前段檢查 10 + 容許值 - 追減 後段候選值 10 - 追減
12 0 −2,記入 ErrorCount,之後 Exit 不應走到此分支的後段
2 0 8,通過 8
10.03 0.05 0.02,這一道負數檢查不會增加 ErrorCount −0.03,須追後續分支、覆寫與寫入結果

這些數字都不是問題項次的歷史數值。 第三列揭露的潛在風險是:檢查時加上容許值,可能讓小幅超出原數量的追減通過;寫入式若不加容許值,便可能算出小幅負數。這不等於已重現 Bug:程式還有已請數量檢查、特殊單位、不同公司的設定、後續覆寫與寫入路徑。要判定負數是否真的留下,必須用實際設定走完該分支,比對 Post、ApplyUpdates 前後及資料庫最後的值。

這是從目前這份程式可確認的控制流程,不能因此宣布整支程式完全正確。AI 的「後段多餘,應刪除」缺乏證據;但容許值與寫入式的落差,值得列成另一個待驗證假說。這兩個判斷可以同時成立,也都不能直接替歷史負數定因。

這也讓我有了更明確的追查方式:沿實際執行順序,分別記錄這幾個觀察點:

觀察點 要核對的資料
操作入口 使用者按了什麼、進入哪個 Event Handler、經過哪些條件分支
前段檢查 異動前數量、本次增減、檢查時計算出的數量與擋下條件
後段計算 讀取哪個 Dataset/欄位、計算的項次與範圍、結果交給誰
寫入結果 TargetDetail 或其他目標的實際值,Post 與 ApplyUpdates 是否執行、有無後續覆寫

在這份 ClientDataset 流程中,程式可以先 Post,後面才呼叫 ApplyUpdates。對不熟 Delphi 的讀者,可以把它理解成「記憶體中的本地待提交狀態」與「將更新送往資料庫」的時間差;Post 不是資料庫 Commit,ApplyUpdates 之後也仍須確認錯誤結果與實際落庫狀態。只有把這條資料流接起來,才能判斷檢查值、Dataset 中的值與資料庫最後的值是否一致。這次並沒有拿到能證明後段造成歷史負數的那組證據。

更難的問題:當時究竟跑的是哪一版?

即使今天確認現行程式會擋負數,也還無法回答「為什麼歷史資料沒有被擋住」。這幾個月系統陸續增加各種防呆,而這個項次的異動橫跨更長的時間。

Git 的 Commit 日期只能告訴我原始碼何時改過,不能直接證明某台 Client 何時安裝了哪支 EXE,更不能證明那次操作走到哪個 Branch。我本機與出問題公司目前的相關設定也不同;拿今天的環境重跑,未必等於重現當時的條件。

要追到「第一次變負數」的那一次,至少得把以下證據對在同一個時間點:

  1. 異動前後的項次數量,以及歷次併入、追減的順序。
  2. 使用者當次的操作入口與實際走過的分支。
  3. 執行那次操作的 Client 版本與當時有效的設定。
  4. 最終寫入資料庫的值,以及是否存在其他寫入途徑。

這些證據目前沒有全部補齊。我還不知道哪一次異動首次產生負數,也不能判定 2026/04/24 的轉入或今天看到的第 5796 行就是根因。 這次只能查到這裡。與其替案件補一個漂亮結局,不如把缺的證據說清楚。

讓 AI 把判斷變成可驗證的假說

回頭整理這次經驗,下次遇到類似情況,我會這樣要求 AI;以下是事後整理的 Prompt 範本,不是當時對話的逐字紀錄:

請分析這段 Delphi 轉入流程,先不要修改程式。

1. 依執行順序列出負數檢查與後段計算的入口、輸入欄位、
   計算目的及最終寫入位置,並標出對應程式碼。
2. 區分「程式碼可確認」、「需要 Runtime 驗證」與
   「需要歷史版本/資料才能確認」;不要把 Dataset 當成 Procedure。
3. 如果認為後段是 Bug,請提供一組具體輸入、預期與實際結果,
   以及可重現的操作步驟。
4. 若證據不足,列出下一個最有用的觀察點,不要提出 Fix。

這樣問的目的,是讓「看起來重複」變成一個可以驗證、也可以被推翻的主張。若拿不出會出錯的輸入與執行路徑,就先保留疑點,不把假說寫成修正單。

下次再遇到,要能找回當時的現場

這次沒有找出歷史負數的完整根因,卻讓我看清另一個問題:防呆可以攔住符合條件的新操作,無法替過去缺少紀錄的操作補出證據。

後續要改善,我會先盤點哪些入口能改動項次與 TargetDetail,再為每次轉入建立一個 OperationId,讓同一次操作的檢查與寫入 Log 能串在一起。至少需要留下:

Log 欄位 為什麼要記
操作時間、使用者、項次與操作入口 知道是哪一次、從哪個畫面進來
OperationId、實際 Client 版本與影響判斷的設定 對上當時執行的程式與條件,而不只看今天的原始碼
異動前數量、本次增減、前段檢查算出的數量 回推哪一次開始變負,以及防呆當時看到了什麼
後段計算結果、TargetDetail 實際寫入值與異動後數量 比對檢查用的值和最後存進資料庫的值
經過的關鍵分支、成功或失敗及失敗階段 區分「有檢查但未通過」與「根本沒走到檢查」

在這套 Delphi ERP 裡,落地的第一步可以是在現有轉入入口建立 OperationId,於重要檢查分支記錄判斷值,並在實際資料庫寫入界線記錄結果;成功事件可規劃寫入獨立的 Audit Log 表,讓它與目標資料對上同一次成功提交。若交易失敗而回滾,錯誤紀錄則需要另一個可靠的寫入時點,不能讓它跟著回滾消失。系統已有多處 Post、ApplyUpdates 與其他寫入入口,實作前得先盤點交易邊界,不能只在畫面按鈕裡加一行 Log 就宣稱已覆蓋所有路徑。Log 也要記實際執行版本;Git Commit 日期無法還原使用者那台電腦當時跑的 EXE。

這是後續改善方向,不是我已完成的修正。現有負數資料也不能因為加了新檢查就自動改回來;請款所需的正確數量仍須先按歷次異動與業務基準釐清,再決定如何處理。

不會 Delphi,也能帶走什麼?

這個案例換成 Java、C#、Python 或 SQL,問題仍然成立。看到「前面驗證過,後面又計算」,先不要只比較兩行算式長得像不像,而是把它們各自拆成驗證條件、實際寫入公式、持久化時點:

在本例看到的事 換到其他系統要問的問題
檢查式加入 QtyTolerance,寫入式沒有 驗證容許的邊界值,最後會存成什麼?驗證與寫入是否使用同一套規則?
ErrorCount 累計完才 Exit 哪些分支會增加錯誤、哪些分支跳過?錯誤是否真的阻止後續寫入?
Dataset 的 Post 與 ApplyUpdates 分開 畫面或記憶體裡的值,何時才真正進資料庫?失敗時會留下什麼?
Git 有防呆的 Commit 發生異常的那次操作,實際執行的是哪個部署版本與設定?

實務上可以拿一筆測試資料,依序記下 輸入 → 驗證使用的值 → 通過的分支 → 寫入前的值 → 資料庫最後的值。若其中一格拿不到,就把它列為缺少的觀測證據;不要用今天的程式推測昨天的執行結果。這個追法不依賴 Delphi,適用於任何有驗證、轉換、寫入與版本部署的系統。

今日小結

AI 指出第 5796 行附近的計算,確實讓我注意到一個值得追的地方。但「前面檢查過,後面又算一次」還不足以證明它是 Bug,更不足以證明它造成了這筆歷史負數。這次我沒有依照那個判斷刪改程式;反而從檢查式的容許值與後段寫入式之間,整理出一個可測的邊界案例。

這件事從請款期的一筆負數開始,最後停在一個誠實的結論:已確認目前有負數資料,也看到現行程式的防呆與可疑的後段計算;尚未確認首次異常發生在哪一次執行。

Day 13 讓我追值,Day 14 讓我追 Branch。今天多練了一件事:當 AI 很肯定地指出 Bug,先請它連起當時的版本、實際執行路徑、這筆資料與最後結果。學會察覺自己的確證偏誤,把 AI 當成推理夥伴而非裁決者,才能避免讓一個看似漂亮的答案,變成我們親手種下的下一個 Bug。


上一篇
Day 14|祖傳系統同一筆資料有兩條處理路徑:你修對 Code,卻可能修錯流程
下一篇
Day 16|80 筆資料存五分鐘:慢的到底是 SQL、Dataset,還是 UI?
系列文
AI 救得了祖傳系統嗎?30 天實戰企業 Legacy System × AI 協作開發 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言