Skip to content

Latest commit

 

History

History
198 lines (126 loc) · 17.3 KB

File metadata and controls

198 lines (126 loc) · 17.3 KB

代理驱动开发:用AI构建软件产品的实验

在过去的五个月里,我们团队一直在进行一项实验:构建并发布一款内部测试版软件产品,其中没有一行代码是手动编写的。这款产品既有内部日常用户,也有外部Alpha测试者。它能够发布、部署、出现故障,然后被修复。不同之处在于,每一行代码——应用程序逻辑、测试、CI配置、文档、可观测性工具和内部工具——都是由Codex编写的。我们估计,用这种方式构建产品所花费的时间,大约是手动编写代码所需时间的十分之一。

人类负责指导方向,AI代理负责执行。我们刻意选择了这个约束条件,这样我们就能构建出将工程速度提高几个数量级所必需的东西。我们只有几周时间来交付最终产出的一百万行代码。为了做到这一点,我们需要理解:当软件工程团队的主要工作不再是编写代码,而是设计环境、明确意图,以及构建反馈循环以让Codex代理能够可靠地工作时,会发生什么变化。

这篇文章讲述的是我们通过使用代理团队构建一款全新产品所学到的东西——哪些地方出了问题,哪些地方产生了复合效应,以及如何最大化利用我们唯一真正稀缺的资源:人类的时间和注意力。

我们从一个空的git仓库开始

第一次提交到空仓库是在2025年8月下旬。

最初的脚手架——仓库结构、CI配置、格式化规则、包管理器设置和应用程序框架——是由Codex CLI使用GPT-5生成的,并由一小组现有模板引导。甚至最初的AGENTS.md文件(指导代理如何在仓库中工作)本身也是由Codex编写的。

系统没有预先存在的人工编写代码作为锚点。从一开始,仓库就是由代理塑造的。

五个月后,该仓库包含了大约一百万行代码,涵盖应用程序逻辑、基础设施、工具、文档和内部开发者实用工具。在此期间,大约有1,500个拉取请求被打开和合并,而驱动Codex的工程师团队只有三人。这相当于每位工程师平均每天产出3.5个PR,而且令人惊讶的是,随着团队现在扩大到七名工程师,吞吐量还在增加。重要的是,这不是为了产出而产出:该产品已在内部被数百名用户使用,包括每天使用的内部重度用户。

在整个开发过程中,人类从未直接贡献任何代码。这成为团队的核心理念:不手动编写代码。

重新定义工程师的角色

缺少人工编码引入了一种不同类型的工程工作,专注于系统、脚手架和杠杆作用。

早期进展比我们预期的要慢,不是因为Codex能力不足,而是因为环境规范不足。代理缺乏朝着高级目标取得进展所需的工具、抽象和内部结构。我们工程团队的主要工作变成了让代理能够完成有用的工作。

在实践中,这意味着深度优先地工作:将更大的目标分解为更小的构建块(设计、编码、审查、测试等),提示代理构建这些块,并使用它们来解锁更复杂的任务。当出现问题时,解决方案几乎从来不是"更努力地尝试"。因为取得进展的唯一方法是让Codex完成工作,所以人类工程师总是介入任务并问:"缺少什么能力,我们如何使其对代理既清晰又可执行?"

人类几乎完全通过提示与系统交互:工程师描述任务,运行代理,并允许它打开拉取请求。为了推动PR完成,我们指示Codex在本地审查自己的更改,在本地和云端请求额外的特定代理审查,响应任何人类或代理给出的反馈,并循环迭代,直到所有代理审查者都满意(这实际上是一个Ralph Wiggum循环)。Codex直接使用我们的标准开发工具(gh、本地脚本和仓库内嵌的技能)来收集上下文,而无需人类复制粘贴到CLI中。

人类可能会审查拉取请求,但不是必需的。随着时间的推移,我们已经将几乎所有的审查工作推向由代理对代理处理。

提高应用程序可读性

随着代码吞吐量的增加,我们的瓶颈变成了人工QA能力。因为固定的约束一直是人类的时间和注意力,我们努力通过使应用程序UI、日志和应用指标等内容本身直接对Codex可读,来为代理添加更多能力。

