☰
AI编程工具让代码库变烂?反Cursor质量闸门实战指南
2026/10/14 1:32:33 网站建设 项目流程

先说个最近让我有点睡不踏实的现象。上周帮朋友做一次代码评审,一条 PR 摆在我面前:1600 行,12 个 commit,其中 10 个 commit 的描述都写着 “fix: adjust AI generated code”。代码本身不能说烂,注释齐全,防御性判断一个不少,可我就是讲不清楚这个改动到底想解决什么问题。更让我后背发凉的是,我发现自己已经很难回答“这段代码为什么长这样”——因为它是 Cursor 这类 AI 编程工具顺手生成的,写它的人自己也说不清。这个体验不是个例,我周围越来越多团队靠 Cursor 提效,半年后再打开代码库,重复代码翻倍、依赖混乱、旧的兼容逻辑没人敢删。今天这篇文章我想认真拆一下:AI 编程越来越猛,代码库为什么反而越来越烂?以及我们真正需要的那个“反 Cursor”工具,到底是什么、怎么搭。

1. 先别骂 AI 编程,先看它到底给代码库签了什么“隐性账单”

1.1 平均化输出:你的代码库正在被“大多数人”接管

大模型训练在几亿个 GitHub 仓库上,它的本质任务是预测“下一行最像什么”,而不是判断“这一行适不适合你的项目”。所以 Cursor 生成出来的代码,几乎必然带一种强烈的“平均味”:防御性 if 塞满、命名走最保守路线、异常处理模棱两可、注释把意图反复解释三遍。单独拎出来看每段都不差,但合在一起,你的代码库就正在被“GitHub 上的大多数人”慢慢接管。

我习惯用装修来类比。你请了一个智能装修公司,它手里只有海量户型图,你告诉它要一个北欧风书房,它确实能给你刷白墙、铺木地板、配一张原木书桌,视觉效果在网上见过。但它不知道你家的书是 40 公斤重的大部头,不知道你的采光下午三点就没了,也不知道你需要在墙上留一排轨道插座。单看每个空间都能住,住久了处处别扭。AI 写代码就是这样——它给出的不是“你的体系里最合适的实现”,而是“全网出现频率最高的实现”。你的项目里原本那套统一错误码、统一日志规范、自定义的请求追踪链路,在“平均化输出”面前会被一点点稀释。

1.2 上下文窗口:它记住你整个项目的方式,一开始就是假的

有人会说,Cursor 不是能@Codebase吗?它能读整个仓库。确实,检索比早期聊天式 AI 强太多,但你要理解背后的机制:它仍然是先把代码库切块、embedding 成向量、按相关性召回若干片段,再把片段拼进有限的上下文里。这意味着它不是“懂了你的系统”,而是“翻了很多页题库,然后凭直觉作答”。召回质量受分块策略、关键词匹配、token 上限、甚至文件权限影响,一旦关键约束没被召回,它就会自信地发明一个不存在的接口,或者重复封装一个已经存在的工具函数。

我见过最典型的场景:一个老项目里已经有src/utils/request.ts封装好了鉴权、重试和埋点,结果让 Cursor 在新模块里写请求逻辑,它直接又生成一个src/utils/http.ts,看起来也是那么回事,但完全游离在既有链路之外。不是它不努力,而是它的上下文窗口里根本没有那条“全局唯一请求入口”的约束。你把这种代码合进主干,等于默认允许“同一种概念以 N 种方式存在”,这恰恰是大型代码库腐化的起点。

1.3 它不背债务,但债务会记在你头上

还有一层更隐蔽的伤害。AI 不关心这个代码上线后谁维护、许可证有没有问题、有没有安全漏扫、三个月后这个依赖会不会被弃坑。它的目标函数是“让眼前这个对话任务看起来完成了”,不是“让这个系统长期健康”。所以它天然倾向于局部最优:给当前函数补一个补丁让编译通过,哪怕这个补丁和两公里外另一个补丁逻辑重叠;为当前需求新写一个工具函数,哪怕仓库里有个参数差不多的旧函数正在被五个地方调用。

