iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
ChatGPT & Codex

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

Day 3|面對一萬行 Delphi 程式,我該先把什麼交給 AI?

  • 分享至 

  • xImage
  •  

前言/情境導入

前一篇提到,在企業 Legacy System 裡,AI 最危險的地方不是完全答錯,而是只看到局部資訊,卻很有自信地說:「這樣改應該就可以了。」

知道 Context 很重要之後,下一個問題馬上就來了:

面對一支超過一萬行的 Delphi 程式,我到底該先把什麼交給 AI?

最直覺的做法,是把整支 .pas、整個專案,甚至資料庫結構全部丟進去,再請 AI 自己研究。看起來資料越多,答案應該越準;實際上卻常常相反。

當資訊一次湧入太多時,AI 可能抓錯重點、忽略真正的事件入口,或把名稱相似但用途不同的 Dataset 混在一起。更麻煩的是,企業專案裡還可能包含客戶名稱、帳號、連線資訊與實際業務資料,不能為了除錯方便就整包上傳。

所以我現在不會先問:「要貼多少程式碼?」

而會先問:

AI 要回答這個問題,還缺少哪一種資訊?

以 Delphi 表單問題來說,我通常會先準備六類材料:需求背景、錯誤現象、.pas.dfm、資料表與欄位的用途,以及資料異動的交易範圍。它們各自回答不同的問題,少了任何一塊,都可能讓推論走偏。


核心技術解析

一、先給需求背景:什麼結果才算正確?

程式碼只能告訴 AI「現在怎麼做」,不一定能告訴它「原本應該怎麼做」。

例如使用者回報:

按下儲存後金額沒有更新。

這句話仍然不夠。AI 不知道金額應該在何時重算、哪些項目要納入,也不知道儲存失敗時是否可以保留部分結果。

我會把需求改寫成可以驗證的描述:

  • 使用者修改明細數量後按下「儲存」。
  • 系統應重新計算目前單據的明細合計。
  • 合計只包含同一張單據,不可受畫面排序或篩選影響。
  • 任一步驟失敗時,不應顯示「儲存完成」。
  • 現況是資料已寫入,但畫面上的合計仍是舊值。

這些內容不是漂亮的需求文件,卻先替 AI 畫出「正確行為」的邊界。否則它可能只幫忙補一行加總,卻完全沒注意到計算時機、資料範圍與失敗處理。

二、再給錯誤畫面:症狀發生在哪一刻?

錯誤截圖的價值不只是那一行錯誤訊息。

一張完整的畫面通常還能提供:

  • 使用者當時位於哪個功能。
  • 按了哪個按鈕後發生問題。
  • 哪些欄位已經有值。
  • 目前選中的資料列是哪一筆。
  • 是否跳出例外訊息、資料庫錯誤或 Access Violation。

如果只貼出「List index out of bounds」,AI 只能列出一長串通用原因;若同時說明「刪除最後一筆後,程式仍嘗試存取目前列」,推論範圍就會縮小很多。

不過,上傳前仍要先處理敏感資訊。實際的公司名稱、客戶代號、專案編號、使用者帳號與金額,可以遮蔽或換成測試資料。錯誤現象要保留,真實身分不必陪它一起上鏡。

三、提供 .pas:讓 AI 看見程式行為

Delphi 的 .pas 是主要程式碼檔,包含表單類別、事件處理、函式與商業邏輯。當問題發生在按鈕、資料集事件或計算流程時,它通常是最主要的材料。

但我不會一開始就貼完整的一萬行,而是先從「事件入口」開始。

假設問題發生在儲存按鈕,我會先提供類似下面的匿名化片段:

procedure TOrderForm.SaveButtonClick(Sender: TObject);
begin
  SaveDetailData;
  RecalculateTotal;
  ShowMessage('儲存完成');
end;

接著再補上這三個呼叫相關的函式,而不是把所有查詢、列印、權限控制和其他按鈕事件一起丟進去。

在提供 .pas 時,我會特別保留:

  • 發生問題的事件,例如 OnClickAfterScrollBeforePost
  • 該事件直接呼叫的函式。
  • 使用到的 Dataset、全域變數與狀態旗標。
  • OpenCloseEditPostApplyUpdates 等會改變資料狀態的操作。
  • 例外處理與成功訊息的顯示位置。

相反地,與問題無關的報表格式、畫面美化與其他模組,可以暫時拿掉。目的不是隱瞞資訊,而是先讓事件因果關係浮出來。

四、提供 .dfm:讓 AI 知道畫面實際綁了什麼

