当 Agent 开始持续生成、修改和验证代码,一部分代码正在逐渐退出人的主要视野。但这并不意味着代码会整体变成无需理解的底层产物。本文从意图表达、验证能力、错误成本和长期演进四个维度,讨论哪些代码可以少看,哪些仍然需要被人理解。
本文是「企业研发 AI 自动化」系列第 37 篇,欢迎关注和交流。
用 Coding Agent(下文简称 Agent)做需求以后,一个越来越常见的情况是:代码已经改完了,测试也过了,但我们未必真的看过它写下的每一段代码。
过去这很难想象。代码既是实现需求的工具,也是工程师理解系统的主要入口。现在,Agent 开始夹在人的意图和代码之间:我们描述要做什么,它负责搜索仓库、修改实现、运行测试,再把结果交回来。人仍然定义目标,也仍然判断结果,但已经不再参与每一个实现步骤。
如果这种工作方式继续下去,一个问题迟早会出现:源代码会不会像今天的汇编语言一样,仍然存在,却逐渐退出大多数开发者的主要视野?
这个类比并不严格。Agent 没有取代编译器,它只是在人和源代码之间增加了一层非确定性的生成过程。但它提出的问题是真实的:当代码越来越容易生成,我们还需要在多大程度上理解代码?
Thoughtworks 的 Valentina Servile 在 《Should we still design code for humans?》 中讨论了一个类似的问题:如果 Agent 可以生成、修改甚至维护大部分代码,我们还需不需要像过去一样,为人类理解去设计代码?
答案很难简单归结为“需要”或者“不需要”。有些代码确实已经可以少看,甚至几乎不用看;有些代码,人还远远不能退出。值得讨论的,是两者之间的边界。
这种变化其实已经发生在一部分代码上。
假设 Agent 根据一份 OpenAPI 定义生成 API Client。接口、参数和返回结构已经确定,结果也可以通过类型检查和自动测试验证。这种时候,Agent 最后用了什么变量名,拆了几个辅助函数,大多数情况下并不值得投入太多注意力。
一次性的格式转换脚本、模板化页面、依赖升级和机械迁移,也有类似特点。它们的输入输出相对明确,做错后容易重来,而且代码通常不会被很多人维护很多年。对这类任务来说,人把关注点从实现过程转向最终结果,是很自然的变化。
这些例子还只是“代码容易重新生成”。如果再往前一步,不只是让 Agent 重新生成代码,而是连它可以怎么写都提前限制住,代码就更容易退到幕后。
领域特定语言,也就是 DSL,就是这种思路的一种典型做法。Unmesh Joshi 在 《DSLs Enable Reliable Use of LLMs》 中讨论过这样的方式:人或 Agent 使用一个受约束的领域表达,再由解析器、类型系统或编译器判断它是否合法,并生成更底层的实现。
这里起作用的,并不是自然语言突然变得足够可靠,而是自然语言之下已经形成了一个表达范围更小、语义更清楚、可以被工具验证的层。
只有当更高层的表达和验证体系接管了一部分设计决策,底层代码才真正具备退到幕后的条件。
所以,一部分代码从“需要长期理解的工程资产”,变成“可以按需重新生成的派生实现”,这件事已经开始发生。问题在于,这个结论能不能继续推到所有代码上。
“代码会变成汇编语言”这个说法之所以有吸引力,是因为它暗含了一个很自然的推论:既然代码可以随时重新生成,人只要维护好规格说明(Spec)和测试,Agent 就可以像编译器一样,把人的意图稳定地转换成代码。
但两者之间有一个很大的差别。
编译器拿到的是一份已经做完大量设计决策的程序。它不会替开发者决定状态应该放在哪里,不会重新考虑模块怎样拆分,也不会因为某个异常不好处理,就临时给系统加一层兜底逻辑。
自然语言 Spec 很少包含这么完整的信息。一份需求可以写清楚用户需要完成什么操作,也可以描述主要流程和验收标准,但进入实现后,仍然会冒出很多没有现成答案的问题。
比如一个看起来并不复杂的需求:给系统增加一套新的账号权限规则。落到实现里,很快就会遇到一串问题:已有角色是否继续沿用旧权限?不同资源是否采用同一套规则?临时授权怎么处理?历史配置是否需要兼容?规则调整后,已经生效的权限是否需要重新计算?
这些问题总要有人回答。过去,它们主要由工程师在实现过程中不断发现、确认和处理;现在,Agent 也开始替人完成其中一部分判断。
相同需求放进不同仓库、给不同上下文,最后可能长出完全不同的模块划分、状态模型和异常策略。原因不只是模型输出存在随机性,更重要的是 Spec 没有覆盖全部设计决策,Agent 必须自己补齐。
编译器主要是在转换已经完成的设计,Agent 则会在生成代码的同时继续做设计。
这也是为什么“把 Spec 写得更详细”并不能自动解决问题。当然,可以不断向 Spec 中加入模块边界、状态变化、异常处理、性能要求和兼容策略。但信息越写越完整,Spec 也会越来越像另一种程序,只是通常缺少编程语言那样明确的语义、类型约束和成熟工具链。
只要还有大量关键判断无法在 Spec 中提前表达,Agent 就仍然会在实现层替人做设计,人也就不能把代码简单当成透明的编译产物。
代码至少承担着三件不同的事:它最终要被机器执行,也承载实现过程中形成的设计,还记录着系统长期演进后真正留下的结构。AI 最容易改变的是第一层,后两层没有那么容易消失。
作为执行产物,代码当然越来越容易由 Agent 生成和修改。只要目标清楚、结果可以验证,人并不需要知道每一个语句是怎么产生的。
但代码同时还是设计展开的地方。数据结构、接口、控制流、模块依赖、异常传播和并发方式,这些并不是高层方案结束后剩下的机械细节。很多时候,只有真正进入实现,才会发现原来的设计并不完整。
一个新规则可能无法放进现有抽象;两个看似独立的状态可能共享生命周期;一种异常处理方式可能破坏上游调用者的假设;一个性能问题也可能迫使团队重新划分模块边界。这样的事实不会因为上游已经有一份 Spec 就消失。
Spec 描述的是我们希望系统成为什么,代码暴露的是系统最后实际长成了什么。
代码还是团队理解当前系统的重要载体。系统运行得越久,这一点通常越明显。
《The Archaeologist’s Copilot》 记录了一次遗留 Java 系统的现代化改造。作者一开始让模型直接阅读仓库并给出运行方案,模型很快生成了一套看起来合理的现代化配置,但其中对依赖版本、目录结构和运行条件的判断并不准确。真正把系统重新跑起来,靠的是回到代码、构建环境和运行结果中,一点点重新确认事实。
README、架构图、设计文档,甚至测试,都可能只保存了系统的一部分真实状态。代码当然也不是唯一事实,配置、数据和运行环境同样重要。但一个系统为什么会演化成今天这个样子,往往仍有大量信息只留在具体实现里。
所以,代码值不值得人继续深入理解,关键不在于它是谁写的,而在于里面是否还藏着上层 Spec、规则和测试没有覆盖的设计决定。
可以把这个问题放得再具体一点。
假设 Agent 分别写了两段十几行的代码。
第一段,把一份 CSV 转换成系统需要的 JSON。输入格式明确,输出可以逐项比较,运行错了也能直接重来,而且脚本用完后就会被丢弃。
第二段,判断一个用户是否拥有某项账号权限。代码同样不长,但它背后可能涉及角色差异、历史规则、资源范围、上下文条件和兼容逻辑。一个错误未必马上从测试中暴露,却可能直接影响真实用户是否能够访问某项功能。
两段代码都是 Agent 写的,长度也差不多,但我们显然不应该用同一种方式对待它们。
**差别首先在于,关键意图有没有被说清楚。**CSV 转换的输入、输出和规则很容易描述完整;权限判断则可能包含大量没有进入文档的隐含知识。某个角色是否继承历史权限,资源范围如何限制,特定上下文下是否需要例外处理,这些都可能影响最终结果。Agent 需要自行补充的判断越多,人越需要进入实现层,确认它到底替团队做了什么决定。
**第二个差别,是结果能不能被独立、可靠地验证。**类型系统、单元测试、端到端测试、视觉基线、接口契约和业务规则,都能帮助判断 Agent 是否完成了任务。但“有测试”并不等于“需求已经被可靠验证”。
论文 《Building to the Test: Coding Agents Deliver What You Check, Not What You Requested》 做过一个规模不大的实验:Agent 在组件迁移任务里拿到了很高的测试得分,却没有真正完成要求中的可复用组件,而是把测试需要看到的行为直接写进了演示页面。这说明测试通过和真实目标完成之间仍然可能存在差距:Agent 往往会围绕实际可观测的检查信号优化结果,而检查信号未必完整代表人的真实意图。
验证器自身也可能出问题。同一个遗留系统案例里,旧测试甚至会在打印异常后继续返回成功状态。自动系统看到的是“通过”,真实任务却已经失败。因此,验证只有在覆盖关键交付意图,而且自身足够可信时,才能替代一部分代码阅读。
**第三个差别,是做错一次的代价。**页面间距不对,可以重新生成;一次性脚本结果错误,可以废弃重跑。资金计算、权限控制、数据迁移、安全策略和合规逻辑则完全不同。很多时候,决定自动化边界的并不是“Agent 大多数时候能不能做对”,而是“它偶尔做错一次,我们能不能承受”。
**最后,还要看这段代码以后是否需要继续演进。**一次性代码完成任务后就可以丢弃,今天写得够不够漂亮,对明天影响不大。长期系统则会不断接纳新需求,也会被不同团队和不同 Agent 持续修改。今天增加的一层兼容、一个局部例外或者一次方便但草率的抽象,都会成为下一次修改必须面对的上下文。
Agent 很擅长解决眼前的问题,但一个系统也可能在很多次“局部正确”之后,变得越来越难理解。
把这些因素放在一起,可以得到一条比较清楚的边界:
代码能否退出人的主要视野,取决于关键意图能否被充分表达、结果能否被独立且可靠地验证、错误成本是否可控,以及实现是否需要长期演进。
当这些条件都比较有利时,人完全可以少看甚至不看底层实现;如果只有部分条件成立,就应该选择性关注关键模块、异常路径和系统边界;当意图仍然模糊、验证能力有限、错误代价很高,又需要长期多人维护时,代码依然是人的主要工程对象。
这比“简单需求交给 Agent,复杂需求人工处理”更接近真实情况。决定人是否需要深入看代码的,往往不是任务有多少行、要开发几天,而是关键意图是否清楚、结果是否可验证、错误是否可承受,以及实现是否需要长期演进。
这里的“读”,并不只是逐行做 Code Review,也包括在关键变化发生时重新进入实现,理解其中的设计选择、系统边界和可能后果。
未来当然不会要求工程师逐行审查所有 Agent 生成的代码。值得投入注意力的,会越来越集中在几类地方。
第一类是改变系统边界的代码。新增公共抽象、调整模块职责、改变数据所有权、修改跨系统接口,这些变化会影响后续很多需求,也会改变其他团队和 Agent 对代码库的理解,不能只看当前任务有没有跑通。
第二类是承载核心规则的代码。资金、权限、安全、合规、关键状态流转,这些位置即使测试覆盖了主要路径,也仍然需要判断规则是否放在正确的位置,以及局部实现是否破坏了整体约束。
第三类是处在验证盲区的代码。如果测试难以覆盖,或者“测试通过”和“真正完成需求”之间还有明显距离,人就需要进入实现里寻找证据。
最后,是会被大量复用和模仿的代码。公共组件、基础设施、通用框架和团队级模式,一旦成为样板,后面的开发者会用,Agent 也会学。一个不合理的抽象,很容易被快速复制成整个系统的默认做法。
代码阅读不会消失,但它很可能从全量、平均的审查,变成风险驱动、证据驱动的选择性审查。
未来的工程师可能不再是每一行代码的直接作者,也不需要知道系统里所有实现细节,但仍然需要知道核心规则在哪里,关键边界为什么这样划,哪些部分已经可以放心交给自动验证,哪些地方还藏着无法被外部表达和检查覆盖的判断。
从这个角度看,Spec-driven Development 的价值也并不是“以后只维护 Spec”。更合理的关系是:Spec 负责表达意图和约束,Agent 负责生成计划和实现,自动系统验证可以确定检查的部分,人则把注意力放在关键设计、风险和系统影响上;实现过程中出现的新事实,再反过来修改 Spec。
Spec 和代码并不是谁替代谁的关系,它们更像两个需要不断互相校正的层。
代码会成为 AI 时代的“汇编语言”吗?
答案可能是:一部分会,一部分不会。
当意图已经足够清楚,结果能够可靠验证,做错了可以恢复,而且实现本身也不需要长期维护时,人确实没有必要继续关注每一处底层代码。这些代码会逐渐退到幕后。
但只要一个系统里仍然存在无法提前说清的设计判断、验证覆盖不到的边界、无法轻易承受的错误,以及需要长期演进的实现,人就还不能完全退出代码这一层。
未来的软件工程也许不会要求我们理解每一行代码,但仍然需要有人理解:
系统为什么会变成现在这个样子。
《企业研发 AI 自动化》是我持续记录 AI 进入真实研发流程后的实践系列。
它关注的不只是 AI Coding 工具本身,而是从需求输入、任务表示、Agent 执行到自动化验证的完整链路:AI 如何在真实工程系统里稳定运行,并逐渐形成可复用的研发自动化能力。
欢迎关注和交流~