为什么 AI 把功能做完了,系统却被改坏了

抱歉抱歉,我确实好久没写博客了,主要是最近确实有那么一点点忙碌,忙着重构团队的 infra 设施。不过不用担心,AI Agent 和 AI Infra 系列教程我还在更新,这里给新来的朋友指一下路:

锦恢 AI Agent 小白教程: https://www.zhihu.com/column/c_2086151921216374554

AI Infra 软核教程: https://www.zhihu.com/column/c_2068025608488096553

或者关注我的公众号「汇尘轩」,觉得熟悉?嗯,这也是我的个人网站名字。

然后嘛,就是和几个朋友一起搞了一个 zero-to-sglang 教程,我主要负责 continuous batching 和 speculative decoding 部分的编写,没想到又遇到好几个玩方舟的,只能说,方舟玩家蟑螂论的又一力证(bushi

好了,进入正题。

作为一个 LLM 辅助写代码练习时长两年半的小白,我经常注意到 Coding Agent 很擅长写那种 “一次性的”,“通马桶的”,“临时的” 代码。但是遇到那种稍微复杂一点的系统设计和代码开发,Coding Agent 总是容易出现不少的问题,虽然它能完成我这个任务,但是代码会堆屎山,完全不去 review 它的代码长期迭代后,项目将变得不再具有维护性,接口混乱,参数优先级混淆,想要加入一个新功能要推翻之前的若干个设计,然后再去打上十倍代码的补丁。

造成这个问题的原因主要来自「回归」和 「架构侵蚀」这两个软件工程上老生常谈的问题,用下文的三张图就能轻易解释清楚这个问题:

为了在网站上完成聊天室需求 $q$,你以为需要考虑的上下文 $U_q$:

图片图片

实际上的完整上下文 $K_q$:

图片图片

Coding Agent 眼中上下文 $A_q$:

图片图片

相信不少深度使用 AI 来写生产代码的朋友也能遇到类似的问题,但是能系统完整地去讲明白为什么会出现这个问题,以及这个问题到底能不能解决,如何解决的文章非常稀少。因为大概只有同时对 Coding Agent LLM 的训练方式和软件工程两个方向都极其了解的人才能完整系统地讲明白这件事。

这篇文章,我将用更加系统的文字和 insight 来带给各位观众这个问题的理解模型,并希望你能理解目前的 Coding Agent 到底能不能做企业级的系统或者复杂系统的开发。

抽象一个场景

想要系统理解一个概念和现象最好的方法就是通过不断实践积累数据,然后综合这些数据想出一个抽象的模型,对着这个模型去讲解和理解这个问题。因此,作为使用 LLM 在软件开发,算法调优等方向都有过不少实践的人来说,我现在算是有了足够的数据,所以让我总结一下它们。

此处感谢我本科的软件工程老师,一位优雅的女士,讲课有趣,导致我罕见地没旷课认真听完了软件工程的基础课,积累了最初的一些基本功。

大部分的系统开发(网站开发,大模型 infra 开发,数据回流管线搭建等等)都可以抽象成这样一个模型:系统中存在 $A,B,C$ 三个模块,每个模块分别具有 $M_A,M_B,M_C$ 个接口,并且为了简化论述,我们假设系统的调用拓扑是:

$$ A\rightarrow B\rightarrow C $$ 在这套系统中,A 作为逻辑上最终的产物消费者,它被我们称为 “上游”,调度模型上,显然 A 属于消费者。C 作为逻辑上被所有人使用的产物生产者,被我们称为“下游”。

我举个例子,比如你在浏览我的个人网站汇尘轩,那么你的浏览器客户端,我的服务器就能构成一个形如 ABC 的系统:(举个人网站为例只是因为希望你们能看得懂,举 AI Infra 系统作为例子,那就真没几个人能看懂了)

图片图片

为了循序渐进讲解系统的复杂性,我们需要以 ABC 作为蓝本进行外拓。下面是几个外拓情况。

外拓情况1. ABC 的模块是复用的

真实情况下,ABC 三个模块的每一个是可以被其他「A’B’C’」系统复用的。比如:

上面的 B,也就是 NGINX,它不仅仅服务汇尘轩这个网站,它也是我的世界游戏服务器的反向代理

浏览器 A 不仅仅访问汇尘轩这个网站,它也会访问知乎,哔哩哔哩,或者我服务器上的其他站点(比如我的 NAS 管理界面,OpenMCP 的文档站点等等)

C 这个模块的 go 后端也不一定只是汇尘轩网站的后端,它也可能是 GitHub 上 webhook 的接收端,或者我的私人机器人助手的数据控制面。

所以真实情况下,ABC 三个模块是可被复用的,如下图所示:

图片图片

外拓情况2. ABC 的描述是递归的

之于复杂系统的描述往往是递归的。

比如「用户通过浏览器访问汇尘轩」这个系统可以被描述为最上面那张图的 $A\rightarrow B\rightarrow C$,C 本身是 Go 实现的后端,它其实也能表示为 $C.a\rightarrow C.b\rightarrow C.c$ 的递归子结构。比如以我过去常用的 Go 后端架构为例(go/gin 快速搭建后端服务),它可以表示为:

$$ \underbrace{\text{Router}}{\text{C.a}} \rightarrow \underbrace{\text{Middleware}}{\text{C.b}} \rightarrow \underbrace{\text{Service}}_{\text{C.c}} $$ 当然,Service 也能继续拆分为 Handler、Model、Database 的二级子结构,但是为了论述方便,我就不继续拆分了。需要额外强调的是,C 能如此拆分并不是我个人的主观意愿,而是软件工程发展这么多年后的一种成熟的设计模式,路由(分组路由、规则路由)、中间件(限流器、鉴权层)、服务(具体实现和响应)的层级设计属于非常常见的一种后端设计模式。我在此处的系统划分绝不是什么过度设计。

对应的系统模型如下:

图片图片

很复杂吗?不,一点也不复杂,真实企业内部的设施复杂度比这个高几百倍。记住,这套系统是我个人的业余玩具网站。所以结构非常简单。

好了,系统模型构建完成,可以开始进行样例分析了。

用 Coding Agent 实现一个需求:聊天室

假设你现在需要使用 Coding Agent 完成一个需求:

我现在想要在我的个人网站上加一个聊天室的功能,请帮我实现。

显然,这并不是一个好的 prompt,因为下面这些事情 AI 并不知道:

设置的聊天室功能应该挂载到哪个前端地址上?

聊天室的数据存储和索引使用数据库还是文件系统?

聊天室各个环节的数据模型应该如何设计?

索引系统如何设计?

…

OK,假设你是一位经验丰富的工程师,或者你在上面开启了 plan 模式(plan 模式原理请看:智能体基本原理:Agent Loop,规划模式与子智能体),经过几轮补充后,你大概能将提示词扩充为:

我现在想要在我的个人网站上加一个聊天室的功能,下面是具体的需求:
1. 聊天室通过 https://kirigaya.cn/chatroom 进行访问,不允许 http 访问。
2. 聊天室的消息索引和元信息使用数据库,消息本体和附件使用文件系统,并使用 COS 进行定期备份。这是 COS 的使用 SKILL: ...
3. 聊天室的数据模型参考 onebot v11 的接口文档全量设计,表为 ...
4. 以 user_id, group_id, message_id 为索引进行存储,外键设计为 ...
5. ...
聊天室需求的上下文范围

我们都知道,如果 AI 想要解决一个问题,便需要知道这个问题的全部上下文。所以我们接下来分析一下这个需求在目前这套系统中的实际需要的上下文范围。

OK,假设你是一位经验丰富的全栈工程师,根据上文的提示词描述和系统架构,就应该能知道如下几个基本的常识:

实时 web 聊天室需要 websocket。

那么 NGINX 也需要修改配置定义。

Go 后端的路由定义、针对 WebSocket 的鉴权都要修改。

所以,在具备了上帝视角后,我们知道了这样一个需求的上下文范围为下图高亮的部分:

图片图片

真的只有这么点吗?

你再给我好好想想。

Bad Case 1. 下游依赖上下文缺失导致系统级回归发生

bad case 1. 下游依赖上下文缺失导致系统级回归发生

原因:AI 无法从已有代码和文档中获取全局下游依赖的上下文,导致修改代码时无法保证消费点的健壮性,从而诱发回归问题。

假如你认为上图高亮的部分就是聊天室需求实际的上下文了,那就大错特错了,我们考虑这样一个场景:Coding Agent 在工作,它完成了代码并重启了后端服务后,发现前端无论如何都无法与后端建立 WebSocket 连接,然后它逐步就发现了原来是 NGINX 的相关配置没有修改。

这是原本的 NGINX 文件,里面记录了三个站点的转发规则:

worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # 站点一:汇尘轩
    server {
        listen 80;
        server_name kirigaya.cn;

        root /srv/huichenxuan/dist;

        location /api/ {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
        }

        location / {
            try_files $uri $uri/ /index.html;
        }
    }

    # 站点二:NAS 管理界面
    server {
        listen 80;
        server_name nas.kirigaya.cn;

        location / {
            proxy_pass http://127.0.0.1:9000;
            proxy_set_header Host $host;
        }
    }
}

