Back to all posts

130万行、每周630个PR:我们如何保持"氛围编码"代码库的可维护性

130万行、每周630个PR:我们如何保持"氛围编码"代码库的可维护性

2026年7月,Robert C. Martin 写道,他已不再阅读编码智能体生成的代码。

这番话引发了广泛关注。但他帖子的其余部分,才是理解其工作方式的关键。Martin 用单元测试、Gherkin 验收测试、QA 流程、变异测试、覆盖率检查和质量指标将智能体层层包围。代码只有通过这套体系,才能获得他的信任。

他几个月前也表达过类似观点。他不审查具体实现,而是查看测试覆盖率、依赖结构、圈复杂度、模块大小和变异测试结果。当有人将此解读为完全放弃代码审查时,他澄清道:"我审查的东西很多——只是不审查代码本身。"

他的评论揭示了AI密集型软件开发中的一个现实问题:编码智能体产生变更的速度,可能超过人类阅读的速度。一旦AI生成代码的体量超出团队的审查能力,逐行检查就不再是建立信心的唯一手段。

过去八个月,我们在 vm0 也面临同样的问题。我们代码库中的大部分实现由智能体编写——用流行的简称来说,就是"氛围编码"(vibe coding)——而六名工程师对这些代码在生产环境中的行为负责。

简要概述

vm0 是一个氛围编码代码库:智能体编写大部分实现,六名工程师对其生产行为负责。经过八个月的发展,代码库已有 1,329,170 行,单周合并了 630 个 Pull Request。五项机制承担了逐行审查无法独立承担的质量责任。

  • 统一的环境。 工程师和智能体在同一个开发容器中工作,每个 Pull Request 都可以拥有独立的数据库分支。智能体能运行的命令,工程师同样可以运行。
  • 可执行的约束。 严格的 TypeScript 配置、约 135 个类型化 API 契约模块、Oxlint、ESLint 和架构规则,CI 不接受任何警告。能够机械检查的规则,不依赖人工审查。
  • 边界聚焦的测试。 采用"测试奖杯"而非测试金字塔:通过公共模块边界进行集成测试,使用真实的 PostgreSQL 和真实的迁移,仅在外部系统处使用 mock。测试代码约占代码库的 36%。
  • 完整的反馈循环。 智能体启动应用和数据库、运行迁移、驱动真实浏览器,并汇报所执行的命令、测试路径和截图。Pull Request 的中位合并时间约为 53 分钟。
  • 持续清理。 Knip 确定性地移除死代码,每日工作流清理 linter 无法识别的"AI 糟粕",频繁出现的模式会被提升为 lint 规则或纳入类型系统。

氛围编码与智能体编码的真正含义

氛围编码是指向 LLM 提示生成代码、运行其输出、请求修改,并完全不关注生成的代码本身。Martin Fowler 的定义刻意保持狭义,他强调"忘记代码的存在"这一说法。它适合原型、一次性软件和影响有限的小工具。

智能体编码,即 Fowler 所称的智能体编程,是团队在需要维护成果时所采用的方式。智能体读取代码库、编辑文件、运行测试,并在较长时间内独立工作,而人类则保持对结构和行为的所有权,并审查运行产生的证据:测试结果、质量信号、预览和生产行为。

两者的区别不在于模型编写了多少代码,而在于人类注意力的投向。

氛围编码智能体编码
智能体自主性提示、运行、再提示读取代码库、编辑、测试、迭代
谁阅读代码无人阅读人类阅读有风险的部分
验证内容输出是否看起来正确类型、契约、测试、预览、生产信号
最适合场景原型和一次性工具有维护周期的软件
典型失败无人理解的代码验证力度不足以应对代码体量

编辑代码库的智能体是更广泛变革的一个分支;我们另行探讨了非开发者侧的情况

我们常将 vm0 称为氛围编码项目,因为这个词已成为"主要由 AI 编写的软件"的通用简称。按 Fowler 的定义,它更接近智能体编码。我们的工程师输入的实现代码比以前少,但仍然对架构、维护成本和生产行为负责。

