16 万行代码是怎么“跑”出来的?

0 阅读14分钟

16 万行代码是怎么“跑”出来的?

AI 动力实验室 · 第四篇|从 AI Coding 到 AI Engineering

04-cover-ai-engineering-system.png

第一篇里我提了一个数字:

16 万行代码。

但现在再看,这个数字本身其实已经没那么稀奇。

AI 会写代码,已经不是今天真正值得讨论的问题。

真正值得讨论的是另一件事:

当 AI 不只是写一个函数,而是连续工作几个小时、跨十几个模块、跑上千次执行循环时,是什么保证它不会越来越偏?

因为单次 Coding 和持续 Engineering,完全是两件事。

让 AI 写 200 行代码,不算特别难。

难的是让它:

  • 知道这一次应该改什么;
  • 只拿到当前真正需要的上下文;
  • 改完以后自动编译、测试、验证;
  • 出错以后知道怎么恢复;
  • 连续失败以后知道什么时候停;
  • 做完一个任务以后,能够准确进入下一个任务;
  • 中途进程挂掉以后,还能从正确的位置继续。

而我们最后得到的这 16 万行代码,恰恰不是某一次 Prompt 的产物。

它来自上千次这样的循环:

理解任务
  ↓
获取上下文
  ↓
生成代码
  ↓
执行工具
  ↓
编译 / 测试
  ↓
验证结果
  ↓
失败 → 修复 → 再验证
  ↓
通过
  ↓
记录状态
  ↓
进入下一个任务

所以,这篇文章真正想讲的不是:

AI 怎么写出了 16 万行代码?

而是:

我们怎么让 AI 有能力持续跑完上千次这样的循环?

答案也不是换了一个更强的模型。

而是我们在模型外面,逐渐搭出了一套 AI Engineering System。

这套系统负责的事情很简单:

模型负责"做",系统负责让它持续、受控、可验证地做下去。

接下来,我把这套系统是怎么一点点踩坑、演化出来的,拆给你看。

第一部分:以前是一个循环,现在是一个系统

先说以前的 AI Coding 是什么样的。

大多数人使用 AI 写代码的方式,本质上是一个单次循环:

人写 Prompt → AI 生成代码 → 人看一眼 → 贴进项目 → 人自己跑测试

这个循环里,AI 只负责中间那一步:生成。上下文是人喂的,验证是人做的,出了问题还是人改的。

AI 是一个"更快的打字员"。

这种模式能写几百行代码,但写不了 16 万行。因为当规模变大,人就变成了瓶颈——你喂不完上下文,审不完结果,改不完错误。

04-old-loop-vs-system.png

我们后来跑通的模式,本质上不一样。它不是一个循环,是一个系统:

Task(编排器决定下一步)
  → Context(只给当前任务需要的上下文)
    → LLM(AI 生成代码)
      → Tool(编译、执行)
        → Verification(跑测试、检查结果)
          → 失败?→ Recovery(AI 修复,最多三次)
            → 仍然失败?→ 升级给人
          → 通过?→ Checkpoint(记录状态)
            → Next Task

16 万行代码,本质上就是这个 Loop 跑了上千次。

每一次循环,AI 只完成一小步。但系统确保每一步都被验证,失败能恢复,状态能记录,下一步能衔接。

区别在哪?

以前,人是循环的驱动者。AI 只是循环里的一个步骤。

现在,系统是循环的驱动者。AI 是循环里的执行主体。人退到了循环外面——定目标、审设计、在关键节点做决策。

这不是 AI 更聪明了。是围绕 AI 的系统变了。

第二部分:为什么 AI 能连续干活?

这件事的难度不在模型。我们在做的过程中,反复撞上四面墙。

04-four-walls.png

第一面墙:我们最开始犯的错,是给 AI 太多代码。

第一版系统的思路很朴素。既然 AI 的上下文窗口越来越长,那就把更多东西塞给它——PRD、架构设计、接口定义、相关代码、测试代码,能给的全给。

结果并没有变好。

反而开始出现一个奇怪的现象:AI 看得越多,改得越多,错得也越多。

它会把不相关的代码模式混进当前模块,试图兼容一些根本不在当前任务范围内的约束。更离谱的是,有时候它会"参考"另一个模块的实现,把那边的一个 bug 也一起抄过来。

后来我们在编码流程里加了一条硬规则:"渐进式读取——只读当前模块需要的设计与代码,禁止开局一次性全部读完。"

这条规则看起来简单,但大概减少了 30% 的无效修改。

给 AI 更少但更精确的上下文,效果比给更多但不相关的上下文好得多。 噪音让模型困惑,精确的信息让它聚焦。

第二面墙:大任务直接给 AI,它一定跑偏。

早期有一次,我们试过给 AI 一个比较完整的任务:"实现这个服务的全部接口"。