更要命的是,这些平均化代码一旦进入仓库,就会成为“风格样本”。后续的 AI 召回它之后,会以为“哦,这个项目就是这么写代码的”,于是继续产出更多同样风格的代码。企业咨询里有个词叫“组织性债务”,放在代码库上也成立:好的代码会带节奏,平庸的代码同样会带节奏,而且平庸的带节奏能力更强。你每一次允许 AI 生成的中庸代码直接进主干,都是在给下一个 AI 投喂错误先例。

2. Cursor 用是真的爽,但爽完的代价往往都在半年后

2.1 先说明一下,我并不是来劝退 AI 编程的

我自己重度用 Cursor,最近还在拿它写一个内部运维工具,纯效率角度,我不可能退回到手写。它的舒适区非常明确:把 Python 代码翻译成 Go 的骨架、写正则、生成单元测试的脚手架、快速理解一段陌生源码、给 CRUD 接口铺一个符合惯例的三层模板,这些事它 30 秒完成的量,我手写至少 20 分钟。所以问题的关键从来不是“要不要用 Cursor”,而是“用了之后,有没有同步调整你的工程防线”。

2.2 高产的背面:PR 变肥、提交变乱、评审失效

AI 编程带来最直观的副作用是 diff 膨胀。人写代码通常是“一点点挤出来”的,一次改动 10 到 50 行,评审者能轻松看懂来龙去脉。AI 则倾向于一口气生成几百上千行,因为它理解的“任务”是整个屏幕上的需求描述,而不是一个增量步骤。评审 1000 行 diff 是什么体验?大多数人直接放弃逐行看,扫一眼绿条,看有没有明显 bug,然后合入。评审这个环节一旦形同虚设,后面所有问题都会被放进主干。

commit 历史也在变烂。Cursor 配合自动提交的功能,会生成类似 “feat: implement login flow” 然后下一行 “fix: correct import path”,整个分支看起来像一晚上的弹幕。不是不能看,而是半年后你回溯某次事故时,完全无法从这些提交信息里定位意图变化。代码评审讲究“可追溯”,AI 时代这个底线被冲得很厉害。

2.3 半年后的典型症状:重复、死代码、命名漂移

我自己的一个项目里,引入 AI 编程辅助三个月后,用jscpd扫了一遍代码库,重复率从 3% 涨到了 11%。不是某个人偷懒,而是 AI 在多个上下文里反复生成“看起来能用”的相似函数。它不知道另一个文件里已经有一个做了 90% 相同事情的函数。这还只是重复率,另一类问题是死代码:AI 发现上下文里找不到合适的调用方,就顺手生成一个新接口,旧接口没人删,新接口没人用,仓库里堆满了“理论上可用”的代码。

更隐蔽的是命名漂移。同一个业务实体,在 A 模块叫UserProfile,在 B 模块叫UserInfo,在 C 模块叫ProfileInfo,每个模块单独看都自洽,全仓库搜索时你会以为这是三个东西。错误处理也在漂移:有的地方返回null,有的地方抛异常,有的地方吞掉异常打印个console.warn。这些都是 AI 逐个上下文“独立决策”的自然结果——它从来看不到自己最近五天写的代码有多分裂。

3. 反 Cursor 工具:不是反对 AI,而是给 AI 招募一个“严格的上司”

3.1 先回答一个关键问题:“反 Cursor”到底反什么?

很多人一听“反 Cursor”,以为是要退回命令行手写时代,或者禁用 AI 编程。我的理解恰好相反。这里“反”字的意思,更像“对偶”和“制衡”,类似测试之于开发、code review 之于提交。Cmd+Z只能撤销单个文件,撤销不了范式问题。我们需要的是另一类工具,它的职责是“对 AI 生成的代码保持怀疑、执行统一约定、阻止无人认领的复杂度进入主干”。所以“反 Cursor”不是一个具体产品,是一族策略:类型检查、lint、架构约束、重复代码扫描、复杂度门禁、依赖审计、测试断言质量检查。

这套策略的核心逻辑非常简单:Cursor 负责高产出,反 Cursor 工具负责高约束。一个拼命写,一个拼命查,两边形成张力,代码库才能在“快速交付”和“长期可维护”之间稳住。

3.2 五大工具族,对应 AI 代码的五种病

我目前工作里的防线主要分五个层面,每层解决一类 AI 生成代码带来的问题:

