当 AI 编程助手已经能在几分钟内生成数百行代码时Uncle Bob 关于 AI 时代软件工程基础的观点反而显得更有分量。Uncle Bob 是 Robert C. Martin 的常用称谓也是《代码整洁之道》《架构整洁之道》的作者和敏捷宣言参与者。他在近年多次讨论中反复强调同一个判断AI 并没有让软件开发变成一件更简单的事它只是让“生成代码”的成本趋近于零而让“确认代码正确”的成本变得更加突出。软件工程的基础——测试、架构约束、重构、代码评审和职业责任——不会因为工具变化而失效反而会成为决定团队能否交付可靠系统的关键能力。这篇内容围绕他的这一视角展开会先解释为什么 AI 时代反而更需要工程基础再给出测试、SOLID 原则、工作流和排错清单这几个可以直接落地的方向。1. 为什么 AI 时代反而要回头补软件工程基础1.1 Uncle Bob 是谁他的判断为什么值得被认真对待Uncle Bob 在软件工程领域的地位主要来自三件事他参与起草敏捷宣言写过《代码整洁之道》和《架构整洁之道》并且在职业生涯中长期坚持测试驱动开发TDD和重构实践。这些经历让他对“软件开发中的纪律”有非常固执的立场。很多新工具刚出现时他的回应都不是“要不要用”而是“用了之后你怎么保证系统仍然可控”。他对 AI 编程工具的态度同样如此。当大量开发者开始用大模型生成函数、接口和配置时他提醒的不是“AI 会取代程序员”而是“AI 产生代码的速度越快代码库中被人工确认过的比重就越低”。一旦这个比重低到某个临界点团队就会失去对系统的理解能力和修改能力。换句话说AI 提高了代码的“产量”但没有提高代码的“可信度”。这一点值得认真对待不是因为 Uncle Bob 有权威而是因为他指出的问题已经在许多项目中真实出现AI 生成代码可以编译通过可以跑通主流程但一旦进入边界条件、错误处理、并发安全和长期维护阶段问题就会集中爆发。这些问题的根源不在 AI而在工程基础薄弱。1.2 AI 改变了代码的生产方式但没有改变代码的失效方式传统开发中代码由人手编写Bug 来自人的疏忽或对需求的理解偏差。AI 辅助开发中代码由模型生成Bug 的来源多了一层模型可能基于过时的训练数据可能受到上下文窗口截断的影响也可能在细节上产生“幻觉”编造出不存在的函数或参数。但代码失效的方式并没有本质变化。无论是手写还是 AI 生成系统出问题仍然集中在几个方向需求理解错误模型根据提示词生成功能但提示词没有表达清楚业务规则。边界条件遗漏空值、超长输入、并发冲突、异常中断没有处理。依赖版本不匹配模型推荐的依赖版本与当前环境冲突。安全问题输入校验缺失、权限校验被绕过、敏感信息被写进日志。架构腐化代码能运行但模块边界混乱后续改动成本指数上升。正因为失效方式没有变软件工程的基本功才没有过时。测试仍然是验证行为的手段架构仍然是控制复杂度的手段代码评审仍然是防止质量问题进入主库的重要手段。AI 改变的是生产工具不是质量保障机制。1.3 工程师的职责从“编写代码”迁移到“验证与治理”在 AI 时代工程师对代码库的核心价值正在变化。以前写出可运行代码本身就是主要贡献。现在大量初稿代码可以由 AI 完成工程师的核心工作变成四件事拆解需求、设计约束、验证结果、修复偏差。传统任务AI 时代的做法依赖的工程基础编写函数AI 生成初稿人负责评审和修改代码阅读能力、设计原则补充测试人先定义行为AI 帮助生成测试初稿测试思维、边界分析排查问题AI 帮助定位线索人确认根因日志分析、系统链路理解架构演进人控制方向AI 辅助重构模块边界、依赖规则这里也是“软件工程银弹是什么”这个问题最适合出现的场景。Fred Brooks 早就说过软件开发没有银弹。AI 也不是银弹它没有消灭复杂度而是把复杂度从“生成代码”转移到了“验证和治理代码”。团队如果把工程基础忽略掉只追求 AI 生成速度短期内看起来效率很高长期会积累大量难以理解和维护的代码。2. 把测试当作 AI 生成代码的第一道防线2.1 测试不是质量部门的事而是 AI 时代的信任机制在 AI 辅助开发中测试的功能不再是“验证别人写的代码”而是“给 AI 生成的代码建立信任”。模型不会因为给你的代码写了很多行就保证它正确测试才是判断正确性的最小可执行证据。很多团队在引入 AI 编程工具后会进入一个误区让 AI 先写代码再让人补测试。这个顺序的问题在于人已经看到了实现容易顺着实现的思路去补测试结果测试验证的是“代码自己以为的行为”而不是业务真正需要的行为。推荐顺序是反过来的先根据需求和边界条件编写测试。让 AI 生成实现目标是让测试通过。人审查 AI 生成的实现确认没有绕过测试的隐藏行为。如果测试暴露问题再迭代修改。这里面最核心的是第 1 步。需求被表达成可执行断言后AI 生成的代码就不再是“凭感觉的代码”而是“被行为约束的代码”。2.2 用 TDD 约束 AI 生成实现一个最小可运行示例下面用一个简单场景演示这个流程。需求是计算订单折扣订单金额满 100 元打 9 折满 500 元打 8 折否则不打折折扣计算必须保留两位小数。先写测试用 Java 和 JUnit 5 的风格演示import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class DiscountCalculatorTest { private final DiscountCalculator calculator new DiscountCalculator(); Test DisplayName(金额不足100不打折) void shouldNotDiscountBelowThreshold() { BigDecimal result calculator.calculate(new BigDecimal(99.99)); assertEquals(new BigDecimal(99.99), result); } Test DisplayName(金额满100打9折) void shouldApplyNinePercentDiscount() { BigDecimal result calculator.calculate(new BigDecimal(100.00)); assertEquals(new BigDecimal(90.00), result); } Test DisplayName(金额满500打8折) void shouldApplyEightPercentDiscount() { BigDecimal result calculator.calculate(new BigDecimal(500.00)); assertEquals(new BigDecimal(400.00), result); } Test DisplayName(折扣结果保留两位小数) void shouldRoundToTwoDecimalPlaces() { BigDecimal result calculator.calculate(new BigDecimal(123.45)); assertEquals(new BigDecimal(111.11), result); } Test DisplayName(金额为负时抛出异常) void shouldRejectNegativeAmount() { assertThrows(IllegalArgumentException.class, () - calculator.calculate(new BigDecimal(-1))); } }测试写完后再把这段测试和需求一起交给 AI要求它生成DiscountCalculator。AI 生成的实现只要满足上述断言就说明主路径和边界路径都符合预期。这里要留意负数的边界条件很多 AI 生成的代码只处理正数因为训练数据里的常见写法没有异常分支测试在这里就把缺陷挡住了。测试通过后还要人工检查实现是否真的使用了BigDecimal而不是把金额转成double计算。否则测试可能因为精度问题偶尔通过进入生产后却出现金额偏差。测试能证明行为但架构和实现质量仍然需要人来判断。代码块后的说明public class DiscountCalculator { public BigDecimal calculate(BigDecimal amount) { if (amount null || amount.signum() 0) { throw new IllegalArgumentException(amount must be positive); } BigDecimal discount BigDecimal.ONE; if (amount.compareTo(new BigDecimal(500)) 0) { discount new BigDecimal(0.8); } else if (amount.compareTo(new BigDecimal(100)) 0) { discount new BigDecimal(0.9); } return amount.multiply(discount) .setScale(2, RoundingMode.HALF_UP); } }这个实现的关键点是比较金额用compareTo而不是equals因为BigDecimal的equals会同时比较精度new BigDecimal(100).equals(new BigDecimal(100.0))返回 false但compareTo不会折扣比例优先处理高档位避免出现“满 500 同时匹配两个折扣”的问题最后用setScale(2, RoundingMode.HALF_UP)控制小数精度。2.3 用测试金字塔给 AI 辅助开发设计质量层次测试金字塔在传统项目里用于平衡不同层级测试的数量在 AI 辅助开发中同样适用而且更重要。AI 生成代码时最容易生成大量“看起来正常”的单元级函数却缺少跨模块的集成验证。测试层级覆盖目标AI 时代的使用方式数量占比建议单元测试单个函数、类、算法先写测试再生成实现约束行为约 70%集成测试模块之间、数据库、外部 API验证 AI 生成的接口调用方式是否正确约 20%端到端测试核心业务链路验证主流程和关键回退路径约 10%这里要注意单元测试数量多不代表价值一定高。如果测试只是把实现内部逻辑原样抄一遍断言没有业务意义那 AI 生成代码时也会给出一堆“看起来在验证”的测试实际上没有防止回归。评估测试质量的关键不是行数而是这些测试能否区分“行为正确”和“行为错误”。2.4 用状态图和活动图补齐 AI 容易漏掉的边界状态图和活动图是软件工程里的经典工具在 AI 时代仍然有不可替代的价值。很多人觉得画图浪费时间但当 AI 生成代码的速度快过人工审读速度时图和状态表反而是最便宜的需求描述方式。以订单状态为例先列出状态迁移当前状态事件目标状态是否允许新订单支付成功已支付允许新订单取消已取消允许已支付发货已发货允许已发货确认收货已完成允许已完成取消无不允许把这个表格给 AI让它生成状态机代码时它就不容易漏掉“已完成订单不允许取消”这类分支。活动图则适合用来表达复杂业务流的判断步骤比如“满减与折扣同时存在时先算哪个”。AI 生成代码前先给一张活动图或状态表相当于把需求里的隐性规则变成了显式输入这是减少 AI 幻觉的低成本手段。3. 用 SOLID 原则审查 AI 生成代码3.1 单职责原则AI 最常犯的“万能函数”问题SOLID 是一组面向对象设计原则但它的思想并不局限于面向对象语言。AI 生成代码时最常见的问题不是语法错误而是“一个函数承担了太多职责”。下面这段 Python 示例很典型def send_notification(order, user, channel, message): # 查询订单信息 order_total sum(item[price] * item[count] for item in order[items]) discount 0.9 if order_total 100 else 1.0 final_price order_total * discount # 决定发送渠道 if channel sms: print(f发送短信给 {user[phone]}: {message}) elif channel email: print(f发送邮件给 {user[email]}: {message}) elif channel wechat: print(f发送微信给 {user[wechat_id]}: {message}) # 记录日志 log_file open(notification.log, a) log_file.write(f{order[id]} {channel} {final_price}\n) log_file.close()这个函数在 AI 生成场景中非常常见输入结构灵活内部逻辑丰富但没有清晰的边界。它同时做了订单金额计算、渠道分发、消息发送和日志记录。如果后续要改折扣规则必须进入整个函数修改如果要新增一个渠道也要进入函数添加分支如果日志写失败还可能影响正常发送流程。重构的思路是按职责拆分def calculate_order_total(order): total sum(item[price] * item[count] for item in order[items]) return total * (0.9 if total 100 else 1.0) def create_message(channel, user, text): address get_channel_address(channel, user) return {channel: channel, address: address, text: text} def send_message(message): if message[channel] sms: return send_sms(message[address], message[text]) if message[channel] email: return send_email(message[address], message[text]) raise ValueError(funsupported channel: {message[channel]})重构后每个函数只有一个明确目的算金额、组消息、发消息。测试可以分别针对这三个函数折扣规则变化不会影响发送逻辑新增渠道时也不需要修改金额计算。3.2 依赖倒置与接口隔离防止代码写死实现AI 生成代码时还有一个高频问题直接new依赖对象。这在小型脚本中问题不大但在需要测试和替换实现的项目里会造成严重不便。以 Python 为例# 错误写法发送逻辑被绑定在具体邮件客户端上 class NotificationService: def send(self, message): smtp SMTPClient(smtp.example.com, 25) smtp.send(message)这个类很难测试因为每次调用都会真实连接 SMTP。推荐做法是依赖注入让上层依赖抽象而不是具体实现class NotificationService: def __init__(self, sender): self._sender sender def send(self, message): self._sender.send(message)测试时可以注入一个内存中的假实现class FakeSender: def __init__(self): self.messages [] def send(self, message): self.messages.append(message) def test_notification_service(): fake FakeSender() service NotificationService(fake) service.send({channel: email, text: hello}) assert len(fake.messages) 1这样AI 生成代码是否容易测试往往取决于它是否遵守了依赖倒置原则。生成代码时如果发现大量外部依赖被直接构造就要在提示词里增加约束所有外部依赖通过构造函数或参数传入。3.3 把 SOLID 变成生成前约束和审查清单与其等 AI 生成完代码再逐条检查 SOLID不如在生成前就把约束写进提示词。下面的提示词模板可以在与 AI 交互时使用请实现订单通知发送功能要求 1. 单职责原则每个函数只做一件事不要同时处理金额计算、发送和日志。 2. 依赖倒置外部依赖邮件客户端、短信服务、日志记录通过构造函数注入。 3. 接口隔离消息对象只包含发送所需的字段不要传入整个订单对象。 4. 错误处理发送失败时抛出明确异常不要吞掉错误。 5. 不输出测试代码由我单独要求。审查时也可以按这个顺序快速过一遍审查点检查方法不合格信号单职责看函数名和函数体是否一致一个函数超过 30 行且有多段无关逻辑开闭原则新增渠道或类型时是否要改已有函数使用大量 if/elif 判断内部类型依赖倒置外部依赖是否通过构造或参数传入代码内部直接 new 外部服务接口隔离函数参数是否暴露过多无关字段整个实体对象被到处传递最小知识是否可以通过参数组合降低耦合函数内部访问多层嵌套属性这里要强调一点SOLID 适合作为审查框架但不要教条化。对于一次性脚本或简单 CRUD过度设计反而降低可读性。判断标准是代码是否处于长期演进路径上。如果确定这段代码只在一个轻量项目里使用单职责和依赖注入适度即可如果是核心业务模块就必须严格审查。4. 搭建一套可复用的 AI 辅助编码工作流4.1 区分学习环境、开发环境与生产环境的工程要求AI 辅助开发很容易在“本地能跑”和“生产可用”之间产生错觉所以先把环境要求分开。维度学习环境开发/测试环境生产环境目标验证思路、快速试验集成测试、质量反馈稳定交付、可回滚AI 代码审查可以宽松必须进入代码评审必须有完整评审记录测试要求可写可不写核心逻辑必须有测试全链路测试和回归测试CI/CD不需要必须有必须有且包含门禁依赖管理可以宽松须锁版本须锁版本并做安全扫描日志监控不要求基础日志完整日志、告警、追踪学习环境里当然可以为了效率直接让 AI 生成代码并运行。但进入团队协作后如果没有统一审查流程AI 生成代码就会以不同风格和不同质量水平进入同一个代码库后续维护成本会急剧上升。4.2 用提示词模板约束 AI 输出结构好的提示词不是越长越好而是要让 AI 的输出结构可预期。一个面向工程落地的提示词模板通常包含五部分目标、输入、约束、输出结构、自检要求。任务计算订单折扣。 目标根据订单金额返回折扣后金额。 输入 - amountBigDecimal订单金额不能为负数 - 折扣规则满100打9折满500打8折 输出结构 - 函数签名 - 业务逻辑实现 - 边界条件处理 - 关键参数说明 约束 - 使用 BigDecimal 避免浮点误差 - 金额比较使用 compareTo - 折扣结果保留两位小数四舍五入 - 不生成测试代码 请先输出你的理解和边界假设再输出实现。最后一句“请先输出理解和边界假设”很重要。它让 AI 在写代码前先暴露需求理解方便人在代码生成前发现偏差。实际使用中如果 AI 理解错了直接调整提示词比事后修改代码更高效。4.3 提交前检查清单把人工评审变成固定流程AI 生成的代码提交到主库前团队成员应该逐项确认。下面是一份可以直接复制到 PR 描述里的检查清单- [ ] 是否理解这段代码的业务目的 - [ ] 是否包含核心路径与边界条件的测试 - [ ] 是否处理了空值、超长输入、异常中断 - [ ] 是否使用显式依赖注入而不是内部 new 外部服务 - [ ] 是否避免使用不受控的全局变量 - [ ] 外部 API 或数据库访问是否有超时和重试 - [ ] 是否记录了关键日志同时避免记录敏感信息 - [ ] 新增依赖是否与现有版本兼容 - [ ] 是否运行了完整测试套件 - [ ] 是否经过另一位工程师评审其中“是否经过另一位工程师评审”是 AI 时代最容易跳过的步骤。AI 生成代码后开发者自己容易产生“它已经通过验证”的错觉但模型没有上下文背景无法判断这段代码是否符合团队架构评审必须由人完成。4.4 建设 CI 和代码评审设施让 AI 代码进入主库前被校验CI 的价值在 AI 时代被放大了。代码是 AI 生成的但质量门禁必须交给自动化设施。一个最小可用的 GitHub Actions 配置可以这样组织name: ai-code-quality on: pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up Java uses: actions/setup-javav4 with: distribution: temurin java-version: 17 - name: Run tests run: mvn test - name: Check coverage run: mvn jacoco:report - name: Lint run: mvn spotless:check关键点是把测试、覆盖率检查和格式检查都设为 PR 通过的前置条件。这样即使开发者个人疏忽AI 生成代码也要先过自动检查再进入人工评审。生产环境还可以在此基础上增加依赖安全扫描、镜像构建、集成环境部署等阶段。识别 AI 生成代码的另一个方法是关注 PR 的说明质量。要求开发者提交 PR 时补充“这段代码解决了什么问题、为什么选择这个方案、测试覆盖了哪些场景”。如果 AI 生成了大量代码但 PR 描述里只有一行“add feature”评审者要主动追问。这里是在把“人审”变成团队规范而不是依赖个人自觉。5. 常见问题排查AI 生成代码最容易在哪里翻车5.1 能运行但无法维护的“面条代码”现象AI 生成的代码可以启动、可以跑通主流程但代码库中出现大量长函数、重复代码、职责混杂的模块。改动一个需求时发现牵一发而动全身。可能原因提示词只描述了功能没有约束结构和边界生成过程中没有做代码评审团队没有统一的架构规范。检查方式统计函数长度和圈复杂度。审查是否存在大段复制粘贴代码。查看一个模块被多少人同时修改。处理建议先不追求一次重写到位。把最长的函数拆成小函数提取常量再逐步消除重复代码。每拆一次就运行一次测试确保没有改变行为。把“结构约束”写进提示词例如“函数不超过 30 行”“每个函数只做一件事”。预防建议在 PR 评审中把“可维护性”作为通过条件而不是只看功能是否可运行。5.2 测试全绿但改一个参数就崩溃现象CI 显示测试全部通过但开发者改一个配置参数或调整业务规则后系统立即出现问题。可能原因测试断言不够严格只验证了“不报错”没有验证“结果正确”测试过度绑定实现比如 mock 了内部方法导致测试通过但真实行为没有被验证AI 生成的测试代码使用了很多宽松匹配比如assertEquals(1, result.size())但没检查元素内容。检查方式检查测试是否包含真实输入输出断言。检查是否大量使用verify(mock, never())这类“没有发生也是正确”的断言。临时修改业务逻辑看测试是否能捕获变化。使用变异测试工具评估测试质量。处理建议对核心业务逻辑补充参数化测试把测试断言从“不报错”改为“返回预期值”减少对内部实现细节的 mock更多地验证公开行为。5.3 依赖版本漂移与 API 幻觉现象AI 生成的代码使用了某个第三方库的 API编译时通过运行时却报NoSuchMethodError或ModuleNotFoundError或者 AI 推荐了一个不存在的依赖版本。可能原因模型的训练数据滞后推荐了旧版本 API模型基于相似代码生成了“看起来合理”的函数名但该版本并未提供该函数。检查方式查看编译日志和运行时堆栈。检查依赖配置文件里是否出现了与实际环境不匹配的版本。使用mvn dependency:tree或pip show确认实际加载的版本。打开第三方库文档比对 API 签名。处理建议提示词中要求“所有依赖必须使用项目现有版本不要假设存在某个函数”引入新依赖后先在本地做最小验证CI 中锁定依赖版本并添加依赖安全扫描。5.4 一套可复用的排查顺序AI 生成的代码有问题时排查顺序和手写代码一样从输入到验证逐层推进输入是否正确调用是否传入了空值、错误类型或不完整对象。路径与命名是否符合项目约定文件是否放入正确目录类名是否与配置一致。依赖版本是否匹配构建工具是否成功解析了所有依赖。配置是否生效环境变量、配置文件是否被正确加载。权限、端口、网络是否正常外部服务是否可达连接是否超时。日志和异常是否明确是否记录了足够的上线文信息。框架或语言版本是否存在限制某些 API 只在特定版本可用。问题现象常见原因检查方式处理建议能运行但无法维护职责混杂、函数过长统计圈复杂度、代码审查小步拆分逐步重构测试全绿但改参崩溃断言过弱、mock 过细检查断言、做变异测试补真实断言减少实现 mock编译通过运行报错API 幻觉、版本漂移查依赖树、比对文档锁定版本提示词约束依赖配置修改后不生效改错文件或未加载配置检查环境、日志和配置来源确认配置文件加载路径6. AI 时代的软件工程基础可执行的落地清单6.1 个人开发者清单如果你是一名使用 AI 编程工具的独立开发者可以从这几条开始落地先用一句话写下业务规则再写测试最后让 AI 生成实现不要跳过测试。每次生成代码后给自己留出 30 分钟重构时间删除重复代码缩小函数职责。不提交没有经过运行验证的 AI 生成代码。复杂逻辑先画状态表或流程图再交给 AI减少覆盖不到的边界。记录提示词版本像记录依赖版本一样记录“这个功能是用什么约束生成的”。6.2 团队协作清单团队引入 AI 编程工具时比工具本身更需要统一的是协作规范统一 AI 工具和插件避免每个人的生成代码风格不一致。禁止把未经评审的 AI 生成代码直接提交主库。建立提示词模板库把团队常用的业务场景沉淀为标准提示词。CI 中增加测试门禁、覆盖率门禁和格式检查。每次评审 AI 生成代码时要求在 PR 描述中补充测试策略和架构说明。定期抽查主要模块确认 AI 引入的技术债在可控范围。6.3 技术负责人需要关注的工程设施技术负责人不需要逐行审查代码但要保证工程设施可以兜住 AI 生成代码的偏差代码质量门禁覆盖率、复杂度、格式检查必须自动化。依赖治理引入新依赖要有流程不能由 AI 自动推荐后直接使用。架构边界通过架构测试或依赖约束防止模块越界调用。回滚能力AI 生成代码引发线上问题时可以快速回滚到上一个稳定版本。学习机制定期组织“AI 生成案例复盘”把质量问题沉淀为团队知识。6.4 学习路径与下一步扩展AI 时代补软件工程基础不一定要回到教科书可以按这个路径学习重读《代码整洁之道》重点看命名、函数、注释和测试章节。练习 TDD选一个小功能强迫自己先写测试再写实现持续一周。学习画状态图和活动图并用它们设计测试用例。理解依赖注入和接口设计在代码评审中主动检查这两个点。自己搭一次 CI把 lint、测试、覆盖率门禁串联起来。把提示词工程当作“需求描述能力”来练习学会把隐性规则写清楚。AI 工具解决的是“快速得到一段候选实现”而软件工程基础解决的是“这段代码能不能被信任、被修改、被长期维护”。Uncle Bob 反复强调的恰恰是这个判断工具越强工程师越需要理解自己在做什么否则生成的代码越多失控的风险就越大。对你来说最有价值的练习不是学会更多 AI 快捷键而是把测试、重构、评审和架构约束装进日常开发流程让 AI 成为一个更快地写出初稿的助手而不是一个绕过思考的替代品。