ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ChatGPT、Codex工程方法:Agent发现5处重复代码,为什么不能马上抽成一个公共模块?

ChatGPT、Codex工程方法:Agent发现5处重复代码,为什么不能马上抽成一个公共模块? 最近让 ChatGPT、Codex 做旧项目重构时很容易碰到一种特别“顺眼”的建议这里有5处重复代码可以抽成一个公共模块。乍一看完全没问题。同样的校验逻辑。同样的参数转换。同样的异常处理。甚至连代码结构都几乎一样。于是Agent很快帮你抽出一个CommonValidator原来的5份代码变成1份。Diff一下少了几百行。测试也全部通过。看上去这就是一次非常漂亮的重构。但真正麻烦的事情往往不是发生在当天。而是两个月以后。订单模块说这条校验规则要放宽。支付模块说这里必须继续严格。会员模块又要求VIP用户走另一套逻辑。这时候你才发现原来那5段“重复代码”只是今天长得一样。它们未来变化的原因完全不同。而你之前为了消除重复把5个业务强行绑进了同一个公共模块。所以这类问题真正要判断的从来不是代码是不是重复。而是这些代码未来是不是会因为同一个原因一起变化。一、先给核心判断重复代码不一定代表同一个抽象很多人做重构时会天然接受一个原则Dont Repeat Yourself。重复代码不好。能抽公共逻辑就抽。但Agent特别擅长发现语法相似。结构相似。变量名相似。因为这些特征都非常明显。问题是Syntax Similarity ≠ Semantic Similarity。两段代码今天可能完全一样。但它们背后服务的是两个不同业务。未来变化方向也可能完全不同。比如订单和退款都需要判断amount 0今天逻辑完全一样。但半年后订单要求金额必须大于0。退款要求允许0金额补偿。营销活动要求负数代表冲正。这时候“同样的判断”已经不再属于同一个业务概念。所以抽象真正应该看的是语义是不是相同。而不是代码长得像不像。二、一个特别典型的现场5个Service都有同样的校验假设Codex扫描仓库以后发现OrderServicePaymentServiceRefundServiceMemberServiceCouponService里都有一段检查用户状态。检查参数。检查金额。异常时抛BusinessException。于是它建议统一抽成CommonValidator.validate()代码看起来马上干净很多。可问题来了。这5个模块对“合法”的定义真的一样吗订单可能允许游客。支付必须实名认证。退款可能允许冻结账号处理历史订单。会员模块又有自己的等级逻辑。今天它们代码一样可能只是因为当前业务规则碰巧重合。不是因为它们本来就是同一个Domain Rule。这就是最容易误判的地方。三、Agent为什么特别容易“过早抽象”因为从代码表面看重复是非常明显的Signal。比如5个文件里都有15行相似逻辑。这对Agent来说很容易识别。而“未来是不是会分化”却没有那么容易从当前代码直接看到。这需要结合业务边界。模块职责。历史变更。产品规则。团队所有权。未来演进方向。所以如果你只给Codex一个任务帮我重构重复代码。它很容易优化当前代码形态。却没有足够Evidence判断未来变化方向。这也是为什么Agent参与重构以后人更应该补上业务演进判断。四、真正应该问的是它们为什么会一起变化这是判断能不能抽公共模块最重要的问题。假设5段代码都在做手机号格式校验。如果未来规则变化时所有模块都必须同步变化。比如统一从11位手机号升级到支持国际号码。那它们大概率确实属于同一个稳定抽象。这时候抽成PhoneNumberValidator就很合理。但如果5段代码虽然都在判断用户状态。可订单、支付、退款对用户状态的要求各不相同那么它们今天相似不代表应该共享。一个非常实用的问题是未来如果这里发生变化谁会要求它变化如果答案是同一个业务规则。可以考虑抽象。如果答案是5个不同业务团队。那就要谨慎。五、错误抽象最麻烦的地方它制造Change Coupling本来5个模块互相独立。订单改订单。退款改退款。支付改支付。抽成公共模块以后情况变了。支付要改一条规则。你修改CommonValidator结果马上要问订单会不会受影响退款呢会员呢优惠券呢于是一次原本只属于支付的需求变成5个模块一起回归。这就是Change Coupling——变更耦合。代码确实少了。但变更影响面变大了。真正危险的是公共模块越来越多人依赖以后任何人都不敢轻易改。最后它会慢慢变成一个看起来“公共”实际上谁都害怕碰的模块。六、所以“减少代码行数”不是抽象成功的指标Agent重构以后经常会给出非常漂亮的结果删除420行重复代码。新增80行公共实现。测试全部通过。从Diff来看非常成功。但工程上真正应该继续看新增一个业务规则时要改几个地方公共模块改变时需要回归多少下游一个团队的需求会不会强迫其他模块同步升级如果这些成本上升了那么代码重复减少并不代表系统复杂度下降。你只是把重复成本换成了耦合成本。而后者往往更难看见。七、什么时候应该暂时保留重复代码这句话很多人不太喜欢有时候重复代码是可以接受的。但实际工程里确实如此。尤其当业务刚开始发展。规则还不稳定。多个模块只是暂时长得一样。未来方向还看不清。这时候保留两三份简单实现有时比过早抽公共模块更安全。因为重复的成本是修改时多改几处。而错误抽象的成本可能是以后每次改动都要担心所有下游。如果现在还不知道两段代码是否属于同一个长期概念我更倾向于Wait for the Pattern to Stabilize。先观察变化。不要看到两次重复就马上抽。八、我更关注“第二次变化”而不是“第一次重复”假设两个模块第一次都需要检查用户等级。这时候代码一样。还不能证明应该抽。后来规则变化了一次。如果两个模块仍然同时变化成同样逻辑说明它们背后的概念可能确实一致。再过一段时间第二次变化。仍然一起变化。这时候再抽象可信度就高很多。也就是说Stable Abstraction往往来自多次共同变化。不是来自第一次代码重复。这点特别适合Agent重构。因为Agent很容易在第一次看到重复时就出手。而人可以提醒它先查这几段代码过去半年是不是经常一起变化。历史变更其实是一种很重要的Evidence。九、Git历史比当前代码更能判断“是不是同一个概念”如果我怀疑5段重复代码是不是值得抽我会先看过去几次修改。比如订单模块修改了4次。退款模块修改了3次。但从来没有一起变化。那说明虽然今天代码相似它们的演化路径其实是独立的。反过来如果过去一年这5处每次规则变化都一起改而且原因也相同那公共抽象就更有价值。所以让ChatGPT、Codex重构之前可以先让它做一件事分析Co-change History。不是只扫描当前代码。十、公共模块还有一个问题到底谁负责假设最终真的抽出了CommonValidator接下来马上会出现一个很现实的问题谁拥有它订单团队支付团队基础架构团队还是谁都能改如果Owner不清楚公共模块很容易慢慢变成谁有需求谁就往里面加参数。最后validate(user, scene, source, mode, strict, ignoreX...)越来越复杂。因为每个业务都想让公共模块照顾自己的例外。于是原来为了“消除重复”建立的抽象最后变成所有业务差异的集中地。这种公共模块比5份重复代码更难维护。十一、一个好抽象应该让接口越来越稳定真正成熟的公共模块通常有一个特点使用方很多。但它本身变化频率越来越低。比如日期解析。统一ID格式。手机号格式。标准加密。协议序列化。这些概念边界相对稳定。相反如果一个公共模块每来一个业务需求就要加一个if。每接一个新模块就多一个参数。每次修改都要通知很多下游。那很可能说明抽象层级选错了。它不是一个真正稳定的公共概念。只是把多份业务代码强行搬到一个文件里。十二、我更建议让Agent先做“重复分类”而不是直接抽象以后看到Codex提示发现多处重复代码建议提取公共方法。不要马上Accept。可以先让它回答三个问题这些代码只是语法相似还是业务语义相同它们过去是否经常一起变化未来一个模块单独变化时会不会被公共抽象限制然后把重复分成Mechanical Duplication例如格式转换、通用解析。通常更适合抽。Domain Duplication不同业务暂时有同样规则。谨慎。Accidental Similarity只是碰巧长得一样。通常不应该抽。这一步比直接“Extract Method”有价值得多。十三、如果已经抽错了怎么判断一个很明显的信号是公共模块开始出现越来越多if (scene ...)if (type ...)if (source ...)甚至每个业务都要传一个特殊Flag。这说明原来被认为相同的逻辑正在不断分化。另一个信号是每次只改一个业务需求却必须让5个下游一起跑完整回归。这时候就要重新问这个公共模块是不是已经制造了更多耦合有时候正确动作不是继续优化它。而是重新拆开。十四、一个简单指标Change Coupling Rate这篇我建议只留一个指标Change Coupling Rate——变更耦合率可以简单理解为某个公共模块发生一次业务修改后需要一起验证或修改的下游模块数量 ÷ 总下游数量假设一个公共Validator有5个业务依赖。支付规则变化一次结果5个模块都必须重新验证。说明耦合很高。如果公共模块长期稳定只有真正的公共规则变化时才影响全部下游那这个抽象就比较健康。所以真正值得看的不是少了多少重复代码。而是一个业务变化会被放大到多少地方。十五、什么时候我会明确支持抽公共模块如果满足下面几个条件我通常会更放心业务语义明确相同。过去经常一起变化。未来变化原因大概率一致。接口边界清晰。Owner明确。可以独立测试。下游不需要不断传特殊参数。这时候抽象真正减少的是重复决策。而不仅仅是重复代码。这才是公共模块最有价值的地方。十六、Plus和Pro怎么判断如果你平时主要让ChatGPT、Codex处理单模块重构。分析几处重复代码。查看少量Git历史。判断一次公共方法是否值得提取。这类任务比较集中Plus通常够用。关键是不要只让Agent看当前Diff。最好把模块职责。历史修改。未来需求。一起给它。如果你的日常已经是大型Monorepo。几十个模块。大量Shared Library。一次重构需要同时分析代码依赖、Git历史、下游调用、测试和兼容范围并让Codex连续做影响分析、重构、回归和二次调整这种长时间、多模块的架构工作已经成为常态那Pro会更适合。真正的判断标准仍然是ChatGPT、Codex是不是已经长期参与大型工程决策而不只是写几段代码。最后Agent发现5处重复代码为什么不能马上抽成一个公共模块因为重复代码不等于重复业务概念。今天长得一样的两段代码未来可能因为完全不同的业务原因分别变化。如果过早抽象你消除的是几百行重复。但可能引入的是长期Change Coupling。所以真正值得判断的不是这5段代码有多像而是它们以后会不会因为同一个原因一起变化如果只是当前实现相似先保留重复并不可怕。等变化模式真正稳定以后再抽往往更安全。Agent时代真正需要防的也不是代码重复本身。而是AI太快地帮我们把“暂时相似”固化成了“长期耦合”。持续分享 Codex、大模型开发与 AI 编程实战内容也整理了稳定的 Plus/Pro 订阅渠道有需要下方可自取。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进