防线层级常用工具检查重点治的是哪种 AI 病
类型体系TypeScript strict、pyright、mypy空值、未定义接口、跨模块类型一致性模型臆想不存在的接口、返回类型含混
代码风格与静态检查ESLint、oxlint、ruff + 自定义规则复杂度上限、未使用变量、禁止静默 catch平均化防御代码、吞异常、死变量
架构约束dependency-cruiser、eslint no-restricted-imports依赖方向、禁止业务层反向依赖基础设施层AI 无视已有分层,随手跨层 import
重复代码扫描jscpd、sonar相似代码块、复制粘贴率AI 独立决策造成的重复函数、重复封装
变更与提交门禁commitlint、GitHub Action diff size 检查、hooks提交信息规范、单次变更规模、PR 文件数大 diff、弹幕式 commit、评审失效

这套组合我称之为“自动 CR”,它承接了原来需要资深工程师人工盯的一部分工作。注意这不是替代人,而是先把那些“机械的、可规则化的约定”自动化,让人力集中在意图判断上。

3.3 为什么传统的 code review 已经不够了

团队里有人会问:“我们有 code review,为什么还要上这么多工具?”答案很现实:AI 的产出量已经超过了人类评审的极限。一个人一小时最多认真评审 300 到 400 行 diff,而 Cursor 一晚上能生成 3000 行。这时候把希望全部寄托在“让人更负责”上,等于用木桶接瀑布,接不住的。自动门禁的优势是:它不疲劳、不吃情绪、不放过任何一条规则,而且规则失败时会给出明确的出错信息。人类 reviewer 只需要回答“这个意图对不对、这个设计合适不合适”,其余的交给规则系统,这样反而能把人类精力聚焦到最有价值的问题上。

4. 从 commit 到合并,搭一套 AI 代码必须闯过的质量闸门

4.1 第一步:在提交前就给 AI 上“规矩”

很多人忽略了一个比工具更前置的环节:Prompt 约束。如果你在让 Cursor 干活之前,不告诉它这个项目的禁忌和约定,它就只能靠统计偏好自由发挥。我现在每个项目根目录都会放一份.cursorrules或AGENTS.md,里面写清楚几条铁律:

  • 只完成我描述的这一件事,不要顺手重构无关代码;
  • 不允许使用我未在提示词中列出的第三方依赖;
  • 所有新封装的函数,先搜索代码库里是否已有等价实现;
  • 不允许静默捕获异常,catch 后必须有日志或向上抛出;
  • 修改后必须执行npm run typecheck和npm run lint。

给 Cursor 的 Prompt 我也会刻意写“不要一次性生成整个功能,只生成一个文件里的一小步”。这听起来很低效,但实测下来,小步生成 + 每步人工确认,比一次性生成 800 行然后返工,总耗时少得多。

4.2 第二步:提交与分支层面的闸门

本地提交时用 lint-staged 配合 commitlint,让不符合规范的提交根本进不了历史。我的lefthook.yml里大概是这样:

pre-commit: commands: lint-and-typecheck: run: npx lint-staged && npm run typecheck commitlint: run: npx commitlint --edit $1

分支层面,我会在 CI 里加一个“PR 规模守卫”,逻辑不复杂,就是统计改动文件数和总行数,超过阈值就直接在 GitHub 标红。因为 AI 很容易一晚上生成 300 个文件的改动,这种 PR 无论是 review 还是回滚都是灾难。我设的阈值通常是:文件数不超过 20 个,总新增行数不超过 800 行,不满足就让 AI 拆任务重做。你可以在 Action 里用一行判断:

git diff --name-only origin/main... | wc -l

拿这个数字对比阈值,超了就 fail。

4.3 第三步:静态检查与架构雷达

静态检查这里,除了常规规则集,我更推荐针对自己项目“历史伤疤”加自定义规则。比如我参与的一个后台项目,规定业务代码禁止直接import某个老旧的utils/fetch-old,所有请求必须走新的统一封装,这个用 ESLint 一行就能搞定:

// .eslintrc.cjs 片段 module.exports = { rules: { complexity: ["error", { max: 8 }], "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "no-restricted-imports": [ "error", { "patterns": ["src/utils/fetch-old"] } ] } };

架构层面更推荐dependency-cruiser,它能把依赖方向画成可执行的规则。比如禁止domain层反向依赖infra层,配置看起来类似:

