Agent 时代,我为何重新拾起 Monorepo

5 分钟阅读

概念说明:
Monorepo(单体仓库)是把多个应用、服务和共享代码放在同一个仓库中统一管理,Google、Meta 是 Monorepo 的典型实践者。
Polyrepo(多仓库)则是为不同应用或服务分别建立仓库。
前者强调共享上下文和原子化变更,后者强调边界隔离和独立演进。

作为前 GitLab 员工、GitHub 的重度使用者。很长一段时间里,我都认为 Monorepo 属于过去式 —— 只有 Google、Meta 这类拥有深厚工程师文化和长期业务积累的团队,才能玩得转单体大仓库。

不过现在,我的想法彻底改变了。

随着 Coding Agent 融入日常开发流程,我意识到一个被忽略的事实:代码仓库的边界早已不止于代码存储,它正在成为 AI Agent 的执行边界。正是这一趋势,重新把我推向了 Monorepo。

一个完整需求,天然跨越多仓库

拿一个很普通的需求举例:为用户新增头像上传功能。

真落地的时候,它一定是贯穿整条链路的:

flowchart LR
    DB[Database] --> API[Backend API]
    API --> SDK[Types / SDK]
    SDK --> FE[Frontend]
    FE --> Tests[Tests]

如果这些代码分散在不同仓库:

api.gitsdk.gitweb.git

一个任务就被拆成了多个 PR,还得按顺序来:

flowchart LR
    API[API PR] --> Release[SDK Release]
    Release --> Web[Web PR]

人来做这件事的时候,多仓库之间的依赖关系是脑子里自带的常识。但 Agent 不是——每个仓库对它来说就是一道上下文的墙。

Agent 在 api.git 里改完代码,很难主动想到 web.gitadmin.gitmobile.git 这些下游消费方也得跟着改。

所以问题的本质变了。以前问的是"代码怎么写好",现在问的是"怎么协调多个 Agent、多个仓库、多个 PR"。

Monorepo 给 Agent 的是完整上下文

Monorepo 下目录长这样:

flowchart TB
    Product[product/]
    Product --> Apps[apps/]
    Apps --> Web[web]
    Apps --> Admin[admin]
    Product --> Services[services/]
    Services --> API[api]
    Product --> Packages[packages/]
    Packages --> SDK[sdk]
    Packages --> Types[types]
    Product --> Database[database/]

Agent 改了一个类型定义,直接全局搜:

1
rg "UserResponse"

一次就能定位到 API、SDK、Web、Admin、Tests 里所有相关的调用和引用。

这对 Agent 来说不是"一个仓库还是多个仓库"的选型问题,是它拿到的是全局完整上下文还是碎片化局部信息的问题。而上下文完整,恰恰是 Agent 能干好活的前提。

原子化变更,Monorepo 天生适配

理想的 Agent 开发流应该是一条直线:

flowchart LR
    Task[1 Task] --> Branch[1 Branch]
    Branch --> PR[1 PR]
    PR --> CI[1 CI]
    CI --> Merge[Merge]

比如一次 API 破坏性变更,要同步动 types、api、sdk、web 四处。Monorepo 里一次提交搞定,整体验证、整体合并。

Polyrepo 下它就变成了:

flowchart LR
    Task[1 Task] --> Repos[4 Repositories] --> PRs[4 PRs]

然后你还得处理 PR 依赖、版本发布、合并顺序、部署节奏——全是协调工作。

一天十几个 PR,人肉还能盯过来。等 Agent 一天产出几百个 PR,这种跨仓库协调的开销就是指数级的了。

划分仓库,得加一个「变更边界」维度

以前拆仓库基本看两个维度:

团队边界(Team Boundary)和服务边界(Service Boundary)。

比如 Frontend Team 管 frontend.git,Payment Team 管 payment.git,Auth Team 管 auth.git

但 Agent 主导开发之后,我觉得必须加第三个维度:

Change Boundary(变更边界)

问自己一个问题:一个真实的业务需求,通常会联动改哪些代码?

如果大量需求都要同时碰 Web、API、数据库、SDK、公共类型,那硬拆成五个仓库,看起来边界优雅,实际上全是无谓的跨仓库协调成本。

所以我现在的判断标准很朴素:

经常一起变化的代码,就考虑放在一起。

Monorepo 的代价:并发冲突

Agent 多了之后,Monorepo 最头疼的问题是并发修改和合并冲突。

几十个 Agent 并行干活,很容易同时改 package.json、锁文件、公共类型、路由配置这些核心文件。一堆 PR 基于旧的 main 分支开发,一个 PR 合并进去,后面的连锁反应就来了:

flowchart TB
    MergeA[PR A merge] --> OutdatedB[PR B outdated]
    MergeA --> ConflictC[PR C conflict]
    MergeA --> RerunD[PR D CI rerun]

这一点上 Polyrepo 有天然优势:

flowchart LR
    AgentA[Agent A] --> Payment[payment.git]
    AgentB[Agent B] --> Search[search.git]
    AgentC[Agent C] --> Auth[auth.git]

各改各的,互不干扰,并发度天然就高。

说白了:仓库边界同时也是并发安全边界。

更务实的选择:混合式架构

我不觉得 Agent 时代就该把所有代码塞进一个巨型 Monorepo。

我现在更倾向混合架构:

flowchart TB
    Org[GitHub Organization]
    Org --> Product[product.git
产品级 Monorepo] Product --> Web[web] Product --> API[api] Product --> SDK[sdk] Product --> Types[types] Product --> Database[database] Org --> Payment[payment.git
独立服务仓库] Org --> Infra[infrastructure.git] Org --> Security[security.git]

产品内部高频联动的部分——Web、API、SDK、公共类型、数据库——收进产品级 Monorepo;团队、权限、生命周期、部署节奏都相对独立的系统,继续独立仓库。

Monorepo 需要工程能力兜底

面向 Agent 的 Monorepo,绝不是"把代码放一起"就完事了。

最起码得做到"一个 Agent 任务对应一条独立分支 / 一个工作区",然后配套一整套工程体系:

Affected CI(增量构建)、Dependency Graph(依赖图谱)、CODEOWNERS(权限管控)、Small PR(小粒度提交)、Merge Queue(合并队列),以及 Agent Instructions(Agent 操作规范)。

完整链路是这样的:

flowchart LR
    Task[Task] --> Agent[Agent]
    Agent --> Worktree[Worktree]
    Worktree --> PR[PR]
    PR --> AffectedCI[Affected CI]
    AffectedCI --> Review[Review]
    Review --> Queue[Merge Queue]
    Queue --> Main[main]

这些东西缺一样,几十个 Agent 同时往主分支推代码,CI 很快就被拖垮,main 分支陷入冲突和返工的循环。

结语

我重新开始使用 Monorepo,不是因为它突然变先进了。是 Coding Agent 改写了软件工程的成本结构。以前我们优化的核心是开发者体验;现在还得兼顾 Agent 上下文完整性、任务协调成本、跨项目变更效率、CI 反馈速度、合并冲突治理。仓库边界的划分也不该只看团队边界和服务边界,还得看变更边界和 Agent 任务边界。

所以我心里的最优解从来不是"Monorepo 比 Polyrepo 好",而是:

产品级 Monorepo + 独立服务 Polyrepo。

写代码的成本被 Agent 拉下来之后,真正贵的就不是"谁来写"了,是怎么给 Agent 准确的上下文,以及怎么让海量变更安全、低成本地进主分支。想明白这件事,重新开始使用 Monorepo 就是顺理成章的事情了。