随着代码生成速度加快,这些责任越来越多地转移到开发环境、类型系统、测试套件和自动化维护工作流中。

氛围编码代码库八个月的成长

vm0 代码库创建于 2025 年 11 月。在我们最新的每周工程规模报告发布时,它已有大约八个半月的历史。

代码库包含:

指标数量
非空逻辑行数1,329,170
生产代码行数850,913
测试代码行数478,257
源文件数5,549
测试文件数1,327
包数量44
主分支提交数14,005

这些数据来自 2026 年 7 月 20 日至 26 日当周的每周工程规模报告,直接从 monorepo 中测量。测试代码约占代码库的 36%。这是代码体量的占比,而非测试覆盖率指标,但它能反映出围绕验证积累了多少实现代码。

在最近三个完整周内,六名工程师分别提交了 541、640 和 631 次。在 7 月 20 日至 26 日当周,GitHub 记录了 630 个已合并的 Pull Request。六名工程师提交了其中 556 个,其余 74 个由发布自动化提交。

Pull Request 从开启到合并的中位时间约为 53 分钟,第 90 百分位约为 7.4 小时。

在这样的变更速度下,人工审查仍然有价值,但无法承担整个质量体系。我们需要在变更生命周期的多个节点获得独立信号。

我们当前的方法有五个反复出现的主题:统一环境、可执行约束、边界聚焦测试、完整反馈循环和持续清理。

通过开发容器统一开发环境

许多难以复现的故障,源于从未出现在 Pull Request 中的状态。

开发者可能有未记录的环境变量、全局安装的工具、旧配置文件,或运行了数月的数据库。人们习惯了这些细节,而智能体通常无法感知它们。

我们逐渐停止将宿主机视为标准开发环境。工程师和智能体都在开发容器内工作。开发镜像包含项目工具链、PostgreSQL、pgvector、Chromium 和浏览器自动化工具。

CI 运行从同一多阶段 Dockerfile 系列构建的版本化工具链镜像。开发镜像和 CI 镜像用途不同,生产环境有其自己的部署形态。有价值的特性是统一性:工具版本、依赖项和运行时假设都是显式且版本化的。

这给了我们一个直接的预期:智能体运行的命令,工程师应能在同一开发容器中复现。开发阶段通过的检查,应在 CI 中使用密切相关的工具链运行。

我们对数据也应用了类似的理念。每个 Pull Request 预览都可以拥有独立的数据库分支,并运行真实的迁移和种子步骤。智能体可以创建数据、修改数据并重复破坏性测试,而无需借用开发者现有的数据库或继承其他 Pull Request 的状态。

环境变更通过代码库传播。工具升级、浏览器版本和数据库扩展,与应用代码以相同方式审查和传播。

将工程标准变为类型和 lint 规则中的可执行约束

书面标准有助于人们理解设计决策,但在保证每次变更都遵循这些决策方面作用有限,尤其是在长时间的智能体会话中。

只要规则能够可靠表达,我们就将其纳入类型系统、linter 和 CI。

vm0 使用严格的 TypeScript 配置,包括 strictnoUncheckedIndexedAccess。部分包还启用了对未使用值和隐式返回的额外检查。

API 使用基于 tRPC 类型机制和 Zod schema 构建的 schema 优先类型化 REST 契约层。Drizzle 将数据库访问与 TypeScript 类型关联。在本次审查时,代码库包含约 135 个契约模块。

这些选择无法捕获错误的产品决策,但能及早发现接口漂移。当字段或响应发生变化时,相关调用方往往在静态分析阶段就会失败。产生的错误通常足够具体,可供智能体在下一次迭代中使用。

Lint 覆盖第二类规则。平台运行 Oxlint、类型感知检查和 ESLint,以及项目特定的架构规则。CI 不接受任何警告。可以无限期保留的警告往往会成为背景噪音,而背景噪音很容易被人和智能体忽视。

当问题重复出现时,我们会考虑检查应归属何处:

  1. 类型系统能否表达它?
  2. linter 或结构化工具能否准确检测它?
  3. 是否需要跨代码库的语义判断?