{ "forbidden": [ { "name": "no-domain-to-infra", "from": { "path": "^src/domain" }, "to": { "path": "^src/infra" } } ] }

这里有个重点:规则不能设得太细,否则 AI 没法干活,人也嫌烦。我一般只维护 5 到 8 条“红线”规则——比如不允许业务层直接拼 SQL、不允许 catch 后无日志、不允许绕过统一错误处理器。多了守不住,少了没意义。把红线放进自动检查后,AI 生成的代码一旦越界,CI 会直接拒绝合并,根本不用等人类 reviewer 来发现。

4.4 第四步:测试覆盖与“断言质量”

AI 非常会写“假测试”,这是我在项目里发现的最迷惑人的坑。它生成的测试通常会通过,函数内部逻辑也能跑通,但断言非常空洞。最典型的就是:

expect(response).toBeDefined();

这种断言等于没测,只能证明“代码没抛异常”,不能证明“行为符合预期”。真正的断言应该锁住具体结果:

expect(response.user.id).toBe("u_123"); expect(response.count).toBe(3);

所以我的 CI 里不仅看覆盖率阈值,还会加一层“断言密度”的意识:重要业务模块的测试,人工 review 时先看断言,再读测试逻辑。Coverage 高但全是toBeDefined的测试,在我这里等于没有。这一条建议写进团队完成定义里——DoD 里加一句:“AI 新增的所有测试,必须由提交者本人逐条检查断言是否锁定了真实行为。”

4.5 第五步:人机结合的 review 清单

自动门禁过了,不等于可以闭眼合入。针对 AI 代码,我手上有一份专门的 review 清单,和普通 CR 不完全一样:首先查命名,看同一个概念在改动范围内是否出现了多个叫法;其次是错误处理,所有 catch 都必须有归宿——日志、告警、向上抛,三者至少占一个;然后是死代码,AI 经常生成没人调用的导出函数,查一遍ts-prune或knip。最后还要看“边界条件”,AI 最擅长处理正常路径,最不擅长处理空数组、空字符串、超时和重试。

这份清单不长,但每次都有效。它解决的不是“这行写得对不对”,而是“这段代码是否从骨子里融入了项目现有的体系”。

5. 我实际踩过的几个坑,以及现在的补救工作流

5.1 十二个“数据请求函数”的事故

有一次我让 Cursor 帮忙重构一个老页面的请求逻辑,把散落的wx.request统一换成公司自定义的fetchWrapper。它确实很听话,把 12 个页面都改了,但问题也来了:每个页面都生成了一份“看起来很合适”的请求函数,有的加了重试参数,有的加了超时配置,有的把错误处理写进页面里。一通扫描之后发现,我的警告名单是真实的——引擎从头写了一遍相同的 body,复制出 12 个近似但又不完全相同的函数。当时项目里没有任何层级的 lint 规则禁止直接import旧请求库,人工智能可以在每个页面上随意造一个“包装器”。

现在补救的做法是:先定唯一入口,然后用no-restricted-imports锁死旧的wx.request,再重新跑jscpd把重复率压回来。这个事故给我的教训是——让 AI 做大规模替换之前,必须先建立“唯一出口”的自动约束,指望它“自己发现”是不现实的。

5.2 被吞掉的异常:数据神秘消失

另一个案例更吓人。一个异步加载用户配置的函数,AI 给它加了一层try/catch,catch 里没有任何日志,直接return null。线上数据神秘消失一整天,用户反馈“配置看不到了”,我们查监控日志一片空白,因为异常被吞得干干净净。最后是错误追踪平台抓到一条 starvation,才定位到这个被吞掉的异常。靠人查代码根本没戏,这个文件上周刚被 AI 写过,reviewer 只看了正常路径。

从那之后,我把“禁止静默 catch”写成了团队的红线规则。ESLint 本身没有特别顺手的禁止 catch 规则,我们的做法是让自定义 AST 规则去扫描catch块,如果块里既没有throw、没有日志调用、也没有错误处理函数,直接报错。这条规则比任何代码规范文档都管用——因为机器不会忘。

5.3 依赖升级事故:它顺手把核心 SDK 升没了

