Monorepo vs Polyrepo 的辯論已經吵了很多年了。兩邊都有道理,選哪個通常取決於團隊規模、專案性質、個人偏好。我選 Monorepo 的理由一開始很單純 — 前後端經常要同步改動,放在一起比較方便。
但最近我發現了一個新的理由,而且這個理由的權重越來越大:Monorepo 讓 AI coding agent 的工作效率高了不只一點。
Monorepo 基本概念
先快速講一下什麼是 Monorepo。
簡單說就是把所有相關的 packages 放在同一個 Git repository 裡面。前端、後端、共用工具、腳本 — 全部在一個 repo。不是把所有 code 混在一起(那叫 monolith),而是在一個 repo 裡面用清楚的目錄結構來劃分不同的 packages,每個 package 有自己的 package.json 和獨立的職責。
Monorepo 的優勢包括:共用 dependencies(不用每個 repo 各自裝一套)、共用 CI/CD pipeline(一個 workflow 管全部)、共用 tooling(一套 linter 和 formatter 規則套用到所有 packages)。
主流的 Monorepo 工具有幾個:pnpm workspace 處理依賴管理、Turborepo 處理 task 的排程和快取、Nx 功能更完整但學習曲線也更陡、Lerna 則是老牌工具但近年比較少新專案用了。我們團隊用的是 pnpm + Turborepo 的組合,輕量夠用。
為什麼選這個組合?pnpm 的 symlink 機制讓 workspace 的 package 互相引用變得很自然,而且硬連結的設計省了很多磁碟空間。Turborepo 則是 task orchestration 做得最簡潔 — 設定檔少、概念直觀、效能也好。比起 Nx 那種「什麼都想管」的架構,Turborepo 的「你只需要告訴我 task 之間的依賴關係」更符合我的口味。
我的 Monorepo 結構
以我們目前在做的一個 TypeScript 全端專案為例(當然不能說是什麼專案),目錄結構大概長這樣:
1 | root/ |
packages/shared/ 是整個架構的核心。所有前後端共用的東西都放這裡 — TypeScript 的 interface 定義、enum、常數、utility functions。前後端都依賴這個 package,所以型別永遠是一致的。
舉個具體的例子。假設有一個「訂單」的資料結構:
1 | // packages/shared/src/types/order.ts |
這個型別定義在 shared package 裡。Server 在回傳 API response 時用這個型別,Client 在接收和顯示時也用這個型別。如果有天要加一個新的 status(比如 Processing),只需要改 shared,TypeScript compiler 就會告訴你所有需要跟著改的地方。
turbo.json 定義了 task 的依賴關係和快取策略。比如 build 要先跑 shared 再跑 server 和 client,test 依賴 build 的結果。Turborepo 會根據這些定義自動決定執行順序和平行化。
pnpm-workspace.yaml 就很簡單:
1 | packages: |
告訴 pnpm 哪些目錄是 workspace 的成員。
為什麼 Monorepo 讓 AI 更有效率
好,進入正題。為什麼我說 Monorepo 對 AI 友善?
1. 完整的 context
這是最重要的一點。AI coding agent 在 Monorepo 裡工作時,可以一次看到前端和後端的 code。當它需要改一個 API endpoint 的時候,它同時能看到 client 端怎麼呼叫這個 API、用了什麼型別、UI 上怎麼呈現回傳的資料。
結果就是:AI 改 API 的時候會同步改 client 的呼叫方式和型別定義。不會出現「API 改了但 client 沒跟上」的情況。
在 Polyrepo 的架構下,AI 只能看到一個 repo 的 context。改 server repo 的時候不知道 client 怎麼用,改 client 的時候不知道 server 回什麼。產出的 code 很容易對不上。
2. 型別共用
shared package 裡的 interface 是前後端的 single source of truth。當 AI 需要修改某個資料結構的時候,它改 shared 的型別定義,然後 TypeScript compiler 會自動告訴它所有使用這個型別的地方需要跟著改。
這個回饋循環對 AI 來說太重要了。它不用猜「改了這個型別之後哪些地方會壞」,compiler 直接告訴它。然後 AI 就可以把所有相關的改動一次處理完。
舉個實際的例子。有一次我讓 AI 在 Order 裡加一個 discount 欄位。它做的事情是:
- 在
shared/types/order.ts加上discount?: number - 跑
turbo build,看到 server 端有幾個地方的 type check 報錯 - 去那些地方補上 discount 的處理邏輯
- 跑
turbo build確認 server 沒問題 - client 端的 type 自動更新(因為依賴 shared),但 UI 還沒顯示這個欄位
- 在訂單詳情頁加上 discount 的顯示
- 全部 build 通過,提 PR
整個過程 AI 自己完成,我只需要 review。如果是 Polyrepo,步驟 2-4 和 5-6 會分成兩個 repo,AI 做不到這種「跨 repo 的改動追蹤」。
3. 不用切 repo
Polyrepo 的一個根本性問題是:AI 的 context window 是有限的。如果前後端分開兩個 repo,AI 要處理一個涉及前後端的 feature 時,它要嘛在兩個 repo 之間切換(很多工具不支援),要嘛只處理其中一端(產出不完整的改動)。
Monorepo 沒有這個問題。一個 repo 就是全部。AI 開了這個 repo,所有需要的 code 都在手邊。
4. 統一的 coding conventions
Monorepo 的一個天然優勢是所有 packages 共用同一套 ESLint 規則、同一套 Prettier 設定、同一套 tsconfig。AI 學一次你的 coding style,就能套用到整個 repo 的所有 packages。
Polyrepo 的話,每個 repo 的設定可能有微妙的差異(是的,即使你覺得「都一樣」,實際上總會有一些不同步的地方)。AI 在不同 repo 之間切換時,可能會把 A repo 的風格帶到 B repo。
5. 原子化 commit
Monorepo 讓前後端的改動可以在同一個 PR 裡完成。AI 做一個完整的 feature — 後端 API + 前端 UI + 共用型別 — 全部在一個 commit 或 PR 裡。Reviewer 可以一次看到完整的改動,而不是在三個 repo 的三個 PR 之間跳來跳去。
這對 code review 的效率也是很大的提升。你可以在同一個 PR 裡確認 API 的 response 格式和 client 的使用方式是不是匹配的,不用靠想像力。
搭配 AI 的 Monorepo 最佳實踐
光是用 Monorepo 還不夠,有一些實踐可以讓 AI 在 Monorepo 裡工作得更順暢。
根目錄放 onboarding 文件。我們在根目錄放了一份 CLAUDE.md(你也可以叫 AGENTS.md 或其他名字),描述整個 repo 的架構、packages 之間的關係、整體的技術決策。這份文件的目標讀者就是 AI — 讓它讀完就能理解這個 repo 的全貌。
內容大概包括:
- 架構概覽(哪些 packages、各自的職責)
- 技術棧(用了什麼框架、什麼工具)
- Coding conventions(命名規則、檔案結構、常見 pattern)
- 地雷區域(哪些地方不要亂改、哪些地方有已知的技術債)
- Build 和 test 指令
一份簡化的 CLAUDE.md 範例:
1 | # Project Overview |
每個 package 也放自己的 README。根目錄的文件描述全貌,但每個 package 的細節放在各自的 README 裡。AI 可以按需讀取 — 如果只改 server 的 code,就讀 server 的 README;如果涉及跨 package 的改動,再去讀其他 package 的。不用一次把整個 repo 的所有文件都讀完。
shared types 是核心。把所有前後端共用的型別定義集中在 shared package,而且要嚴格執行。不要在 server 裡偷偷定義一個跟 shared 「差不多」的型別,也不要在 client 裡用 any 繞過型別檢查。shared types 是 AI 理解你的資料模型的入口,如果這裡混亂了,AI 的產出品質會直接下降。
Build pipeline 要可本地跑。turbo build 和 turbo test 要能在 local(或 DevContainer)裡直接跑。AI 改完 code 之後可以馬上驗證,不用等 CI。快速的回饋循環是 AI 高效工作的關鍵。
依賴關係要清晰。在 turbo.json 裡定義好 task 之間的 dependsOn。AI 需要知道:build shared → build server → build client 的順序。如果依賴關係沒定義清楚,AI 可能會在 shared 還沒 build 的情況下去 build client,然後看到一堆莫名其妙的 error。
一份實際的 turbo.json 設定:
1 | { |
"^build" 表示「先 build 我依賴的 packages」。這樣 Turborepo 就會自動算出正確的 build 順序。
Monorepo 的痛點和解法
Monorepo 不是沒有缺點。誠實說,有些痛點還蠻煩的。
CI/CD 速度 是最常被抱怨的問題。所有 packages 在同一個 repo,CI 要跑全部嗎?如果每次 push 都重新 build 和 test 所有 packages,CI 的時間會越來越長。
解法是用 Turborepo 的 remote cache。Turborepo 會根據檔案的 hash 判斷哪些 packages 有改動,只 rebuild 有變的部分。搭配 remote cache,甚至可以跨 CI run 共用 cache — 如果某個 package 在上一次 CI 已經 build 過,而且 code 沒變,就直接用 cache。實際用下來,CI 時間從十幾分鐘降到了三四分鐘。
設定 remote cache 的方式(用 Vercel 的服務):
1 | npx turbo login |
然後在 CI 裡設定 TURBO_TOKEN 和 TURBO_TEAM 環境變數就可以了。
Repo 太大。隨著時間推移,repo 會越來越大。Git clone 的時間變長、IDE 的索引時間變長。可以用 git sparse-checkout 只 checkout 你需要的 packages,或是善用 .gitignore 排除不必要的檔案。但老實說,以我們的規模(幾個 packages、不到十萬行 code),還沒遇到 repo 太大的問題。大型開源專案那種百萬行 code 的 Monorepo 才會比較明顯。
IDE 效能。VS Code 在大型 TypeScript Monorepo 裡的語言服務可能會變慢 — IntelliSense 反應遲鈍、型別檢查跑很久。解法是用 TypeScript 的 project references(tsconfig.json 裡的 references 欄位),讓 TypeScript compiler 可以增量編譯,不用每次都重新分析整個 repo。
權限控制。如果不同的 packages 由不同的團隊負責,你可能需要控制誰能改什麼。GitHub 的 CODEOWNERS 檔案可以做到這點 — 定義哪些目錄的改動需要哪些人 review。不過以小團隊來說,這個問題不太嚴重,大家都會改到所有地方(笑)。
從 Polyrepo 遷移到 Monorepo
如果你現有的專案是 Polyrepo,想遷移到 Monorepo,這裡分享一個實際的遷移經驗。
第一步:選一個「主 repo」。通常是後端或是最活躍的那個 repo。在這個 repo 裡建立 packages/ 目錄結構,把原本的 code 移進去(比如 packages/server/)。
第二步:用 git subtree 合併其他 repo。這樣可以保留完整的 git history。命令大概是:
1 | git subtree add --prefix=packages/client git@github.com:your-org/client-repo.git main |
第三步:建立 shared package。把原本分散在 server 和 client 裡的共用型別抽出來。這一步通常最花時間,因為你會發現兩邊的型別定義「差不多但不完全一樣」,需要決定以哪邊為準。
第四步:設定 workspace 和 Turborepo。加上 pnpm-workspace.yaml、turbo.json、根目錄的 package.json。
第五步:調整 import path。原本 client 裡寫 import { Order } from '../types' 的地方,改成 import { Order } from '@your-org/shared'。這步驟很機械化,但很容易漏。建議用 search & replace 一次處理,然後跑 build 看哪裡還有錯。
第六步:更新 CI/CD。原本兩個 repo 各自的 CI workflow 要合併成一個。
整個遷移大概花了我們一週的時間(包括處理 merge conflict 和修正 type error)。遷移完的第一個感覺是「終於不用在兩個 repo 之間切來切去了」。
和 Polyrepo 的實際比較
最後來做個比較,用列表的方式比較清楚:
- Context 完整度:Monorepo 高,AI 可以看到所有 packages 的 code / Polyrepo 低,AI 只能看到目前所在的 repo
- 型別同步:Monorepo 自動,改 shared types 全 repo 立刻生效 / Polyrepo 要手動發 npm 版本,client 要更新依賴才能拿到新型別
- AI 改動範圍:Monorepo 可以跨 package 做完整 feature / Polyrepo 限制在單一 repo,跨 repo 的改動需要人工協調
- CI 複雜度:Monorepo 需要 task pipeline 工具(Turborepo/Nx)/ Polyrepo 各 repo 獨立,比較簡單
- 初始設定:Monorepo 比較高,需要設定 workspace、turbo、shared packages / Polyrepo 低,各 repo 獨立
- PR Review:Monorepo 可以在一個 PR 看到完整改動 / Polyrepo 要在多個 repo 的 PR 之間跳來跳去
- 版本管理:Monorepo 所有 packages 同步演進 / Polyrepo 各自版本獨立,但同步成本高
Polyrepo 在某些場景下仍然是合理的選擇 — 比如 packages 之間真的完全獨立、不同團隊不同節奏、或是 repo 規模大到 Monorepo 工具撐不住。但對於前後端緊密耦合的專案(大多數的全端 web app 都是),Monorepo 的優勢是很明顯的。
再加上 AI 協作這個維度,Monorepo 的「完整 context」優勢就更重要了。
結語
Monorepo 不是萬能的。它有學習曲線、有工具設定的成本、有些場景不適用。
但在 AI 參與開發的時代,「讓 AI 能看到全貌」這個優勢變得越來越重要。當 AI 可以在一個 repo 裡看到前端、後端、共用型別、build pipeline、coding conventions — 它的產出品質和效率會遠高於只能看到片段的情況。
如果你的專案前後端經常要同步改動(老實說,大部分 web app 都是這樣),如果你的團隊正在開始使用 AI coding agent,如果你希望 AI 能做出完整的 feature 而不是片段的 code — 認真考慮一下 Monorepo 吧。
初始設定確實要花點時間。但這個投資回報率,在 AI 時代只會越來越高。
用一句話總結:你讓 AI 看到的 context 越完整,它給你的產出就越好。Monorepo 就是讓 AI 看到全貌最直接的方式。而且這個道理不只適用於 AI — 對人類開發者來說,能在一個地方看到所有相關的 code,一樣會讓工作更順暢。Monorepo 的好處是普遍的,只是 AI 讓這個好處變得更明顯而已。