对抗AI Slop:用“写更少代码”守住代码质量底线
2026/9/7 15:16:05 网站建设 项目流程

最近技术圈有人在聊一个非常实际的问题:AI slop 不止出现在图文内容里,也开始出现在代码仓库里。典型症状是——每个函数单独看都能看懂,放到一起没人敢改,功能的增加速度远远赶不上代码量的膨胀速度。最近 Dex Horthy 呼应 Matt Pocock 关于 AI 代码生成的讨论时,给了一个非主流但很值得实操的观点:减少 AI slop 的办法,是写更少的代码。

这句话不是劝你别用 AI,也不是让你把现有代码乱删一遍。我的理解是:真正的工程问题,不是“AI 写了多少代码”,而是“有多少代码根本不该存在”。代码越少,AI 出错的空间就越小,评审和测试的覆盖面就越可控,模型下一次改动时,需要重写和猜的部分也会更少。

这篇文章会把这句话拆开讲:先看什么是代码里的 AI slop,再讲“写更少代码”解决问题的机制,然后用一套完整的实操思路——提示词怎么写、类型怎么约束、死代码怎么清理、PR 怎么卡——帮你把 AI 生成代码的质量曲线拉回来。

1. AI slop 到底是什么:代码库里正在堆积的无意图代码

“slop”最早常被用来形容 AI 生成的批量低质量内容:看起来逻辑通顺,实际上没有观点、没有信息增量。代码领域的 AI slop 也差不多,只是更隐蔽:它不一定报错,甚至测试能过,但缺少“人设计出来的意图”。

换句话说,传统烂代码是人的能力或时间不够导致的;AI slop 是模型根据概率平滑生成的“合理代码”。模型不觉得自己在堆复杂度,它只是把训练数据里最常见的写法拼了出来。于是代码库里会出现下面这些特征:

特征表现背后的成本
防御性过度一个字段判空、一个返回值包两层 Optional每次调用链都变长,读代码要翻更多层
大量相似片段一样的逻辑在不同文件里复制多次后续模型生成新代码时只会继续复制同类写法
伪抽象为了分层建了很多 Service/Manager,但每个类里几乎没有行为改动一个需求要连带改四五个文件
命名没有语义变量名是dataresulttempList,函数名是泛化的process人类评审时无法判断它到底承担什么职责
注释解释“怎么做”而不是“为什么”每行都有注释,拿掉注释代码反而更好读注释和代码会出现二次维护成本

很多人第一次意识到 AI slop,是在代码评审里看到模型生成的代码“太顺滑了”。它不会给你明显的语法错误,也不会写一行完全跑不通的伪代码,它最大的问题在于:新增了一个函数,但系统里已经有更合适的函数存在;新增了一个分支,但上游已经保证了状态不可能出现;新增了一个类型,但只是为了少写一次as

模型的生成逻辑决定了这个问题会被放大:语言模型最大化的是“下一个 token 出现的概率”,不是“整个系统的设计一致性”。把它丢进一个没有本地上下文的仓库,它不知道哪个模块已经有等价函数,也不会因为你刚删过一段重复代码,就避免生成风格相似的重复代码。所以,只靠提示词里写“请写出高质量代码”,基本治不了本。

2. 为什么“写更少代码”能直接减少 AI slop

先给一个直观对比。

假设一次需求评审里,你手上有一个 200 行的 PR,和一个 10 行的 PR。200 行的 PR 即使全部由资深工程师手写,评审者也很难每一行都看出隐藏在数据流里的边界问题。10 行的 PR,你可以把每个分支都推演一遍,甚至可以立刻追问:这 10 行里是否有 3 行根本不需要?

AI slop 之所以能混进主干,靠的就是“一次性生成超出人类评审能力的大代码块”。你 review 不过来,就只能信它。所以,写更少的代码本质上是把一个大型生成任务,拆成人类可以完全理解的微小型验证任务。

更关键的是,代码量减少会带来四个连锁反馈:

第一,错误面变小。每一行新增代码都是一个 potential bug。少写 100 行不是“节省了 100 行打字时间”,是少给系统放了 100 个隐藏炸弹。

第二,评审成本可控。AI 生成代码合入前,最重要的一道防线是人工评审。代码块越小,评审者越容易对比“原有实现”和“新实现”的差异,也越容易发现模型把某个判断写反了。

第三,模型下一次生成更准确。模型生成代码时会参考上下文。仓库里重复代码越少、命名越一致、结构越清晰,模型在下一次生成时就越容易命中正确模式。反过来说,一个被历史代码塞满的仓库,模型总会从历史代码里学到坏习惯。

第四,测试更容易覆盖。代码量不等于测试量,但代码路径少了,状态分支少了,测试用例的边界自然就少。模型要跑通测试的成本也会降低,你不用再为了一个纯装饰性抽象层补齐一大堆测试。

所以,可以把“写更少代码”理解为:不断减少系统的表面积。你的接口更小、依赖更少、分支更直接,AI 即使想写出一堆没有业务必要性的代码,它也没有地方可以落笔。

3. 实操第一环:契约先行,让 AI 只填一个函数

很多开发者在让 AI 写代码时,习惯性输入的是这种提示词:“帮我写一个用户管理模块,要支持登录、注册、修改密码、刷新 token”,然后把 AI 生成的 300 行代码直接复制进去。

这个方式最大的问题不是模型写得慢,而是模型在缺少边界的情况下,会替你决定所有设计细节:它会把 token 存到哪个表里、它会自己脑补一个中间件、它会默认两边 API 的使用方式一致。这些“自定义决策”叠加起来,就是你代码库里真正冗余的部分。

要减少 AI slop,第一步不是让模型“多写”,而是让上下文先把边界锁死。

3.1 把任务拆成“人写契约 + 模型写实现”

最稳的模式是:

  1. 人先定义好函数签名、输入类型、输出类型。
  2. 人写好失败测试或验收条件。
  3. 人指定模型只能修改某一个文件,最好只修改某一个函数。
  4. 模型生成实现后,运行测试,看是否满足要求。

下面是一个最精简可复制的契约。注意看,这份代码里人类的职责是画边界,模型的职责是填空:

// src/charge-stage.ts // 这部分由人先写好,明确表达“合法状态有哪些、事件怎么流动” export type ChargeStage = 'draft' | 'pending' | 'paid' | 'refunded'; export type PaymentEvent = | { type: 'mark_paid'; paidAt: string } | { type: 'refund'; reason: string }; /** * 根据当前状态和支付事件,返回下一个合法状态。 * 如果不允许发生该转移,请返回 null。 * * TODO: 让 AI 填充下面这个函数的函数体。 */ export function nextChargeStage( current: ChargeStage, event: PaymentEvent ): ChargeStage | null { throw new Error('not implemented yet'); }

接着,人再补一条失败测试:

// test/charge-stage.test.ts import { describe, expect, it } from 'vitest'; import { nextChargeStage } from '../src/charge-stage'; describe('nextChargeStage', () => { it('draft 状态收到 mark_paid 后进入 paid', () => { expect( nextChargeStage('draft', { type: 'mark_paid', paidAt: '2025-01-01' }) ).toBe('paid'); }); it('refunded 状态下不能再次 mark_paid', () => { expect( nextChargeStage('refunded', { type: 'mark_paid', paidAt: '2025-01-01' }) ).toBeNull(); }); });

然后给模型的任务描述就非常简单了:

请实现 src/charge-stage.ts 中的 nextChargeStage 函数。 要求: 1. 不要修改这个文件中的类型定义。 2. 不要新增文件。 3. 不要改动测试文件。 4. 补全函数体后,运行 pnpm test,确保全部测试通过。 5. 如果分支很多,请先用状态转移表表达逻辑,再写成代码。

这个 prompt 和“帮我写用户管理”最大的区别是:模型这次不需要做系统设计,不需要伪造接口,不需要决定测试策略。它唯一能输出的,就是这个函数体内部几十行甚至十几行代码。哪怕模型生成得不够好,人类 review 的成本也极低——因为红线已经确定了。