前两类能对每次变更提供快速、确定性的反馈。第三类由本文后面描述的周期性工作流处理。

收窄 AI 生成代码中的易错模式

某些语言特性有其合理用途,但也频繁出现在脆弱的智能体生成代码中。

try/catch 可能模糊错误边界。当智能体遇到失败时,添加 catch 块和回退是保持当前路径运行的简便方法,但会丢失原始错误。将 Promise 的 .then().catch()async/await 混用,可能导致控制流分散在多种风格中。

React 的 useEffect 在状态管理中产生类似问题。它常被用于复制状态、同步两个数据源,或编码从数据模型中难以看出的顺序依赖。

核心 Web 平台默认限制这些模式。合理的例外可以保留,但需在旁边附上明确说明。

核心 Web 平台的生产代码目前不包含任何 useEffect。我们使用 ccstate 和其他无副作用模式来更显式地建模依赖和副作用。monorepo 的其他地方(主要是共享 UI 和桌面代码)仍存在少量生产 useEffect 调用,因此这一说法的适用范围需要注意。

这些规则源于本代码库中反复出现的故障。当某个模式持续造成相同的维护问题时,我们会将其从审查指导意见转变为可执行约束。

在模块边界测试:测试奖杯,而非测试金字塔

vm0 不遵循传统的测试金字塔。我们的测试指南描述的是"测试奖杯":底部是静态分析,主体层是集成测试,顶部是少量针对关键用户旅程的端到端测试。

单元测试相对少见。我们有选择地将其用于安全敏感逻辑、算法和状态机。大多数业务行为通过模块的公共边界进行测试。

API 测试通过契约调用真实应用。测试数据在可行的情况下通过生产端点准备,断言基于公共行为进行。测试避免直接访问内部服务或修改数据库表,因为这会将测试与当前实现耦合。

内部基础设施在成本合理的情况下保持真实:

  • PostgreSQL 和 pgvector
  • 数据库迁移
  • 文件系统
  • 内部服务
  • 仅在外部系统边界处使用 mock

这些集成测试比高度隔离的单元测试运行更慢,且需要更完整的环境。但它们缩小了"测试通过"与"应用能在真实数据库上执行此操作"之间的差距。

边界测试为大规模内部重构留有余地。智能体可以重组模块、拆分服务或更改数据访问层,而公共行为仍受到保护。

Uncle Bob 偏好不同的组合,大量使用单元测试、Gherkin 和变异测试。共同的原则是独立验证:生成代码的过程,不应是声称代码有效的唯一来源。

为编码智能体提供完整的反馈循环

我们早期的编码智能体运行,常以一个熟悉的报告结束:代码已修改,TypeScript 通过;请启动应用并检查页面。

这让开发循环的后半段仍由工程师承担。仍然需要有人准备数据库、运行服务、打开浏览器、创建数据、观察失败,并将其描述反馈给智能体。

现在,智能体拥有足够的开发环境来完成更多这类工作。它们可以启动应用和数据库、运行迁移、创建测试数据、访问 Pull Request 预览,并执行真实的浏览器交互。

浏览器验证能捕获类型检查和 API 测试不能很好覆盖的一类问题:导航中断、永远不稳定的加载状态、仅在完整流程中出现的权限错误,以及视觉回归。

运行结束时,智能体会报告所执行的命令、测试路径和观察结果。对于用户界面变更,它可以附上截图。工程师可以在决定是否亲自打开预览之前,先检查这些证据。

截图不是测试,也不能证明不存在其他故障。它降低了重建智能体上下文的成本。对于小型界面变更,可复现的步骤和最终截图,远比"应该已修复"这样的消息有用得多。

智能体工作质量在很大程度上取决于它无需等待人类就能获得的反馈。

通过主干开发保持分支简短

快速的代码生成可能产生大量分支积压。

长期存在的功能分支会积累合并冲突、重复工作和过时上下文。随着 Pull Request 增大,审查变得更难、更慢。我们使用主干开发(trunk-based development)来保持分支简短,并围绕 main 持续集成。