只提供 .pas,有時候仍然不夠。

Delphi 表單的元件設定與事件綁定通常存放在 .dfm。程式裡雖然有一個 SaveButtonClick,但究竟是哪個按鈕觸發它?某個欄位綁定哪個 DataSource?Dataset 的 ProviderName、CommandText 或參數是什麼?這些資訊可能都藏在 .dfm

例如:

object SaveButton: TButton
  Caption = '儲存'
  OnClick = SaveButtonClick
end

object DetailSource: TDataSource
  DataSet = DetailData
end

.pas 告訴 AI「按下後做了什麼」,.dfm 則補上「畫面上的誰觸發它,以及元件彼此如何連接」。

這在 Delphi 特別重要。很多問題並不是函式本身寫錯,而是事件重複綁定、DataSource 指錯 Dataset,或元件屬性與程式假設不一致。如果沒有 .dfm,AI 只能猜測畫面配置;猜得再有禮貌,終究還是在猜。

五、說明欄位用途:欄位名稱不等於業務意義

就算 AI 已經拿到 .pas.dfm,它仍可能不知道資料真正代表什麼。

Legacy System 經常使用縮寫、流水號式欄位或沿用多年的狀態碼。人類維護者可能早已知道某個欄位代表「整張單據狀態」,但 AI 只看名稱,很容易把它誤認為「目前簽核關卡狀態」。兩者都叫狀態,修改後的後果卻完全不同。

例如,以下程式碼雖然不複雜,但如果沒有欄位說明,AI 仍然無法判斷條件是否合理:

if SignData.FieldByName('DOCUMENT_STATUS').AsString = 'PROCESSING' then
  Exit;

AI 至少還需要知道:

  • DOCUMENT_STATUS 是整張單據狀態,還是單一簽核關卡狀態?
  • PROCESSING 代表簽核中、資料處理中,還是暫存中?
  • 這個欄位允許哪些其他狀態?
  • 哪一段流程負責更新它?
  • 狀態改變後,是否還要同步解除其他業務資料的鎖定?

因此,我會再附上一份簡短的欄位字典:

匿名化欄位 用途 範例狀態值 主要規則
DOCUMENT_ID 識別同一張業務單據 一般字串 同一張單據的各流程關卡使用相同值
STEP_NUMBER 表示流程關卡順序 一般整數 數字越小,代表流程越前面
STEP_STATUS 記錄單一關卡狀態 WAITINGCOMPLETED 不等同於整張單據的狀態
DOCUMENT_STATUS 記錄整張單據狀態 DRAFTPROCESSINGRETURNEDCOMPLETED 由整體流程更新
EDIT_LOCK 表示業務資料是否暫停修改 UNLOCKEDLOCKED 進入流程後鎖定,退回後依規則解除

上表中的欄位名稱與狀態值都是為文章重新設計的虛構資料,不對應實際公司的資料庫結構。使用完整單字而不是沿用真實代碼,一方面能保護內部設計,另一方面也能讓讀者理解案例的角色分工。

除了逐欄解釋,還要說明欄位之間的關係。例如:

DOCUMENT_STATUS 表示整張單據的狀態。
STEP_STATUS 表示單一流程關卡的狀態,兩者不可混用。

當單據退回時:
1. 指定關卡之後的 STEP_STATUS 要回到 WAITING。
2. DOCUMENT_STATUS 要改為 RETURNED。
3. 相關業務資料的 EDIT_LOCK 要依規則改為 UNLOCKED。

匿名化也不能只把原始欄位改成 FIELD_AFIELD_B。這雖然藏住了名稱,卻連欄位意義也一起藏起來。比較好的方式,是改成具有相同角色、但不暴露真實設計的虛構名稱,例如 DOCUMENT_STATUSSTEP_STATUSEDIT_LOCK

六、說明交易與鎖定範圍:畫面存檔不等於資料庫已提交

在 Delphi 企業系統中,看到 PostDelete,不能立刻判定資料已經永久寫入資料庫。

TClientDataSet 為例,幾個動作代表的層次可能完全不同:

  • Post:把目前編輯內容寫入 ClientDataSet,資料可能仍只存在用戶端。
  • Delete:把目前資料標示為待刪除,不一定已經刪除資料庫紀錄。
  • ApplyUpdates:把累積的異動送往 Provider、AP Server 或資料庫。
  • Commit:確認資料庫交易中的異動。
  • Rollback:交易失敗時撤銷尚未提交的異動。

例如:

DetailData.Edit;
DetailData.FieldByName('QUANTITY').AsFloat := NewQuantity;
DetailData.Post;

// Post 可能只完成 ClientDataSet 內部異動
DetailData.ApplyUpdates(0);

// 是否真正 Commit,仍要確認 AP、Provider 或資料庫端的交易控制

因此,AI 除了要看事件與 Dataset 操作,還需要知道:

  • 系統使用直接資料庫連線,還是 ClientDataSet → Provider → AP Server
  • Post 後是否立即執行 ApplyUpdates
  • 主檔與明細是否分開呼叫 ApplyUpdates
  • StartTransaction 位於 Delphi、AP、預存程序或資料庫哪一層。
  • 任一步驟失敗時,是否會執行 Rollback
  • 是否使用 Row Lock、Table Lock、UPDLOCKHOLDLOCK 等鎖定機制。
  • 多位使用者同時修改同一筆資料時,系統如何偵測與處理衝突。

尤其要注意,多次 ApplyUpdates 不一定屬於同一個資料庫交易:

// 以下名稱皆為匿名化範例
WorkflowData.ApplyUpdates(0);  // 流程資料寫入成功
DocumentData.ApplyUpdates(0);  // 業務單據更新失敗

如果這兩次更新沒有共同的 Transaction,可能出現第一步已成功、第二步失敗的「半完成」狀態。此時問題不只是畫面沒有更新,而是資料庫中的兩組資料已經不一致。

提供給 AI 時,可以補上一段交易背景:

交易背景:

1. 此功能會依序更新流程資料與業務單據。
2. 兩個 ClientDataSet 目前分別執行 ApplyUpdates。
3. Delphi 前端沒有呼叫 StartTransaction。
4. 是否由 AP 或資料庫建立共同 Transaction,仍待確認。
5. 請先判斷可能產生的半提交風險,不要假設兩次 ApplyUpdates
   一定屬於同一個交易。

如果目前只能從程式註解得知「此段操作包在 Transaction 中」,也不能直接把註解當成已證實的事實。仍應追查實際的 StartTransactionCommitRollback,或後端預存程序的交易範圍。否則人類寫下的過期註解,也可能成為餵給 AI 的錯誤 Context。


程式碼實作:整理一份 AI 看得懂的問題包

假設現在遇到的問題是:刪除最後一筆明細後,程式出現存取錯誤。

我不會只貼出整支表單程式並說「幫我仔細檢查」,而會先整理成以下內容。

1. 需求與重現步驟

功能:刪除訂單明細

預期結果:
1. 使用者確認後刪除目前明細。
2. 若刪除後已無資料,畫面清空並結束處理。
3. 不可再讀取已不存在的目前列。

重現步驟:
1. 開啟一張只有一筆明細的單據。
2. 選取該筆明細。
3. 按下刪除。

實際結果:
資料刪除後出現 Access Violation。

2. .pas 的事件入口與相關函式

procedure TOrderForm.DeleteButtonClick(Sender: TObject);
begin
  if DetailData.IsEmpty then
    Exit;

  if MessageDlg('確定刪除目前明細?', mtConfirmation,
    [mbYes, mbNo], 0) <> mrYes then
    Exit;

  DetailData.Delete;

  // 問題可能發生在這裡:刪除最後一筆後仍讀取目前列
  RefreshSummary(DetailData.FieldByName('ITEM_NO').AsString);
end;

3. .dfm 中的相關元件

object DeleteButton: TButton
  Caption = '刪除'
  OnClick = DeleteButtonClick
end

object DetailSource: TDataSource
  DataSet = DetailData
end

4. 補充資料異動與交易背景

資料異動背景:

1. DetailData 是 ClientDataSet。
2. Delete 後尚未確認是否立即執行 ApplyUpdates。
3. 是否由 AP 或資料庫控制 Transaction,目前資訊不足。
4. 請勿假設 Delete 已經永久刪除資料庫紀錄。
5. 若判斷需要交易資訊,請指出還需要查看哪一段程式。

5. 使用防禦性 Prompt,要求 AI 不要自行補完

請先不要直接改寫整支程式。

回答時請分成以下三部分:

1. 已知事實:
   只能列出目前程式碼、錯誤畫面與需求明確提供的內容。

2. 可能原因:
   每項原因都要標示為「推測」,並說明判斷依據。

3. 尚缺資訊:
   明確列出還需要查看的 Procedure、事件、全域變數、
   Dataset 設定、DFM 元件或交易程式碼。

