那些“每个决定都正确”的项目,最后是怎么烂掉的

去年我接手一个项目。第一天翻代码,心里还挺美:组件在 components,Hook 在 hooks,请求在 services,工具在 utils。目录干净得像样板间,每个文件都在"它该在的地方"。

三个月后,我逢人就吐槽这套目录。倒不是它乱------它一点都不乱,是规整得毫无用处。

产品经理提了一个需求:下单时加一个"企业采购"选项。听着就是加个字段。真动手才发现,"下单"这件事的零件散落在六个目录里:表单在 components/CheckoutForm.tsx,状态在 contexts/CheckoutContext.tsx,校验在 utils/validateCheckout.ts,请求在 services/orders.ts,类型在 types/order.ts,连"把表单转成订单"的逻辑都单独住在 utils/formatPayload.ts。

六个目录,没有一个叫"下单"。

我改了一圈,自以为改全了。上线当晚,运营在群里 @ 我:企业客户全下不了单。一查,utils 里还躺着一个两年前写的老校验器,也在管下单。我压根不知道它存在,自然没改。新表单允许的字段,被一个没人记得的老古董一票否决了。

我漏改了,不是因为不认真,是因为我根本没法确定自己没漏。

一、烂掉的起点,往往不是"错误",是"正确"

后来我看到一个医疗 CRM 团队的复盘,里面有个说法让我愣了半天。

他们管自己遇到的问题叫 "架构决策退化"(Architecture Decision Degradation,ADD) 。定义写得很克制:通过累积妥协逐渐侵蚀架构质量。没有戏剧性崩溃,只有缓慢爬行。

和"技术债"不一样。技术债是你主动借的,先上线,回头还。ADD 是另一回事:团队每一个决定在当时都是站得住的,但十几个"站得住"的决定叠在一起,就把架构推向了没人想去的方向。

那个医疗团队最初做了什么?Redux-Toolkit 集中状态,createAsyncThunk 处理异步,乐观更新支持拖拽。每一步都"正确",代码看起来"真干净"。然后需求来了:WebSocket 实时更新、冲突检测、按医生筛选。3 个 slice 变成 10 个,一个拖拽操作要触达 4 个 slice。开发者在 Slack 里问的是:"我该 dispatch 哪个 action?"

第 12 个月,41% 的 bug 和状态管理有关。第 16 个月,团队才开始讨论重写。但那时候,已经需要三个月重构才能活过来。

没有人做错什么。这才是最让人后背发凉的地方。

二、"搬家"不等于"收敛"

前面那个审计报告里,还有一句更扎心的话。

一个项目刚完成"大规模迁移":旧结构整体搬进新的五层骨架,ESLint 规则和边界测试都配好了。骨架方向是对的。但报告写得很直白:"迁移只做了搬家没做收敛。" 143 个旧组件原样搬进新结构,巨型文件一个没拆,双错误体系、样式复制、localStorage 各写各的,全部带进了新家。

最典型的是一个 5647 行 的 SessionWorkspace.vue。它同时管着 6 个 store 的装配、按线程分桶的运行时状态、消息投影合并、SSE 流编排、输入队列、审批、附件、面板拖拽和 ResizeObserver。脚本 3090 行,样式 1730 行,还在 scoped 块里 @import 把整套全局样式复制进了组件。

它跑得起来。它只是变成了一个没人敢拆的东西。

报告里还有一句话,比那个 5647 行更隐蔽:依赖方向存在真实的 反向 import 环。apis 依赖 identity,identity 又反过来依赖 apis。靠"在函数体里延迟调用"暂时没有爆栈------属于结构性隐患,不是结构性错误。

"暂时没爆"和"没问题",是两件完全不同的事。

三、AI 让"新建"变便宜了,于是"收敛"变得更难

2026 年,这件事多了一个新的变量。

AI 让组件的生成速度上了一个台阶。但你让它做一个卡片,它大概率会直接生成一个新组件,而不是先问你"项目里是不是已经有一个了"。它默认的目标是先满足当前页面 ,不是维护组件体系的长期秩序。

于是出现了一个很微妙的反转:以前你倾向于复用,是因为自己重写一套也不便宜。现在 AI 几分钟就能"就地生成"一个差不多的版本,复用的动力反而变弱了。

结果是:同一个展示卡片,项目里能找到四五个版本。基础组件的 props 越堆越多,最后谁都不敢动。公共组件本来是为了复用,结果变成了最容易积累技术债的地方。

AI 没有制造这个问题。它只是把"组件治理缺失"这件事,用更快的速度放大到了你无法忽视的程度。

四、我现在判断一段代码"危不危险",不看它乱不乱

经历了那个"企业采购"的夜晚之后,我换了一个判断标准。

不看目录整不整齐,看一件事:同一个原因才会一起改的代码,是不是住在一起。

"下单"的校验,只在下单需求变的时候才改。那它就应该是"下单"的,不该住在全局 utils 里,和一堆八竿子打不着的东西做邻居。那个偷偷否决了新表单的老校验器,之所以能藏两年,恰恰是因为它住在"它该在的地方",而不是"下单该在的地方"。

"搬家"和"收敛"的区别也在这里。搬家是把旧东西放进新柜子。收敛是决定哪些东西不该继续存在 。143 个组件原样搬进五层骨架,骨架再对,里面还是 143 个旧习惯。结构可以一夜之间换掉,习惯不会。

至于那个"每个决定都正确"的医疗团队,他们最后活过来的方式,也不是"把所有 slice 写得更规范"。是花了三个月,重新回答一个最朴素的问题:这个状态,到底该归谁管。

烂掉从来不是因为某个瞬间做错了什么。是因为一直没有人停下来,做那个"把东西收回去"的决定。

相关推荐
徐小夕1 小时前
万字长文!DeepSeek Super-agent × 企业知识库架构深度拆解与落地实践
前端·vue.js·github
陈随易1 小时前
TanStarter,从搭建到支付,一步到位开始赚美元
前端·后端·程序员
前端双越老师2 小时前
程序员 1v1 访谈系列第十二期:普通人没有核心竞争力
前端·ai编程·全栈
柳杉2 小时前
没有写一行代码:我用 Codex 制作 了一个 3D 智慧展厅可视化大屏
前端·ai编程·three.js
IT_陈寒2 小时前
Redis开发实战:从入门到精通
前端·人工智能·后端
恋猫de小郭2 小时前
Flutter Impeller Vulkan 内存持续上涨,问题没想到是文字阴影
android·前端·flutter
0xBADCODE2 小时前
2026 强网杯 Web 题 cwp · 全链路复盘
前端·安全·web安全·网络安全·系统安全·ctf
麦麦麦当劳大王11 小时前
【油猴脚本】Boss直聘 岗位职责复制
开发语言·前端·javascript
小lu飞12 小时前
2026小红书小工具怎么制作:纯前端 ZIP 打包、沙箱限制与笔记挂载流程
前端·笔记·ai·ai编程