iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
ChatGPT & Codex

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

Day 6|AI 說「資料來自這張表」,證據呢?建立 Legacy Code 證據鏈

  • 分享至 

  • xImage
  •  

前言/情境導入

上一篇提到,在 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 時,非常重要的一條規則。


我真正想要的是一條 Evidence Chain

假設畫面上有一個核取方塊:

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

這兩個答案差很多。

第一個是:

結論。

第二個則是:

可以被工程師重新驗證的結論。

這就是我今天想建立的:

Legacy Code Evidence Chain

中文可以直接叫:

程式證據鏈


證據鏈第一層:畫面到底綁哪一個欄位?

先從 .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 時很重要的一個習慣:

證據走到哪裡,就只下到哪裡的結論。

不要自己補完剩下的故事。


證據鏈第二層:DataSource 綁到誰?

接著繼續看 .dfm

object dsAccount: TDataSource
  DataSet = cdsAccount
  Left = 120
  Top = 200
end

現在證據鏈變成:

DBCheckBox1
↓
ACTIVE_FLAG
↓
dsAccount
↓
cdsAccount

因此我們可以再前進一步:

畫面上的 ACTIVE_FLAG 來自 cdsAccount

可是還是不能宣布:

「來自 ACCOUNT_MASTER。」

因為 ClientDataSet 本身未必直接查資料庫。


證據鏈第三層: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。


證據鏈第四層:最後才輪到 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 說

如果把整個專案交給 AI,然後直接問:

「DBCheckBox1 的資料來源是哪張表?」

AI 很可能快速搜尋:

ACTIVE_FLAG

接著看到:

SELECT ACTIVE_FLAG
FROM ACCOUNT_MASTER

於是回答:

「資料來源是 ACCOUNT_MASTER.ACTIVE_FLAG。」

答案可能剛好是對的。

但這時候我還是不會直接接受。

因為:

答案正確,不代表推理過程可靠。

這點在維護舊系統時非常重要。

如果今天只是查欄位來源,猜對好像沒什麼。

可是等到之後問題變成:

為什麼金額變成 0?

或者:

為什麼刪除這筆資料之後,另外一個畫面的資料也不見?

如果 AI 還是用「搜尋到最像的 Code 就開始推論」,風險就完全不同了。


工程師判決

所以我現在不會只問 AI:

「這個欄位來自哪裡?」

我會直接要求它:

不要只給答案,把你找到答案的過程列出來。

實際上可以使用下面這個 Prompt。

可以直接使用的 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

這才是一個我認為可以接受的答案。


但是還有最後一個問題:這段 SQL 真的有執行嗎?

到目前為止,我們證明的是:

元件設定與程式碼之間存在這條路。

但如果我要更嚴格,我還會再確認:

使用者操作這個畫面時,真的有跑到這裡嗎?

最直接的方式,就是下 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 找到的答案分成三個等級

做到這裡之後,我開始把 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,也可以帶走什麼?

這件事情其實完全不只適用 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

技術名稱不同,問題其實一樣:

你看到了一段相關程式,還是你已經證明資料真的經過它?

所以真正值得帶走的,不是 TDataSourceTClientDataSet 怎麼使用。

而是一個更通用的 Debug 習慣:

找到線索
≠
證明關係
≠
證明 Runtime Path

三件事情必須分開。


今日小結

今天沒有修改任何商業邏輯。

但我替後面的維護工作建立了一條很重要的規則:

AI 可以提出結論,但必須附上證據。

當 AI 說:

「這個欄位來自這張資料表。」

接下來我要問的已經不是:

「真的嗎?」

而是:

「請把 Control → Dataset → Query → SQL → Table 的路徑列給我。」

如果中間有一層找不到,就標記:

尚未證實

而不是靠命名、經驗或「看起來很像」補完。

因為 Legacy System 最危險的地方,往往不是程式碼太老。

而是:

太多人知道「大概是怎麼跑」,卻沒有人能證明「實際上怎麼跑」。

AI Agent 的確可以大幅縮短搜尋時間。

但至少到目前為止,我認為工程師真正不能交出去的工作仍然是:

判斷哪些東西只是線索,哪些東西已經足以成為修改系統的證據。

找到 Code,只是調查開始。

建立證據鏈,才是我敢動手修改 Legacy Code 的起點。

把搜尋的工作交給 AI,把「證明」的責任留給工程師。

這才是 AI 時代維護 Legacy Code 時,我認為最重要的一道安全防線。


上一篇
Day 5|畫面有值不代表資料正確:讓 AI 幫我們追出祖傳系統的資料流
系列文
AI 救得了祖傳系統嗎?30 天實戰企業 Legacy System × AI 協作開發6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言