我们的主分支规则要求:

  • 通过 Pull Request 进行变更
  • 线性历史和压缩合并
  • 合并队列
  • Turbo、Rust 和安全检查
  • 不得常规绕过必要的门控

自动化遵循相同路径。智能体可以创建 Pull Request,某些低风险维护任务可以启用自动合并,但变更仍须通过 CI 和合并队列。

Pull Request 的大小和生命周期,有助于解释为何一周内能合并 630 个。小变更携带的上下文更少,更易于验证,也更不容易与产品工作或其他自动化修复产生冲突。

Knip 作为代码库垃圾回收:移除死代码

代码生成自然会添加文件和抽象。删除通常需要单独的提示。

重构后,旧文件可能留在代码库中。移除一个功能可能留下导出、依赖项和入口点。这些残留物很少会导致测试失败,但随着时间推移会使代码库越来越难以导航。

我们使用 Knip 查找未使用的文件、导出、依赖项和入口点。TypeScript 可以确认代码是有效的;Knip 则询问它是否仍然参与系统运行。

这些残留物对编码智能体还有额外的成本。代码库是它们最重要的上下文来源之一。一个过时的辅助函数或废弃的实现,在下一个读取它的智能体眼中,可能看起来像是一个经过认可的模式。保持上下文整洁,是上下文工程中不那么光鲜的一面:智能体从代码库中读取的内容,与给它的提示同样重要。

移除死代码也改善了未来运行可用的输入。Knip 处理这项工作的确定性部分,速度足够快,可以成为常规质量检查。

周期性 AI 糟粕清理工作流

Knip 和 ESLint 有明确的局限性。许多形式的退化需要项目上下文和语义判断。

我们用"AI 糟粕"作为这类残留物的实用标签:不必要的回退、重复的抽象、绕过公共边界的测试,或针对不可能状态的防御性分支。每个实例看起来可能无害,但累积起来会使代码库更难理解,并给未来的智能体提供不良示例。

多个周期性 vm0 工作流按计划扫描这些模式。

每日 AI 糟粕清理会查找新的残留物,并选择一小批高置信度、低风险的修复。其他工作流检查访问内部服务的 API 测试、React 和 ccstate 反模式,以及可以安全处理的技术债务。

每个工作流保持变更范围狭窄。它开启一个 Pull Request,然后依赖正常的类型检查、lint 规则、测试和合并队列。配置为自动合并的 Pull Request 仍须通过相同的门控。

按计划运行智能体并非没有成本,我们另行撰文介绍了如何控制这些成本。这些工作流不会试图在一次运行中清除所有技术债务。每日小批量处理比每隔几个月进行一次大规模清理更易于验证,也更少干扰。

当周期性工作流足够频繁地发现同一模式时,我们会考虑将检查移入 ESLint、Knip 或类型系统。语义工作流充当观察和完善规则的场所,然后再将其转化为更廉价的确定性检查。

将不稳定测试失败转化为自动化修复

另一组工作流从 GitHub Actions 失败开始。

当测试在主分支或合并队列中失败时,工作流会读取日志,检查不稳定性的证据,并查看重试、时序和环境因素。如果证据支持特定修复,它会更新测试或实现,开启一个 Pull Request,并再次运行完整的 CI 流程。

重试通过并不能使原始失败变得无害。依赖重试按钮的团队会逐渐失去对红色构建的信任。一旦发生这种情况,失败的检查就会成为另一种背景噪音。

自动化修复工作流将间歇性失败转化为可追溯的代码变更。诊断、补丁和验证都保留在 Pull Request 中可见。工程师可以检查风险较高的变更,而有充分证据的狭窄修复则可以通过合并队列推进。

我们的质量体系目前有三个主要层次:

阶段机制典型关注点
编写时TypeScript、契约、Drizzle、ESLint类型错误、接口漂移、已知代码模式
合并前Knip、集成测试、真实数据库、预览、合并队列死代码、模块行为、完整运行时结果
合并后计划性和事件驱动的 vm0 工作流AI 糟粕、语义反模式、不稳定测试、架构漂移