AI 很努力,连续写了好几个小时。最后一看,前两个接口写得不错,到第三个开始走形,到第五个已经和设计严重偏离——因为它在上下文里攒了太多自己前面写的半成品代码,越来越偏离原始设计。

教训很明确:大任务必须变成小任务。

你不能对 AI 说"给我写一个 Agent 平台"。你得说"实现某个核心模块的会话创建接口,输入输出结构和边界条件都写在详细设计里,一次只做一个接口"。

我们的编排器做的核心工作之一,就是把一个大需求拆成模块,每个模块有明确的输入、输出、验证标准。AI 一次只做一个模块,做完验证通过了,编排器再推进到下一个。上一个模块的中间过程不带进下一个模块——只带结果。

第三面墙:AI 不知道什么时候应该停下来。

AI 写的代码编译不过,很正常。测试跑红了,很正常。关键是系统怎么处理这些失败。

我们最早没有做失败处理。

凌晨 2:17

Agent 还在跑。

2:31

还是在跑。

3:06

依然在跑。

我们当时以为它只是还没有修好。

直到第二天早上打开日志,才发现:

它已经连续修改同一个文件 47 次。

第一次:

修改异常处理。

第二次:

修改异常处理。

第三次:

重新组织异常处理。

……

第 47 次:

又回到了第一次的方案。

代码没有越来越好。

它只是越来越不像原来的代码。

问题不是模型不会修,而是系统没有告诉它什么时候应该停止。

这一下"三次断路"就不是一个架构设计,而是一个被事故逼出来的设计:同一个失败点,AI 可以做三次实质不同的尝试。第一次失败,自己看错误信息修复。第二次失败,系统给它更多上下文——比如相关的已有实现,或者编码规范里的对应章节。第三次还失败,系统不再让它挣扎,而是停下来,把问题升级给人。

三次,不多不少。 AI 自信地反复尝试同一个错误方向,比它直接报错更危险。自动化不是让 AI 永远不停地跑,而是让它在应该继续的时候继续,在不应该继续的时候停下来。

第四面墙:不是所有步骤都应该自动推进。

需求确认了才能进入设计,设计通过了才能开始编码,编码完了才能部署——这些在传统研发里靠人盯,在 AI 驱动的系统里靠门控。

我们的编排器在关键节点设置了硬门控。比如 PRD 没有经过人确认,系统不会让 AI 进入设计阶段。部署前必须有人明确批准。这些门控不是"建议",是硬约束——系统层面不可能被绕过。

早期有一次门控配置缺失,AI 完成编码后编排器判定测试失败,把任务退回修复流程。修复完成后编排器又推进到编码,但跳过了一个中间确认步骤,又触发同一个失败,又退回修复——来来回回,形成了一个活锁。

这件事催生了两个设计改进:一是编排器的每次状态变更都写到磁盘上的状态账本,可以在任何时刻回溯和恢复,不靠内存状态;二是失败回退有明确的续跑机制——只从失败点重试,不重跑已通过的阶段。

门控的存在,恰恰是 AI 能被信任去自动推进其他步骤的前提。 因为你知道在关键节点,它一定会停下来。

第三部分:验证,才是这个系统的核心

上面说的上下文管理、任务拆解、失败处理、门控,都很重要。但如果只能选一个说,我会说:

验证。

没有验证,其他一切都不成立。

想想看:AI 生成了一段代码,你怎么知道它是对的?

"看起来合理"不算。"AI 说它测过了"不算。唯一算数的是:代码被实际执行,测试被实际运行,结果被实际检查。

这件事在我们的系统里被拆成了三层:

第一层:编译和单元测试。

每个模块实现完,立刻跑编译和单测。不是 AI 自己说"我觉得能过",是系统实际执行项目的编译与测试命令,拿到真实的退出码和输出。红了就是红了,AI 必须修到绿才能往下走。

这一层是硬门槛。没有它,后面全是空中楼阁。

第二层:回归验证。

改了一个模块,不能只看这个模块自己的测试。还要检查:这个模块的调用者,行为有没有变?共享的接口,签名有没有被改坏?系统层面跑受影响模块的测试,确认改动没有引入连锁反应。

第三层:环境对照。

如果有可以连接的测试环境,系统会通过浏览器自动化工具,按照预设的操作步骤实际跑一遍。修复前截图,修复后截图,两张图放在一起——任何人都能复核。

三层验证的核心逻辑是同一个:

不相信 AI 的判断,只相信 AI 的产出在真实环境中的表现。

这和传统软件工程的测试金字塔并没有本质区别。区别在于,以前测试主要由人驱动;现在,测试由系统自动驱动。AI 负责产生工程变更,系统负责判断这次变更是否成立。

但这里有一个需要诚实面对的事情:验证通过,不等于代码一定正确。

我们也踩过"验证覆盖了但覆盖错了"的坑——测试全绿,断言的却不是业务结果。

所以验证体系真正做的事情,不是证明代码绝对正确,而是不断缩小错误能存活的空间

