最近关于 AI 编程质量的讨论里,“AI slop”是一个绕不开的词。它指的是 AI 生成的那类“看起来完整、读起来正确、实际上没有信息密度”的代码,比如大量重复的样板、为未来扩展服务的空抽象、复述代码逻辑的注释、临时拼凑又无调用关系的工具函数。Dex Horthy 在回应 Matt Pocock 相关观点时,给出的解法非常直接:减少 AI slop 的办法,是写更少的代码。
这句话听起来像在倡导向“极简主义”妥协,但它真正指向的是代码库结构设计与 AI 辅助开发之间的关系。代码越少,AI 可供低质量繁殖的土壤越少;代码库越克制,AI 生成的每个字符都必须待在有明确边界的上下文里。这篇文章会拆解这条思路的底层机制,并用一个可运行的代码横向对比,说明“写更少的代码”在 AI 辅助编程时代为什么是一项工程能力,而不是一句口号。
1. 从一条讨论说起:AI slop 是如何繁殖的
1.1 讨论背后的工程判断
Matt Pocock 在 TypeScript 开发者圈子里长期关注类型安全和工程效率,而 Dex Horthy 的回应把焦点拉回到了一个更朴素的现象:项目里代码量越大,代码库里的“熵”就越高,AI 生成新代码时就越容易被周边的高熵样本带偏。
这不是一个玄学判断,而是一个概率问题。AI 编程补全工具的基本工作方式,是根据项目上下文预测“下一段最可能出现的代码”。如果上下文里充满了风格混乱、重复造轮子、防御过度的代码,AI 给出的结果自然也会延续这种风格。
所以“写更少的代码”并不是一种审美偏好。它是一种系统性压缩 AI 犯错空间的手段:
- 代码总量小了,AI 能模仿的坏样本就少了;
- 抽象边界清晰了,AI 在已有模块里生成重复实现的机会就少了;
- 项目里没有那么多“半成品工具函数”,AI 也就不太可能把新逻辑接到一个不存在共识的抽象上。
1.2 什么是真正意义上的 AI slop 代码
一个很容易产生的误解是:AI slop 等于“写得丑的代码”。实际上,很多 AI slop 代码单看局部是规范的,它甚至有清晰的类名、完整的注释和正确的缩进。它的真正问题是“贡献了代码行数,却没有贡献业务规则”。
我经常在代码评审里看到这些信号:
- 某个子模块里出现一个通用工具函数,但全项目只有一处调用;
- 为了一段很简单的业务逻辑,AI 引入了 data class、接口、工厂,三个文件只为了表达一个
if error的返回; - 注释仔细解释了代码正在做什么,相当于把源码又读了一遍;
- 新增的依赖被大量使用,但解决的是本来可以用标准库三行代码完成的事情。
这些代码不是不能运行,而是它们会给后续变更带来巨大负担。因为每引入一层抽象,未来所有阅读者都要先理解这个抽象是否真的有必要,然后再判断自己的改动应该落在哪一层。项目里这种“低信息密度”的代码越多,AI 在后续生成时也越难判断到底应该复用哪个抽象,于是它最省力的选择是“再生成一个类似的”。
1.3 一句话总结这条逻辑链
代码库的大小,决定了 AI slop 的可生存空间。
当业务代码被大量无意层包裹时,任何一个后续需求都会触发 AI 生成大量“配套代码”。这些配套代码有很高概率雷同、重复、与既有业务无关,并且在测试里表现为“为了通过而通过”。压缩代码量的本质,是把项目里偶然复杂度降下去,让本质复杂度清晰地暴露出来。这样 AI 的每一次自动补全,都在处理最小必要问题,而不是在一堆历史包袱里玩扫雷。
2. “写更少的代码”在 AI 时代意味着什么
2.1 先厘清一个误区:不是追求行数最少
如果把“写更少的代码”理解为“把每个文件压到最短”,那会走入另一个极端。有人为了追求行数少,写出超级复杂的链式调用,所有人看不懂,只留下 AI 在下次修改时也一头雾水。
这里说的“少”,减少的是“不必要的代码”,而不是“必要的表达”。
正确目标应该是在表达清楚业务规则的前提下,移除多余的封装、冗余的分支、历史遗留的过渡设计和未被使用的抽象。换句话说,是要让每一行留下来的代码都拥有合理的存在理由。行数只是结果,不是目标。
2.2 更小的代码库等于更高的上下文一致性
AI 编程工具生成质量,非常依赖上下文一致性。它需要在你的项目里找到“这个项目是怎么写 parser 的”“这个项目如何做错误处理”“这个项目是否偏好函数式写法”等粒度。
如果代码库很小、模块分布很整齐,AI 精确定位上下文的能力会变强。它只需要读取最近的两个相关文件,就能生成风格一致、调用正确的代码。相反,如果代码库里到处是“同款功能的三个变体”,AI 在选型时很容易选错,或者把三个变体的缺点融合到新代码里。
在实际效果上,这就是很多开发者的体会:代码库整洁的仓库,AI 生成结果明显更“靠谱”;代码库混乱的仓库,AI 常常像喝醉了以后在写新模块。
2.3 写更少的代码要求更强的减法判断力
在 AI 出现之前,写代码的瓶颈主要在于“能不能写出功能”。AI 出现之后,尤其对于有经验的开发者,瓶颈转向了“能不能判断哪些代码不需要写”。
这需要几种能力的支撑:
- 能识别当前业务里哪些是偶然复杂度。很多时候我们写通用层,并不是因为当前真的有两个调用方,而是因为“以后可能会用到”。这种判断在 AI 时代成本太高,因为 AI 会把未来抽象当成现有约定,然后基于错误假设继续生成。
- 能识别已有代码的约定。当 AI 说“我添加了新的 util 函数”时,你要能快速判断项目里是不是已经有功能相近的工具函数。
- 能承受“留白”带来的不确定性。不做提前抽象,意味着未来真的出现第二个调用点时需要再改一次。这恰恰是一种更稳妥的策略,因为那时候你已经掌握了两份真实需求,而不是一份想象中的需求。
3. AI 辅助编程时代,代码膨胀通常从哪里来
3.1 AI 默认的“大兴土木”倾向
我观察到,很多 AI 编程工具在面对一个需求时,倾向于生成一个完整的小型架构,而不是做一个最小改动。原因并不难理解:AI 训练的语料里,完整项目、OSS 代码、企业级 Demo 占了很大比例,这些代码通常有包管理器、配置中心、接口抽象层。
于是当你让 AI“实现一个文件导入功能”时,它给出的结果可能包括:
- 一个
FileImportService类; - 一个
FileParser接口; - 一个
ImportResult数据类; - 一段事务管理;
- 一个异步任务调度的钩子。
但你的项目可能只需要一个业务 Service 里的私有方法。真正需要警惕的是这种代码极容易通过 Code Review,因为它“结构完整”“很像企业级写法”,唯一的问题是它贡献了过多没人负责的维护面。
3.2 低信息密度代码的主要来源
在 AI 辅助编程中,代码膨胀的常见来源通常有几类:第一类是样板式封装,它把简单计算逻辑包装进多重类结构,主要目的是看起来“优雅”,但业务价值为零;第二类是假复用,AI 在项目中发现了某个通用函数,但返回类型或者异常处理方式不匹配,于是它不在原有函数上扩展,而是新建了一个参数字段更复杂的变体;第三类是防御式代码,AI 会用大量try-except包裹住每一个可能出错的点,但异常处理只是被吞掉或打印日志,没有恢复动作,也没有给用户一个可理解的提示;第四类是过度参数化,AI 会为一个只在当前调用的逻辑添加多个默认参数,认为这能提升通用性。
这些代码有一个共同点,它们不是被“真实需求”逼出来的,而是被“代码风格惯性”带出来的。如果整个代码库存在大量这样惯性生成的代码空间,后续 AI 在生成时就会跟着这个惯性走,形成循环增强。
3.3 自我检查信号
如果你想判断自己的项目是否已经出现了 AI slop 膨胀趋势,可以从几个方面检查:
第一个信号是,新增一个简单业务功能时,提交里往往包含三个以上新增文件,但这些文件方法都很短,像是把一个函数拆成了多级目录结构。
第二个信号是,项目里出现大量只有一处引用的工具类或者 Service,而且这些类的命名很宽泛,比如CommonUtils、DataHelper、ResponseBuilder。
第三个信号是,AI 补全时频繁给出“新建函数”的候选。如果连 IDE 补全都觉得项目里没有合适函数可复用,通常说明项目里并不是缺少代码,而是隐藏了太多结构相似的碎片。
第四个信号是,升级依赖或修改接口时,波及面异常大。原本一个内部实现变化,却因为很多模块依赖了某个宽泛工具类,导致大量改动。这种耦合本身就是代码量膨胀带来的代价。
4. 实战对照:同一需求的 AI 生成与精简写法
4.1 场景与运行环境
为了把上面的概念落到实处,下面用一个非常常见的实际场景来对比:读取销售 CSV 文件,按地区汇总销售额,最后打印统计结果。数据中部分行可能地区为空或金额非数字,需要跳过。示例基于 Python 3.10+ 编写,不依赖任何第三方库。文件结构如下:
examples/sales_report/ ├── sales.csv ├── redundant_version.py └── minimal_version.py测试数据sales.csv内容如下:
region,amount,date 华东,1200.50,2025-03-01 华北,800.00,2025-03-01 华东,600.00,2025-03-02 ,900.00,2025-03-03 华南,400.00,2025-03-04注意第四行的地区为空,这条记录要被过滤掉。预期输出结果应该是:
华东: 1800.50 华北: 800.00 华南: 400.004.2 第一种写法:AI 常见的“小工程”版实现
下面这个版本参考了很多 AI 编程工具默认的“大兴土木”倾向。它有数据类、有行解析器、有文件加载器、有统计聚合器、还有报表打印类。写法上并不算错误,甚至可以说“层次清晰”,但它为一个只需要十几行的功能添加了大量间接跳转。
# 文件路径:examples/sales_report/redundant_version.py import csv from collections import defaultdict from dataclasses import dataclass from typing import Dict, List @dataclass class SalesRecord: """单条销售记录。""" region: str amount: float date: str class SalesRowParser: """把 CSV 的一行解析成销售记录。""" def parse(self, row: Dict[str, str]) -> SalesRecord: return SalesRecord( region=row["region"].strip(), amount=float(row["amount"]), date=row["date"].strip(), ) class SalesFileLoader: """读取销售 CSV 文件,返回合法记录。""" def __init__(self, parser: SalesRowParser): self.parser = parser def load_records(self, file_path: str) -> List[SalesRecord]: records: List[SalesRecord] = [] with open(file_path, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: if not row.get("region") or not row.get("amount"): continue try: records.append(self.parser.parse(row)) except ValueError: continue return records class SalesAggregator: """按地区汇总销售额。""" def aggregate(self, records: List[SalesRecord]) -> Dict[str, float]: total_by_region: Dict[str, float] = defaultdict(float) for record in records: total_by_region[record.region] += record.amount return dict(total_by_region) class SalesReportPrinter: """打印最终汇总结果。""" def print_report(self, total_by_region: Dict[str, float]) -> None: for region, total in sorted(total_by_region.items()): print(f"{region}: {total:.2f}") def main(file_path: str) -> None: parser = SalesRowParser() loader = SalesFileLoader(parser) aggregator = SalesAggregator() printer = SalesReportPrinter() records = loader.load_records(file_path) total_by_region = aggregator.aggregate(records) printer.print_report(total_by_region) if __name__ == "__main__": main("sales.csv")这段代码大约 80 行。它有清晰的职责分离,但每个类只有零到两个方法,并且这些方法至今只有一个调用点。如果你继续扩展需求,比如增加“按日期过滤”,AI 大概率会在SalesAggregator或SalesFileLoader里新增一个方法,而不是重构。类与类之间的依赖网络会越铺越大,最终每个方法都变成“薄薄一层皮”,面向对象的设计沦为了代码分散的工具。
4.3 第二种写法:用最小必要代码表达规则
下面是同一个功能的精简实现。它没有抛弃标准库,而是直接使用csv.DictReader和defaultdict,把“读取、过滤、解析、汇总、输出”放在同一段主流程里。
# 文件路径:examples/sales_report/minimal_version.py import csv from collections import defaultdict def summarize_sales(file_path: str) -> None: total_by_region: dict[str, float] = defaultdict(float) with open(file_path, "r", encoding="utf-8") as f: for row in csv.DictReader(f): region = (row.get("region") or "").strip() amount_text = row.get("amount") or "" if not region or not amount_text: continue try: total_by_region[region] += float(amount_text) except ValueError: continue for region, total in sorted(total_by_region.items()): print(f"{region}: {total:.2f}") if __name__ == "__main__": summarize_sales("sales.csv")这个版本只有 23 行,功能与第一个版本完全等价。它的可读性并没有变差,反而变得更直接:你一眼就能看出它在遍历每一行、跳过空地区或空金额、把金额累加到对应地区。没有类之间的跳转,也没有为了“未来可能复用”而存在的空壳。
如果想进一步缩短到几行链式调用,其实并不难,但我不推荐。过度的“一行流”会损失错误处理和可读性。精简的边界应该是:代码没有多余的抽象,但保留了必要的分支语义。
4.4 两种写法应该如何选择
| 对比维度 | 第一种“小工程版” | 第二种“精简版” |
|---|---|---|
| 代码行数 | 约 80 行 | 约 23 行 |
| 业务复杂度 | 完全等价 | 完全等价 |
| 新增字段的改动范围 | 数据类、解析类、测试辅助均可能改动 | 只改主流程内部 |
| AI 后续补全时的选择 | 有多个类可供模仿,也容易生成新变体 | 只能在已存在的函数上做增量 |
| 维护成本 | 需要理解四段调度关系 | 只需要理解一个函数 |
| 面对未来扩展 | 初期看着方便,但每个新需求都可能新增类文件 | 等真实需求出现后再决定是否提取抽象 |
这个选择很容易被人误解为“不鼓励设计”。这个版本并不是说所有需求都应该写成一个函数。它想强调的是:当一个需求昨天刚出现,今天还只有一个调用场景时,强行引入面向对象的层次结构并不是在做设计,而是在预支复杂度。
4.5 什么情况下应该保留“多一点”的代码
在以下场景,第一个版本的拆分是有价值的:
- 同一份销售数据的解析结果会被多个独立功能消费,比如入库、生成报表、计算佣金;
SalesRecord在系统多个模块间传递,而不是只在一个脚本里使用;- 不同文件的解析规则差异很大,例如存在不同 CSV 模板,且每个模板的字段映射需要独立测试;
- 团队有明确的分层规范,比如强制要求 Controller、Service、Repository 分层,并希望 AI 生成代码遵循同样的结构。
这意味着“写更少的代码”不等于“消灭一切抽象”。正确的判断标准是代码是否支撑了真实存在的关系和规则。如果规则足够复杂,留少量结构并不是 slop,而是避免未来的函数变成只能靠if-else堆砌的混沌体。
5. 把“写更少的代码”落实到 AI 编程工作流
5.1 Prompt 阶段就要建立约束
很多开发者使用 AI 编程工具的方式,是把整个需求一句话抛给 AI。这很容易让 AI 输出大而全的工程样板。改进的办法是在 Prompt 里给出边界,要求它在已有抽象里工作。
一个典型的不良 Prompt 是:
帮我写一个用户 CSV 导入功能。这个 Prompt 缺少项目约束,AI 基本上会从“新建UserImportService、新建ImportResult、设计一个模板方法”开始。改进后的 Prompt 可以是:
在现有 users 模块中实现 CSV 用户导入。参考 user_service.py 里已有方法的事务/错误处理风格。只读取指定列,跳过手机号为空的行。不要新增通用文件解析工具类,使用 Python csv 标准库即可。差异很明显:后者限制了实现位置、约定了错误处理风格、明确了过滤规则,还堵住了“新增通用工具类”的扩展口。AI 生成结果里再出现大规模无关抽象的概率会低很多。
5.2 让 AI 在已有抽象边界内工作
写更少的代码,并不是不让 AI 写代码。关键是在代码仓库中建立明确的“抽象边界”,并告诉 AI 哪些边界外不允许自由发挥。
比如项目中已经存在audit_logger.py,所有业务操作都应该调用它来记录日志。但 AI 不知道这个约定,它更可能直接在业务代码里自己写一套日志逻辑。这时可以在任务描述中写:
记录操作日志使用 audit_logger.py 中提供的方法,不要直接 print 或 self.log。这样,AI 生成的新代码就不会继续堆加日志实现。这种边界约束比“请写出高质量代码”这种空泛要求有用得多。
5.3 用配置文件固化“做减法”规则
很多 AI 编程 IDE 支持通过项目配置文件注入长期规则,例如.cursorrules或AGENTS.md。不同工具读取方式有差异,所以关键是思路一致。下面是一份可以放入项目根目录的配置示例,重点不是格式,而是规则本身:
# 编码约束 - 优先复用 src/common 下已有函数;不要为单次调用新增工具函数。 - 新增公共函数前,需要先考虑能否扩展现有模块,而不是新建同名功能文件。 - 保持函数只有一个明确职责,不要为了“未来扩展”增加未使用参数。 - 标准库能实现的逻辑,优先使用标准库;引入第三方依赖前需要增加理由说明。 - 生成代码应贴合项目中最短的同类实现风格,避免重复性样板。 - 错误处理要暴露真实错误信息,不要只写 pass 或注释占位。这份配置的价值在于,它是一种“反向 Prompt”。当 AI 准备为某个简单场景增加传统架构时,它可以先读到项目里存在“禁止为了扩展而创造空泛抽象”的约束。长期执行后,代码库会明显比不加约束时更精简。
5.4 代码评审聚焦“新增代码的正当性”
在 AI 生成代码以极低边际成本增加的背景下,代码评审不能只停留在“这段代码是否正确”,要再增加一个核心问题:
“这次变更里的每一行代码,是否都有真实的当前业务支撑?”
评审模板里可以加上这部分内容:
- 这个改动是否新增了文件、依赖、接口或抽象?
- 如果是,新增它们要解决的具体问题是什么?
- 项目里是否已经存在风格类似的实现?
- 能否通过修改已有代码而不是新增一个变体来完成需求?
这种评审视角会让 AI 生成的“伪架构”无处可逃。开发者也能逐渐意识到,写代码的增值时刻发生在判断“该改哪里”“该删什么”上,而不是“再多产出几十行”。
6. 常见误区与排查清单
6.1 常见误区
“写更少的代码”这条原则在落地过程中容易跑偏,下面几个误区需要特别留意。
第一个误区是认为代码少就代表质量高。代码行数只是结果,如果为了少而写出复杂的推导式或深层链式调用,那只是把 A 类型的高熵代码转换成了 B 类型的高熵代码。判断标准不是行数,而是每个抽象、每个封装、每个依赖点,是否经得起“当前有用途”这一问。
第二个误区是所有 AI 生成代码都不该使用。AI 生成本身没有对错,它是效率工具。真正有害的是不加审查地合并。一个成熟的做法是让 AI 先产出“草案”,再由开发者用减法思维过滤掉那些不需要的抽象,而不是在阶段一就禁止 AI 写代码。
第三个误区是只对新增代码做减法,从不清理旧代码。如果旧代码库里已经存在大量重复概念,AI 仍然会源源不断地模仿旧代码风格,生成新的相似结构。因此,控制 AI slop 不只是防止新增,还要阶段性地清理历史高熵区域。
6.2 排查清单
| 检查项 | 常见信号 | 处理思路 |
|---|---|---|
| 抽象数量 | 一个简单需求引入了 2 个以上新类/接口文件 | 先合并到函数或直接写在调用方;发现第二个真实调用点后再提取 |
| 工具函数复用 | 新代码又写了一个读取文件的 parse 函数 | 全仓库搜索已有实现,优先扩展现有函数参数 |
| 注释价值 | 注释只是在逐行翻译代码 | 删除注释;用有意义函数名替代 |
| 防御深度 | try-except分支里全是空 pass 或日志输出 | 识别可恢复错误,保留真实处理逻辑,其余让异常向上抛出 |
| 依赖体积 | 一个文件读取逻辑引入了第三方库 | 检查标准库能力,避免为了“省事”增加维护负担 |
| 参数数量 | 新函数出现多个默认参数,且调用方并没有用到 | 移除未用参数;只保留真实数据流需要的参数 |
6.3 当 AI 生成的代码很短但很难懂时怎么办
还有一种情况:AI 生成了一段极其简短但难以阅读的代码,例如酷炫的列表推导式加多个条件。这种代码虽然行数少,但它制造了另一种“隐藏复杂度”,阅读者需要花很多时间才能反向推导出业务规则。
处理办法是不要拘泥于让 AI“写出最短代码”,而是要求它“用项目现有风格写出可读性最高的版本”。在 Prompt 里明确补一句:
优先可读性而不是长度。避免过度的链式调用,核心业务步骤用普通语句表达。“写更少的代码”真正减少的,永远是那些没有信息量的代码。如果一段代码减少之后,反而让读者更看不懂,那就说明它不是需要被减少的部分,而是需要被重新表达的部分。
7. 团队层面的工程实践建议
7.1 把“减少重复代码”做成定期动作
如果团队已经在使用 AI 编程工具,建议把“重复代码治理”放入常规迭代,而不是等到每次评审时临时发现。例如使用静态扫描工具检查重复率、死代码,在 CI 里对新增代码做简单统计,也可以每周抽一个模块做减法重构。
有一种简单有效的方式:在代码评审时增加“谁删了代码最多”的统计维度。很多团队只统计新增代码数,忽略删除代码的价值。实际上,一次成功的重构往往表现为“删除代码比新增代码多”。让这个行为被看见,会鼓励团队成员克制新增、积极清理。
7.2 依赖治理也需要做减法
“写更少的代码”还包括“引入更少的第三方依赖”。AI 工具为了快速满足功能,经常会建议pip install一个库,或者在 Maven/Gradle 里增加一个依赖。它提供的理由是代码可以变短,但它没有计算版本兼容、安全补丁、体积和学习成本。
在提审依赖时,可以给团队准备一个固定问题列表:
- 这个依赖解决了什么问题,标准库或项目中已有类是否完全无法覆盖?
- 它的维护状态如何,是否有足够大的社区验证?
- 如果未来移除这个依赖,是否会造成大规模重写?
- 这个依赖是否会带来传递依赖冲突?
依赖越少,AI 需要理解的生态面就越窄,生成时的试错成本也越低。
7.3 让“核心抽象”清晰可查
不是所有抽象都应该被删除。一支团队仍然需要核心抽象,比如领域模型、日志规范、认证体系中间件。真正的问题是这类核心抽象被淹没在大量临时抽象里,让人找不准边界。
更好的做法是,把“核心抽象”写成专门的架构说明文档,并且让 AI 工具在生成代码时能访问到。说明文档里明确写:
本项目的通用数据库操作统一走 UserRepository; 操作日志统一使用 logger.py 的 audit(); 不要为具体业务模块创建新的 repository 基类。这部分文档虽然增加了文字量,但它能减少的代码量远大于维护文档的成本。因为 AI 有了确定性选择后,就不会再创造新的潜在复用点。
7.4 实时统计代码增量质量
在项目发展到一定阶段,建议对每个合并请求增加“代码扩张率”概念。比如计算模块里新增的代码行,在需求完成后实际存留的比例。有些代码刚提交时看起来必要,下一轮重构时就被删除,说明之前的抽象判断其实是失败的。这类数据如果周期性复盘,能很好地帮助团队修正对代码库结构的预期。
不过要注意,指标只作为参考,不应变成强 KPI。否则团队可能又陷入“为了减少行数而减少”的形式主义。
8. 总结:AI 时代会做减法比会写加法更值钱
回到开头那条讨论:Dex Horthy 呼应 Matt Pocock 的观点时,最值得学习的地方是把“AI slop 治理”从 Prompt 技巧层拉回到系统设计层。代码是团队设计决策的沉淀,AI 只是让沉淀速度变快了。如果沉淀下来的是大量结构性噪音,那么无论你把提示词包装得多精细,AI slop 依然会反复繁殖。
反过来,当代码库持续做减法,AI 能自由发挥的空白会被压缩到边界清晰的小块里。这时候你会突然发现,自己不需要那么多复杂的提示工程,AI 生成质量就会明显上升。因为真正为质量兜底的,从来不是某个神秘模型能力,而是代码库本身自洽的结构信息。
这条经验不需要昂贵工具,也不需要大型重构。可以直接从今天开始,审视下一个 MR 里有没有只为单次调用而存在的新类或新工具函数,然后把它删掉,看看功能是否依然正常。代码库会感谢这种克制。如果你也曾在代码评审里见过那些“看起来非常努力、细看没有信息量”的新增代码,欢迎在评论区聊聊,你最后是怎么完成这次减法的。