各层相互反馈。工作流发现的问题可以成为静态规则。CI 或生产中发现的失败可以成为测试和新的工程指导。

审查 AI 生成代码时,人类注意力的去向

vm0 工程师仍然阅读代码,尤其是架构变更、安全敏感工作、支付和数据迁移。我们没有将"永不阅读代码"定为团队规则。

改变的是代码审查中注意力的分配。代码阅读是契约、测试边界、预览行为、截图、质量指标和工作流诊断等众多信号之一。

以下几类决策仍需要有经验的判断:

  • 需求是否完整
  • 模块边界应设在何处
  • 哪些失败是可恢复的
  • 一个故障可能造成多大的业务影响
  • 安全模型是否合适
  • 当前规则未能覆盖哪些新的失败模式

设计侧也发生了类似的转变,设计即代码将视觉决策移入同一代码库和同一审查路径。工程师还维护智能体周围的环境。当问题重复出现时,我们决定是否添加类型约束、lint 规则、测试或周期性工作流。开发系统本身已成为一个重要的工程产物。

可读的代码仍然重要。下一个读者可能是工程师,也可能是另一个智能体。混乱的代码消耗更多上下文,扩大未来变更的范围,并使验证变得不那么可靠。

安全性与不加速的变更

速度并非均匀施加。vm0 保留了一份按自身节奏推进的变更清单:安全敏感工作、支付、数据迁移和架构决策。工程师会逐行阅读这些内容,无论是谁或什么写的。

根据我们的经验,AI 生成代码的安全风险,与其说是奇异的漏洞,不如说是无人负责的貌似合理的代码。重要的控制措施是普通的,但需要一贯执行:

  • 单元测试(我们在其他情况下少用)被刻意用于安全敏感逻辑、算法和状态机。
  • 测试仅在外部系统边界处使用 mock。内部基础设施保持真实,因此破坏内部契约的变更会在 CI 中失败,而非在生产中失败。
  • 智能体在开发容器内针对每个 Pull Request 的数据库分支工作,而非在开发者的机器上或共享数据库上。
  • 合并队列和必要检查没有常规绕过途径,即使是智能体开启并标记为自动合并的 Pull Request 也不例外。
  • try/catch 默认受到限制,因此失败不太可能被智能体为保持路径运行而添加的回退所吞噬。

凭据处理是一个独立的设计问题,有其自己的答案:我们在另一篇文章中描述了将令牌置于智能体之外的代理模式

这些措施本身并不能使生成的代码安全。它们缩小了人类决策是唯一控制手段的变更范围,并使这一范围明确可见。

这些数字未能呈现的内容

每周报告展示了代码库规模和交付速度,但本身并不能证明生产可靠性。

评估对运行时质量的影响,需要可用性、生产错误率、事故数量、变更失败率、回滚频率和平均恢复时间等数据。绿色的 CI 运行只描述了交付流程的一部分。

我们正在继续汇总这些结果指标。工程实践解释了系统如何管理风险;生产数据则显示这种管理的实际效果。

常见问题

什么是氛围编码? 氛围编码是指向 LLM 提示生成代码、运行其返回结果、请求修改,并且不阅读生成的代码。Martin Fowler 的定义刻意保持狭义:开发者应该"忘记代码的存在"。它适合原型、一次性软件和影响有限的工具。

什么是智能体编码? 智能体编码是一种较长时间运行的 AI 辅助开发形式,智能体读取代码库、编辑文件、运行测试,并在较长时间内独立迭代。人类仍然拥有架构和行为的所有权,并审查运行产生的证据,而非每一行代码。

氛围编码和智能体编码有什么区别? 区别在于注意力,而非代码作者。在氛围编码中,代码从不被检查。在智能体编码中,智能体独立工作,而工程师检查其周围的证据:测试结果、质量信号、预览和生产行为。vm0 通常被描述为氛围编码;按 Fowler 的定义,它是智能体编码。

