AI 在 monorepo 里写代码:一套守住车道的按包工作流

AI Agent 在 monorepo 里要么交出干净 PR,要么跨 6 个包炸开。这是一套用 AGENTS.md + 显式 scope + 两阶段 commit 把它控制住的流程(截至 2026 年 6 月)。

把 Cursor 对准一个 12 包的 monorepo,让它在 apps/web 里加个功能。结果它跨 6 个包打开 47 个文件,顺手”重构”了 packages/utils,还留下另一个毫不相干的包编译失败。这就是 AI Agent 在 monorepo 里默认的崩盘方式:上下文太多、归属不清、耦合藏得深。这篇给 Turborepo、Nx、pnpm-workspace、Yarn-workspace 用户一套靠谱的按包工作流——让 Agent 守在自己车道里,同时也能安全地做跨包改动。

一句话总结

  • 给每个包都配一份 AGENTS.md “就近文件优先”如今已是跨工具的事实标准(Cursor、Claude Code 经导入、Codex、Copilot 都认)。OpenAI 自家的 monorepo 就放了大约 88 份嵌套 AGENTS.md,每个重要的包一份。
  • 每条 prompt 都显式 scope 到目标包,并写明排除清单。“只在 packages/api 改;不要碰 packages/uiapps/web。”
  • 跨包改动拆成两个 commit:先消费方(按尚不存在的契约写调用),后提供方。每个 commit 只是一个关注点。
  • 用受影响图验证,而不是跑全世界。 turbo run build test --affected -- --base=origin/main(Turborepo 2.1+)或 nx affected -t build test,只跑变更的包加它们的下游。
  • 这套在 3 个以上包且存在跨边界耦合时才划算。低于这个量,默认工具就够。

为什么 monorepo 会把 AI Agent 搞炸

单 app 仓库给 Agent 一个清晰的爆炸半径:每个文件都能改,构建就一条命令。monorepo 把这个反过来了。Agent 看到几十个包,分不清哪个包拥有某个契约,也无法从文件夹名推断出依赖图。没结构的话它就干两件事之一:要么忽略真正该改的包,要么”优化”压根没让它碰的兄弟包。两种结果都会产出没人想审的 PR。

解法不是换个更聪明的模型,而是三块结构——用来收窄 Agent 的上下文、约束它的产出:按包的指令文件、显式的 prompt scope、跨边界改动走两阶段 commit。

这篇适合谁

Turborepo、Nx、pnpm-workspace、Yarn-workspace 用户,以及有清晰包边界的大单仓。给团队写 AI 规范的工程 lead 也受益——这些约定正是让 AI 产出的 PR 在规模上还能被审的前提。

什么时候该用这套流程:在 3 个以上包的 monorepo 里做任何非琐碎改动,尤其是改动可能跨边界时——新 API 端点被 web 消费、新共享组件、生成 client 更新。单包改动用默认工具就行;结构关键的时刻是”Agent 需要知道别碰兄弟包”。

对极小 monorepo(2-3 个包、什么都共享)和还在往 monorepo 拆分、结构未定型的仓库来说,它是过度工程——先让结构稳定。单 app 项目请参考用 AI 审计 React Native 项目

现在的指令文件是 AGENTS.md

到 2026 年,项目级指令文件已经统一收敛到 AGENTS.md——同一份纯 Markdown 文件被 Cursor、Codex、GitHub Copilot 以及(经导入的)Claude Code 共同读取。让它在 monorepo 里管用的,是就近文件优先:Agent 在编辑 packages/api/src/route.ts 时,会读取从 git 根目录到这个目录沿途的 AGENTS.md 链,离得最近的那份优先级最高。这恰好就是 monorepo 需要的按包 scope。

各主流工具如何消费它(截至 2026 年 6 月):

