为什么 AI 写的代码总是一股“防御味”?

📅 2026/7/1 3:28:34
为什么 AI 写的代码总是一股“防御味”?
为什么 AI 写的代码总是一股“防御味”在大量使用 AI 辅助开发后我发现了一个非常普遍的现象AI 生成的代码往往带着一种极度缺乏安全感的“防御性风格”。它到处判断空值、到处塞默认值、到处包try-catch。特别是在读取环境变量或配置时它有一种强迫症似的喜欢加.trim()和fallback。比如你经常会看到 AI 甩出类似这样的 Java 代码publicStringgetJwtSecret(){try{StringsecretSystem.getenv(JWT_SECRET);if(secret!null!secret.trim().isEmpty()){returnsecret.trim();}// AI 贴心的“兜底”returndefault_secret_key_for_dev;}catch(Exceptione){// 悄悄吞掉异常returnnull;}}表面上看这段代码堪称“滴水不漏”空值兜住了、字符串清洗了、异常也 catch 了。但在真实的工程里这类写法往往不是让系统更可靠而是把本该尽早暴露的致命问题悄悄藏了起来。环境变量确实是不受控的外部输入AI 认为需要防范这只对了一半。另一半极其危险的事实是不是所有配置缺失都应该给默认值也不是所有错误都该被自动修正。真正的问题不在于 AI 懂不懂防御而在于它不知道防御的边界在哪。为什么 AI 编码如此“小心翼翼”AI 写代码为什么总是在“和稀泥”这主要归结于两个原因1. 上下文缺失导致的“局部自保”我们日常使用的 Cursor、Copilot、Claude 等工具本质上是基于 LLM API 实现的。受限于上下文窗口Context WindowAI 不可能在写每一行代码时都把整个仓库读进脑子里。Agent 只能采取“按需读取”的策略但它很难精准判断哪些全局依赖是必须的。在缺乏对全局异常处理Global Exception Handler和系统架构认知的情况下AI 只能选择最保守的策略保证自己写的这几十行代码绝对不报 NPE空指针异常。这就导致了 AI 在写小型 MVP 时如鱼得水但在写大型企业级项目时满屏都是多余的防御。2. 训练语料的“幸存者偏差”大模型是吃着市面上开源代码、Stack Overflow 问答和官方教程长大的。在这些公开场景里什么样的代码最受欢迎是那些“复制粘贴后直接能跑绝对不崩”的独立代码块。AI 学到的不是某个具体商业项目的架构约束而是这些社区里最高频的“单体防御模式”。这就是为什么像 Spring 这样优雅的框架里核心流转逻辑依然需要人工来把控。防御应该留在边界而不是污染核心防御性编程本身没错但到处兜底就是在破坏系统的契约。真正需要严防死守的地方是系统的边界HTTP 请求参数 / 表单输入上传文件 / 数据导入第三方 API 返回值Webhook payloadCLI 参数 / 环境变量跨租户资源访问 / 权限判断在这些地方数据来自外部必须严格校验、清洗、拒绝非法输入。但在业务核心逻辑里到处兜底反而会制造灾难。来看看 AI 在业务层经常写的这种代码publicStringgetUserName(StringuserId){if(userIdnull||userId.isEmpty()){return;// 埋雷开始}try{UseruseruserRepository.findById(userId);if(user!nulluser.getName()!null){returnuser.getName();}return;// 继续埋雷}catch(Exceptione){log.error(Error,e);return;// 把异常消化掉强行返回空字符串}}调用方拿到一个空字符串以后根本不知道发生了什么“凶杀案”是userId没传用户不存在数据库挂了还是数据本身被污染了统统不知道。更好的做法是把失败的语义清晰地暴露出来利用 Java 的契约异常或 Optional去表达而不是用空值去掩盖publicStringgetUserName(StringuserId){// 1. 边界防御契约断言非法参数直接 Fail Fastif(userIdnull||userId.isBlank()){thrownewIllegalArgumentException(userId 不能为空);}// 2. 核心逻辑找不到数据是明确的业务异常绝不偷偷返回 UseruseruserRepository.findById(userId);if(usernull){thrownewUserNotFoundException(用户不存在: userId);}returnuser.getName();}(注如果你的架构规范不推荐用异常做业务控制这里也应该返回OptionalString或者自定义的ResultString包装类而不是直接给个)总结如何审查 AI 的代码AI 喜欢写防御性代码是因为它面对着不完整的上下文倾向于用空值判断、try-catch和trim()来掩饰局部逻辑的脆弱。特别是在处理环境配置时server.port缺失可以给默认值 8080但JWT_SECRET缺失必须抛出IllegalStateException让应用启动失败普通的 URL 可以.trim()但密钥绝对不能偷偷去掉空格因为空格可能本身就是密钥的一部分。一个优秀的工程它的防御性代码应该遵循以下准则在边界处严阵以待严格校验。在核心里保持契约清晰互相信任。对不可恢复的错误Fail fast尽早崩溃。对可恢复的失败使用结构化表达如业务异常、Optional 或 Result 模式。对关键配置拒绝弱默认值非法直接拒绝启动。在使用 AI 辅助编程的时代Code Review 的重心已经变了。AI 生成代码最需要审查的地方往往不是它有没有考虑异常而是它有没有把真正应该暴露的问题悄悄吞掉。