如果上述程式碼不足以判定,請直接回答「資訊不足」,
並明確列出還需要查看哪一個 Procedure、全域變數或元件設定。
不要假設未提供的元件設定、資料庫交易或程式行為,
也不要先產生完整修改版本。

這樣做的重點,不是發明一條神奇 Prompt,而是讓 AI 能區分三件事:

  • 已知事實:刪除最後一筆後發生錯誤。
  • 目前推測:刪除後仍存取目前列。
  • 尚待確認Delete 後是否觸發其他 Dataset 事件,或 RefreshSummary 是否又重新查詢資料。

只要這三者沒有混在一起,AI 的答案就比較不容易從「合理推測」瞬間變成「自信定案」。

防禦性 Prompt 不能保證 AI 不會答錯,但可以迫使它把推測與證據分開。工程師看到「尚缺資訊」後,也比較知道下一步要找哪支 Procedure、哪個 Dataset 事件,或哪一層的交易控制,而不是直接接受一份看似完整的修改版本。


不要一次丟整個專案,不等於只准貼十行

所謂不要一次丟整個專案,並不是程式碼越少越好。

如果只貼一個事件,卻省略它呼叫的函式、Dataset 狀態與 .dfm 綁定,AI 同樣無法判斷。真正的目標是:

先提供足以建立第一個假設的材料,再依照 AI 指出的缺口補充證據。

這比較像醫生問診,而不是把整本病歷往桌上一放,期待對方在三秒內找到十年前哪一次感冒埋下了伏筆。

對 Delphi Legacy System 而言,我目前會先按這個順序整理:

  1. 用一小段文字說明需求、預期結果與實際結果。
  2. 提供錯誤畫面及可重現步驟。
  3. .pas 找到最接近使用者操作的事件入口。
  4. 補上它直接呼叫的函式與相關 Dataset 操作。
  5. .dfm 補足元件、事件與資料來源綁定。
  6. 說明相關資料表、欄位、狀態值與欄位之間的關係。
  7. 補充 PostApplyUpdates、Transaction 與 Lock 的所在層次。
  8. 將名稱與資料匿名化,保留真正影響流程的結構。
  9. 先要求 AI 說明判斷依據與缺少資訊,不急著產生完整修改。

這份順序不保證 AI 一次答對,但能讓錯誤推論更早被看見,也讓工程師知道下一步該查哪裡。

今日小結

面對一萬行 Delphi 程式,第一步不是把一萬行全部交給 AI,而是先建立一個清楚的問題入口。

今天整理出的六類 Context,各自負責不同任務:

  • 需求背景:定義什麼結果才算正確。
  • 錯誤畫面與重現步驟:說明問題在什麼狀態下發生。
  • .pas:呈現事件、函式與資料操作流程。
  • .dfm:補足元件設定、事件綁定與資料來源關係。
  • 欄位字典與狀態規則:解釋資料代表什麼,以及欄位之間如何互相影響。
  • 交易與鎖定範圍:確認異動在哪一層送出、何時提交,以及失敗時能否完整復原。

AI 不會自動知道一套企業系統的歷史,也不會憑空理解某個欄位背後的商業意義。我們提供的 Context 越接近真正的因果鏈,它就越可能成為可靠的閱讀助手,而不是一本很會即興創作的錯誤訊息百科。

下一篇,我會進一步處理今天留下的問題:

程式彼此呼叫、事件互相觸發,相關檔案越找越多時,該如何整理「最小但足夠」的 Context,讓 AI 看得懂,又不被整個專案淹沒?


今日重點

  • 不要用整個專案取代問題描述;資料很多,不代表 Context 正確。
  • .pas 用來追行為,.dfm 用來確認元件與事件綁定,兩者不能互相取代。
  • 欄位名稱、用途、狀態值與欄位關係也要說明,不能要求 AI 靠縮寫猜測商業規則。
  • PostDeleteApplyUpdates 與資料庫 Commit 不一定是同一件事,必須確認真正的交易邊界。
  • 錯誤截圖要搭配操作步驟與發生時機,才不會只得到通用答案。
  • 真實案例應將公司、客戶、表單、資料表、欄位、帳號與識別資料匿名化。
  • 第一輪先請 AI 區分已知事實、推測與缺少資訊;資料不足時應明確回答資訊不足,而不是自行補完。

上一篇
Day 2|先別急著改程式:建立 AI Agent 的 Legacy Code 維護契約
下一篇
Day 4|從按鈕開始追:如何還原 Delphi 表單的事件流程
系列文
AI 救得了祖傳系統嗎?30 天實戰企業 Legacy System × AI 協作開發5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言