首页 / 文章

为何模块化单仓库适合编码代理:一览无余,操作极少。

多仓库盲代理与无结构单仓库充斥着这些系统。开放式上下文、封装机制以及真实的依赖关系图使得模块化单仓库能够为代理提供支持。

发布者:REACTAPP.TOP阅读约 7 分钟1695 词

来源: The Shine of the Modular Monorepo in the Agentic Era

老争论,新意义

多年来,工程师们一直将自身分为两类。一类将所有代码存放在同一个代码库中,另一类则为每部分代码单独设立存储空间,中间则存在诸多折中方案。没有哪种方式在所有情况下都是正确的,那些声称某种方式绝对正确的人通常只是基于某个项目进行推断。若问单代码库与多代码库哪种更好,真实的答案是“视情况而定”:这取决于系统的规模、各组成部分的耦合程度、团队成员构成以及代码发布的频率。一些关联度较低的服务适合放在独立的代码库中,而那些紧密围绕共享契约构建的产品则适合采用另一种方式。

编码代理让这一问题更加复杂。一旦给它分配需要涉及前端、共享库以及后端接口的任务,代码仓库的架构就不再只是个人偏好问题。其影响会体现在代码质量上,体现在代理需要猜测的频率上,以及它变得多么混乱不堪。这个案例来自一个覆盖六个平台的 TypeScript NX 环境。这只是一个特定的视角,未必适合所有人,因此请将其视为一种强烈的观点,旨在引发讨论。用四个字概括这一主张:一览无余,极少干预。

第一道障碍:代理无法看透的壁垒

想象一下大多数团队所处的分离式架构:一个前端代码仓库、两个后端服务代码仓库、私有注册表中的一个 shared-types 包,以及一个基础设施代码仓库。这样的划分合乎逻辑,每个部分都有对应的负责人。

假设前端代码库中的代理被要求修改它调用属于另一个无法访问的服务代码库的端点的方式。由于没有对应的读取处理程序,它只能猜测响应的内容。过时的类型存根或孤立的测试用例就成了它的“证据”。这样的代码在本地机器上可以编译通过,但一旦集成到实际系统中很可能会出错。每个代码库的边界都像一堵墙,代理无法看穿,于是它只能用看似合理的假设来填补这些空白。那些为人类提供保护的措施,反而让代理更加渴望获取信息。

团队主要通过三种方式来弥补这一缺陷,其中一些解决方案相当不错:

  • Git子模块将依赖的代码库置于主代码库内部,从而使文件结构看起来更加统一。代理因此能够读取所有内容,但代价也是显而易见的:固定的提交版本会逐渐偏离,保持代码最新状态会变成一项繁琐的任务,而且这样的工作流程也无法让任何人满意。
  • 多根 IDE 工作区允许 Cursor 或 VS Code 将多个代码库一起索引,这在对这些代码库进行浏览时非常有用。不过,这种配置属于同一台机器而非某个项目,因此代理会收到大量文件却无法获取依赖关系图。它知道代码存在,但不知道哪些代码相互依赖。
  • 合成型单代码库才是更合适的选择。例如 NX 的 Polygraph 就能将多个独立的代码库整合为一个依赖关系图,同时保持代码原有的位置。协调代理会查询该关系图,确定某项更改会影响哪些代码库,然后在每个代码库中启动相应的代理,并在跟踪整个 CI 流程的同时创建对应的拉取请求。这种方式在多代码库架构之上最接近单代码库的运行方式。

虽然这种综合方法值得尊重,但看看它实际做了什么:它只能穿透仍然屹立的墙体。工程师们不得不在工具层上投入大量精力,才能以零成本重现单仓库在结构上所提供的可见性。而当这些工具都不存在时,就只能靠人工来充当“通道”——读取服务B的内容,将经验传递给服务A中的代理,把类型定义粘贴到聊天中,还在封闭的会话之间充当信使。

第二道障碍:无结构的混乱堆砌

那么把所有东西都放到一个仓库里,问题就解决了吗?并非如此。没有内部结构的单仓库问题会更严重,因为它充满了诱惑。所有内容都一览无余,看起来似乎已经足够了。但实际上,那里只有一个巨大的应用目录,各个库从各个角度互相导入,没有强制性的边界划分,也搞不清哪部分内容属于谁。

在这里,过度的可见性反而有害。一个能够看到所有内容的智能体却无法得知该忽略什么。界面会被磁盘上相近但与任务无关的代码填满,一次修改会波及到那些无人跟踪的导入文件,而最关键的问题——这一改动会影响什么——得到的回答却是“可能一切”。缺乏结构的可见性只会带来噪音,而非真正的上下文,这也解释了为何不能将所有代码都放在一个仓库中。

模块化单仓库的三大支柱

模块化单仓库之所以能成功,是因为它能同时做到三件事。缺了其中任何一件,都会使其功亏一篑。