有次让 Cursor 修一个旧依赖的兼容问题,它居然顺手把几个核心依赖更新到了 latest。等我反应过来,构建已经挂了半小时。你没法完全怪 AI——它的直觉是“版本不对就升级”,但升级这种操作在真实项目里牵扯许可证、API 变更、子依赖冲突,是最需要人工决策的一类操作。现在的防护很简单:package-lock.json和yarn.lock纳入保护,CI 里如果检测到 lock 文件变更但用户没有在 commit message 里显式声明“dep:”,就自动拦截。同时 prompt 里明确写“不要做主依赖升级”。这条规则执行之后,这类事故再没发生过。

5.4 一晚上 300 个文件的 PR,最终以回退收场

还有一次是让 AI 做跨多个模块的重构,一觉醒来,分支上 300 个文件变更,5000 多行 diff。这种 PR 是没法 review 的,没人能在一上午看完 5000 行并给出有效意见;团队最后的选择是整体回退,把重构拆成五轮,每轮限定一个目录,逐步推进。那次之后,我彻底接受了一个观念:AI 是放大器和加速器,但你得给它装上“节流阀”,否则它会把你带到一个无人能接管的地方。

6. 把 AI 当结对 Junior,把规则当 Senior——我的协作心态与日常 checklist

6.1 心态转变:从“让 AI 写”到“让 AI 改”

如果你想用一句话总结我现在的方法论,那就是:人写意图,AI 写实现,规则写底线。这句话想了很多年才落地。早先我用 AI 的习惯是给它一个大的目标,比如“实现订单模块”,然后躺着等它写完。后来发现,它写的东西我不能完全理解,但看着能用,这种“半懂状态”最危险。现在我的工作流回到最古典的模式——先自己把需求拆成小任务、定义好接口签名和边界条件,甚至先写测试用例,再让 AI 去填充实现,最后通过自动门禁和人工 review 验收。它的定位从一个“外包团队”降级成“结对 Junior”,而我重新回到“技术负责人”的角色。

6.2 我现在的日常 workflow 长这样

拆解下来的步骤并不复杂,按顺序写出来给大家参考:

  1. 需求拆解:把本次任务拆成 1 小时内能完成的小步骤,每步只碰一个目录。
  2. 人先定契约:先写接口定义、类型、函数签名、预期的错误处理行为。
  3. AI 填充实现:让 Cursor 只实现最内层的一个函数或一个组件,提示词里附带仓库规范说明。
  4. 自动闸门:本机跑 lint + typecheck + 单测,提交时跑 commitlint,PR 时跑 diff size 守卫和 architecture 检查。
  5. 代码质量体检:jscpd扫重复、knip扫死代码、eslint扫复杂度。
  6. 人工 review:只做意图与设计的判断,对照我上面那份 checklist,不逐行读样板代码。
  7. 小步合并:每次合入都控制在两三个文件以内的规模,合并频率提到一天多次。

这套流程看起来笨,但实际运行起来非常稳。Cursor 负责把机械劳动的成本打下来,规则系统负责把机械错误的概率打下来,人只需要处理真正需要判断力的那部分。

6.3 我给团队的“代码库健康体检”清单

如果你也想给现有代码库做一次“反 Cursor 体检”,我建议按这个顺序跑一遍。先跑重复代码扫描,看重复率是不是在一个季度内明显上升;再跑依赖关系检查,看有没有逆向依赖、跨层 import;然后跑死代码检测,统计无人调用的导出函数数量;接着看 PR 历史,统计最近一个月平均 PR 文件数和提交数,如果单个 PR 平均超过 20 个文件,就要警惕了。最后检查测试断言质量,抽样读几个新写的测试,看看是不是大量toBeDefined()空断言。这一套下来,基本能判断你的代码库是否正被 AI 编程悄悄“平均化”。

把话说直白一点,工具不会毁掉代码库,没人管的工具才会。Cursor 给了我们史无前例的产出速度,但它不会为你的代码库后果负责,会负责的只有你自己工程体系里的那些硬规则。“反 Cursor”式工具不是要回到手写时代,而是把 AI 的疯狂输出纳入人类可控的轨道。以我个人经验,规则越硬,AI 反而越乖;门禁越严格,你才越敢放开手把那些重复劳动全部交给它。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询