工具指令文件按包行为scope 手柄
Cursor.cursor/rules/*.mdc(或 AGENTS.md包内嵌套的 .cursor/rules/ 覆盖根目录;规则按 glob 限定范围文件夹 scope + @ 提及文件
Claude CodeCLAUDE.md(导入 AGENTS.md从 CWD 向上走、增量合并;不加载兄弟包的文件设工作目录;@file 引用
Codex CLIAGENTS.md拼接全局 + git 根到 CWD 沿途的每份 AGENTS.md只传目标文件
GitHub CopilotAGENTS.md编辑时就近文件优先文件/选区 scope

两个实操要点。Claude Code 不直接读 AGENTS.md——要么在 CLAUDE.md 里写 @AGENTS.md 导入,要么用 ln -s AGENTS.md CLAUDE.md 做软链。还有,每份文件都要小:健康的根文件大约 200-300 行,单份文件超过 150-200 行就该拆成嵌套的,既为了可读性也为了 token 预算。2026 年比较实在的默认做法是:先用 AGENTS.md,碰到真正的 scope 限制再加 Cursor .mdc 规则,团队统一用 Claude Code 时再加 CLAUDE.md

开始前的准备

  • 盘点包、写清边界。 哪个包拥有 API 契约?哪个拥有共享类型?哪个是内部包?对 Agent 来说,“禁止依赖谁”这条往往比”可以依赖谁”更有用。
  • 为这个任务锁定一个工具。 Cursor、Claude Code、Codex、Aider 都行;任务中途换工具就是在丢上下文。
  • 定好测试策略。 跨包工作单包测试不够。至少要一条跨包集成测试和一条能跑通的全量构建命令。
  • 确认能构建单包或受影响集合。 单包用 turbo run build --filter=@acme/api,变更图用 --affected。Agent 用它来验证,不必重建整个世界。

具体步骤

  1. 维护一份根 AGENTS.md 加每个包一份简短的。 每个包条目写:名字、一句话用途、公共 API surface、归属、禁止依赖的对象。Agent 的上下文加载器会自动拣最近那份。
  2. 每条 prompt 都显式 scope。 目标和排除都要写明:“只在 packages/api 改。不要碰 packages/uiapps/web、或 packages/api 以外的任何测试。”
  3. 把检索限制在目标包内。 Cursor 用文件夹 scope;Claude Code 设工作目录;Codex/Aider 只传目标文件。这是防 Agent 读错包最有效的一根杠杆。
  4. 跨包改动分两遍做。 第一遍:在消费方按一份尚不存在的契约写调用。commit(构建会故意失败)。第二遍:在提供方实现该契约。commit。每个 commit 只是一个关注点,审起来很轻松。
  5. 显式追问下游消费方。 任何跨包改动后都问一句:“这次改动公共 API surface 了吗?有的话列出每个消费该 API 的包,以及各自需要改什么。“模型默认会忘掉下游消费方。
  6. commit 前用受影响图验证。 turbo run build test --affected -- --base=origin/main(Turborepo 2.1+,2024 年 8 月)或 nx affected -t build test。Nx 会读真实的依赖图,因此能抓到只看变更文件抓不到的下游。跨包契约破裂只会在集成构建里浮出,单个包的测试永远抓不到。

一个真实的跨包例子

packages/api 加一个新的 GET /usage 端点,给 apps/web 里的仪表盘调用:

  1. scope 到 apps/web。按你想要的响应结构写调用方(fetchUsage() 和渲染它的组件)。commit。构建会失败——packages/api 还没这个端点,而这正是重点:消费方此刻定义了契约。
  2. scope 到 packages/api。实现 GET /usage,匹配消费方期望的结构。如果有类型化 client 就重新生成。commit。
  3. turbo run build test --affected -- --base=origin/main,再跑 e2e 套件——它是唯一能同时验两个包的东西。
  4. 开 PR,2 个干净 commit。审的人按顺序读 commit 一里的契约、commit 二里的实现,不用去 bisect 一个跨 6 包的 diff。

容易踩的坑

  • 在仓库根目录跑 Agent 不给 scope。 它会从错包里检索一堆无关文件。
  • 一条 prompt 跨边界改动。 review 痛苦,diff 没法 bisect。
  • AGENTS.md 里漏写每个包的约定(测试框架、lint 规则),Agent 就会用错的那套。
  • 跳过集成构建。 单包测试 pass,集成系统却挂;只有 --affected 或全量构建才能抓到。
  • 让 Agent 把生成代码放错包。 一定要写明哪个包拥有生成物(GraphQL 类型、protobuf、Prisma client),以及重新生成它的命令。
  • AGENTS.md 当一次性写。 包会被加、被拆;过期的边界文档把 Agent 引向跟没文档一样的坑。每季度审一次。

进阶技巧

  • 每个包一份简短 AGENTS.md,胜过根目录一份巨文件。 它让每份文件都低于 ~150-200 行的拆分阈值,也加载更少无关上下文。
  • @ 引用做条件式上下文。 Claude Code 里 @docs/database-conventions.md 只在需要时才把文档拉进来,而不是撑大每条 prompt。
  • 把重新生成命令写进文件,比如”改完 schema 后从仓库根目录跑 pnpm gen:types”,让 Agent 知道要跑、在哪跑。
  • 为超大 monorepo 维护一行式模块地图:每个包写它的输入、输出、Agent 应该先读的那个文件。
  • 把近期跨包改动预加载给 Agent。 新工作往往跟随近期模式;把最近几个类似 PR 指给它,能改进它设计契约的水平。

完成后检查

  • PR 里每个 commit 只动一个包(刻意走两阶段时是两个)。
  • 受影响构建通过——用 --affected 或全量 turbo run build && turbo run test,不是只通过 Agent 改的那个包。
  • PR 描述列出所有动过的包。没列说明 prompt 没要求。
  • 生成代码重新生成并提交。Agent 忘了,说明 AGENTS.md 缺重新生成命令。

常见问题

  • 该用 AGENTS.md 还是 CLAUDE.md:先用 AGENTS.md——它是被 Cursor、Codex、Copilot 共同读取的跨工具标准。只有团队统一用 Claude Code 时才加 CLAUDE.md,而且即便那样也用 @AGENTS.md 或软链导入同一份文件,保持单一信息源。
  • 一份指令文件还是多份?:多份。每个包一份简短的,加一份小的根索引。就近文件优先意味着 Agent 读最近那份,小文件也让你保持在 ~150-200 行的拆分阈值和 token 预算之内。
  • monorepo 里 Cursor 还是 Claude Code?:都行。Cursor 按文件夹和 glob 限定的 .mdc 规则来 scope;Claude Code 从工作目录向上走、增量合并,所以一个会话里永远不会加载兄弟包的文件。挑你已经在用的那套 UX。
  • 怎么只跑变更的包?:Turborepo 用 turbo run build test --affected -- --base=origin/main(2.1+),Nx 用 nx affected -t build test。Nx 懂依赖图,所以它还会跑变更包的下游,而不只是变更包本身。
  • 怎么防 Agent 顺手”优化”无关代码?:prompt 里显式排除:“不要改 packages/api 以外的文件。不要跑会自动格式化其他包的 linter。”
  • 包之间没有清晰边界怎么办?:先把边界画出来。没边界 AI 没法在 monorepo 帮忙——审 diff 的人工程师也帮不上。

相关阅读

标签: #AI 编程 #教程 #工作流