开放式上下文。所有内容都存储在同一棵结构树中,代理可在单次会话中遍历前端、后端、共享库、测试用例以及将它们连接在一起的类型契约。无需任何隔阂、猜测或中间传递环节。NX的路线图最强调这一核心优势,这也是理所当然的——因为代理若能一次性读取某个契约的所有生产者及消费者,就能进行更精准的修改。

封装。库被明确界定,并提供清晰的公共接口,因此代理无需记住整个系统,只需处理其中的一部分即可。正是这种边界设计使得“全局可视”与“局部操作”能够共存;若没有封装,开放式上下文就会退化为第二种糟糕状态。

真正的依赖关系图,以及用于探索它的工具。 这正是人们往往低估的关键要素。NX将这种关系图视为可被查询的一等对象。无需通过手动搜索来试图推断结构,开发者可以直接运行nx show projects,打开nx graph,询问某个变更会影响哪些部分,从而获得机器能够解析的精确答案。这份结构图存储在代码仓库中,而非维基页面或资深工程师的记忆中,而且它是可执行的。

这些要素共同构成了那个四字论点:全面可视是第一要素,最小化干预是第二要素,而明确知晓该干预哪些部分则是第三要素,正是这一要素让前两者能够安全地结合在一起。

获取与验证

智能体在获取上下文、修改代码、检查修改结果,然后再重复这一流程的循环中运行。所有单仓库管理措施都旨在实现两个目标之一:以较低成本获取上下文,或以较低成本检查操作结果。

  • 强类型(获取)。共享的类型库为智能体提供了它能够读取且编译器能够强制执行的契约。这解决了第一阶段中智能体缺乏明确指引的问题,因为当响应格式被明确写出并经过检查后,就无需再猜测其形式。类型同时还充当了永不过时的文档。
  • 代码整洁度与模块边界规则(已验证)。借助 NX 的 @nx/enforce-module-boundaries,被禁止的导入操作会直接抛出异常而非悄悄通过,错误信息会作为反馈出现在代理自身的循环中。如果代理试图将 UI 库接入数据层,该规则会予以拒绝,从而无需人工编写即可从失败案例中学习到正确的架构。
  • TDD 与受影响的测试用例(已验证)。 nx affected 使得简单的检查变得可行。修改某部分代码后,代理只会运行其下游的测试用例,很快就能得知测试结果为通过还是失败,无需等待整套测试完成,也不必怀疑修改是否有效。快速的循环让代理的工作更像是构建而非监督。
  • 技能与 AGENTS.md 或 CLAUDE.md(需获取)。在工作区中运行 npx nx configure-ai-agents 可以安装 MCP 服务器、智能体技能以及相关规范文件。技能是按需加载的,不会一次性全部出现在提示符中,因此构建库的方法也需在实际构建过程中学习。这些技能还能引导智能体优先通过图查询而非简单搜索,这与第三点内容相呼应。
  • 模块级原子化指南(需获取)。每个库都可以包含简短的 README 或 AGENTS.md 文件,为智能体刚进入该库时提供必要的上下文信息。进入该库即可获取其契约与规范,而无需连带加载整个库的结构。
  • 统计一下列表内容,依次为获取、验证、验证、获取、获取,不过顺序并无特殊意义。每种功能都对应这两项任务中的一项。模块化单仓库并非为智能体设计的一项功能,而是由许多小型功能组成,这些功能能让处理流程保持高效且务实。

    反对观点

    销售宣传对任何人都没有好处,因此我们需要考虑反论点。上下文窗口不断扩展,多根工作区以及合成单仓库工具也在不断缩小差距。持怀疑态度的人可能会认为单仓库的优势只是当前技术限制下的暂时现象,只要上下文窗口足够大且跨仓库工具足够完善,这种优势就会消失。更深入的分析则不涉及上下文窗口的问题。多项式图表明,依赖图可以在多仓库环境中重新构建,因此通常归功于单仓库的结构其实并非单仓库所独有。

    每个版本都误判了优势的来源。问题从来不在于阅读更多原始文本,而在于结构。更大的视野能让迷失的智能体在更细微的细节中迷失方向,却无法找到正确的路径。合成图也是一种结构,它能解释其运作原理,但这种结构是在拆分基础上重建并维护的,并非由布局直接决定。原始上下文会随着模型的发展而扩展,而结构则通过规则体系得以完善,正是这种结构将原本可能带来风险的元素转化为优势。随着视野的扩大,缺乏模块的单一代码库风险也会增加,因为“泄洪闸”打开了得更宽,而形容词则承担着描述这种状况的重任。

    智能体时代揭示了什么

    项目图表、模块边界以及共享类型曾看似只是为人类团队提供的便利工具。但在智能代理时代,它们的真实面目被暴露出来:其实只是为非人类协作方设计的接口。原本为人类思维方式设计的架构,实际上早在“上下文工程”这一术语出现多年前就已经演变成了仓库级别的上下文管理方式。正因如此,非单仓库架构再度流行起来。模块化单仓库悄然回答了一个当时还无人提出过的问题:全面掌控一切,却只需极少干预,而智能代理则只是让这一切更加清晰明了。