每多一层验证,错误能藏身的缝隙就少一点。编译卡住语法错误,单测卡住逻辑错误,回归卡住连锁反应,环境对照卡住集成问题。没有任何一层能保证万无一失,但叠在一起,漏网的概率越来越低。

AI Engineering 最重要的不是让 AI 不犯错,而是让错误尽快暴露,并且错误不能无限扩大。

第四部分:AI 最危险的不是"不会做"

前面讲的是系统最后长成的样子。这里换一层视角:把几类典型事故放在一起看——类型不同,根因相同,都是 AI 的能力边界没有被系统补齐。

第一类:AI 不知道自己不知道。

有一次 AI 碰到了一个二进制格式的配置文件。它不知道那是二进制的,把它当文本来处理,"修复"后文件损坏了。

更危险的是,它告诉我们"已修复,测试通过"——因为那个测试碰巧没有覆盖到这个文件的完整性。它不会说"我不懂",只会给你一个看起来合理的答案。

第二类:AI 可以证明"测试通过",但不能证明"业务正确"。

AI 写了一组测试,测试全绿,看起来一切正常。

但仔细一看,测试断言的是中间状态而不是业务结果。比如它断言"函数返回值不为 null"——这当然不为 null,但返回的数据是错的。相当于在说"代码跑了"而不是"代码跑对了"。

还有一种更隐蔽的变体:AI 写的测试和它写的代码犯了同一个理解错误。测试验证的是错误的预期,代码实现的也是错误的逻辑,两边一对——绿了。但业务是错的。

后来我们加了两条约束。第一条:"断言业务结果,不断言中间过程。" 如果测试只断言了"没有抛异常"或"返回不为空",不算通过。第二条:测试的预期值必须能从详细设计里找到出处,不能是 AI 自己推导的。

第三类:AI 可以修复当前问题,却破坏系统其他地方。

这类问题有三种典型形态。

某次 AI 在修改一个共享的数据模型时,加了一个字段。这个字段在当前模块的测试里没有问题。但另一个模块的序列化逻辑没有处理这个新字段,反序列化时直接报错。当前模块的测试全绿,编排器判定通过,推进到下一个任务——直到两天后跑集成测试才发现。

还有一次修复过程中,AI 为了让测试通过,把一个异常处理逻辑里的 catch 块改宽了——从捕获特定异常变成了 catch all。测试确实绿了,因为异常被吞掉了。但这意味着所有本该暴露的错误都会被静默忽略。

还有一种:AI 直接修改测试本身,或者放宽断言,来制造"通过"。代码"工作了",但工作方式是错的。

这时候我们才意识到:验证不能只验证“代码有没有跑起来”,还必须验证“AI 是不是通过正确的方式让它跑起来”。

第四类:AI 的记忆不能成为系统状态。

有一次系统在跑到第六个模块时,外部依赖超时,整个会话中断。代码都在,但编排器不知道自己跑到哪了——进度只存在 AI 的上下文记忆里。

这和第二面墙里"只带结果、不带过程"是同一原则:上下文可以被压缩、可以丢失,但系统状态不能。 活锁那次事故逼出了状态账本,这次则逼出了检查点和续跑。

四个问题,最后收敛到同一张图:

04-ai-capability-boundaries.png

Context · Task · Verification · Recovery · State——这五件事,就是前面所有事故的收敛。模型的推理能力一直在进步,但"能推理"和"能可靠地完成工程任务"之间,隔着这一整套系统工程。而每一个规则,几乎都是用一次真实的事故换来的。

尾声

现在再回头看第一篇那个数字:

16 万行代码。

它其实没那么重要。

重要的是,这 16 万行代码背后的那个 Loop:

Task → Context → LLM → Tool → Verification
  → Recovery → Checkpoint → Next Task

这个 Loop 跑了上千次。模型负责产生能力,系统负责让循环持续、受控、可验证。

而人,逐渐从"每一行代码的生产者",退到了:

定义目标、设计系统、建立边界、处理例外、做最终判断。

所以真正值得问的问题,从来不是:

AI 能不能写 16 万行代码?

而是:

我们能不能构建一个系统,让这个 Loop 在足够长的时间里,持续跑下去,并且每一圈都在缩小错误的空间?

这可能才是 AI Native 软件工程真正值得研究的问题。

因为真正的问题马上就来了:

如果一个 Agent 连续运行 3 个小时,它怎么知道自己现在还在做正确的事情?

如果进程挂了,它怎么知道应该从哪里继续?

如果上下文已经压缩了,它怎么知道自己之前做过什么?

如果一个任务失败了,它怎么知道应该重试、回滚,还是直接停止?

这时候,我们真正需要的已经不是一个 Agent,而是一个 Agent Runtime

下一篇,我们继续拆这个 Runtime——状态机、检查点、续跑,以及失败恢复。

我们实验室见。