1. 从“神助攻”到“泥球制造机”:一个资深工程师的观察
最近在开发者社区里,Matt Pocock 这个名字和他的“技能集”概念被频繁提及,连带“泥球制造机”这个比喻也火了起来。作为一名写了十几年代码、带过不少项目也踩过无数坑的老兵,我对这个话题感触颇深。所谓“泥球”,在软件工程里指的是那种结构混乱、耦合度高、难以理解和维护的代码块,就像一团湿泥巴,粘上就甩不掉,越滚越大。而“泥球制造机”,讽刺的正是那些本应帮助我们提高效率的AI编码助手,在某些场景下,反而成了批量生产这种糟糕代码的元凶。
Matt Pocock 提出的“技能集”很有意思,它不是指你会用多少种框架或语言,而更像是一种工程化的思维模式和工具箱,核心在于如何写出清晰、可维护、经得起时间考验的代码。这恰恰是当前许多AI编码工具(比如GitHub Copilot、Amazon CodeWhisperer等)的盲区。它们擅长根据上下文和注释生成代码片段,速度快得惊人,但在代码的结构设计、抽象层次、长期可维护性上,往往表现得像个“天才的实习生”——能完成任务,但可能留下一堆需要你深夜加班收拾的“惊喜”。
这篇文章,我就想结合自己这些年的实战经验,拆解一下这个现象背后的原因,并分享如何将Matt Pocock倡导的工程学思维,融入到我们与AI助手协同工作的流程中。目标读者是每一位在日常开发中已经离不开AI辅助,但又时常对生成代码的质量感到隐隐不安的工程师。我们不仅要会用AI,更要懂得如何“驾驭”它,让它从潜在的“泥球制造机”,转变为真正可靠的“脚手架搭建者”。
2. 泥球是如何被“制造”出来的:AI编码助手的局限性分析
AI编码助手生成“泥球代码”,并非出于恶意,而是其底层工作机制与软件工程最佳实践之间存在天然鸿沟。理解这一点,是避免问题的第一步。
2.1 模式匹配的胜利与设计的缺席
当前主流的AI编码助手,本质上都是基于海量开源代码训练的大型语言模型。它们的强项是模式识别和补全。当你输入“写一个函数计算斐波那契数列”时,它能瞬间给出十几种实现,因为它“见过”太多次了。然而,软件工程不仅仅是实现功能,更是关于如何组织代码。
当任务稍微复杂,比如“为电商购物车设计一个折扣计算系统”时,问题就来了。AI可能会生成一个庞大的、包含无数if-else或switch语句的函数,因为它从训练数据里学到了很多处理折扣的“模式片段”,并把它们堆砌在一起。它不会主动思考:“是否应该将‘满减’、‘折扣券’、‘会员价’这些概念抽象成不同的策略类?”“计算逻辑和优惠券验证逻辑是否应该分离?”“这个函数的单元测试该怎么写才方便?”它生成的是“曾经被这样写过的代码”的统计概率组合,而非一个经过设计的、符合单一职责和开闭原则的解决方案。
注意:这并不意味着AI无用。对于明确的、模式化的任务(如编写数据转换函数、定义API接口类型、生成样板代码),它的效率无与伦比。危险在于,开发者容易对生成的复杂代码产生“信任”,不去审视其结构,直接采用。
2.2 上下文的短视与“局部最优解”
AI助手通常只拥有有限的上下文窗口(比如当前文件、打开的标签页)。这导致它极易陷入“局部最优解”。例如,在一个已有User和Product类的项目中,你让AI“添加一个将商品加入用户收藏夹的函数”。
AI可能会在User类里直接添加一个addToFavorites(productId)方法,里面包含了数据库查询和更新逻辑。从局部看,这个函数能工作。但从工程角度看,它混淆了领域模型(User)和数据持久化逻辑,造成了紧耦合。更好的设计或许是引入一个FavoritesService,或者使用仓库模式。但AI缺乏对整个项目架构的宏观视野,它只会基于它看到的User类已有的模式,给出一个最“像”正确答案的补全。
这种短视会悄然破坏项目的架构边界,让本应清晰的层(如Controller, Service, Repository)变得模糊,这正是“泥球”开始滋生的温床——所有东西都慢慢粘到了一起。
2.3 命名的随意性与抽象的缺失
清晰的命名是良好代码的基石。Matt Pocock 就非常强调类型安全和精确的命名。然而,AI在命名上常常表现得非常“随意”。它可能生成像handleData()、processInput()、doStuff()这样的函数名,或者temp,result,data这样的变量名。
更糟糕的是,它几乎不会主动创建有价值的抽象。比如,面对一段处理不同支付状态(PENDING,SUCCESS,FAILED,REFUNDED)的代码,AI生成的可能是一串字符串常量和if (status === "PENDING")的判断。而有经验的工程师会第一时间想到:“这应该是一个枚举(Enum)或字面量联合类型。”抽象能力的缺失,使得代码停留在“操作数据”的层面,而非“表达概念”的层面,直接降低了代码的表意能力和可维护性。
3. Matt Pocock “技能集”的工程学内核:对抗泥球的武器
那么,如何对抗这种趋势?我们需要一套明确的工程学“技能集”作为审查和引导AI的标尺。Matt Pocock 的理念可以提炼为几个核心原则,这些原则应当成为我们大脑中的“代码审查插件”。
3.1 原则一:类型即文档,追求编译时安全
尤其是在TypeScript这类类型系统中,类型是最高效的文档。AI经常生成any类型或过于宽泛的类型。你的技能之一,就是将AI生成的“能跑”的代码,升级为“类型安全”的代码。
- 实操示例:AI生成了一个函数
function getPrice(item) { return item.price * item.quantity; }。 - 技能集应用:立即追问并定义类型。
更进一步,如果业务逻辑中价格不应为负数,可以考虑使用interface LineItem { price: number; quantity: number; // 可能还有折扣码、税率等,未来扩展时类型会提示你 } function getPrice(item: LineItem): number { return item.price * item.quantity; }branded type或PositiveNumber这样的概念(通过类型体操实现),将业务规则编码进类型系统。让AI生成实现,你来赋予它“契约”和“意义”。
3.2 原则二:单一职责与清晰的边界
这是对抗“泥球”最有力的武器。每当AI生成一个函数或类,立刻用以下问题审视它:
- 这个模块只做一件事吗?
- 它是否混杂了业务逻辑、数据访问和外部API调用?
- 如果需求变更,修改点是否集中?
- 避坑技巧:对于AI生成的超过50行、或者出现了明显“段落感”(比如一段处理数据,一段调用API,一段更新数据库)的函数,必须进行拆分。一个实用的方法是:为AI生成的复杂函数添加清晰的“步骤注释”,然后要求AI将每个步骤提取成独立的、命名良好的小函数。这样,你是在用高级设计思维,指挥AI完成低级重构工作。
3.3 原则三:可测试性作为设计导向
难以测试的代码,通常也是设计糟糕的代码。AI几乎不会考虑测试。你的技能在于,在认可一段AI生成的代码之前,先在心里为它构思单元测试。
- 实操心得:如果发现一个函数依赖全局状态、紧密耦合了外部服务(如数据库、HTTP客户端),导致无法轻松地注入Mock进行测试,那么这段代码就需要重构。这时,你可以直接对AI下指令:“将这段逻辑中的数据库查询部分抽离,使其可以通过参数注入。” 引导AI向依赖注入、端口与适配器等模式靠拢。可测试性是一个强大的设计约束力,能自动帮你过滤掉许多糟糕的结构。
3.4 原则四:防御性编程与错误处理
AI生成的代码往往只有“快乐路径”。它默认输入正确、网络通畅、资源存在。而工程代码必须处理边界和异常。
- 常见问题:AI生成
const user = await db.getUser(id); user.name = newName; await db.saveUser(user); - 技能集补全:你需要手动或引导AI补充:
将AI视为“草稿生成器”,而你是负责完善鲁棒性和安全性的“终审工程师”。const user = await db.getUser(id); if (!user) { throw new NotFoundError(`User with id ${id} not found`); } // 可能还需要验证 newName 的合法性 if (isValidName(newName)) { user.name = newName; await db.saveUser(user); } else { throw new ValidationError('Invalid name'); }
4. 实战工作流:将工程思维注入AI协作全流程
掌握了原则,我们需要一个可操作的工作流,让这些原则在每天与AI的互动中落地。
4.1 阶段一:精准提示——提出“好问题”
低质量的提示得到低质量的代码。要像对待一位资历尚浅但学习能力极强的同事一样给它布置任务。
反面例子:“写一个登录函数。”
正面例子(融入技能集): “请用TypeScript编写一个用户登录函数。要求:
- 输入是
{ email: string, password: string }。 - 输出是一个Promise,解析为
{ user: UserProfile, token: string }或抛出自定义错误。 - 需要验证邮箱格式和密码非空,验证失败抛出
ValidationError。 - 依赖一个
authService对象(可通过参数注入),该对象有validateCredentials和generateToken方法。 - 将密码比对和令牌生成等具体逻辑委托给
authService,本函数只负责流程编排。 请先给出函数类型签名,再实现。”
这样的提示,实际上已经勾勒出了函数的职责边界、接口契约和依赖关系,极大地限制了AI生成“泥球”的可能性。
- 输入是
4.2 阶段二:批判性审查——像审查新人代码一样
绝对不要直接接受AI生成的代码。建立一套快速的审查清单:
- 类型检查:是否有
any?类型是否足够精确? - 职责审视:函数/类是否做了多件事?能否用一个简单的句子描述它的功能?
- 依赖评估:是否引入了不必要的紧耦合?外部依赖是否可注入?
- 错误处理:“快乐路径”之外的情况考虑了吗?
- 命名品味:变量、函数名是否清晰表意?能否一眼看懂?
4.3 阶段三:引导式重构——让AI成为你的副驾
当审查发现问题时,不要自己埋头重写,而是继续引导AI。
- 操作示例:你发现AI生成的
OrderProcessor类既计算税费,又发送邮件。 - 你可以对AI说:“将
OrderProcessor类中的‘计算税费’逻辑提取到一个独立的TaxCalculator类中,并通过构造函数注入到OrderProcessor。OrderProcessor只保留协调流程的职责。” - 进阶技巧:对于复杂的重构,可以分步进行。先让AI提取接口,再让AI实现新类,最后让AI修改原类使用依赖注入。你把控设计和方向,AI负责执行和填充细节。
4.4 阶段四:持续学习与模式投喂
AI是可以“培养”的。当你通过反复提示和审查,得到了一段非常符合你项目标准和设计模式的优美代码时,将它加入到你的代码库中。未来,当AI看到更多你项目中的“好代码”模式时,它生成类似高质量代码的概率就会增加。这相当于在为你项目的“代码DNA”进行定向优化。
5. 常见陷阱与高阶应对策略
在实际操作中,还有一些更隐蔽的陷阱和需要高阶技巧的场景。
5.1 陷阱一:对“魔法值”和配置的漠视
AI经常把数字、字符串常量直接硬编码在逻辑中。你的技能是敏锐地识别出这些“魔法值”,并将其提取为有意义的常量或配置。
- AI生成:
if (user.role === 'admin') { ... } - 你应修改:
这不仅提高了可读性,也使得集中修改成为可能,避免了在代码中散落寻找的“泥球”式维护。const UserRoles = { ADMIN: 'admin', USER: 'user', EDITOR: 'editor', } as const; // 使用 as const 获得字面量类型 if (user.role === UserRoles.ADMIN) { ... }
5.2 陷阱二:过度优化与过早抽象
有时AI会卖弄“聪明”,生成一些过于复杂、使用了设计模式但场景并不匹配的代码。例如,为一个简单的、永远不会变化的策略,生成一个完整的策略模式工厂。
- 应对策略:遵循“你不需要它”原则。如果当前需求简单明了,直接拒绝AI的过度设计,要求一个“最简单、最直接的实现”。记住,AI没有对项目阶段和复杂度的判断力,而你有。保持代码的简洁性,直到变化真正发生时再进行抽象,这本身就是一项重要的工程决策能力。
5.3 陷阱三:对第三方库的盲目引入
AI可能会在代码片段中引入它“认为”合适的第三方库,但并未考虑你项目的依赖管理策略、包大小或许可证问题。
- 实操要点:建立项目“许可库”清单。对于AI建议的新库,必须手动审查:其维护状况、包大小、许可证是否兼容、是否与现有技术栈有冲突。永远不要将依赖管理的决策权交给AI。
5.4 高阶策略:创建项目专属的“提示词手册”
对于大型或长期项目,可以总结出一套本项目的“AI协作规范”,作为团队共享的提示词手册。例如:
- “在本项目中,所有数据访问必须通过
Repository接口。” - “错误处理统一使用
Result<T, E>模式。” - “API响应格式遵循
ApiResponse<T>包装器。” 在给AI提需求时,开头就加上“请遵循本项目规范:...”,能极大提升生成代码的可用性,减少重构成本。
6. 工具链整合:让审查自动化
人的注意力是有限的,我们可以借助工具,将部分审查工作自动化,让工程师更专注于高层次的设计决策。
6.1 静态分析工具作为第一道防线
在AI生成代码被保存甚至提交前,必须通过严格的静态检查。
- TypeScript/ESLint:配置严格的规则(如
no-explicit-any,@typescript-eslint/explicit-function-return-type),让类型不安全和不良模式无处遁形。 - Prettier:统一代码格式,避免在风格问题上浪费时间。
- SonarQube / CodeQL:集成到CI/CD中,检测代码异味、安全漏洞和重复代码。
一个高效的流程是:AI生成代码 → 手动进行初步设计和边界审查 → 运行格式化工具 → 运行Lint检查并修复错误 → 运行单元测试(如果已编写)。将工具作为你技能集的延伸和强制执行者。
6.2 利用AI进行“元审查”
一个有趣的策略是,用另一个AI(或同一AI的不同会话)来审查AI生成的代码。你可以提示它:“请从代码可维护性、单一职责原则和测试友好性的角度,评审以下代码片段,指出问题并提出改进建议。” 这有时能提供你未曾想到的视角,但最终判断必须由你——掌握工程上下文的人——来做出。
6.3 版本控制的智慧:将AI生成视为“草稿提交”
在Git工作流中,可以考虑将AI生成的大段代码先提交到一个特性分支,并打上[AI-DRAFT]这样的标签。然后,在这个分支上,基于你的工程技能集进行重构、拆分、添加测试和类型定义。最后,将一个干净、经过工程化处理的版本合并到主分支。这清晰地分离了“生成”和“设计”两个阶段,保留了历史记录,也符合规范的开发流程。
说到底,AI编码助手是史上功能最强大的“代码自动补全”。但它补全的,是字符、是语法、是常见的模式片段。而软件工程,是关于设计、关于抽象、关于长期维护中人与人的协作。Matt Pocock 所强调的“技能集”,正是后者。我们不能因为前者效率的炫目,而荒废了后者更根本的能力。
我的体会是,最好的状态不是“让AI写代码,我来审核”,而是“我来做设计、定边界、想测试,让AI去填充那些我明确知道该怎么做、只是懒得敲键盘的细节”。把AI当成一个理解力超强、但缺乏经验和全局观的新人搭档,你的角色是架构师和导师。当你用清晰的工程思维去引导它时,它就不再是“泥球制造机”,而会成为你手中将想法迅速转化为健壮、清晰代码的利器。这个过程本身,也是对你自己设计能力和代码品味的一次次锤炼和确认。