iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Software Development

AI時代下的軟體工程系列 第 21 篇

Day21: Dependency Graph:product 關係圖

  • 分享至 

  • xImage
  •  

昨天提到了 module:一種比 class 涵蓋更多功能的 boundary,通常代表整個 application 中一個完整的功能。而一個 application,就是由許多這樣的 module 組成的。

以開源的 coding agent pi 為例,它主要由四個 module 組成:

  • pi-ai: 統一呼叫各家 LLM provider
  • pi-agent-core: 呼叫工具並管理 agent 的 state
  • pi-tui: 把結果顯示在 terminal
  • pi-coding-agent: 組合以上三者,成為使用者操作的 CLI

這些 module 並不是各自獨立的。pi-coding-agent 要用到其他三個 module 才能運作,pi-agent-core 則要透過 pi-ai 才能呼叫 model。Day17 提過,一段 code 必須知道另一段 code 的資訊才能正確工作,就是依賴它;module 與 module 之間也是如此。

如果把每一個 dependency 都畫成一條箭頭,整個系統就會連成一張有向圖,稱為 dependency graph。透過這張圖,可以看出一次 change 需要理解多少、會影響多遠,以及 module 的 boundary 是否還守得住。這就是今天的主題。

把 dependency 畫成一張圖

Module 之間的 dependency,在 code 裡就是 import:A 要使用 B,就得 import B 公開的名稱。所以畫法很簡單:把每個 module 當成一個點,A import 了 B,就從 A 畫一條箭頭指向 B,表示 A 依賴 B。

把 pi 的四個 module,連同它們周邊的四個 module 畫出來,是這樣:

https://ithelp.ithome.com.tw/upload/images/20261004/20128319vGsK2hvyYn.png

畫這張圖不必讀任何實作,只要看每個 module import 了誰。它卻能回答開頭的兩個問題:一次 change 需要理解多少、會影響多遠。這兩個問題,正好對應箭頭的兩個方向。

順著箭頭:需要理解的範圍

箭頭從 A 指向 B,表示 A 的 code 用到了 B。所以修改 A 的時候,B 是我們必須理解的對象。

假設我們要修改 pi-agent-core,例如調整呼叫工具的流程。順著它的箭頭看出去,只指向 pi-ai,這就是這次修改需要理解的全部;pi-tui、pi-mcp、pi-codemode 都不在箭頭上,不必讀。

而且需要理解的,只到對方的門口為止。昨天說過,module 對外只留一扇門,也就是它公開的名稱與說明。pi-agent-core 需要知道 pi-ai 公開了哪些名稱;至於每一家 provider 的 API 如何串接、pi-ai 自己又依賴了誰,都是門後的事。

順著箭頭,是完成一次修改需要理解的範圍;每一條箭頭,都只需要讀到對方的那扇門。

逆著箭頭:可能影響的範圍

理解是順著箭頭走的,change 的傳播則相反:B 改變了,指向 B 的 A 才可能需要跟著改。

假設 pi-tui 的 contract 改了。逆著箭頭往回看,指向它的只有 pi-coding-agent;其餘的 module 都不在這條路上,完全不受影響。

pi-ai 則不同。pi-agent-core、pi-coding-agent 與 pi-durable 都指向它,它的 contract 一改,這三個 module 都可能要跟著改。

這裡說的,是 contract 的改變。門後的修改,例如 pi-ai 改寫某一家 provider 的串接方式,根本不會沿著箭頭傳出去;這正是每個 module 只留一扇門的意義。

逆著箭頭,是一次 contract 的修改可能影響的範圍;被越多箭頭指著,影響就越廣。

回頭看這張圖,沒有任何一條箭頭繞回來,這樣的圖稱為 DAG(directed acyclic graph,有向無環圖)。正因為沒有繞回來,兩個範圍才有盡頭。

Cycle:繞回來的箭頭

但箭頭要繞回來,並不困難。假設我們請 agent 加一個功能:讓 pi-ai 收到 model 的 tool call 時,直接把工具執行完。最省事的做法,是在 pi-ai 裡 import pi-agent-core 現成的工具呼叫邏輯。

只多了一行 import,卻多了一條從 pi-ai 指向 pi-agent-core 的箭頭。而 pi-agent-core 本來就指向 pi-ai,兩條箭頭接起來,就形成了一個環,也就是 cycle(循環依賴)。

環上的 module,兩個範圍都會合而為一。想理解 pi-ai,得先理解 pi-agent-core,而後者又建立在 pi-ai 之上,兩者再也無法單獨理解;任何一邊的 contract 改了,另一邊都可能要跟著改。

換句話說,兩個 module 雖然各有一扇門,門後卻是同一個房間。

環上的 module,理解範圍與影響範圍都會合而為一;boundary 還在,卻已經擋不住 change。

讓圖保持形狀

Cycle 往往不是刻意寫出來的,而是像上面那樣,為了省事多加一行 import。語言對此的態度並不一致:Go 的 compiler 遇到循環的 import,會直接拒絕編譯;Python 則只可能在執行時丟出 circular import 的 ImportError,而且把 import 搬進 function 裡,錯誤就消失了。以消除錯誤為目標的 agent,很自然會這麼做:錯誤沒了,環卻還在,只是再也看不見。

語言不守的時候,做法與昨天相同:交給工具。前面那張圖是由上往下畫的,每一條箭頭都從上層指向下層。以 Python 的 import-linter 為例,可以把這樣的分層寫成規則:上層可以依賴下層,下層不能依賴上層。只要所有箭頭都必須由上往下,就不可能形成環。

規則守住的是圖的形狀;要知道形狀有沒有改變,則得先看得見這張圖。圖本身也可以交給工具產生,例如 pydeps 能從 import 直接畫出 dependency graph。一次任務裡,agent 可能修改了十幾個檔案,逐檔讀 diff 很難看出整體的形狀;比對修改前後的兩張圖,多了哪條箭頭、有沒有箭頭往回指,一眼就能看出來。新的箭頭不一定是錯的,但每一條都值得問一句:為什麼這裡需要知道那裡?

Dependency graph 真正想表達的事

Module 的 boundary 決定一個 module 的裡與外;dependency graph 則決定這些 boundary 如何連在一起。只有前者,每扇門都守得住,change 卻仍可能沿著箭頭繞一圈回來;兩者都成立,local reasoning 才能從一個 module 擴大到整個 system。

它背後的理念是:箭頭要有方向,而且不回頭。

對 coding agent 而言,這張圖是一份不必讀實作的地圖。接到任務時,順著箭頭就知道要讀哪幾扇門;改了某個 contract,逆著箭頭就知道要檢查哪些 module 與它們的 test。

而當 agent 寫出一條往回指的箭頭,工具會指出它違反了哪一層。agent 能據此調整依賴的方向,而不是把錯誤藏起來了事。

Dependency graph 沒有環,人與 agent 就能從任何一個 module 出發,只看局部,也只改局部。

明天,我們回頭整理整個階段二。

Reference


上一篇
Day20: Module:對外只留一扇門
系列文
AI時代下的軟體工程 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言