# 站点三:Minecraft 服务器
stream {
    server {
        listen 25565;
        proxy_pass 127.0.0.1:25566;
    }
}

然后 AI 一看:诶?Minecraft 服务器和 NAS 和我们本次任务没关系,所以我删了。然后得到的配置文件如下:

worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    server {
        listen 80;
        server_name kirigaya.cn;
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl;
        server_name kirigaya.cn;

        ssl_certificate     /etc/letsencrypt/live/kirigaya.cn/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/kirigaya.cn/privkey.pem;

        root /srv/huichenxuan/dist;
        index index.html;

        # 新增:浏览器连接 wss://kirigaya.cn/api/ws
        location = /api/ws {
            proxy_pass http://127.0.0.1:8080;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 1h;
        }

        location /api/ {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location / {
            try_files $uri $uri/ /index.html;
        }
    }

    # 原本属于 nas.kirigaya.cn 的 server 块被删除
}

# 原本转发 Minecraft 服务器的 stream 块被删除

此时的你对此事浑然不知,然后你健身回来后,AI 告诉你已经完成了任务,然后功能上还有一些小瑕疵,多用户鉴权你也没测试夹具进行验证,但是大体上看着很不错,聊天基本功能都能用。

结果下午,群友告诉你,MC 服务器进不去了,说他的盾构机通宵造了一晚上,刚刚起床准备测试性能。

