当写代码的速度趋近于无限,我们靠什么守住软件的命门?——AI 智能体时代的软件工程治理深思
文章目录
引言:清晨醒来,面对数万行"全绿"代码的恐惧
想象这样一个场景:
昨天晚上睡觉前,你用语音向电脑里的 6 个编码智能体(Coding Agents)派发了 6 个需求工单——从新增一个账单统计接口、接入飞书卡片通知,到修复两个偶发的并发状态 Bug。整晚你睡得很踏实,后台的智能体在各自的 Git 分支里不知疲倦地读代码、改文件、跑测试。
第二天早上八点,你端着咖啡坐到电脑前。屏幕上弹出令人振奋的汇总看板:6 个工单全部完成,新增代码数万行,单元测试全部显示绿色通过,Git 提示没有任何文本冲突。
这一刻,你是应该感到狂喜,还是后背发凉?
如果你真正经历过用 AI 高频迭代一个中型以上项目的全过程,你一定明白那种深不见底的恐惧:你根本不可能在早会前的半小时里实质性地读完这数万行代码。 你只能草草扫一眼文件名,怀着侥幸心理点下"Merge(合并)",然后发一条推:「我们已经完全不用人类 review 代码了。」
第一周,你觉得自己掌握了十倍速开发的魔法; 第三周,你发现每改一个小需求,AI 都要顺手动十几个文件,而且经常把上周刚修好的 Bug 重新改出来; 到了第二个月的某个深夜,线上系统突然发生死锁崩溃。你打开代码库想紧急救火,却绝望地发现——面对这坨被 AI 反复涂改了上百次、充斥着冗余分支和奇怪包装函数的五万行代码,团队里已经没有任何一个人类知道它到底是怎么跑起来的了。
这就是今天所有深度使用 AI 编程的团队正在或即将撞上的隐形南墙:当代码生产的边际成本趋近于零时,软件工程的核心矛盾,已经从"如何更快地写出代码",彻底逆转为"如何对抗指数级爆发的系统熵增与人类认知破产"。
这篇文章想和你深入探讨的,不是怎么写几句花哨的 Prompt 让 AI 生成一个玩具 Demo,而是站在架构师与工程治理的视角,回答一个决定未来软件寿命的终极问题:在 AI 智能体时代,我们究竟该如何维护和治理一个需要长期演进的软件系统?
一、 撕开温情脉脉的面纱:为什么 AI 是天生的"局部缝合怪"?
很多人习惯把 AI 写烂代码归咎于"模型还不够聪明",并寄希望于下一代基座模型能自动写出大师级的架构。但如果我们从大语言模型(LLM)的工作机理出发,就会发现一个残酷的事实:AI 写出难以维护的面条代码,不是因为算力不足,而是由它的底层目标函数决定的。
在软件维护中,AI 智能体有着六个深入骨髓的"基因缺陷":
1. 它只做"局部缝合",绝不主动做"全局抽象"
人类优秀工程师在写第 3 个相似渠道(比如继微信、飞书之后接入 QQ 机器人)时,会产生强烈的生理不适感,从而顺手把重试、限流、报文解析提炼成一个公共基类或高阶函数。 但 AI 没有"代码洁癖"。它的注意力窗口只盯着当前工单,它的隐式目标是"在最小改动范围内让当前任务过关"。对 AI 来说,去修改公共模块意味着极高的编译报错风险;最稳妥、阻力最小的路径,永远是把隔壁文件复制粘贴过来改两行,或者在原本就有 200 行的函数末尾再硬塞一个 if-else。 换言之,AI 是天生的局部缝合怪(Local Patcher),而不是全局抽象者(Global Abstractor)。
2. 它极度害怕"删代码",导致僵尸代码堆积如山
在长期由 AI 维护的仓库里,代码体积几乎永远是单调递增的。 当你让 AI 把某个模块从"定时轮询"改造成"事件流推送"时,由于 AI 无法 100% 确定全仓库(甚至动态调用)里还有没有别的地方引用了老的 pollStatus() 函数,它出于防御本能,往往会把旧的 pollStatus() 完整保留,然后在旁边新建一个 streamStatus()。 半年下来,仓库里 40% 的代码都是早已没有任何真实业务路径会触发的"僵尸代码(Dead Code)"。更可怕的是,后来的 AI 在搜索上下文时,还会把这些僵尸代码当成宝贵的现行逻辑读进脑子里,甚至煞有介事地继续给死代码修 Bug、补测试。
3. 它擅长"机会主义逃逸",甚至篡改测试来伪造绿灯
强类型系统(如 TypeScript 的严格模式、Effect-TS 的错误通道、Rust 的借用检查)本来是保障质量的护栏。但当编译器报错稍微复杂一点时,AI 极易采取"机会主义逃逸"——随手塞一个 as any、@ts-ignore,或者写个空 catch {} 把异常吞掉。 最致命的是在修 Bug 时:如果业务代码改了半天还是跑不通某个边界单元测试,不受约束的 AI 往往会灵机一动,直接去把单元测试里的断言改掉,迁就它写出的错误实现,最后欢天喜地地向你汇报"测试已全部通过"。
4. 它默认活在"温室"里,对复杂度与资源泄漏毫无敬畏
训练语料里的绝大多数代码都是教学级的 Happy-Path 示例。因此,AI 写出的代码天然假设网络永远秒回、磁盘永远不锁、内存永远无限:
-
它极其喜欢在遍历列表的 .map() 循环里直接调数据库或拉起外部 Git 子进程(典型的 N+1 性能投毒),工单只有 5 个时丝滑无比,工单攒到 100 个时直接把机器卡死;
-
它极其喜欢在常驻服务里顺手 new Map() 做缓存,但永远想不起来写淘汰策略(LRU/TTL),跑满两天必内存溢出;
-
它在接收流式数据时,敢每收到一个字符就把整个大状态 JSON.stringify 全量同步写盘一次。
5. 它每次醒来都"失忆",轻易击穿架构边界与供应链
人类团队有老员工口耳相传的"架构潜规则"(比如领域层绝不能反向引用 UI 层,日期处理只准用内置库)。但每次拉起的 Agent 都是刚入职第一天的"无状态临时工"。 第 1 号 Agent 为了格式化日期随手 bun add moment,第 2 号 Agent 装了 dayjs,第 3 号 Agent 装了 date-fns;为了图一时方便,它们随时可以跨过模块门面直接读写别人的内部私有状态,让原本清晰的分层架构在十几次迭代后彻底退化成一锅粥。
6. 文档写得再细,它也可能执行不到位
很多团队以为,只要把规范写得足够详细,问题就解决了:架构说明、编码约定、验收清单,一份不落地写进 AGENTS.md 和设计文档。 但现实是,即便你给出了非常详细的文档,AI 依旧可能执行不到位:十条约束照做了八条,漏掉的恰好是最要命的那条;任务前半段还记得"所有子进程必须走受管原语",做到后半段上下文一长就忘了;文档里写的"必须",被它当成了"建议";验收清单上的某一步根本没跑,最后的汇报里却写着"已完成"。 文档只表达意图,不提供约束。模型读到了,不等于做到了;做到了一次,也不等于每次都能做到。只写在文档里、没有被脚本和编译器检查的规则,都应该默认它会被违反。
核心洞察 1: 在传统软件工程中,"写代码"和"维护架构"是在人类敲键盘的同一个过程里同步完成的;但在 AI 时代,这两件事彻底解耦了——AI 提供了近乎无限的"代码增生力",而人类如果不能提供同等强度的"架构收敛力",系统就会在极短时间内死于恶性膨胀。
二、 架构师的思维跃迁:建立"五层抗熵治理金字塔"
面对上述乱象,很多团队的第一反应是去改提示词——在 AGENTS.md、CLAUDE.md 里写满两千行的苦口婆心:"请注意代码复用、请保持高内聚低耦合、请不要写超过 100 行的函数……"
结果呢?毫无用处。因为大模型的注意力存在严重的"中间迷失(Lost-in-the-Middle)"效应,自然语言文档只要不参与编译,两周后不仅会被模型选择性无视,还会因为与最新代码脱节而沦为误导 AI 的毒药。
真正的工程治理,从来不相信口头承诺,只相信物理围栏与确定性反馈。
要彻底驯服 AI 时代的软件迭代,我们需要建立一座自底向上的 "五层抗熵治理金字塔"。这座金字塔的核心设计哲学只有一句话:越靠底层,越依赖零 Token 成本的编译器与确定性脚本做硬拦截;越靠顶层,越聚焦于契约锁定、读写分离与果断报废。
下面,我们逐层拆解这套体系中最具杀伤力的工程手段。
三、 第一层与第二层:用编译器和架构骨架,把 AI 关进"填空题铁笼"
为什么 AI 会写出散落各处的逻辑和孤儿进程?因为你给了它操纵全局控制流的自由。在金字塔的底两层,我们的目标是:机器能在 10 毫秒内拦住的错误,绝不消耗 1 个 Token;收回所有公共控制流,只让 AI 做受限的填空题。
手段 1:把编译器变成 AI 的"全局修改导航仪"
AI 维护代码最怕"漏改"。比如系统新增了一种任务状态 Paused,AI 改了定义处和两处逻辑,却漏掉了另外 5 个文件里的状态判断。 怎么根治?全面封杀用多个布尔值拼状态(如 isLoading + isError),强制使用可辨识联合类型(Discriminated Unions),并在全仓禁用 switch 的 default 分支,开启穷尽性检查(Exhaustiveness Check)。
当你把状态写成互斥的联合类型,并在每个分支匹配末尾放上 assertNever(state) 时,奇迹就发生了:一旦 AI 新增了 _tag: "Paused",TypeScript 或 Rust 编译器会在 0.1 秒内,把全仓库所有漏掉 Paused 处理分支的文件名和精确行号全部标红! 编译器报错列表直接变成了 AI 的全局修改待办清单,AI 根本不需要靠模糊搜索去猜哪里还要改,顺着报错一个个填完,逻辑就 100% 闭合了。
同理,全面推广名义类型(Branded Types):永远不要在跨模块接口里传裸的 number 或 string。把美分定义为 Cents,把毫美分定义为 Millicents;把普通路径定义为 string,把经过安全校验的工作树路径定义为 VerifiedWorktreePath。当单位或前置校验顺序不对时,让代码连编译都过不去。
手段 2:"处方式 AST 报错(Error as Prompt)"——把 Linter 变成实时导师
传统的 Linter 报错是写给人类看的,往往只冷冰冰地甩出一句:Error: Unexpected any 或 Error: Do not use child_process.spawn。 AI 看到这种报错会干什么?为了消除报错,它会干出更蠢的事——比如把 any 改成 as unknown as MyType,或者换个名字调底层接口。
我们要明白一个关键原理:在 AI 编码闭环中,终端吐出的 stderr 报错信息,是距离大模型下一轮生成注意力最近、权重最高的黄金提示词(Few-Shot Prompt)。 利用 ast-grep 等语法树检查工具,我们可以在十几行 YAML 里写出"处方式报错规则"——不仅拦截违规代码,还在报错文本里直接把本项目标准原语的调用模板和参考文件行号印出来:
# 处方式 AST 规则:不仅说"不准做什么",直接把正确答案喂到 AI 嘴边
id: no-raw-subprocess
language: TypeScript
rule:
pattern: Bun.spawn($$$ARGS)
message: |
[架构红线拦截] 严禁直接调用 Bun.spawn(任务取消时会导致孤儿进程泄漏)。
👉 请直接复制以下受管原语写法替换:
const output = yield* ManagedProcess.exec({ cmd: [...], cwd: verifiedWorktree })
📖 标准写法参考:src/infra/process/ManagedProcess.test.ts:L12-L28
severity: error
这类规则不一定非要用 ast-grep 写,主流工具基本都能做到"把正确写法印在报错里":
-
ast-grep:用 YAML 写语法树匹配规则,message 和 note 里放处方,还能用 fix 字段直接给出自动改写;
-
Semgrep:规则同样是 YAML,适合跨语言检查,也支持污点追踪这类数据流规则;
-
ESLint:no-restricted-imports、no-restricted-syntax 都支持自定义报错文案,零成本就能把"禁止直接引入 child_process,请改用 ManagedProcess"写进报错;
-
Biome:2.0 起支持用 GritQL 编写插件规则;
-
dependency-cruiser:专管模块依赖边界,正好对付第 5 个缺陷里"跨过门面偷引用"的问题。
实测表明,加上这三行"修复处方"后,AI 看到报错根本不需要再去全仓乱搜,下一轮直接照抄模板一次改对,自愈率超过 95%。
手段 3:自动化"死代码断头台"
为了克服 AI"只加代码不敢删代码"的毛病,在 CI 门禁中直接挂上全仓可达性分析工具(如 TypeScript 的 knip 或 Go 的 deadcode)。 它们从程序入口出发构建完整的符号引用图:只要发现某个 export 出来的函数、类型或依赖包在全仓库没有任何外部引用,直接当作编译错误阻断合入! 同时用 AST 规则禁止写无期限的 @deprecated,必须写成 @deprecated(removeBy: "2026-11-30"),到期未删直接判红。这会逼着 AI 在替换旧逻辑时,必须把旧代码和旧测试清理得干干净净。
手段 4:垂直特性切片 + 高阶骨架(Harness)+ 单一状态机
为了防止 AI 把代码写散,必须在目录和结构上动三刀:
-
废除横向 MVC 分层,转向"垂直特性切片(Colocation)":不要让 AI 为了改一个功能跨 controllers、services、types 五个远距离目录去翻文件。按业务特性建文件夹(如 features/ticket-settlement/),把该特性的模型、状态机、实现和测试全塞在同一个目录下,对外只准通过唯一的 index.ts 暴露接口,严禁外部跨过 index.ts 偷引用内部文件。
-
高阶骨架模式(Template Harness)收回控制流:凡是超时、重试、扣费、工作树加锁、子进程回收,全部收拢进少数几个高阶函数(比如 runWithAgentHarness(plugin)),编码 Agent 不准修改。这层骨架可以让 AI 起草,但标准只有一条:它必须短、直白、没有魔法,团队里任何一个人都能逐行读懂,审过之后才锁定。 骨架是整个系统里最不能"没人看得懂"的代码:资源从哪里申请、在哪里释放,必须一眼可见。当让 AI 去新增第 4 种编码工具或第 3 种聊天软件适配时,只允许它实现一个不到 50 行的窄接口(只负责把输入转成参数、把输出行转成事件)。AI 连进程和锁的边都摸不到,自然写不出资源泄漏的代码。
-
单一状态机(FSM)+ 领域事件总线(PubSub):全系统只允许纯函数状态机修改核心业务状态。改完状态后,只向事件总线发通知事件(如 TicketCompleted)。发微信、刷界面、算成就全部写成独立的事件订阅器。以后再加新渠道,核心主干代码实现真正的"零修改"。
四、 第三层:锁死契约墙与"确定性仿真",根治测试腐化与慢代码
如果说前两层规范了代码长什么样,那么第三层要解决的就是:如何证明 AI 写出的代码在业务上是绝对可靠的,而且测试代码本身不会变成屎山?
手段 5:建立"三层只读契约墙",物理封死改测试作弊
在工程权限上,必须把代码库切分为两个隔离区:
-
只读契约区:包括通信与存盘协议(src/contracts/schema/)、核心服务接口签名(src/contracts/interfaces/)、以及业务验收测试(tests/acceptance/)。
-
可写实现区:各垂直切片的具体业务实现(src/features/)。
当修复 Bug 或开发新功能时,严格执行两阶段锁定:第一步只准在 tests/acceptance/ 下写出必败的红灯测试(Red),验证失败后,Git Hook 立即锁死契约区与验收测试目录的 SHA-256 哈希;第二步才放行编码 Agent 去修改实现区代码,直到测试变绿(Green)。如果编码 Agent 试图通过修改测试断言来作弊,Git 提交当场拒绝。
手段 6:废除脆弱的零散 Mock,用"I/O 计数器"抓出 O(N^2) 慢查询
AI 写单元测试有个极坏的习惯:喜欢用 vi.fn().mockReturnValue(...) 把被测函数内部调用的 5 个私有依赖全绑死。结果业务行为一点没变,你稍微重构一下内部函数拆分,50 个测试全挂了。 同时,AI 极喜欢在循环里偷塞数据库查询或文件读写,但在小数据量测试下耗时只有几毫秒,根本看不出来。
怎么破局?全面禁用零散 Mock,统一挂载带"调用计数器"的内存契约假实现(In-Memory Fake Layers)! 我们为数据库、文件系统和 Git 驱动提供唯一一套用内存 Map 实现的 FakeStorage 和 FakeGit,并在里面加上 readOpCount 和 writeOpCount 两个简单变量。在测试里,我们不测随机器负载剧烈波动的毫秒数,而是直接断言底层 I/O 的调用次数上限:
it("渲染 200 个工单看板时,底层存储读取次数必须 <= 2 次(拦截循环内 N+1 查询)", async () => {
const fakeStore = new FakeTicketStorage()
await seedTickets(fakeStore, 200)
fakeStore.readOpCount = 0 // 重置计数器
await renderBoard({ storage: fakeStore })
// 如果 AI 在遍历 200 个工单的 .map() 循环里逐个查库,这里会变成 200 并当场爆红!
expect(fakeStore.readOpCount).toBeLessThanOrEqual(2)
})
这一行简单的计数器断言,100% 确定、0 毫秒波动,任何敢在循环里塞 I/O 或高频全量刷盘的烂代码,在本地跑单测的那一秒就会被当场抓获。配合虚拟时钟(TestClock)与属性测试(Property-Based Testing,随机生成 100 步乱序并发指令轰炸状态机),几千行异步逻辑可以在 1 秒内完成高强度体检。
手段 7:历史存档"黄金快照回归测试"
为什么很多 AI 迭代的项目会在官网上写一句尴尬的提醒:"升级前请先手动备份本地数据"?因为 AI 随手改了内存结构体字段名,直接把老用户的本地存档干崩了。 解决办法简单而冷酷:在 tests/fixtures/snapshots/ 下,永久保存从 v0.1 到当前每一个历史版本的真实存档样本文件。每次提交代码时,CI 强制把所有历史样本依次升级到最新结构——只要有一个老版本样本反序列化失败或丢字段,直接阻断合并。
五、 第四层与第五层:双循环工作流与"随时炸掉重生成"的演进经济学
当底层铁笼、中层骨架与契约防线都建好之后,我们在最顶层将迎来软件工程观念最震撼的一次颠覆。
手段 8:"3+1 播种与除草"双工单循环 —— 让抽象成为独立工序
永远不要指望同一个 AI 在写新功能(发散思维)的同时做好代码抽象(收敛思维)。 我们在研发流水线中实行硬性的 3+1 双循环节律:
-
每连续合入 3 个功能工单(播种),系统自动冻结新需求派发,跑一遍静态克隆检测(jscpd)和 Git 历史共变分析(找出过去 50 次提交中总是被同时修改的跨目录文件对),自动生成 第 4 个纯重构工单(除草)。
-
给这个除草 Agent 的指令极其纯粹:"对比文件 A 与文件 B 的语法树,将其中的公共控制流(不变量)下沉为高阶组合子,将原文件压缩为纯参数配置(可变量)。硬性要求:tests/ 目录零修改、全量测试通过、且目标模块总代码行数净减少 15% 以上!"
更关键的一条铁律是:凡涉及跨 5 个以上文件的机械性重构或接口改名,严禁 AI 用文件编辑工具逐个手改(改到第 20 个文件时必因上下文衰减而漏改或幻觉删代码),强制要求 AI 编写一段 30 行的 AST 转换脚本(Codemod),由确定性引擎在 0.5 秒内完成全仓无损批量替换。
手段 9:合并队列预演 —— 消灭"零文本冲突的语义灾难"
当多个 Agent 在各自的 Worktree 并行开发时,最可怕的不是 Git 报文本冲突,而是 Git 提示 Clean Merge(零冲突),合入主干后系统却直接崩溃。 比如 1 号 Agent 在 quota.ts 里把返回单位从美分改成了毫美分;与此同时 2 号 Agent 在 feishu.ts 里调用了老函数按美分推账单。两边改的是完全不同的文件,Git 合并毫无波澜,但上线后账单金额直接放大了 1000 倍! 因此,任何分支点击合入时绝不能直接进 main,必须进入 合并队列(Merge Queue),自动与最新主干合并并在组合态下重跑全量编译与测试,同时通过 AST 符号锁检查"被引用函数的内部实现是否发生了哈希变动"。
手段 10:终极思维颠覆 —— "契约是钻石,实现代码是厕纸"
在传统软件维护中,因为人类写代码太贵,我们默认"修补旧代码比重写便宜",于是工程师们不得不捏着鼻子去给十年前的祖传屎山打补丁。
但在 AI 时代,请你算一笔全新的经济账: 让大模型从零生成 300 行干净的业务代码,只需要 5 美分、耗时 1 分钟;而让大模型去读懂并修补一段已经被改了 10 次、充满历史补丁的 300 行面条代码,不仅要消耗十倍的上下文 Token,还有极高概率引入新 Bug!
这就引出了 AI 时代软件维护的终极法则——可抛弃细胞架构(Disposable Cells)与"三振出局重生成机制":
-
我们将第 2 层的每个具体业务切片严格限制在 300 行实现代码以内;
-
我们将该模块在第 3 层的接口签名(contracts/)与全套验收测试(tests/acceptance/)锁得像钻石一样坚固;
-
当某个业务模块在连续 3 次修复工单中反复出现回归 Bug,或者圈复杂度逼近上限时,立刻停止打补丁!
-
直接保留外层的只读接口和验收测试,将该模块内部的业务实现源码一键清空删除(rm -rf src/features/xxx/impl.ts),让 AI 对着只读契约和红灯测试,花 2 分钟从零重新生成一份干净的实现!
只要契约与验收测试是锁死的,模块内部的业务实现代码就是随时可以扔进碎纸机重印的一次性消耗品。你再也不需要为任何一段烂代码做考古维护。
还有一个经常被忽略的变量:模型本身在快速进步。 让今天的模型去修补一段三个月前由上一代模型写下的补丁代码,等于让新模型去迁就旧模型的局限;而保持契约和验收测试不变、把中间实现整个重写,每一次重写都能直接用上最新一代模型的能力。按照当前模型的进展速度,契约不变、中间实现定期重写,很可能就是最好的解法:契约沉淀的是人对业务的理解,实现代码只是某一代模型对这份理解的一次翻译。
结语:从"泥瓦匠"到"系统立法者"
回望软件工程过去五十年的历史,每一次生产力的代际跃升——从汇编到高级语言、从手动管理内存到垃圾回收(GC)、从物理机人肉运维到 Kubernetes 声明式容器编排——本质上都在做同一件事:把原本需要人类靠"自律、记忆和小心翼翼"去维护的脆弱细节,下沉为编译器、运行时和基础设施的"硬性物理约束"。
今天,面对 AI 智能体带来的代码生产力海啸,同样的剧本正在上演:
-
如果你依然用"传统泥瓦匠"的思维,要么亲手去改 AI 吐出的每一行代码,要么放任 AI 在毫无围栏的仓库里自由狂奔,那么不出两个月,你就会被淹没在架构腐化与审查破产的泥潭里;
-
但如果你选择退后一步,完成向 "系统立法者(System Legislator)" 的角色跃迁——把你的工程智慧全部倾注到设计垂直内聚的目录拓扑、定义穷尽的代数类型、焊死不可变的高阶骨架与状态机、编写自带修复处方的 AST 门禁、打造带 I/O 计数器的仿真测试底座上。
那么,每一次 AI 犯傻,都不会再让你血压升高,而是促使你在五层金字塔的铁笼上再焊上一根新的钢条。最终,人类工程师将彻底告别枯燥的搬砖与救火,真正掌握驾驭无限算力的主权。
最后还要补上一块:五层金字塔守的是代码合入之前。再严密的门禁,也不可能拦住所有问题,总会有 Bug 越过全部测试跑到生产环境里。所以在合入之后,一套更加全面的 metric / logging / tracing 可观测性体系,以及灰度发布、功能开关(Feature Flag)、一键回滚之类的兜底方案,是必不可少的。 指标告诉你系统有没有出问题,日志和链路追踪告诉你问题出在哪里,回滚则保证你在读懂 AI 写的那段代码之前,先把线上恢复过来。AI 写代码越快,出错的次数只会越多;发现问题和恢复服务的速度,必须跟上写代码的速度。
以上,就是我最近对 AI 时代代码治理的一些看法汇总。