例如,我们使应用程序可以按每个git工作树启动,这样Codex就可以为每个更改启动和驱动一个实例。我们还将Chrome DevTools协议集成到代理运行时中,并创建了用于处理DOM快照、屏幕截图和导航的技能。这使Codex能够直接重现错误、验证修复并推理UI行为。

我们对可观测性工具做了同样的事情。日志、指标和追踪通过本地可观测性堆栈暴露给Codex,该堆栈对于任何给定的工作树都是临时的。Codex在该应用程序的完全隔离版本上工作——包括其日志和指标,这些在任务完成后会被销毁。代理可以使用LogQL查询日志,使用PromQL查询指标。有了这些上下文,像"确保服务启动在800毫秒内完成"或"这四个关键用户旅程中没有跨度超过两秒"这样的提示变得可行。

我们经常看到单个Codex运行在一个任务上工作长达六个小时以上(通常是在人类睡觉的时候)。

我们将仓库知识作为记录系统

上下文管理是让代理在大型复杂任务中有效工作的最大挑战之一。我们学到的最早教训之一很简单:给Codex一张地图,而不是一本1,000页的说明手册。

我们尝试了"一个大的AGENTS.md"方法。它以可预见的方式失败了:

  • 上下文是稀缺资源。一个巨大的指令文件会挤占任务、代码和相关文档——因此代理要么遗漏关键约束,要么开始针对错误的约束进行优化。
  • 太多的指导变成了非指导。当一切都"重要"时,就没有什么是重要的了。代理最终会在局部进行模式匹配,而不是有意识地导航。
  • 它会立即腐烂。一个单体手册会变成过时规则的墓地。代理无法分辨什么仍然是真的,人类停止维护它,文件悄悄地变成了一个有吸引力的麻烦。
  • 它很难验证。单个大文件不适合机械检查(覆盖率、新鲜度、所有权、交叉链接),因此漂移是不可避免的。

所以,我们不是将AGENTS.md视为百科全书,而是将其视为目录。

仓库的知识库存放在结构化的docs/目录中,被视为记录系统。一个简短的AGENTS.md(大约100行)被注入到上下文中,主要用作地图,指向其他地方更深层次的真实来源。

AGENTS.md
ARCHITECTURE.md
docs/
├── design-docs/
│   ├── index.md
│   ├── core-beliefs.md
│   └── ...
├── exec-plans/
│   ├── active/
│   ├── completed/
│   └── tech-debt-tracker.md
├── generated/
│   └── db-schema.md
├── product-specs/
│   ├── index.md
│   ├── new-user-onboarding.md
│   └── ...
├── references/
│   ├── design-system-reference-llms.txt
│   ├── nixpacks-llms.txt
│   ├── uv-llms.txt
│   └── ...
├── DESIGN.md
├── FRONTEND.md
├── PLANS.md
├── PRODUCT_SENSE.md
├── QUALITY_SCORE.md
├── RELIABILITY.md
└── SECURITY.md

设计文档被编目和索引,包括验证状态和一组定义代理优先操作原则的核心信念。架构文档提供了领域和包分层的顶层地图。质量文档对每个产品领域和架构层进行评分,跟踪随时间变化的差距。

计划被视为一等公民。临时的轻量级计划用于小的更改,而复杂的工作则被记录在执行计划中,包含进度和决策日志,这些都会被检入仓库。活跃的计划、已完成的计划和已知的技术债务都是版本化的并且放在一起,使代理能够在不依赖外部上下文的情况下运行。

这实现了渐进式披露:代理从一个小的、稳定的入口点开始,并被教导下一步该看哪里,而不是一开始就被压垮。

我们通过机械方式强制执行这一点。专用的linters和CI任务验证知识库是最新的、交叉链接的并且结构正确。一个定期运行的"文档整理"代理会扫描不反映真实代码行为的过时或陈旧文档,并打开修复的拉取请求。

代理可读性是目标

随着代码库的演进,Codex的设计决策框架也需要演进。

因为仓库完全是由代理生成的,它首先针对Codex的可读性进行优化。就像团队致力于为新入职的工程师提高代码的可导航性一样,我们的人类工程师的目标是使代理能够直接从仓库本身推理整个业务领域。

