1. 项目概述当AI成为代码审查员我们该如何“提问”最近在带新人也观察了不少团队引入AI编程助手后的代码变化发现一个挺有意思的现象当新人让AI帮忙补全异常处理代码时生成的代码看起来“天衣无缝”逻辑通顺语法正确但一上线就埋下了深坑。最典型的一个错误就是把本应暴露的失败悄无声息地“处理”成了成功。比如一个文件读取操作失败了AI生成的代码可能只是记录了一条日志然后程序继续“正常”执行仿佛什么都没发生但后续的逻辑因为缺少关键数据早已偏离正轨最终导致业务结果错误排查起来却异常困难。这不仅仅是新人的问题也是我们所有开发者需要重新审视的一个关键点我们是否在滥用“异常处理”这个工具把“掩盖问题”当成了“解决问题”尤其是在AI辅助编程成为标配的今天我们与AI的交互方式直接决定了代码的质量和系统的健壮性。这个项目就是想深入聊聊当我们把“补异常处理”这个任务交给AI时背后隐藏的认知陷阱、常见的错误模式以及如何通过正确的“提问工程”和代码审查引导AI生成真正健壮、可维护的异常处理代码。2. 核心错误模式拆解AI是如何“好心办坏事”的AI模型特别是大型语言模型在代码生成上有一个根深蒂固的倾向追求代码的“完整性”和“表面正确性”。它的训练目标是生成语法正确、符合常见模式的代码片段。当它接收到“为这段代码添加异常处理”的指令时它的首要任务是“让这段代码不报错”而不是“让这段代码在出错时行为正确”。这个根本性的目标错位导致了以下几种高频出现的错误模式。2.1 模式一日志吞噬一切程序“带病运行”这是最常见也最危险的一种模式。AI倾向于生成try-catch块在catch中仅仅打印或记录一条错误日志然后程序继续执行。// 用户提供的原始代码无异常处理 public UserData loadUserData(String userId) { String json fileSystem.readFile(“users/” userId “.json”); return objectMapper.readValue(json, UserData.class); } // AI可能生成的“错误示范” public UserData loadUserData(String userId) { try { String json fileSystem.readFile(“users/” userId “.json”); return objectMapper.readValue(json, UserData.class); } catch (Exception e) { log.error(“读取用户数据失败用户ID: {}”, userId, e); // 问题所在这里没有返回对于调用方来说方法似乎“正常结束”了 } // 实际上方法执行到这里会隐式返回 null }错误分析对于调用loadUserData的方法来说它收到了一个null。如果调用方没有对null做检查后续的userData.getName()就会抛出NullPointerException但这个异常发生在远离原始错误的地方栈信息完全丢失排查难度剧增。更糟糕的是如果调用方也没处理这个NPE错误会继续向上层传递最终可能以一个完全无关的错误表象呈现出来。注意记录日志是必要的但日志不能替代对错误的处理决策。日志是给运维和开发者看的而程序的执行流是给业务逻辑用的。两者必须分开。2.2 模式二返回空对象或默认值掩盖业务异常这种模式在查询类操作中尤其常见。AI可能会生成返回空集合、空字符串或某个预定义的“无效默认对象”的代码。# 用户提供的原始代码 def get_user_by_email(email: str) - User: conn database.get_connection() result conn.execute(“SELECT * FROM users WHERE email %s”, (email,)) row result.fetchone() return User(**row) if row else None # AI可能生成的“错误示范” def get_user_by_email(email: str) - User: try: conn database.get_connection() result conn.execute(“SELECT * FROM users WHERE email %s”, (email,)) row result.fetchone() return User(**row) if row else None except DatabaseConnectionException: log.error(“数据库连接失败”) return User() # 返回一个空的、无效的User对象 except Exception as e: log.error(“查询用户失败: {}”, e) return User() # 同上错误分析调用方收到了一个User对象它可能会愉快地使用user.id或user.name但这些属性可能是None或默认值。业务逻辑会基于这些错误的数据继续运行导致数据污染或错误的业务结果。例如一个“空用户”可能被错误地判定为VIP或者其订单被错误地关联。核心原则“查无此人”和“系统故障查不了人”是两种完全不同的情况。前者是正常的业务边界情况应返回None或抛出特定的UserNotFoundException后者是系统异常通常意味着当前请求无法被正确处理应该向上传播或触发降级/熔断机制。2.3 模式三过度捕获与异常类型模糊化AI喜欢用catch (Exception e)或catch (Throwable t)这种“一网打尽”的方式。这抹杀了异常的类型信息让调用方无法根据异常类型做出精细化的处理。// 错误示范过度泛化的捕获 try { processPayment(order); updateInventory(order); sendConfirmationEmail(order); } catch (Exception e) { // 捕获了所有异常 log.error(“订单处理失败”, e); // 那么是支付失败库存更新失败还是邮件发送失败 // 我们无法区分因此也无法做出正确的补偿操作比如回滚支付。 }错误分析支付失败需要回滚交易库存更新失败可能需要释放预占库存而邮件发送失败可能只需要记录日志并重试队列。用一个笼统的Exception捕获所有情况迫使你只能用同一种方式处理所有错误这必然是不正确的。2.4 模式四在错误的层级处理异常这是架构层面的问题但AI生成的代码会加剧它。AI可能会在某个底层工具方法里“妥善处理”掉异常让调用方感知不到任何错误。// 一个低级的HTTP请求工具函数 async function fetchWithAIHandledError(url) { try { const response await fetch(url); return await response.json(); } catch (error) { console.error(请求 ${url} 失败:, error); return { status: ‘error’, message: ‘Request failed’ }; // 在底层统一返回错误结构 } } // 业务调用方 async function getProductDetail(productId) { const data await fetchWithAIHandledError(/api/products/${productId}); // 现在我需要检查 data.status 是 ‘success’ 还是 ‘error’ // 这迫使所有调用方都要进行重复的状态检查并且失去了利用try-catch进行流程控制的能力。 }错误分析这实际上是用返回值模拟了错误状态回到了C语言时代。它破坏了JavaScript以及大多数现代语言的异常传播机制使得错误处理逻辑分散在各个调用方无法在某个统一的中间件或边界进行集中处理如全局错误页面、API统一错误格式封装。3. 正确的“提问工程”如何引导AI生成健壮的异常处理代码要让AI写出好代码关键在于我们如何下达指令。模糊的指令得到模糊的且可能错误的结果。我们必须成为AI的“产品经理”明确描述“需求”。3.1 明确异常处理的目标与策略在让AI补代码之前你自己必须先想清楚以下几个问题并把答案融入提示词这个操作可能失败的原因有哪些网络、IO、数据格式、权限、业务规则哪种失败是“致命”的应该让整个请求失败如数据库连接失败哪种失败是“局部”的可以降级或重试如某个次要的缓存服务失败调用方需要根据不同的失败原因采取不同行动吗如支付失败要跳转失败页库存不足要提示换货这个方法的契约返回值、抛出异常应该是什么一个糟糕的提示词“为下面的方法添加异常处理。”一个优秀的提示词“为下面的loadUserData方法添加异常处理。要求1. 如果文件不存在应抛出UserDataNotFoundException。2. 如果文件存在但读取IO错误如权限不足应抛出DataAccessException并附带原始异常。3. 如果JSON解析失败应抛出DataFormatException。4. 只捕获你知道如何处理的特定异常其他运行时异常应允许其向上传播。请使用SLF4J记录错误日志日志级别为ERROR。”// 基于优秀提示词AI更可能生成如下代码 public UserData loadUserData(String userId) throws UserDataNotFoundException, DataAccessException, DataFormatException { Path filePath Paths.get(“users”, userId “.json”); if (!Files.exists(filePath)) { throw new UserDataNotFoundException(“User data file not found for ID: “ userId); } String json; try { json Files.readString(filePath, StandardCharsets.UTF_8); } catch (IOException e) { log.error(“Failed to read user data file for userId: {}”, userId, e); throw new DataAccessException(“Could not access user data”, e); // 注意保留了原始异常e } try { return objectMapper.readValue(json, UserData.class); } catch (JsonProcessingException e) { log.error(“Invalid JSON format in user data file for userId: {}”, userId, e); throw new DataFormatException(“User data file is malformed”, e); } // 其他未声明的异常如NullPointerException将自动向上传播迫使开发者修复代码逻辑错误。 }3.2 利用上下文和角色设定在提示词中为AI设定一个“角色”并给出更丰富的上下文能极大提升生成代码的合理性。示例提示词“你是一个经验丰富的后端架构师注重代码的健壮性和可维护性。现在需要为一个电商系统的订单支付接口编写异常处理。该接口首先调用支付网关扣款然后更新本地订单状态为‘已支付’。请遵循以下原则1. 支付失败是核心失败必须抛出明确的PaymentFailedException并且不能更新本地订单状态。2. 支付成功但更新本地数据库失败如网络抖动属于部分成功需要抛出OrderSyncException并告知调用方支付已成功但状态未同步后续需要通过补偿任务来修复。3. 使用Spring的Transactional注解来保证支付和更新的原子性如果支付网关调用无法回滚则需要考虑使用SAGA等分布式事务模式这里先按最简模型处理。请基于这些原则补全下面方法的异常处理逻辑。”这样的提示词能引导AI思考事务边界、补偿机制而不仅仅是机械地包裹try-catch。3.3 要求AI解释其处理逻辑一个进阶技巧是在提示词中要求AI在生成代码后用注释解释每个异常处理块的设计理由。示例提示词“生成代码后请为每个catch块和重要的错误处理逻辑添加行内注释解释为什么选择这样处理例如为什么抛出而不是吞掉为什么记录这个级别的日志。“这不仅能得到更好的代码还能作为一次对新人的实时教学帮助你审查AI的“思考过程”是否正确。4. 代码审查要点如何一眼看出AI生成的异常处理有问题即使有了好的提示词对AI生成的代码进行严格的审查仍是必不可少的。以下是在CRCode Review中需要重点关注的“红灯”信号。4.1 审查点一检查catch块是否改变了方法的正常出口这是识别“静默失败”最直接的方法。查看每个catch块它是否以return语句结束返回的是什么null, 空对象默认值它是否没有return, 也没有throw导致控制流自然流出try-catch块如果方法返回值是集合是否返回了Collections.emptyList()这适用于当前场景吗例如查询失败和查询结果为空语义相同吗审查动作对于每个catch块追问“如果程序执行到这里调用方接下来会看到什么这是调用方期望的吗”4.2 审查点二检查异常日志的“可排查性”日志不是用来凑行数的。审查日志记录是否记录了足够的上下文例如不仅仅是“文件读取失败”而应有“文件路径xxx操作模式xxx”。是否记录了完整的异常链log.error(“…”, e)中的异常对象e被传入了吗这是获取堆栈信息的关键。日志级别是否恰当ERROR用于需要人工立即干预的错误WARN用于预期内但不正常的情况INFO用于重要的业务流水。AI可能滥用ERROR。一个坏的例子log.error(“Error occurred.”);毫无信息量一个好的例子log.error(“Failed to process order [{}] due to payment gateway timeout. UserId: [{}]“, orderId, userId, exception);4.3 审查点三分析异常类型的粒度查看catch语句捕获的是具体的异常类还是宽泛的Exception/RuntimeException。如果过于宽泛要求重构分解为多个具体的catch块或者至少在最外层捕获宽泛异常后根据异常类型进行不同的处理。如果捕获了Error或Throwable这需要特别警惕。除非你在编写底层框架或容器代码如应用服务器否则通常不应该捕获Error如OutOfMemoryError因为这类错误通常表示不可恢复的JVM问题。4.4 审查点四评估资源清理与状态回滚对于涉及资源连接、文件、锁或状态变更数据库事务的代码审查catch块和finally块资源是否在finally块中正确关闭AI有时会忽略这一点。发生异常时已经变更的状态是否被正确回滚例如在try块中先更新了A表后更新B表时失败A表的更新是否应该回滚这需要结合事务注解如Transactional(rollbackFor Exception.class)或手动回滚逻辑来审查。5. 实战案例从错误到正确的完整重构让我们通过一个完整的案例看看一个有问题的AI生成代码如何被重构。原始需求一个函数从指定URL下载图片调整大小后保存到本地并返回保存路径。初始的AI生成代码有缺陷import requests from PIL import Image import os def download_and_resize_image(image_url: str, save_dir: str, width: int, height: int) - str: 下载图片调整大小保存到本地目录。 try: # 1. 下载图片 response requests.get(image_url, timeout10) response.raise_for_status() image_data response.content # 2. 调整大小 with Image.open(io.BytesIO(image_data)) as img: img_resized img.resize((width, height), Image.Resampling.LANCZOS) # 生成文件名 filename os.path.join(save_dir, f“resized_{os.path.basename(image_url)}”) img_resized.save(filename) return filename except requests.exceptions.RequestException as e: print(f“网络请求失败: {e}”) return “” # 错误点1返回空字符串掩盖错误 except IOError as e: print(f“文件操作失败: {e}”) return “” # 错误点2同上 except Exception as e: print(f“未知错误: {e}”) return “” # 错误点3捕获所有异常并返回空字符串问题诊断静默失败任何错误都返回空字符串“”。调用方无法区分“下载失败”和“调整大小失败”也无法进行重试或降级处理。日志不足使用print在生产环境中无效。且没有记录关键信息如图片URL、目标路径。资源与状态如果保存文件失败函数已经返回了空字符串但调用方可能以为失败了实际上文件可能已部分创建这里依赖img_resized.save的原子性但通常不理想。重构后的代码基于清晰指令import requests from PIL import Image import os import io import logging from typing import Optional from requests.exceptions import Timeout, ConnectionError, HTTPError logger logging.getLogger(__name__) class ImageProcessingError(Exception): 自定义异常用于标识图片处理过程中的业务错误。 pass def download_and_resize_image( image_url: str, save_dir: str, width: int, height: int ) - str: 下载图片调整大小保存到本地目录。 成功时返回保存的文件路径。 失败时抛出 ImageProcessingError 或其子类异常。 Raises: ImageProcessingError: 图片处理过程中的任何业务逻辑错误。 ValueError: 参数无效。 # 参数校验应放在最前面 if not os.path.isdir(save_dir): raise ValueError(f“保存目录不存在或不是目录: {save_dir}”) if width 0 or height 0: raise ValueError(“宽度和高度必须为正整数”) filename None try: # 1. 下载图片 - 可能抛出 requests.exceptions.RequestException logger.info(f“开始下载图片: {image_url}”) response requests.get(image_url, timeout(3.05, 10)) # 连接超时和读取超时分开设置 response.raise_for_status() # 如果HTTP状态码不是200抛出HTTPError image_data response.content logger.debug(f“图片下载成功大小: {len(image_data)} bytes”) # 2. 调整大小并保存 - 可能抛出 PIL.UnidentifiedImageError, IOError等 with Image.open(io.BytesIO(image_data)) as img: img_resized img.resize((width, height), Image.Resampling.LANCZOS) # 生成安全的文件名 original_name os.path.basename(image_url) safe_name “”.join(c for c in original_name if c.isalnum() or c in (‘.’, ‘-’, ‘_’)).rstrip() if not safe_name: safe_name “image” filename os.path.join(save_dir, f“resized_{safe_name}”) # 先保存到临时文件再原子性地移动到目标文件名避免文件损坏 temp_filename filename “.tmp” img_resized.save(temp_filename) os.replace(temp_filename, filename) # 原子操作 logger.info(f“图片处理并保存成功: {filename}”) return filename except (Timeout, ConnectionError) as e: # 网络层面错误可能是暂时的适合重试 logger.warning(f“网络连接失败图片URL: {image_url}“, exc_infoTrue) raise ImageProcessingError(f“无法连接到图片服务器: {image_url}”) from e except HTTPError as e: # HTTP错误如404, 500等需要业务逻辑处理 status_code e.response.status_code if e.response else “Unknown” logger.error(f“图片下载HTTP错误 ({status_code}), URL: {image_url}“, exc_infoTrue) if status_code 404: raise ImageProcessingError(f“图片资源不存在: {image_url}”) from e else: raise ImageProcessingError(f“图片服务器返回错误 ({status_code}): {image_url}”) from e except (IOError, OSError) as e: # 本地文件系统错误 logger.error(f“文件系统操作失败目标路径: {filename or save_dir}“, exc_infoTrue) # 尝试清理可能残留的临时文件 if filename and os.path.exists(filename “.tmp”): try: os.remove(filename “.tmp”) except OSError: pass raise ImageProcessingError(f“无法保存图片到本地: {e}”) from e except Exception as e: # 其他未预期的异常如PIL库内部错误 logger.critical(f“图片处理过程中发生未预期错误URL: {image_url}“, exc_infoTrue) # 同样需要清理临时文件 if filename and os.path.exists(filename “.tmp”): try: os.remove(filename “.tmp”) except OSError: pass raise ImageProcessingError(f“图片处理内部错误”) from e重构要点解析明确的失败信号函数现在通过抛出ImageProcessingError来明确表示失败。调用方必须使用try-except来处理无法忽略。精细化的异常分类区分了网络超时、HTTP错误、文件IO错误等调用方可以根据异常类型决定重试、降级还是直接报错。丰富的上下文日志使用结构化的日志记录器记录了操作步骤、关键参数和完整的异常堆栈。资源与状态安全引入了临时文件原子重命名的模式确保不会留下半成品文件。在异常处理中也加入了临时文件的清理逻辑。保留了原始异常链使用raise ... from e语法保留了异常的根因便于调试。6. 总结与AI协作的异常处理心法让AI补异常处理就像让一个极其勤奋但缺乏经验的实习生干活。你不能只说“把这里处理一下”而必须给出清晰、明确、带有上下文和约束条件的“工作说明书”。核心心法三条失败必须可见永远不要用返回一个“正常值”如null、空对象、默认值来替代抛出异常或返回错误结果。让错误在调用链上尽早暴露是构建健壮系统的基础。处理你该处理的传播你该传播的只捕获那些你确切知道如何恢复或转换的特定异常。对于其他异常特别是表示程序bug如NullPointerException或系统严重错误如OutOfMemoryError的应该允许它们向上传播直到被全局处理器捕获或导致服务失败这样你才能发现并修复它们。日志是给“人”看的异常是给“程序”用的日志用于事后追溯和监控报警异常用于控制程序当前的执行流。不要混淆两者的职责。记录日志后依然要做出正确的“抛”或“返”的决定。最后记住一句老话“错误处理不是事后添加的装饰而是程序设计时就必须考虑的核心逻辑。”在给AI下指令前花几分钟思考清楚上面提到的几个问题你得到的代码质量会天差地别。AI是强大的杠杆但杠杆的方向始终掌握在你这端。