16 万行代码是怎么“跑”出来的?
AI 动力实验室 · 第四篇|从 AI Coding 到 AI Engineering
第一篇里我提了一个数字:
16 万行代码。
但现在再看,这个数字本身其实已经没那么稀奇。
AI 会写代码,已经不是今天真正值得讨论的问题。
真正值得讨论的是另一件事:
当 AI 不只是写一个函数,而是连续工作几个小时、跨十几个模块、跑上千次执行循环时,是什么保证它不会越来越偏?
因为单次 Coding 和持续 Engineering,完全是两件事。
让 AI 写 200 行代码,不算特别难。
难的是让它:
- 知道这一次应该改什么;
- 只拿到当前真正需要的上下文;
- 改完以后自动编译、测试、验证;
- 出错以后知道怎么恢复;
- 连续失败以后知道什么时候停;
- 做完一个任务以后,能够准确进入下一个任务;
- 中途进程挂掉以后,还能从正确的位置继续。
而我们最后得到的这 16 万行代码,恰恰不是某一次 Prompt 的产物。
它来自上千次这样的循环:
理解任务
↓
获取上下文
↓
生成代码
↓
执行工具
↓
编译 / 测试
↓
验证结果
↓
失败 → 修复 → 再验证
↓
通过
↓
记录状态
↓
进入下一个任务
所以,这篇文章真正想讲的不是:
AI 怎么写出了 16 万行代码?
而是:
我们怎么让 AI 有能力持续跑完上千次这样的循环?
答案也不是换了一个更强的模型。
而是我们在模型外面,逐渐搭出了一套 AI Engineering System。
这套系统负责的事情很简单:
模型负责"做",系统负责让它持续、受控、可验证地做下去。
接下来,我把这套系统是怎么一点点踩坑、演化出来的,拆给你看。
⸻
第一部分:以前是一个循环,现在是一个系统
先说以前的 AI Coding 是什么样的。
大多数人使用 AI 写代码的方式,本质上是一个单次循环:
人写 Prompt → AI 生成代码 → 人看一眼 → 贴进项目 → 人自己跑测试
这个循环里,AI 只负责中间那一步:生成。上下文是人喂的,验证是人做的,出了问题还是人改的。
AI 是一个"更快的打字员"。
这种模式能写几百行代码,但写不了 16 万行。因为当规模变大,人就变成了瓶颈——你喂不完上下文,审不完结果,改不完错误。
我们后来跑通的模式,本质上不一样。它不是一个循环,是一个系统:
Task(编排器决定下一步)
→ Context(只给当前任务需要的上下文)
→ LLM(AI 生成代码)
→ Tool(编译、执行)
→ Verification(跑测试、检查结果)
→ 失败?→ Recovery(AI 修复,最多三次)
→ 仍然失败?→ 升级给人
→ 通过?→ Checkpoint(记录状态)
→ Next Task
16 万行代码,本质上就是这个 Loop 跑了上千次。
每一次循环,AI 只完成一小步。但系统确保每一步都被验证,失败能恢复,状态能记录,下一步能衔接。
区别在哪?
以前,人是循环的驱动者。AI 只是循环里的一个步骤。
现在,系统是循环的驱动者。AI 是循环里的执行主体。人退到了循环外面——定目标、审设计、在关键节点做决策。
这不是 AI 更聪明了。是围绕 AI 的系统变了。
⸻
第二部分:为什么 AI 能连续干活?
这件事的难度不在模型。我们在做的过程中,反复撞上四面墙。
第一面墙:我们最开始犯的错,是给 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 的上下文记忆里。
这和第二面墙里"只带结果、不带过程"是同一原则:上下文可以被压缩、可以丢失,但系统状态不能。 活锁那次事故逼出了状态账本,这次则逼出了检查点和续跑。
⸻
四个问题,最后收敛到同一张图:
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——状态机、检查点、续跑,以及失败恢复。
我们实验室见。