你说,怎么会呢,都稳定运行 48 天了。结果你一点 MC 服务器发现确实离线了,连带着你的 NAS 也打不开了。经验丰富的你就立刻认识到:之前的一次 AI 修改代码把这些配置改烂了。

AI:你就说聊天室功能有没有做完吧。

你:…

什么是回归问题 Regression

AI 确实把聊天室功能做完了,但是另外两个站点却因此下线了。为啥呢?因为 NAS 和 MC 服务器的完整服务也需要依赖 NGINX 进行转发,可是 AI 在之前的工作中把这两个站点的转发规则删除了。

这就好比啥呢,这就好比你办理入住一个新的小区,结果办理完成之后,你邻居的身份认证全都被删除了,导致他们不能刷脸刷进小区大门。这种过去明明是好的,但是因为新的功能加入导致过去本来的好的东西爆炸的现象在软件工程中被我们称为「回归问题」(regression)。

请注意,这和机器学习中的回归没有半毛钱关系。

显然回归问题有大有小,可能是一个软件中加入一个新功能,导致原本的某个老功能不能用了;也可能是我们这里讨论的这种,一个功能的加入导致系统中其他模块不能用了,这种一般我们更确切称为系统级的回归问题。其实如果是基础设施完善的情况下,系统级回归问题是几乎不会发生的,比如我们遇到的这个问题,不同站点的 NGINX 配置就不应该可以在一个状态被同时修改,有很多基础设施可以保障你修改 NGINX 这类 router 时每个项目有自己独立的配置区,不同配置区互不干扰,这样无论 AI 如何胡闹,上面的系统级回归问题都不会发生。但是这必须在整套系统还没被搭建的时候就考虑到,并且除非你系统上了规模,一般人是想不到要上这类“很重”的基础设施的。

