上一篇提到,在 Delphi 這類資料繫結很重的系統裡:
畫面有值,不代表你已經知道資料從哪裡來。
一個畫面上的欄位,可能經過:
畫面元件
↓
DataSource
↓
ClientDataSet
↓
Provider
↓
Query
↓
資料表
甚至中間還可能再穿過 Stored Procedure、Server Module 或其他資料處理邏輯。
也因此,當我把一個欄位丟給 AI 問:
「這個畫面的資料是從哪張資料表來的?」
AI 通常很快就能回答。
有時甚至會很有自信地說:
「這個欄位來自
ORDER_HEADER資料表。」
乍看之下很方便。
問題是:
它憑什麼?
它是因為真的沿著程式執行路徑找到 SQL?
還是只因為搜尋到某個同名欄位?
或者只是看到 Dataset 名稱裡有 ORDER,就猜它一定來自 ORDER_HEADER?
這三種情況,表面上都可能得到同一個答案。
但只有第一種,我才敢拿來修改 Production 系統。
所以今天不急著叫 AI 改 Code。
今天先要求它做另一件更重要的事:
回答問題時,把證據一起交出來。
維護 Legacy Code 時,我很常做一件事:
搜尋。
例如我要追一個欄位:
ACCOUNT_TYPE
我可以直接在整個專案搜尋。
很快就可能找到:
FieldByName('ACCOUNT_TYPE')
或者:
SELECT ACCOUNT_TYPE
FROM ACCOUNT_MASTER
看到這裡,很容易產生一個直覺:
找到了,這個欄位就是來自
ACCOUNT_MASTER。
但這其實只證明:
專案裡某個地方出現過這個字串。
它沒有證明目前這個畫面真的走到那段程式。
這件事尤其容易發生在 Legacy System。
因為同一個欄位名稱,可能存在:
ACCOUNT_MASTER.ACCOUNT_TYPE
ACCOUNT_TEMP.ACCOUNT_TYPE
ACCOUNT_HISTORY.ACCOUNT_TYPE
甚至程式裡還可能有:
ACCOUNT_TYPE := 'A';
也就是說:
Search Hit ≠ Runtime Path
這是我現在要求 AI 協助看 Legacy Code 時,非常重要的一條規則。
假設畫面上有一個核取方塊:
DBCheckBox1
使用者問:
「這個勾選狀態到底存在哪裡?」
我不希望 AI 只回答:
存放在 ACCOUNT_MASTER.ACTIVE_FLAG。
我希望它回答成:
DBCheckBox1
↓
DataField = ACTIVE_FLAG
↓
DataSource = dsAccount
↓
dsAccount.DataSet = cdsAccount
↓
cdsAccount.ProviderName = dspAccount
↓
dspAccount.DataSet = qryAccount
↓
qryAccount SQL:
SELECT ACTIVE_FLAG
FROM ACCOUNT_MASTER
↓
因此目前可以確認資料來源為:
ACCOUNT_MASTER.ACTIVE_FLAG
這兩個答案差很多。
第一個是:
結論。
第二個則是:
可以被工程師重新驗證的結論。
這就是我今天想建立的:
中文可以直接叫:
程式證據鏈
先從 .dfm 開始。
以下都是經過匿名化的示意程式碼。
object DBCheckBox1: TDBCheckBox
Left = 120
Top = 80
Width = 97
Height = 17
Caption = '啟用'
DataField = 'ACTIVE_FLAG'
DataSource = dsAccount
TabOrder = 0
end
這裡我們得到第一份證據:
Control:
DBCheckBox1
DataField:
ACTIVE_FLAG
DataSource:
dsAccount
現在我們只能說:
DBCheckBox1顯示的是dsAccount所提供 Dataset 裡的ACTIVE_FLAG。
注意。
我們現在還不能說它來自哪張資料表。
這就是追 Legacy Code 時很重要的一個習慣:
證據走到哪裡,就只下到哪裡的結論。
不要自己補完剩下的故事。
接著繼續看 .dfm:
object dsAccount: TDataSource
DataSet = cdsAccount
Left = 120
Top = 200
end
現在證據鏈變成:
DBCheckBox1
↓
ACTIVE_FLAG
↓
dsAccount
↓
cdsAccount
因此我們可以再前進一步:
畫面上的
ACTIVE_FLAG來自cdsAccount。
可是還是不能宣布:
「來自 ACCOUNT_MASTER。」
因為 ClientDataSet 本身未必直接查資料庫。
例如:
object cdsAccount: TClientDataSet
ProviderName = 'dspAccount'
Left = 200
Top = 200
end
現在可以繼續往下追:
cdsAccount
↓
ProviderName = dspAccount
接著搜尋:
dspAccount
如果 Server 端看到:
object dspAccount: TDataSetProvider
DataSet = qryAccount
end
證據鏈才又往前了一步:
DBCheckBox1
↓
dsAccount
↓
cdsAccount
↓
dspAccount
↓
qryAccount
到這裡其實還沒結束。
因為真正決定資料來源的,是 qryAccount 執行的 SQL。
假設接著找到:
qryAccount.Close;
qryAccount.SQL.Text :=
'SELECT ACCOUNT_NO, ACCOUNT_NAME, ACTIVE_FLAG ' +
'FROM ACCOUNT_MASTER ' +
'WHERE ACCOUNT_NO = :ACCOUNT_NO';
qryAccount.ParamByName('ACCOUNT_NO').AsString := SD_ACCOUNT_NO;
qryAccount.Open;
這時候,我才有足夠的程式證據說:
DBCheckBox1
↓
DataField = ACTIVE_FLAG
↓
dsAccount
↓
cdsAccount
↓
dspAccount
↓
qryAccount
↓
SELECT ACTIVE_FLAG
FROM ACCOUNT_MASTER
最後才得到:
在目前這條查詢路徑下,畫面上的
ACTIVE_FLAG是由ACCOUNT_MASTER.ACTIVE_FLAG讀取。
我會刻意寫:
「目前這條查詢路徑下」
而不是:
「這個欄位永遠來自 ACCOUNT_MASTER。」
因為 Legacy Code 最大的特色之一,就是你永遠不知道另外一個按鈕會不會又走另外一條路。
如果把整個專案交給 AI,然後直接問:
「DBCheckBox1 的資料來源是哪張表?」
AI 很可能快速搜尋:
ACTIVE_FLAG
接著看到:
SELECT ACTIVE_FLAG
FROM ACCOUNT_MASTER
於是回答:
「資料來源是
ACCOUNT_MASTER.ACTIVE_FLAG。」
答案可能剛好是對的。
但這時候我還是不會直接接受。
因為:
答案正確,不代表推理過程可靠。
這點在維護舊系統時非常重要。
如果今天只是查欄位來源,猜對好像沒什麼。
可是等到之後問題變成:
為什麼金額變成 0?
或者:
為什麼刪除這筆資料之後,另外一個畫面的資料也不見?
如果 AI 還是用「搜尋到最像的 Code 就開始推論」,風險就完全不同了。
所以我現在不會只問 AI:
「這個欄位來自哪裡?」
我會直接要求它:
不要只給答案,把你找到答案的過程列出來。
實際上可以使用下面這個 Prompt。
請幫我追查畫面欄位 [填入元件名稱] 的資料來源。
請嚴格按照以下順序建立完整證據鏈,
並列出對應的程式碼位置、元件名稱或檔案名稱:
1. UI 元件名稱
- DataField
- DataSource
2. DataSource 綁定的 DataSet
3. ClientDataSet / Provider 設定
- ProviderName
- Provider 對應的 DataSet
4. Server-side Dataset / Query
5. Query 實際執行的 SQL
6. 最後讀取的資料表與欄位
請最後整理成:
UI Control
↓
DataSource
↓
DataSet
↓
Provider
↓
Query
↓
SQL
↓
Table.Column
【重要規則】
如果其中任何一環在目前提供的程式碼中無法明確串連,
請直接標示「尚未證實」。
不要根據:
- 元件名稱
- Dataset 名稱
- 欄位名稱
- 過往經驗
- 常見 ERP 架構
自行推測缺少的流程。
如果你只是搜尋到相關程式碼,
但無法證明目前畫面真的會執行到該段程式,
也請明確標示為「相關線索,尚未證實為 Runtime Path」。
這個 Prompt 最重要的其實不是前面的六個步驟。
而是最後那條:
不知道,就標示「尚未證實」。
因為我不怕 AI 告訴我它不知道。
我比較怕的是:
它不知道,卻幫我把故事補完了。
這也是我現在使用 AI Agent 維護 Legacy Code 時,會刻意加入的限制。
AI 的任務不是替我製造一個完整故事。
而是幫我把:
已確認
和:
尚未確認
分開。
假設今天我要追:
畫面上的「啟用」Checkbox
第一步先看到:
object DBCheckBox1: TDBCheckBox
DataField = 'ACTIVE_FLAG'
DataSource = dsAccount
end
得到:
DBCheckBox1
→ ACTIVE_FLAG
→ dsAccount
接著找到:
object dsAccount: TDataSource
DataSet = cdsAccount
end
得到:
DBCheckBox1
→ ACTIVE_FLAG
→ dsAccount
→ cdsAccount
再找到:
object cdsAccount: TClientDataSet
ProviderName = 'dspAccount'
end
變成:
DBCheckBox1
→ ACTIVE_FLAG
→ dsAccount
→ cdsAccount
→ dspAccount
Server 端再找到:
object dspAccount: TDataSetProvider
DataSet = qryAccount
end
現在是:
DBCheckBox1
→ ACTIVE_FLAG
→ dsAccount
→ cdsAccount
→ dspAccount
→ qryAccount
最後找到真正執行的 SQL:
procedure TServerModule.OpenAccount(const AAccountNo: string);
begin
qryAccount.Close;
// 依照目前帳號讀取帳號主檔
qryAccount.SQL.Text :=
'SELECT ACCOUNT_NO, ' +
' ACCOUNT_NAME, ' +
' ACTIVE_FLAG ' +
'FROM ACCOUNT_MASTER ' +
'WHERE ACCOUNT_NO = :ACCOUNT_NO';
qryAccount.ParamByName('ACCOUNT_NO').AsString := AAccountNo;
qryAccount.Open;
end;
到這裡,我才會把完整結果整理成:
DBCheckBox1
↓
DataField = ACTIVE_FLAG
↓
DataSource = dsAccount
↓
DataSet = cdsAccount
↓
ProviderName = dspAccount
↓
Provider.DataSet = qryAccount
↓
qryAccount.SQL
↓
ACCOUNT_MASTER.ACTIVE_FLAG
這才是一個我認為可以接受的答案。
到目前為止,我們證明的是:
元件設定與程式碼之間存在這條路。
但如果我要更嚴格,我還會再確認:
使用者操作這個畫面時,真的有跑到這裡嗎?
最直接的方式,就是下 Breakpoint。
例如:
procedure TServerModule.OpenAccount(const AAccountNo: string);
begin
// 可以先在這裡下 Breakpoint
qryAccount.Close;
...
end;
接著實際操作:
開啟畫面
↓
查詢帳號
↓
確認 Breakpoint 是否命中
↓
確認 AAccountNo
↓
確認 SQL
↓
確認回傳 ACTIVE_FLAG
到了這一步,證據就不再只是 Static Code Analysis。
而是加入:
Runtime Evidence
整條證據鏈會變成:
UI Binding
↓
Dataset Binding
↓
Provider
↓
Query
↓
SQL
↓
Runtime Breakpoint
↓
實際回傳資料
這個時候我才比較敢說:
我不是「覺得」資料從這裡來,我有證據知道它從這裡來。
做到這裡之後,我開始把 AI 提供的程式分析分成三種層級。
| 等級 | 意義 | 說明 |
|---|---|---|
| 1. 找到相關 Code | 可能有關,但還沒有證明 | AI 透過關鍵字搜尋找到的片段,可能是廢棄程式碼、舊流程,或根本沒有被呼叫的 Method。 |
| 2. 建立靜態證據鏈 | 元件、Dataset、Query 可以連起來 | 透過 .dfm、.pas、Provider 與 SQL 等資訊,證明程式結構上的依賴關係確實存在。 |
| 3. Runtime 驗證 | 實際操作時確定走過這條路 | 透過 Breakpoint、Log 或實際執行結果,確認使用者操作真的會經過這條 Runtime Path。 |
這三層之間差距其實很大。
例如:
找到 ACCOUNT_MASTER
只能算第一層。
找到:
DBCheckBox
→ DataSource
→ ClientDataSet
→ Provider
→ Query
→ ACCOUNT_MASTER
可以進到第二層。
最後真正操作畫面,Breakpoint 也確實停在那一段 Query:
才進到第三層。
我現在對高風險修改的基本態度是:
至少做到靜態證據鏈;如果涉及重要資料異動,再盡量做 Runtime 驗證。
因為 AI 很擅長找:
相關的東西。
可是維護系統真正需要的通常不是:
Relevant Code
而是:
Executed Code
兩者不一定相同。
例如 AI 搜尋到:
procedure TForm1.LoadAccount;
不代表這個 Method 現在真的有人呼叫。
搜尋到:
UPDATE ACCOUNT_MASTER
也不代表目前這個按鈕會執行這段 SQL。
找到:
FieldByName('ACTIVE_FLAG')
更不代表它一定是畫面上那個 Checkbox 使用的欄位。
這也是 AI 在 Legacy System 裡很容易讓人產生錯覺的地方。
它真的很會搜尋。
也真的很會解釋。
可是:
「找到一段合理的程式」和「證明這段程式就是事故現場」,是兩件完全不同的事情。
這件事情其實完全不只適用 Delphi。
React 可能是:
Component
↓
State
↓
API
↓
Service
↓
Repository
↓
SQL
Java 可能是:
Controller
↓
Service
↓
DAO
↓
ORM
↓
Database
C# 可能是:
View
↓
ViewModel
↓
Service
↓
Entity Framework
↓
Database
Delphi 只是:
Control
↓
DataSource
↓
DataSet
↓
Provider
↓
Query
↓
Database
技術名稱不同,問題其實一樣:
你看到了一段相關程式,還是你已經證明資料真的經過它?
所以真正值得帶走的,不是 TDataSource 或 TClientDataSet 怎麼使用。
而是一個更通用的 Debug 習慣:
找到線索
≠
證明關係
≠
證明 Runtime Path
三件事情必須分開。
今天沒有修改任何商業邏輯。
但我替後面的維護工作建立了一條很重要的規則:
AI 可以提出結論,但必須附上證據。
當 AI 說:
「這個欄位來自這張資料表。」
接下來我要問的已經不是:
「真的嗎?」
而是:
「請把 Control → Dataset → Query → SQL → Table 的路徑列給我。」
如果中間有一層找不到,就標記:
尚未證實
而不是靠命名、經驗或「看起來很像」補完。
因為 Legacy System 最危險的地方,往往不是程式碼太老。
而是:
太多人知道「大概是怎麼跑」,卻沒有人能證明「實際上怎麼跑」。
AI Agent 的確可以大幅縮短搜尋時間。
但至少到目前為止,我認為工程師真正不能交出去的工作仍然是:
判斷哪些東西只是線索,哪些東西已經足以成為修改系統的證據。
找到 Code,只是調查開始。
建立證據鏈,才是我敢動手修改 Legacy Code 的起點。
把搜尋的工作交給 AI,把「證明」的責任留給工程師。
這才是 AI 時代維護 Legacy Code 時,我認為最重要的一道安全防線。