什么是 AI 糟粕? AI 糟粕是 AI 生成代码留下的残留物:不必要的回退、重复的抽象、绕过公共边界的测试、针对不可能状态的防御性分支。每个实例看起来无害,但累积起来会使代码库更难理解,并给下一个智能体提供不良示例。

如何在每周 630 个 Pull Request 的情况下审查 AI 生成代码? 不是逐行审查。在 vm0,人工审查用于需要判断的地方:架构、安全敏感工作、支付、数据迁移。其余由机制承担:严格的类型和契约、模块边界的集成测试、每个 Pull Request 预览中的真实数据库、浏览器验证、Knip,以及没有变更能常规绕过的合并队列。

规模化氛围编码的最佳实践是什么? 在 vm0 八个月的实践中,五项原则经受住了考验:统一开发环境,使智能体和人类运行相同的命令;将标准变为类型、linter 和 CI 中的可执行约束,而非文档;在真实基础设施的模块边界进行测试;为智能体提供包含浏览器的完整反馈循环;持续清理,而非偶尔进行大规模清理。

AI 生成代码有哪些安全风险? 常见风险不是奇异的漏洞,而是无人负责的貌似合理的代码。vm0 对安全敏感工作、支付和数据迁移保留人工审查,对这类逻辑刻意使用单元测试,在测试中保持内部基础设施真实,限制会吞噬失败的 try/catch 等模式,并不允许常规绕过必要检查。

AI 生成代码会产生技术债务吗? 它会产生一种特定类型的技术债务:仍能编译并通过测试但不再参与系统运行的代码,以及 linter 无法命名的语义残留物。Knip 移除确定性部分。周期性工作流以每日小批量处理其余部分,频繁出现的模式会成为 lint 规则或类型约束。

vm0 代码库有多少是由 AI 编写的? 大部分实现。在 2026 年 7 月 20 日至 26 日当周合并的 630 个 Pull Request 中,六名工程师提交了 556 个,其余由发布自动化提交,而这些 Pull Request 中的大部分代码由智能体编写。工程师拥有的是架构、约束和生产行为。

什么是测试奖杯,为什么不用测试金字塔? 测试奖杯将静态分析置于底部,集成测试作为主体层,顶部是少量端到端测试。vm0 采用它,是因为通过模块公共边界编写的测试,在智能体重组底层实现时仍能保护行为,而大量单元测试层则做不到这一点。

如何在 AI 编写的代码库中查找死代码? 代码生成会添加文件和抽象;删除通常需要单独的提示。vm0 将 Knip 作为常规检查运行,以查找未使用的文件、导出、依赖项和入口点。TypeScript 确认代码是有效的;Knip 询问它是否仍然参与系统运行,这才是死代码问题的关键所在。

氛围编码的代码可以安全地在生产环境中运行吗? 这取决于验证机制,而非代码的输入者。我们要求的信号没有改变:类型和契约、通过公共边界的测试、真实基础设施,以及必须为绿色的流水线。本文中的数字描述了代码库规模和交付速度。可用性、错误率和变更失败率才是回答生产问题的指标,我们仍在汇总这些数据。

八个月后

八个月还太早,不足以宣布一种最终方法。模型、智能体工具和代码库在持续变化,我们的规则和工作流也随之演进。

一个转变已经清晰可见:随着代码生成速度加快,环境、约束、测试和反馈系统承担了更多的质量责任。工程师花在输入实现代码上的时间减少了,而花在定义行为、设计边界和改进验证上的时间增多了。

vm0 代码库将继续增长。Knip 移除确定性残留物。静态规则阻止我们已理解的失败模式。集成测试保护模块行为。周期性工作流处理我们尚无法机械表达的退化。

代码仍然重要。本文中的最佳实践,不是关于少写代码,而是关于在变更归入主分支之前,决定什么必须为真。我们现在使用更多机器可执行的证据来决定一个变更是否属于主分支,以及其实现是否应保留在代码库中。

Stay in the loop

// Get the latest insights on AI teammates and collaboration.