你用 AI,AI 自然也不会告诉你应该这么做,因为对于没有经验的人来说,当你在一个新的工作区指挥 AI 干活的时候,你输入的 prompt 里面是不可能含有代表你未来设计意图的文字的。我举个例子,假设你打算做一个自己的个人网站,此时,你输入给 AI 的 prompt 最多表示下图的内容:

图片图片

你此时不可能在你的 prompt 里面包含未来可能需要拓展的功能,比如我们上文说到的 MC 服务器和 NAS。对于个人,想要做的东西不断在变多;对于公司来说,业务也在不断拓展,每天都有莫名其妙的新需求。你不可能预测未来会发生什么,这里就体现出了程序员的核心能力之一——架构设计。

架构设计能力要求程序员根据业务理解去设计一个拓展性极强的架构,使得未来的新业务可以用最小的代价和最低的测试成本接入,注意,新功能的测试成本也包括老功能的回归性检测,别忘记了,除非架构层保障了逻辑上新功能的引入不可能诱发老功能的回归问题,但是这只能通过基础设施保障,基础设施的选择和引入也是只有经验丰富的程序员才能在一开始就想到的。

这一点相信所有做着前沿技术的朋友(大模型,具身智能,AI 药物研发等)应该都深有体会,一位经验丰富的架构师可以给团队带去多少的红利。DeepSeek 和 Google 强大的 Infra 能力已经证明了这一点。

所有递归的下游依赖也是上下文的一部分

因此从业务上看起来 NAS 和 MC 服务器与我们的聊天室需求无关,但是在上下文的层面,由于这两个玩意儿也依赖了 NGINX,就必须要考虑到上下文里面,保证它们在聊天室需求完成后照样正常工作。

这便是系统开发的一条铁律——所有微小的需求开发都一定要完整了解所有的下游依赖。在大模型时代,这句话的意思就是,当我们用 AI 实现某个需求的时候,这个需求所有相关的下游依赖也全都需要放入上下文里面。

在业界,「功能点的上下游依赖 + 全局设计意图 + 别的什么东西」还有一个更加广为人知的说法——业务理解。

因此,聊天室需求在我们系统中实际应该占有的上下文是下图中被高亮的部分:

Middleware 还会指向 Redis,Kafka 这类中间件服务,它们也是上下文的一部分,此处没画出来

图片图片

这时候就有观众会说了,既然如此,我把这部分上下文加入文档里面不就好了?

但是从业务上 MC 服务器,NAS 和我们的个人网站后端就是独立的,所以正常人写文档的时候是不会把这部分信息写入文档的,实际文档的覆盖面可能只有 C 模块的一个子集。具体为什么是子集,而不是全部,请继续看后续的内容。

并且就算你的工程习惯非常好,从一开始就在文档系统中保留了整个系统的全部细节,由于大模型的上下文有限,它也无法一次性看到整个文档系统。更别说超大型项目中,会引入大量基础设施去进行分仓管理了,这个时候你更无法看到项目的全貌。

缺失上下文是一个无法从根本上解决的问题,它是工程师工作的常态。上下文的不足也是目前 AI 写大型项目的最主要瓶颈,这个瓶颈主要在人上面,因为业务理解往往是通过人进行承载,SOP 足够成熟后才会沉淀为基础设施。这并不是目前的 AI 能解决的问题。

符号约定与建模

好吧,上面那段文字写的我已经有点语无伦次了,因为文字的表达能力在我们的语境中快到极限了,所以为了更好进行后续的论述,我们综合上面了解到的案例和上下文,做出如下的数学建模。在抽象领域,数学符号的表达能力比文字要强。而且还能利用数学工具进行一定程度上的自动逻辑推演。

显然从我们上述的论述中,你可以了解到一个完整的系统由如下的几个要素构成:

源代码:字面意思。因为叫 code 嘛,所以记作 $C$。

