ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

减少AI Slop:写更少的代码如何提升生成式编程的可维护性

减少AI Slop:写更少的代码如何提升生成式编程的可维护性 AI 代码生成走进日常研发后很多团队的焦虑从“代码写不完”变成了“代码多到看不完”。开发者社区里流传着一句很扎心的回应减少 AI slop 的办法是写更少的代码。这句话看字面像是让开发者少写功能、少交付实际上指的是另一回事。它真正想说的是当生成式 AI 把大量“看起来正常”的代码源源不断送进仓库时代码体积本身就变成了一种风险。代码越多可以被混淆、被复用、被滥用的模型输出空间就越大AI 生成的“垃圾内容”就越难被识别和清理。这里的 AI slop 并不是指 AI 生成的所有代码而是指那些结构完整、语义上却缺少必要约束的代码接口可以被填成几乎任意形状模块可以承担任何职责业务逻辑和传输层参数互相复制配置越来越长却没有人能说清哪些配置在生产环境真的生效。这篇文章围绕“写更少的代码”这条主线展开讨论它为什么能降低生成式代码的劣化速度以及在实际项目中可以怎样落地而不是停留在口号层面。1. AI slop 到底指的是什么代码问题1.1 slop 不能等同于“AI 生成的代码”AI 生成代码本身没有原罪。很多团队已经把补全、单测生成、重构建议嵌入日常开发关键看生成结果是否进入主分支后仍然能被维护。所谓 AI slop更准确的说法是一类“缺少真实约束”的代码。比如你有十几个形状相似的接口每个接口都可以接收 undefined比如一个简单配置读取过程被包装成 Config Schema、Config Loader、Config Repository 三层比如 CRUD 接口明明业务上只需要查询生成的代码却先把创建、更新、删除全部铺开。这些代码并不是完全不工作而是它给出太多选择让后续的每个调用方都有机会产生错误行为。换句话说AI slop 的现象是数量带来的失效代码在局部看起来合理在整体上却构成一种很难推理、很难测试、很难删除的胶状结构。这也是为什么有人会把减少 slop 和写更少的代码联系起来。代码库不是一个文本集合它是一个给开发者、给测试框架、给 AI 工具共享的决策上下文。上下文越拥挤真正有效的工程信号就越弱。1.2 代码仓库里的 slop 通常长什么样AI slop 不一定出现在大型开源项目里更多是在中等规模业务项目中悄然积累。常见的形态可以从下面几个角度判断特征表现维护代价全可选字段类型定义里大量字段都可以为空调用方无法从类型层面判断哪条数据是完整的每个调用点都需要空值处理生成代码会不断复制这些判空逻辑多层薄包装同类逻辑被路由、服务、仓储、模型转换逐层包住每层只做简单转发改动一份业务决策往往要修改多个层漏一层就会出现不一致过度配置一个功能有大量环境变量、开关和回退策略但没有谁确认哪些是真正必要的测试环境的组合爆炸排查问题时需要同时猜测多个开关状态模板式 CRUD只是一个列表页却生成了列表、详情、创建、编辑、删除、批量操作整套接口和页面未使用接口占据文档和模型定义生成器在下一次补全时继续引用这些模型深层可选链业务数据路径上大量使用?.和空数组兜底出错时信息被吞掉日志里只剩下 undefined无法定位来源这些特征经常一起出现。它们被识别为 AI slop 不是因为包含的代码量多而是因为代码中的每个点都没有形成“必须这样写”的约束。类型可以是这种形状也可以是另一种形状这个函数可以被这个模块调用也可以被另一个模块调用配置可以在这里读取也可以由外部注入。当决策空间没有被收窄时代码生成器每次补全都只是一次掷骰子。2. 为什么更少的代码能够打击 AI slop2.1 更多代码为概率补全提供了更多输出空间现代大语言模型本质上是在给定前文上下文后预测下一段最可能出现的文本。它的“最可能”来自大量公开代码训练出的统计结果而不是对当前业务约束的理解。这带来一个直接后果当你把一个复杂代码文件的全部内容作为上下文传给 AI 时生成的代码会非常像那段上下文。如果上下文中充满了可选字段、Optional 类型、双保险空值判断和各种配置回退AI 就会继续在同样的风格线上生成。它并不意识到这些防御式写法已经让代码难以维护只会认为“这是这段代码的风格我应该延续”。这就是减少 AI slop 的第一个原理减少代码输入空间就是减少概率补全可能滑入坏路径的空间。文件越短、上下文越干净模型可以模仿的错误模式就越少。2.2 接口收窄生成路径必然减少一个函数或类型被暴露到系统里以后它就变成公共契约。AI 看到这个类型定义时会把它当作可用素材。定义中每多一个可选字段AI 在生成业务逻辑时就多一个分支需要背书。举个例子下面这种生成出来的用户类型很常见type ApiUser { id?: string; email?: string; name?: string; avatar?: string; locale?: string; lastLoginAt?: string; role?: admin | operator | viewer; };这个类型在语法上完全正确甚至很适合直接对接接口返回的 JSON。但它把大量判断交给了调用方。每个页面组件里都可能出现user?.name ?? unknown每个模块都可能因为role可能为空而构造自己的默认角色。如果业务事实上要求用户必须存在 id、email 和 name那么类型应该写成type ApiUser { id: string; email: string; name: string; avatar?: string; locale: string; role: admin | operator | viewer; };这样写表面上是少了一个?实际上是减少了一条可能出错的代码路径。AI 在补全函数参数时不能再用“用户可能不存在”来写一堆空值兜底调用方必须提供完整数据。等到接口真的出现异常数据时问题会在边界暴露出来而不是扩散到所有消费方。2.3 真实目标不是无限压缩行数而是削减无差异的复杂度需要替“写更少的代码”设置一个边界削减目标应当是那些“没有信息价值”的复杂度而不是对业务现象的必要描述。如果一个复杂故障处理过程本身必须具备多阶段回退逻辑强行把代码压成一行只会增加理解成本。如果一个模块需要向外部调用方展示真实业务含义那么清晰的类型、几行必要的防御式判断都应该保留。所谓更少代码真正少的是重复表达、模糊分支、从未生效的抽象层。理想状态下的微型实现具有两个特点第一任何逻辑在代码库中只有一处主要入口第二任何类型的可选性都只出现在确实可能为空的边界。这两点共同工作让 AI 生成代码时的“选择自由度”下降。模型不需要去猜测你要用哪一个 service、哪一个 mapper、哪一个 fallback它只能面对最小且明确的一组符号。2.4 代码更少整个仓库的结构也会变得更适合 AI 协作很多团队讨论提示词工程时都希望找到一种万能提示让模型输出完美方案。实际上提示质量不只是自然语言写得好不好还包括代码库本身是否具备足够的约束力。在一个接口明确、类型收紧、导出符号极少的仓库里AI 可以更加容易定位“边界在哪里”。模型看到的数据结构是干净的它就不需要额外生成一段代码去猜测字段含义模型看到的导出数量少它就只能使用真正公开的 API。可以说代码库本身就是给 AI 的最大提示词。这也回应了为什么这个话题往往出现在生成式编程的讨论里。过去人工编写代码时代码多少只是维护成本问题当 AI 加入开发后仓库中历史代码会不断影响下一次生成的统计分布历史质量会变成生成质量的乘数。如果源仓库已经充满 slopAI 随后生成的新代码也会继续携带同一种劣质风格。3. 工作中如何真正执行“更少的代码”3.1 先减少类型字段里的“假自由”类型里要小心自由度。自由度意味着生成时可以自由扩展使用时却要增加异常分支。很多 AI 生成结果的共同问题是“把所有字段都变成可选”因为它无法确认接口一定返回哪个字段。可选字段本身没有错错误的是让可选性蔓延到所有消费函数中。一种可行操作是在定义输入类型后立刻问一句“如果这个字段不存在我的函数应该做什么”如果答案是“抛出明确错误”或“不处理”那么这个可选性应该在接口边界处理掉而不是写进业务类型。// 不好的生成结果业务函数不关心用户是否为空 function renderUserName(user?: ApiUser) { return user?.name ?? 访客; } // 收窄类型后调用方必须传入真实用户 function renderUserName(user: ApiUser) { return user.name; }这里可能有人说界面某些场景确实没有用户怎么办。那应该在展示层处理访客状态而不是让底层函数接受半真半假的数据。移除了参数的可选性以后所有调用点都必须明确这个函数只在已登录上下文中使用。整个系统的错误位置更集中而不是散落在每个小函数里。3.2 只交付当前真实需要的入口另一个非常突出的 slop 来源是模板式功能膨胀。业务需求是“管理员可以查询某个用户的基本信息”但 AI 根据类似项目的经验常常直接生成一个具备增删改查全部能力的 User API。于是路由多出三四个方法前端多出三四个页面模型多出大量永远不会变化的字段。此时“更少的代码”意味着尽可能缩小本次交付的边界。如果一个需求只需要查询代码清单就应该只包含查询逻辑。路由层可以先这样写router.get(/admin/users/:id, getAdminUserById);而不是一上来就铺开router.get(/admin/users, listAllUsers); router.get(/admin/users/:id, getAdminUserById); router.post(/admin/users, createAdminUser); router.patch(/admin/users/:id, updateAdminUser); router.delete(/admin/users/:id, deleteAdminUser);很多团队觉得多写几个 REST 入口没有关系以后迟早会用到。但这里的成本并不只是文件行数而是从数据库权限模型到前端按钮权限从接口文档到自动化测试整套逻辑都围绕并不存在的业务展开。更严重的后果是下一次 AI 在生成其他功能时会把已经存在的 create、update、delete 当作可用能力继续生成围绕它们的调用代码。3.3 让 AI 的任务指令是“收缩”而不是“新增”想真正和生成式 AI 协作可以在任务描述中增加明确的“最小改动”约束而不要只提结果目标。对比以下两种提示方式。第一种“请帮我写一个用户配置中心模块记录用户的所有偏好设置。”第二种“现在系统的用户资料已经存在但缺少一个读取用户配置的入口。请新增一个getUserConfig(userId)函数只负责返回该用户已经保存的配置对象。不要新增数据库表不要修改现有类型不要在未使用的文件里增加额外导出。”第二种描述可能看起来不够“智能”但它给我们的代码库带来的结果通常更可控。因为它限制了模型新增内容的区域也就是限制了以后需要维护和迁移的范围。在实际代码评审中也可以让生成代码保持一个原则如果一个函数或类型在提交说明里无法对应到一个真实业务需求就应该在合并前删除哪怕它写得很好。3.4 不要在业务代码里重复搭建基础设施由于模型训练数据中包含大量现成架构AI 特别倾向于把简单功能包装成完整服务。例如读取一个环境变量它会生成配置校验、类型定义、Provider、依赖注入等一连串结构。如果项目当前规模只有一个小服务这种包装不会让生产环境更安全反而会让启动链路变长。更好的做法是把真正需要变动的配置收敛到一个文件中让系统其余部分直接引用export const serviceConfig { apiBaseUrl: process.env.AUTH_SERVICE_URL ?? http://localhost:8080, timeoutMs: Number(process.env.AUTH_SERVICE_TIMEOUT_MS ?? 5000), } as const;这只是返回一个常量对象没有额外机制。它把“配置从哪里来”和“配置如何使用”放在一起描述。当配置项增加时评审者能清晰看到增长。如果希望做生产级校验可以加上一段幂等检查而不是用整个校验框架去管理两个环境变量。3.5 注意“更少”不能滑向混淆代码写更少代码不是追求每行都不容易删。可读性本身也是维护成本。有时为了语义清晰多写一个命名的类型或者一段显式 if 判断反而比使用一个高度压缩的表达式更有利于后续修改。例如下面的写法很简洁但可读性不稳定return items.filter((x) x.status status).map(({ id }) id);而下面多了一行变量定义并不算多出来的复杂度反而是把过程写清楚了const enabled items.filter((item) item.status status); return enabled.map((item) item.id);因此更合适的评价指标不是代码字符数也不是纯行数而是“系统中必须理解的知识点数量”。AI 需要理解的知识点越少它可以犯错的表面就越小。4. 识别与审减已经存在的 AI slop4.1 在代码评审阶段辨认 slop很多团队做 AI 代码审查时主要关注代码能不能运行、测试能不能通过却很少检查这段代码是否应该存在。添加一段从来不会被调用的函数似乎不会造成直接故障但它会让下一次生成的上下文变复杂。可以从下面的信号判断一段新生成的代码是否属于 slop识别信号建议处理参数大量可选但函数内没有明确处理 missing 的策略与调用方确认数据边界能收窄就收窄多个接口形状相同但各自独立维护确认是否只保留一个真实业务模型配置项只有默认值从未被外部注入删除配置入口直接使用常量模块导出数量超过当前调用方需要的数量只导出真实被使用的符号其余设为模块内部新加入的包装层只是转发没有额外约束删除包装让调用方直接使用底层能力生成代码中大量重复实现已有库或框架自带的能力用标准能力替换减少手工维护逻辑没有对应需求的页面、按钮、接口、表字段暂时不实现需要时再按真实需求加入这份清单可以在代码评审开始前贴到 PR 描述里让评审者把注意力从“能不能运行”提升到“该不该并入主分支”。4.2 现有代码里的历史包袱如何清理清理已有的 AI slop 不建议采用“目录级重写”这种高风险方式。一次性推到主分支的巨型重构很难被验证生成过程中还可能引入新的隐藏问题。推荐按一条链路来收窄先找到某个被很多模块共同引用的基础设施类型例如 User、Order、Config。修改类型定义把业务上必须存在的字段变为必选观察哪些调用点报错。通过编译错误把所有可能为空的调用点都暴露出来。逐个确认业务路径这个位置真的需要处理空值还是因为接口漏给了数据清理过程中不新增任何接口和抽象层只让现有逻辑在更窄的类型下编译通过。持续维护这个边界不让新代码重新引入可选链和全可选类型。使用支持严格类型检查的语言时这个流程会很有效。如果没有静态类型系统也可以通过全局搜索类似“function 名称随意加一系列?.快速定位常见空值扩散点。4.3 无法确定是否该删除时使用最小验证法有些代码不像是完全无用只是团队暂时没人知道它由哪个真实流量触发。如果把所有可疑代码一次性删除可能会让隐藏功能直接失效。最小验证法是这样操作提交代码时不需要先删除而是把调用它的路径排除在常规入口之外同时保留日志观察一段时间。如果没有任何跑批任务或用户请求触发该路径下一轮迭代再删除。在代码评审阶段这种验证方式比“我觉得没人用”更有说服力。4.4 处理常见的反对意见推进代码精简会遇到阻力。最常见的反对说法有以下几种反对意见合理回应这段代码以后可能会用到版本控制系统可以找回不需要为未来写未验证的业务逻辑多抽一层是为了将来扩展真正的扩展应该发生在重构时而不是在预判需求时AI 生成成本很低多写无所谓代码生成成本低但评估、测试和删除成本不会因此降低全场景考虑防御是更安全对不存在场景的防御会让真正可能出错的场景更难被发现测试覆盖已经足够没必要删测试覆盖高不等于测试目标一致删除虚假能力后才能看清真正需要保护的逻辑这些回应的核心都是同一个观点源代码需要降格为一种“可以有人维护、可以随着业务演进、可以让 AI 谨慎参与修改”的长期资产而不是一次性草稿。5. 把“更少的代码”变成团队机制而不仅是个人风格5.1 在 PR 评审时引入“删除视角”开发一个新功能时自然习惯是添加代码。但评审时可以参考一种反向方法询问这个改动是否可以不增加文件、不增加依赖、不增加导出符号就完成。如果把“新增文件”看作例外而不是常态团队会更快发现为了配合 AI 生成结构而额外建立的层。很多情况下一个 30 行的小函数可以直接放在调用模块旁边没有必要新增 utility 目录、service 目录和测试目录。一条可以写入 PR 模板的规则是改动是否涉及新增外部依赖如果新增请列出必须新增的理由。改动是否新增了公共导出如果新增请列出第一个调用方。改动中是否同时包含新增和删除如果只增不删能否顺手删除旧逻辑中已经失去作用的部分。这些规则并不复杂它们的作用是把“更少代码”的思考嵌入到每天的工作流中。5.2 用静态约束配合机制而不是只依赖自觉如果项目使用 TypeScript、Java、Go 等支持类型系统和 linter 的生态可以把部分精简原则固化为自动化检查。例如在 TypeScript 项目中对于新增的类型可以通过 ESLint 规则限制未使用导出或者通过 lint 规则要求模块顶层导出数量处于一个阈值范围内。这些指标不一定能直接判断复杂度但能把“每次新增导出都需要评审”变成显性过程。更直接的机制是 CI 代码评审阶段检查“新增加注释掉的代码”“新增未使用参数”“新增导出生效但主分支源码未调用”等常见特征。这些恰恰是 AI 生成内容的高发区域。如果团队进入自动化阶段较多还可以在提交信息里约定由 AI 生成的大段代码需要单独标记并在人工评审完成后移除标记。这不是不信任 AI而是让人工评审者的注意力集中到 AI 最容易出错的地方。5.3 学习环境与生产环境的差异在学校或个人练习阶段写更少代码很容易执行可以用一个很小的 API 服务尝试只用一个类型、一个函数和一个入口完成完整功能。当 AI 生成 300 行代码时可以要求它继续压缩到 50 行以内并且保持可读性。这种强制限制会训练开发者分辨“必要逻辑”和“样板逻辑”。生产环境则复杂一些。不能为了追求小型代码库而强行砍掉日志、监控、可观测性等基础设施相关代码。更合理的目标是环境主要目标关于代码量控制个人练习理解最小可运行示例尽量使用单文件或少量模块快速看到全貌团队开发让协作可预期控制公共 API 数量约束抽象层增长生产运行稳定、可诊断、可回滚不削减必要的日志和重试但避免重复实现基础能力外部 SDK 或开源库保持兼容性不是一味缩短接口而是用更少概念描述统一的开放方式生产环境中有一个容易误用的地方是“平台能力复用”。如果公司已经有统一网关、统一权限 SDK 和统一配置中心新服务就不应该再由 AI 生成一套本地配置加载逻辑。这也是减少代码的重要方向尽可能调用平台约定让业务代码只表达业务差异。5.4 组织一次“AI slop 清理”实践如果团队已经积累了相当数量由 AI 生成的代码可以按下面的方式组织一次清理而不是一次性大重构。第一步选取一个相对独立的业务模块。第二步统计该模块的导出符号数量、类型定义行数和测试目标。第三步创建“功能地图”把模块对外提供的每一个方法对应到实际发起调用的页面或接口。第四步找出没有对应调用方的导出并记录其可能被未来使用的概率。第五步优先删除那些既无调用方、又无明确需求文档的抽象层。第六步在删除过程中同步收紧类型字段可选性。最终结果不必追求文件数降到最低而是要让剩下的代码能够被一个中等熟悉项目的工程师完整读懂。只要团队成员能在不打开关联文件的情况下复述某个模块的边界代码量就基本处于可控状态。5.5 不适合用“更少代码”简化的情况虽然本文一直强调减少代码但也要避免另一种极端把必要的业务保护逻辑裁掉只为了让 AI 生成的代码短。有三类代码不应该为了减少行数而牺牲涉及资金、权限、数据删除和高风险操作的地方显式判断和失败回滚不能省略。可观测性数据采集点包括关键路径日志、追踪上下文、指标指标采集功能正常时它们看起来多余故障时非常关键。外部契约边界比如接口入参校验、解析错误提示这部分删了只会让问题延迟到更靠后的系统中。这类代码在压缩后可能仍然会显得繁琐但是不能把它们视为 AI slop。它们与 slop 的关键差异在于slop 是为不知道将会发生的调用写的防御这些必要逻辑是为已经确认可能发生的问题写的防护。6. 从生成时代重新理解代码量围绕生成式编程的讨论已经很多但最值得长期坚持的技术判断其实是代码量问题会在 AI 时代被放大因为历史代码不仅是“需要维护的资产”还是模型下一次生成的重要上下文。当开发者社区说“减少 AI slop 的办法是写更少的代码”时它并不是号召大家偷懒而是提醒行业重新关注结构性约束。短代码比长代码更容易证明正确窄类型比宽类型更容易被调用方理解少数入口比多数入口更容易被测试覆盖。与此同时AI 生成出来的大段代码在多数情况下也会因为没有窄接口的约束而变成问题。真正有效的工作方式不是不写代码也不是把所有代码推回给 AI 重写而是让每一段代码都具备明确的存在理由。对普通开发者来说一个可以立刻开始的练习是随便找一段最近发布过的工作代码尝试在不改变外部行为的前提下删除一半数量并且不引入新的依赖。完成这个练习后再让 AI 尝试为余下代码补全相关功能。对比两次生成结果通常会发现后一次更接近期望设计因为代码库已经没有之前那么多混乱路径可供模型延续。当生成工具越来越熟练时最稀缺的资源不再是大规模创造代码而是主动删除代码、收窄类型、收敛输入输出以及确认哪些新功能根本不值得进入主分支。能做到这些AI slop 就会从源头被压低生成式编程也才能真正服务于长期可维护的软件系统。
返回列表