从代理的角度来看,它在运行时无法在上下文中访问的任何内容实际上都不存在。存在于Google Docs、聊天线程或人们脑海中的知识对系统来说是不可访问的。仓库本地的、版本化的工件(例如代码、markdown、模式、可执行计划)是它能看到的全部。

我们了解到,随着时间的推移,我们需要将越来越多的上下文推入仓库。那个让团队在架构模式上达成一致的Slack讨论?如果代理无法发现它,它就是不可读的,就像三个月后加入的新员工不知道它一样。

给Codex更多的上下文意味着组织和暴露正确的信息,以便代理可以对其进行推理,而不是用临时指令压垮它。就像你会向新队友介绍产品原则、工程规范和团队文化(包括表情符号偏好)一样,给代理提供这些信息会产生更符合预期的输出。

这个框架澄清了许多权衡。我们倾向于可以在仓库内完全内化和推理的依赖项和抽象。通常被描述为"无聊"的技术往往更容易被代理建模,因为它们具有可组合性、API稳定性以及在训练集中的表示。在某些情况下,让代理重新实现功能子集比绕过公共库中不透明的上游行为更便宜。例如,我们没有引入通用的p-limit风格的包,而是实现了自己的带并发控制的map辅助函数:它与我们的OpenTelemetry仪表紧密集成,具有100%的测试覆盖率,并且完全按照我们的运行时期望的方式运行。

将系统的更多部分转换为代理可以直接检查、验证和修改的形式,增加了杠杆作用——不仅对Codex如此,对同样在代码库上工作的其他代理(例如Aardvark)也是如此。

强制执行架构和品味

仅靠文档无法保持完全由代理生成的代码库的连贯性。通过强制执行不变量,而不是微观管理实现,我们让代理快速交付而不破坏基础。例如,我们要求Codex在边界解析数据形状,但对如何实现不做规定(模型似乎喜欢Zod,但我们没有指定那个特定的库)。

代理在具有严格边界和可预测结构的环境中最有效,因此我们围绕严格的架构模型构建应用程序。每个业务领域被划分为一组固定的层,具有严格验证的依赖方向和有限的允许边。这些约束通过自定义linters(当然是Codex生成的!)和结构测试机械地强制执行。

下面的图表显示了规则:在每个业务领域(例如App Settings)内,代码只能通过一组固定的层"向前"依赖(Types → Config → Repo → Service → Runtime → UI)。横切关注点(auth、connectors、telemetry、feature flags)通过单一的显式接口进入:Providers。任何其他情况都是不允许的,并通过机械方式强制执行。

这是你通常会推迟到拥有数百名工程师时才会采用的架构类型。对于编码代理来说,这是一个早期的先决条件:这些约束正是允许在没有衰退或架构漂移的情况下保持速度的关键。

在实践中,我们通过自定义linters和结构测试,加上一小组"品味不变量"来强制执行这些规则。例如,我们通过自定义lints静态强制执行结构化日志记录、模式和类型的命名约定、文件大小限制以及平台特定的可靠性要求。因为lints是自定义的,我们编写错误消息以将修复指令注入到代理上下文中。

在以人为主的工作流中,这些规则可能显得迂腐或约束过强。对于代理来说,它们成为倍增器:一旦编码,它们就会立即应用到所有地方。

同时,我们明确说明约束在哪里重要,在哪里不重要。这类似于领导一个大型工程平台组织:集中强制执行边界,在局部允许自主权。你深切关注边界、正确性和可重现性。在这些边界内,你允许团队——或代理——在如何表达解决方案方面拥有相当大的自由度。

生成的代码并不总是符合人类的风格偏好,这没关系。只要输出是正确的、可维护的,并且对未来的代理运行可读,它就达到了标准。

人类品味会持续反馈到系统中。审查评论、重构拉取请求和面向用户的bug被捕获为文档更新或直接编码到工具中。当文档不足时,我们将规则提升到代码中。

吞吐量改变合并理念

随着Codex吞吐量的增加,许多传统的工程规范变得适得其反。

该仓库以最少的阻塞合并门控运行。拉取请求是短暂的。测试不稳定性通常通过后续运行来解决,而不是无限期地阻塞进度。在代理吞吐量远远超过人类注意力的系统中,纠正是便宜的,而等待是昂贵的。