文档:用于描述边界条件,设计意图等等的文字信息。某种意义上,源代码的注释也算是文档。Document,所以简单记为 $D$。

依赖关系:不同模块之间的依赖关系,谁的什么接口在什么情况下被什么模块的什么接口在什么数据模型下被消费了,大概就是这种粒度的信息。好吧,你能看出来,这很像是图的边信息。

模块:比如上文的Go 后端、NAS 服务端、MC 游戏服务器,都算是一个模块,模块代表了业务层面上的一个最小单元。显然模块可以和依赖关系合在一起,用图论的方式进行数学建模:$G=(V,E)$,其中 G 代表系统结构,而 V 和 E 分别代表模块和依赖关系,它们分别是系统架构上的节点和边。

追踪性:追踪性(traceability)是软件工程中的一个概念,它用于从需求找到设计与实现,也用于分析修改会影响什么。许多人在讨论架构设计的组成元素时,都会忘记提到追踪性。我们将追踪性记为 $T$,它代表不同知识系统之间的对应关系。比如源代码和文档就是两套不同的知识系统。绝大部分项目中,$T$ 都是隐变量,不存在任何形式化的记录。

需求:我们记需求为 $q$,并且任何知识系统 $X$ 和需求 $q$ 有关的部分记作 $X_q$。并且我们记为了实现需求 $q$ 的全部项目上下文为 $K_q$。

那么显然,一个项目需求的实现可以建模为:

$$ \mathcal S_q = {C, D, G, T, q}\ $$ 而我们的目标就是要找到 $K_q$,或者为它建立某些形式上的约束。

比如在我们上面的 Bad Case 1 中,$q$ 就是 “在我的个人网站上增加聊天室功能 …”,那么:

$$ Cq={c{\mathrm{Go}},\,c{\mathrm{Vue}},\,c{\mathrm{NGINX}}}\ $$ 文档 $D_q$ 就是:

$$ Dq = {D{汇尘轩}, D_{我的服务器}}\ $$ 此外,我们假设这是一套足够完备和理想的系统架构,足够完备和理想指的是,它的文档写的足够规整和完备,任何接受过计算机和数学教育的本科生都能通过阅读文档了解到全部的业务信息、设计意图、技术细节等等信息。

并且代码系统采用足够规范的方法进行编写,比如对于需求「编写一个函数,实现牛顿迭代法」,实现这个需求的代码是唯一的。

那么基于软件工程和工程实践,我们有如下的一些不证自明的结论。

T1. 实现需求的完整上下文 $K_q$ 来自于源代码、文档、系统结构和它们之间的依赖关系:

$$ \forall X\in \mathcal S_q,\qquad K_q\cap X\ne\varnothing $$ T2. 给定实现规范 $f$ 后,我们可以从设计意图推导出代码,但是无法从代码推导出设计意图(具体原因 bad case 2 会讲述):

$$ H(C|D, f) = 0, \quad H(D|C, f) > 0 $$ 为了方便我们后续的论述,我们将这种“逻辑推导”定义为知识系统间的一类偏序关系:

$$ C \preceq D |_f $$ 对于我们想要约束的实现需求的完整上下文 $K_q$,有如下的约束:

$$ C_q \preceq K_q, \quad D_q \preceq K_q $$ 你可以结合我们上面那个聊天室需求对照看看上面的 T1 和 T2 是否真的成立。

T3. 通常情况下,项目中不会存在 $T$ 的形式化表达,即:

$$ T_{observation} = \varnothing $$ 这下舒服了,继续讨论吧。

Bad Case 2. 补丁代码导致的架构侵蚀

bad case 2. 补丁代码导致的架构侵蚀

原因:架构信息是文档信息的一部分,AI 可以通过架构信息确定代码的设计路线,但是不能通过代码反推出架构信息,AI 召回与修改代码的语义粒度不包含架构信息,只包含碎片化的函数。因此,从语义层面,AI 不具备在保持架构信息不变的前提下修改代码完成需求的能力。

我们继续上面那个聊天室需求的实现,假设 Coding Agent 能看到完整的 $C_q$ 和 $D_q$ 了,也就是下图高亮的部分。

图片图片