3.2 复杂任务继续拆,而不是让 AI 一步生成

如果需求足够复杂,比如需要新增一个 API、一个 service、一个 repository,建议也不要让 AI 一次性生成全部。你可以把流程拆成几个小步:

  • 第一轮:人定义请求/响应类型,让 AI 生成 schema 或 DTO。
  • 第二轮:人定义 service 的函数签名和错误码,让 AI 实现业务逻辑。
  • 第三轮:人准备好外部依赖接口,让 AI 写一个 adapter。

这样做看起来“多花了几轮对话”,实际上每次模型改动都被约束在一个可验证的局部。模型生成的行数越少,AI slop 的生存空间就越小。如果你连续三四轮都发现模型在疯狂新增文件,那大概率不是模型的错,而是你给的任务边界太宽了。

4. 实操第二环:用类型和测试收窄生成空间

很多代码写得多,不是因为需求复杂,而是因为类型表达得不够精确。一旦类型里有大量“可空字段”或“任意对象”,模型就会觉得每个字段都有可能不存在,于是生成大量判空分支和层层防御逻辑。

TypeScript 是最容易看到这个效果的场景。举个例子,下面是容易诱发 AI 写出大量if (!xxx)的类型:

type Charge = { status: 'pending' | 'paid'; amount: number; paidAt?: string; // paid 时有值,pending 时可能是 undefined };

这时候让 AI 实现“描述一笔订单”,它第一反应就是每写一个分支都判一下paidAt。代码看起来非常严谨,实际上因为类型系统根本没有表达清楚“pending 状态下不可能有 paidAt”,所以模型只能靠运行时判断来兜底。

更好的做法是把类型改成交互式联合,把非法状态直接排除在编译层之外:

type PendingCharge = { status: 'pending'; amount: number; }; type PaidCharge = { status: 'paid'; amount: number; paidAt: string; }; type Charge = PendingCharge | PaidCharge; function describeCharge(charge: Charge): string { // 模型在这个函数里几乎不需要判 undefined switch (charge.status) { case 'pending': return `等待支付:${charge.amount}`; case 'paid': return `已支付:${charge.amount},时间:${charge.paidAt}`; } }

在第二个版本里,模型进入case 'paid'分支后可以放心访问paidAt,因为 TypeScript 编译器已经收窄了类型。它不会再写charge.paidAt ?? ''这种为了通过编译而存在的代码。

所以,当你发现 AI 一直生成防御性代码时,优先怀疑类型设计,而不是要求 AI “少做防御”。强类型是给模型的止损线,让它在编译层面就知道哪些分支根本不可能到达。

测试同理。测试描述的不是实现细节,而是“合法输入 / 非法输入 / 边界输入”的验收单。模型如果能看到清晰的测试,它就不用自己创建一套“我认为应该这样”的判断逻辑。建议项目和人的验收习惯保持一致:先写功能测试,再让模型补实现;先写接口类型,再让模型补调用代码。

但这里要提醒一句:约束强不等于类型体操。不要为了“让 AI 好写”去堆叠大量泛型、重载或 builder 模式。如果约束本身变成了另一种复杂度,那只是把 AI slop 换成人手写的类型 slop,总代码量没有下降。

5. 实操第三环:真正删掉多余代码

“写更少代码”不只是控制 AI 未来生成量,还包括清理存量代码库。存量代码里的重复逻辑、死代码、伪抽象,会持续污染模型上下文——只要这些历史代码还在,AI 总会学着它们的样子继续生长。

5.1 先跑一轮未使用代码检查

先用静态工具把明显没人用的代码找出来。不同语言有不同工具,下面以 TypeScript 项目为例:

# 检查是否开启严格未使用检查,通常配置在 tsconfig.json 中 npx tsc --noEmit --noUnusedLocals --noUnusedParameters # 用 ESLint 查未使用变量、无意义的 console.log npx eslint . --ext .ts,.tsx # 统计当前分支相对主分支新增/删除行数(观察 PR 规模) git diff --numstat origin/main...HEAD | awk '{ add += $1; del += $2 } END { printf "add: %d, del: %d\n", add, del }'

如果你的项目规模较大,可以用knipts-prune这类工具扫描未被导出的模块、未被引用的文件。输出结果通常会是一张“可疑文件清单”,然后你逐个确认能否删除。

对于 Java、Python、Go 等项目,分别去找对应的未使用依赖检查与代码覆盖率工具即可,核心思路一致:如果一个函数、模块、字段在整个系统里没有任何调用路径,那么它大概率是历史包袱,不再是有效资产。留着它只会让模型的上下文更乱,让后来的 AI 更分不清“什么可以复用”。

5.2 让 AI 生成“删除/精简 diff”,而不是“重构重写”

清理存量代码时,很多人习惯让 AI“重构一下这个文件”。这个 prompt 太危险了,因为模型会倾向于把所有东西重写一遍,最后 diff 巨大,你根本没法 review。

更安全的做法是让 AI 产出一个只减不增的补丁:

在 src/payment/v1/order.ts 中删除无用代码,具体要求如下: 1. 只做删除和合并,不要引入新函数。 2. 如果你发现两个函数行为完全相同,保留其中一个,删掉另一个。 3. 删除任何未被当前模块导出的私有函数。 4. 不要在文件顶部新增 import。 5. 直接输出 git diff,不要粘贴整个文件。

这时候你要重点检查的是:这个 diff 是否真的在减少代码量。它有没有为了“减少行数”而把两个职责完全不同的函数粗暴合并?有没有把一个复杂但正确的判断逻辑删成语义丢失的空壳?

5.3 同类分支合并成数据表

还有一种常见的“代码多但信息量低”的情况,是模型生成了大量if / else if分支,每个分支只是返回一个固定结果。与其让模型继续抄一份新的 switch,不如人先给出一个数据表方向。

仍以下面的状态迁移为例。一个完整的迁移表,用代码写出来可以是状态映射,而不是几十行条件判断:

// 状态迁移表:key 是“当前状态 + 事件”,value 是下一状态 const TRANSITION_TABLE: Record<string, ChargeStage | null> = { 'draft:submit': 'pending', 'pending:mark_paid': 'paid', 'paid:request_refund': 'refunded', }; export function nextStage(current: ChargeStage, event: string): ChargeStage | null { return TRANSITION_TABLE[`${current}:${event}`] ?? null; }

这种情况下,新增一个状态或者新增一个事件,不需要修改函数逻辑,只需要在表里加一行。模型的输入历史越“表驱动”,它后续生成的代码就越少依赖散落的 if 分支。

6. 评审与合入门禁:把净增代码量当成检查点

光是让开发者写少还不够。只要 PR 没有门槛,人们为了快速交付很容易让 AI 一次性生成大段代码,然后合并进主干。所以在流程层面,需要把“代码净增长”变成显式的评审指标。

这里说的不是简单地从制度上规定“每次 PR 不能超过 200 行”。如果你第一次就让 AI 做一个大功能并生成 800 行代码,你很难把它拆成 200 行以内。更合理的做法是:在 PR 描述里填写“新增行数和删除行数”,并说明为什么这次必须净增这么多。

推荐在代码评审模板里加这几项:

检查项说明
本次需求的净增行数如果只加了功能没加业务复杂度,行数应该不高
是否新增了文件新增文件是强信号,开发者在评审时重点看这个文件能不能被合并
是否重复了现有逻辑评审者是否能在仓库里找到相似函数
是否出现防御式判空是否能通过类型设计消除这些分支
是否有调试残留console.log、临时写死的if (false)
是否带失败测试没有测试情况下,AI 生成代码的可信度很低

在自动化层面,可以先写一个脚本把“PR 是不是太大了”这件事暴露出来。比如每次 Push 之后输出新增和删除行数,把它加入 CI 日志:

#!/usr/bin/env bash # 在 CI 中统计 PR 新增/删除行数,辅助 reviewer 判断是否需要拆 PR BASE="${GITHUB_BASE_REF:-main}" git fetch origin "$BASE" --quiet git diff --numstat origin/"$BASE"...HEAD | awk '{ add += $1; del += $2 } END { printf "total_additions=%d total_deletions=%d\n", add, del }'

如果某次 PR 的总新增行数超过预期,比如 600 行以上,CI 日志里给一个 warning,提醒仓库维护者优先拆解。这个门槛值需要根据团队习惯调整。强硬规则可能产生“为了少改行而故意把代码压成一长行”的负优化,因此比较好的做法是先暴露、再人工决策。

还有一个非常简单的实践:要求 AI 生成代码后,在提交信息里写清楚是由 AI 辅助完成还是完全自动生成,并在 PR 描述里标注涉及范围。这样 reviewer 看到不合适的抽象时,可以直接问一句:“这段只有这一处调用,为什么不直接内联?”这个问题的潜台词正是“写更少代码”。

7. 怎么观察 AI slop 是不是真的在减少

如果你的团队已经开始按上面的方式控制 AI 代码生成,下一步需要验证效果。不能只看“我们用了 AI,效率提高了”,还得看仓库是否变得更难维护。

建议关注下面几个信号:

信号一:单次需求平均净增行数是否在下降。这是一个过程指标。如果新需求多次都只增加了 20~50 行,说明团队拆解需求和复用逻辑的能力在变强。如果每个需求动不动新增 300 行,通常不是业务真的复杂,而是又没有找到旧的复用点。

信号二:重复代码率是否稳定。你不需要直接看“重复代码率数字”,只要在 review 时定期搜索相同模式的出现次数。比如一个订单状态的转换逻辑,如果被复制到了三个文件里,这就是 AI slop 的一种典型残留。

信号三:评审评论的有效性。如果 review 里经常出现“这里不需要这么防御”“这个文件是不是和旧文件重复了”,说明 AI 生成的大块代码开始成为团队负担。反过来,如果 review 能集中讨论业务规则和测试边界,说明系统的“代码表面积”变小了。

信号四:改动一个已有小功能时,需要动几个文件。如果本来只需要改一个函数,但实际要连带改接口类型、两个 service、三个 mock,那这套代码里有相当一部分“伪结构”正在增加维护成本。

信号五:留下了一段代码后,三个月内是否真的被修改过。不活跃代码不一定是死代码,但一个系统和团队成员长期都没有触达的抽象层,大概率只有解释成本,没有复用价值。

另外,可以做定期“砍半实验”。每个月挑一个相对稳定的模块,要求组内成员挑战:在保持行为不变、测试全绿的前提下,代码行数能不能降一半?这个实验不一定真能降一半,但执行过程中会暴露大量隐藏的重复分支、无意义中间变量和不必要封装。它比任何指标都能更直接地训练出“写更少代码”的直觉。

8. 适用边界与常见认知误区

讲到这里,必须补充一些边界,避免“写更少代码”被误解成“少写代码就行了”。

AI 有它擅长的工作:一次性脚手架、数据格式转换、正则表达式、单元测试样例、把一段 Python 翻译成 TypeScript、生成一个模拟接口的假数据文件。这些代码的特点是:生命周期短、重复度高、不需要深层领域知识、就算写得不好也容易替换。让 AI 在这些场景快速生成,反而是降低开发者负担的有效方式。

但下面的场景要非常谨慎:

场景谨慎原因建议
核心业务状态机状态之间隐含业务约束,模型只能看到邻近代码,看不到全局规则人工实现并用类型/测试锁死
权限与计费逻辑安全与资损风险高,错误分支可能不会立刻暴露强制人工对每个分支 review
大规模重构模型生成新结构时会假设很多旧调用点存在,容易形成影子 API先由人设计目标结构,再让 AI 执行小步 diff
跨模块数据一致性需要同时理解多个服务的数据流不适合让 AI 一次性新增大段调用链

同时,针对“写更少代码”本身也有一些常见误区。

误区一:认为 AI slop 就是因为 AI “写太快”,所以限制输出长度就行。实际上,简单地在提示词里加一句“不要写太多代码”不会起作用。模型不知道系统里有哪些可以复用的函数,它只会降低自己的输出量,结果可能是把该写的边界处理也省了。真正解决问题的是缩小任务边界和增强编译/测试约束。

误区二:认为减少 AI slop 只能等下一代模型。模型能力当然会影响生成质量,但代码库本身的混乱程度会影响任何一代模型。你给一个塞满重复代码和伪抽象的仓库,模型下一次生成时最好的策略就是继续复制现有风格。仓库干净了,模型的输出会立刻变干净。

误区三:认为删除代码就是删“AI 写出来的代码”。人工写的历史烂代码同样会产生 AI slop 的土壤。很多“祖传代码”本身就很冗余,AI 只是延续了那种写法。因此清理目标应该是所有非必要代码,而不是看代码是人写的还是 AI 写的。

误区四:以为“代码量少”是最终目标。有些场景下 50 行代码优于 10 行“聪明”代码,比如可读性更好、边界更明确。写更少代码的最终追求是降低系统维护成本,而不是强行追求最短实现。

另外要注意合规边界。生成式 AI 的代码版权与训练数据来源仍在讨论期,公司项目使用前先确认企业内部政策和授权范围。涉及私有仓库、用户数据、密钥的代码,不要随意发送到不被允许的外部模型服务。你可以在本地安装私有化模型,或用企业内部批准的编码助手,但不能因为“AI 生成了代码”就默认代码没有版权或合规风险。

9. 最容易立刻见效的三个动作

如果你现在就想开始实践“减少 AI slop,写更少代码”,不需要一次性推动整个团队改革。从下面三个动作开始,花半小时就能产生可感知的变化。

动作一:把下一个 AI 任务改写成“填空任务”

下次再让 AI 生成代码时,不要直接说“帮我写一个 XX 功能”。先花五分钟写清楚函数签名、输入类型、输出类型,再写一条失败测试,然后让 AI 只补实现。如果你的任务太难拆,就继续把函数拆小。这样跑一次后,你会直观感受到 AI 产出的“废话代码”变少了。

一个可以直接复制的提示词模板:

在 src/fetch-user.ts 中实现 fetchUser 函数。 现有签名: function fetchUser(id: string): Promise<User | null> 要求: 1. 只修改该函数内部实现。 2. 不要新增文件,不要修改其它函数。 3. 接口返回 404 时应返回 null,其它错误继续抛出。 4. 运行 pnpm test 确保测试通过。

动作二:给你的代码库做一次“未使用代码”初筛

针对当前项目,跑一遍未使用代码检查。把扫描结果里明显没有调用者的私有函数删掉。不要试图一次删光所有内容,先挑那些“你凭直觉就能判断没人用”的部分。删除之后再跑一遍全量测试,你会发现测试全绿往往意味着那些代码在很长一段时间里根本没有参与系统行为。

动作三:下一次提交 PR 前,主动在描述里写净增行数

净增行数是一个很好的元认知信号。写这个数字时,你大概率会想:这个需求是不是非得新增 300 行?中间有没有一段其实是在重复旧的工具函数?这段代码能不能直接放到调用方文件里而不用新增一个新文件?无论最后是否调整,这个动作已经强迫你站在“代码总量”的角度重新审视自己的提交。

回到 Dex Horthy 和 Matt Pocock 的讨论。减少 AI slop 其实不需要一套复杂的“AI 治理平台”,也不需要禁止模型参与开发。把它做成一件具体的事:让每个新功能都以更小的代码增量落地,让每个新函数都能找到明确的存在理由,让每次模型生成都发生在人已经画好的边界内。当代码总量持续下降时,AI 生成的垃圾自然就没有地方落脚了。

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

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

立即咨询