这在低吞吐量环境中是不负责任的。在这里,这通常是正确的权衡。

"代理生成"实际意味着什么

当我们说代码库是由Codex代理生成的时,我们指的是代码库中的一切。

代理产生:

  • 产品代码和测试
  • CI配置和发布工具
  • 内部开发工具
  • 文档和设计历史
  • 评估工具
  • 审查评论和回复
  • 管理仓库本身的脚本
  • 生产仪表板定义文件

人类始终保持在循环中,但工作在与以往不同的抽象层。我们对工作进行优先排序,将用户反馈转化为验收标准,并验证结果。当代理遇到困难时,我们将其视为信号:识别缺少什么——工具、护栏、文档——并将其反馈到仓库中,始终通过让Codex本身编写修复来完成。

代理直接使用我们的标准开发工具。它们获取审查反馈,内联响应,推送更新,并经常压缩和合并自己的拉取请求。

提高自主性水平

随着越来越多的开发循环被直接编码到系统中——测试、验证、审查、反馈处理和恢复——该仓库最近跨越了一个有意义的门槛,Codex现在可以端到端地驱动一个新功能。

给定一个单一提示,代理现在可以:

  • 验证代码库的当前状态
  • 重现报告的bug
  • 录制演示故障的视频
  • 实施修复
  • 通过驱动应用程序来验证修复
  • 录制第二个演示解决方案的视频
  • 打开拉取请求
  • 响应代理和人类的反馈
  • 检测并修复构建失败
  • 仅在需要判断时上报给人类
  • 合并更改

这种行为在很大程度上依赖于该仓库的特定结构和工具,不应假设在没有类似投资的情况下可以推广——至少现在还不行。

熵和垃圾回收

完全的代理自主性也引入了新问题。Codex复制仓库中已经存在的模式——即使是不均匀或次优的模式。随着时间的推移,这不可避免地导致漂移。

最初,人类手动解决这个问题。我们团队过去每个周五(一周的20%)都花在清理"AI垃圾"上。毫不奇怪,这无法扩展。

相反,我们开始将我们称为"黄金原则"的东西直接编码到仓库中,并构建了一个定期清理流程。这些原则是固执己见的、机械化的规则,使代码库对未来的代理运行保持可读性和一致性。例如:(1) 我们倾向于共享实用工具包而不是手工编写的辅助函数,以保持不变量的集中化,(2) 我们不以"YOLO风格"探测数据——我们验证边界或依赖类型化的SDK,这样代理就不会意外地基于猜测的形状进行构建。我们定期运行一组后台Codex任务,扫描偏差,更新质量等级,并打开有针对性的重构拉取请求。这些大多数可以在不到一分钟内审查并自动合并。

这就像垃圾回收一样运作。技术债务就像高息贷款:以小增量持续偿还几乎总是比让它复利积累并在痛苦的爆发中解决要好。人类品味被捕获一次,然后在每一行代码上持续强制执行。这也让我们能够每天捕获和解决不良模式,而不是让它们在代码库中传播数天或数周。

我们仍在学习的东西

到目前为止,这一策略在OpenAI的内部发布和采用中运行良好。为真实用户构建真实产品有助于将我们的投资锚定在现实中,并引导我们走向长期可维护性。

我们尚不知道的是,在一个完全由代理生成的系统中,架构连贯性如何在多年间演进。我们仍在学习人类判断在哪里增加最大的杠杆作用,以及如何编码这种判断使其复利增长。我们也不知道随着模型随时间推移变得更有能力,这个系统将如何演进。

现在已经清楚的是:构建软件仍然需要纪律,但纪律更多地体现在脚手架而不是代码中。保持代码库连贯性的工具、抽象和反馈循环变得越来越重要。

我们现在最困难的挑战集中在设计环境、反馈循环和控制系统上,这些系统帮助代理实现我们的目标:大规模构建和维护复杂、可靠的软件。

随着像Codex这样的代理承担软件生命周期的更大部分,这些问题将变得更加重要。我们希望分享一些早期经验能帮助你思考在哪里投入精力,这样你就可以专注于构建东西