你终于可以安心交给 AI 去干活,自己则去刷知乎了,结果回来一看 AI 确实把需求实现好了,并且 NAS 和 MC 节点依旧可以正常访问,至少回归问题没发生了。

当然,先让我们省去那该死的 ICP 备案的扩充协议(bushi,总而言之你的聊天室上线了。

某天你的朋友小奈告诉你,他想要用你的聊天室建一个 MC Mod 作者群,只因你的聊天室在前端实现了 blender 模型的预览组件,对于建模佬来说,这属于 killer feature。

你在网站的后台管理系统给小奈添加了管理员后就去继续在 va-11 里给亲爱的客人去调酒了。结果小奈过了一会儿告诉你,他一直无法成功创建新的群聊。你说不会呀,因为在架构设计中,你的个人网站的管理员自动加白了全部的子系统,包括这个聊天室,不可能创建失败的。

然后你就去看了一下代码中关于聊天室请求组是如何被鉴权的,看完后你直接被气死了,因为有点常识的人都知道,鉴权的触发和令牌的缓存应该发生在 Middleware,这边已经提供了相当成熟的基础设施能够完成这件事了。但是 AI 实现的逻辑是,为聊天室请求组单独又写了一个新的鉴权器,没有走 Middleware,而是新建了一个 chatroom_verify_handler.go。直接绕开了原本的鉴权管线,所以原本的老的后台管理系统给新用户管理员后,它仍然无法创建群聊(我的设计中,管理员才能创建群聊)。

什么是架构侵蚀问题 architectural erosion

在软件工程中,我们通常用架构侵蚀(architectural erosion)描述上面 AI 造成的这个问题。

架构侵蚀指的是:需求一次次被实现,但新增代码逐渐偏离原有的模块边界、设计约束和职责划分,最终使系统越来越难修改。

在上面的样例中,我们的架构将 Go 后端设计为 Router -> Middleware -> Service,各个模块的职责如下:

Router:定义接口的路由,定义并验证请求和响应的数据模型。

Middleware:定义通用的拦截器,常见的拦截器有:鉴权器、限流器、参数透传器。

Service:定义接口的具体功能实现。

但是 AI 却看不到这层关系,它眼中只能看到一个个函数,但是无法将这些函数在思维链中推导为语义和我们对齐的架构信息,如下图所示:

图片图片

就算文档中存在这部分的文档信息,但是由于我们前面说了,绝大部分项目中 $T_{observation} = \varnothing$,当系统的这部分架构信息超出大模型上下文时,大模型便依旧无法识别到这部分的架构信息。

此类问题可以外推到用 AI 写文档中和架构信息/设计意图相关的部分,用 AI 解释数据结构的消费路径等等使用场景。

大家可以做一个小实验,让你的 Coding Agent 去克隆 pytorch 项目的代码,并让它绘制 pytorch 的架构图,并基于架构图在 10 分钟内找到如何在 torch 中自定义 OP 和对应的 kernel selection 算法的实现。你会发现,AI 绘制的架构图非常差。和架构师脑中的那个简洁明了,易读的架构图很难对齐。

为什么 AI 会犯这个问题呢?除了后训练方式施加的偏好外,一个很主要的原因就是我们上面提到的引理 T2:

$$ C_q \preceq K_q, \quad D_q \preceq K_q $$ 我们上文还没对这个引理做出解释,因为这个引理是造成 AI 会犯下架构侵蚀问题的原因之一。这里正好一起讲了。

业务信息可推导出代码,反之不亦然

其实引理 T2 说明了这么一件事:设计意图是可以确定代码怎么写的,但是反过来,已知代码却无法确定设计意图(设计意图也是文档的一部分)。

前者我觉得是个人都知道肯定是对的,对齐设计意图并写代码本来就是程序员的天职,当然,这里肯定是需要包含计算机的基础知识和公司基础设施的知识等等,但是把这些都列举了,讨论就太复杂了,所以你们能懂我意思就行。

关键在于后者,「已知代码却无法确定设计意图」,想要证明非常简单,反证法就行,我们只需要举出一个例子,即,已知代码实现,但是它能推导出的设计意图不唯一,就能证明。

这个例子太好举了:

def price(is_student: bool, is_member: bool) -> int:
    result = 100

    if is_student:
        result = 80

    if is_member:
        result = 70

    return result

乍一看,这个函数在说要如何根据用户身份去判断什么乱七八糟的混球玩意儿要多少钱。

两个布尔值输入给这个函数的输出定义了 2 * 2 = 4 种可能性,但是能描述这个代码的设计意图不唯一:

设计意图 A:会员价优先。同时具备学生和会员身份时,无论哪个价格更低,都采用会员价。因此第二个 有意覆盖第一个。

设计意图 B:取最低价。学生价是 80,会员价是 70;两种身份都有时采用 70。开发者知道当前 70 < 80,于是把取最小值简化成了这两个 。

从而我们证明了「已知代码却无法确定设计意图」,所以 T2 成立。

回到 AI 会犯下架构侵蚀问题这个上下文中,它和引理 T2 有什么关系呢?

非常简单!

Router -> Middleware -> Service 的架构信息和这三个模块的职责边界确实在 $D$ 中,但是因为 $T{observation} = \varnothing$ 和 引理 T2,所以 AI 既不能理解这条落在文档某个犄角旮旯里的“架构信息”,它和“聊天室需求”之间的关系是什么;也无法通过阅读已有的后端代码,去构建这个 $T{observation}$。因此,AI 长期处在缺失架构信息的上下文中,便无法保证它的修改不会造成架构侵蚀。100 个需求下去了,架构不烂掉才怪。

架构烂掉后的黑利如下:

字段优先级开始混淆。

代码质量无法保障,复杂系统几乎不可读,复杂调度算法无法实现。

模块复用性下降,代码仓库和依赖项开始膨胀。

无法保证产物的交付能否在不引发回归的情况下满足全部下游部门的需求。

这里有一份值得品鉴的 PR:https://github.com/oven-sh/bun/issues/30897

系统的规模效应

一句话说就是,当系统复杂后,架构设计上的优点和缺点都会在系统尺度扩大后被无限放大,形成技术红利或者灾难。

很多时候,人们错把接口使用的便利当成一种理所当然,但是你让一个水平一般的人来设计,可能会充满灾难,灾难类型包括但不限于:

没有专人维护测试与持续集成模块,导致代码质量下降,可读性几乎为零。

没有全局架构原型或者专人掌舵,导致字段出现冗余堆积,进而导致优先级混淆,最终系统维护崩塌。

没有统一的数据模型,部门间产物交付门槛变高。

观测 profile 和质量保障系统不统一,导致使用和复杂数值实验几乎没有复现的可能性。

大型数值实验,比如大模型训练和大模型评测,在没有可观测 profile 基础设施的情况下,做了 90% 和做了 0% 没有任何区别。质量保障系统,在大模型实验中,比如 anti-hack 过的 benchmark SOP

从模型训练的角度讲讲 AI 如今的缺陷

许愿机派看完后立刻要反驳了:你说得对,但是 AI 会把这一切给吞噬的!你们都会被 AI 取代的。

看完后我的表情:

图片图片

为了避免让某些宵小之辈说我这是老登的挽尊(但是其实锦恢我今年才毕业哦?严格意义上我算小登),我们就从科学的角度讲讲大模型的 Coding 能力到底是怎么训练出来的,只有了解这个,你才能知道大模型在写代码这件事上为什么能做到什么事情(比如做算子开发),为什么做不到什么事情(比如做不了复杂系统的理解和开发)。

想要理解 Coding Agent 你大概需要阅读我过去的很多文章,比如 锦恢的 AI Agent 小白教程,emm,这不太现实。

所以我会尽可能将大模型的 Coding 能力训练简化为一个最小化模型。

我们都知道大模型本质也属于深度学习网络,深度学习的训练方式无非那么几种:自监督、有监督、强化学习。

以 DeepSeek 4.1 为例,从公开的 tech report 中能看到很多信息(论文下载地址:https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash/blob/main/DeepSeek_V41_Tech_Report.pdf),DeepSeek 4.1 的训练顺序为:

$$ \mathrm{pretrain} \rightarrow \mathrm{sft} \rightarrow \mathrm{rl} \rightarrow \mathrm{opd} $$ Coding Agent 能力在 RL 训练过程中明显提升,在 Agentic RL 中的 TITO 原则 中我提到过部分目前 Agentic RL 的训练流程。

在新版本 dressage 完成后,我会写一个 Agentic RL 的系列文章。

RL 过程中的奖励信号主要来源于评分器 grader,目前评分主要有两种基本方式—— RLVR 和 LaaJ。

RLVR 就是通过可验证的方法来获取程序计算的奖励信号。说人话就是,AI 在训练的过程中写了一段代码,然后我们用事先准备好的程序来验证 AI 写的这个代码能不能通过语法检查,能不能通过全部的单元测试等等。用这些通过率综合打一个分数作为 reward,来进行 RL。

所以能看到训出来的模型,它写的代码确实能通过语法检查,确实可以完成你给的需求,但是会犯回归问题、架构侵蚀问题等等。因为,回归问题和架构侵蚀问题这些问题的相关 reward 在训练集里面根本没有,绝大部分的题目就是只需要完成单元测试就算满分了。

我以 Terminal Bench 2 这个题为例子(原题 https://github.com/harbor-framework/terminal-bench-2/blob/main/write-compressor/instruction.md)。任务提供一个 C 语言解压器 和原文 ,要求 Agent 反推压缩格式,生成 。验收同时检查两件事:解压结果与原文完全一致,且压缩文件不超过 2500 字节。

也就是说,只要 AI 能完成这样一个功能,并通过检查就算满分。然后你就可以说 AI 能取代程序员了。但是这个题目你会发现具备几个特征:

没有任何上下文,没有业务知识的人也能读懂。

没有上下游依赖,你只需要实现功能。

但是真实业界写代码的时候,比如我们上面那个实现聊天室需求,你会发现,这两个特征一个都不满足。并且这道题的 grader 也没有对代码风格,架构信息识别等等做出检查,就算 AI 写的是屎山,只要能通过全部单元测试,那就是满分。

这很像我们的选拔考试一样,一个人能考到满分只能说明他是一个合格的牛马,但是代表不了他能胜任科学家或者工程师的位置。AI 目前的 RL 训练也有这样的问题,对系统能力和架构能力没有任何有效的监督信号用于更新参数。

那么你就会说了——我们搞一些能反映架构信息理解的数据不就行了?

那么你就需要先去造数据造题,你自己可以评估一下,造一道这样无上下文的题目,和造一个完整的处处结构都有校验的,结构如下的题目,谁的成本更大,成本大多少,grader 如何设计?

图片图片

之前的 grader 只需要能计算出当前 AI 编写的编码器的压缩率多少就行了,这是一个可以通过程序验证的事情。但是你觉得架构侵蚀这类问题是可以通过程序化的方法验证的吗?

你如何编写一个程序去计算出当前一段代码是否会造成架构侵蚀。

好,你说,我们可以做 RLHF,请一堆的专家过来给反馈。那你算过成本吗?你知道一个资深程序员工资多少吗,他从零阅读一个复杂项目并基于阅读体验对 AI 的编辑进行架构评估需要多少时间吗?并且这样的题目是否存在?你都了解吗?

结论

终于是赶在国庆之前把这篇文章写完了。

相信读完这篇文章后,你应该对「为什么 AI 会写出屎山代码」这件事有了更加本质深入系统的理解。

你会发现,AI 时代,程序员的基本功——系统能力,架构能力,这些我们的传统教育忽视的部分,越来越重要了,并且这也是 AI 无法取代的部分。

okey dokey,这里是 00 后全栈工程师锦恢,一个正在尝试撰写全网最小白最全面最系统的 AI 全栈系列教程(Agent + 大模型算法 + AI Infra)的学徒。我们最新做的 Agentic RL 框架 Dressage 也开源了,欢迎大家 star 支持一下:Accio-Lab/Dressage,欢迎想要了解和学习大模型底层技术的相关朋友来阅读讨论。

图片图片