H38 这期,我想聊聊异常处理和日志规范。上周我把一块核心链路的异常处理代码交给 AI 去生成,当时心想:以现在大模型的能力,写个 try-catch、打几个日志还不是手到擒来。结果代码出来之后我盯着屏幕看了半天,越看越不是滋味——AI 比我想象中更保守,保守到让我怀疑它是不是在故意“打太极”。
先说结论:AI 不是不会写异常处理,而是它默认选择了“最不容易被骂”的写法,而不是“最正确”的写法。这不是模型能力不够,而是训练目标和人类工程师的代码评审标准存在偏差。这篇文章我会拿实际生成结果说话,分析 AI 保守的三个典型表现,然后给出我在项目里总结的调教方法:提示词怎么设计、日志规范怎么喂给 AI、静态检查怎么兜底。如果你正在用 AI 辅助写业务代码,或者你负责团队日志规范落地,这篇文章应该能帮你省掉不少排查时间。
1. 我发现 AI 在异常处理上“怂”得有点过分
1.1 测试场景:让 AI 生成文件下载模块
我先交代一下背景。项目里有个文件导出功能,逻辑不复杂:读本地文件、校验大小、转成下载流,过程中可能遇到文件不存在、权限不足、磁盘 IO 异常。我让 AI 直接生成这个函数,要求是“异常处理合理,日志清晰”,没有给更多约束。
AI 给的版本大概长这样:
def download_file(source_path): try: with open(source_path, "rb") as f: data = f.read() except Exception as e: logger.info("download failed: %s", e) return None return data这段代码能用,但放在生产环境就是事故。文件不存在和磁盘损坏被混在一个 except 里,错误信息用 info 级别打出去,然后返回 None,调用方根本不知道到底是“文件不存在”还是“磁盘坏了”,只会看到一个空结果。我把这段代码拿给团队里一个刚入职的同学看,他的第一反应是:“这代码看起来没什么问题啊,捕获了异常还打了日志。”问题就出在这里——AI 给的写法是“看起来没问题”,而不是“真正可靠”。
1.2 保守的三种典型表现
我把 AI 生成过的异常处理代码归了一下类,发现它的保守集中在三个地方:
第一,异常捕获过宽。AI 特别习惯写except Exception,把所有异常吞进同一个分支。我让 AI 生成依赖外部服务的调用,它甚至写了except Exception后 return None,把连接超时、认证失败、数据解析错误全混在一起。这种写法在教材里常见,但在真实系统里等于屏蔽了故障。
第二,日志级别严重偏低。明明是不可恢复的故障,AI 经常用 info 或者压根不打日志。我让 AI 给支付回调生成日志,它把“验签失败”记成 warning,把“重复回调”记成 info,把“DB 写入失败”记成 error 之后,后面又跟了一句“不影响主流程”。在告警规则里,error 才是触发通知的门槛,AI 为了“不打扰人”,把所有错误都压到了 warn 以下。
第三,倾向于吞异常而不是抛出。AI 特别不愿意让异常继续向上传播。它宁可返回一个默认值、空列表、None,也不愿意让上层感知到失败。表面上看程序没有崩溃,实际上数据错了、流程断了,而且很难追溯到源头。
1.3 这种保守带来的真实问题
我在本地试过把 AI 写的异常处理代码塞进一个压测脚本里,模拟文件读取异常,结果所有失败都被静默吞掉,监控面板上看不到任何一条 error 日志,只有业务数据对不上时才发现问题。排查成本翻了至少三倍。
更麻烦的是日志的检索性。AI 打的日志缺乏关键上下文,比如没有 traceId、没有错误码、没有文件名、没有异常堆栈。生产环境一天几千万条日志,单纯一句“download failed: No such file or directory”,根本定位不了是哪个用户、哪个批次、哪台机器上的哪个文件。这就引出一个核心观点:AI 帮我们写代码,但我们对异常处理和日志规范的思考不能省略。它给的是一种“统计意义上的安全”,而不是“工程意义上的正确”。
2. 为什么 AI 会这么保守:从训练到对齐的“不敢错”
2.1 训练语料的统计惯性
AI 不是天生保守,而是它从训练数据里学到的“主流”就是保守的。开源代码里充满了except Exception: pass、print(e)、return None,尤其是教程项目、示例代码和短期脚本,异常处理大多处于“能跑就行”的状态。大模型在生成时本质上是在做 token 级别的概率采样,它会优先选训练语料中出现频率最高的模式,而不是最优质的实现。
我做过一个小统计:让同一个模型生成 20 段不同的异常处理代码,其中 14 段都包含except Exception,只有 3 段区分了具体的异常类型。这不是模型不会,而是它觉得“写 Exception 最安全”——因为语料里就是这么多。
2.2 对齐训练带来的“讨厌风险”
这里要说一点背景。语言模型在上线前都会做一轮对齐,目标之一是让模型给出的回答更“无害”。在这个目标的驱动下,模型会倾向于避开可能引发争议、报错或无法编译的形式。具体到代码生成,它就表现为:宁可多捕获一些异常,也不想漏掉某个异常导致程序崩溃;宁可少打 error 日志,也不想因为误报打扰运维。
这不是模型故意偷懒,而是它在用“不惹麻烦”的策略来满足对齐目标。但工程代码恰恰需要适当的“惹麻烦”:该抛的异常要抛,该打 error 的要打,该中断流程的要中断。我们把问题拆开看,就是 AI 在“让程序继续跑”和“让问题暴露出来”之间,总是选前者。
2.3 AI 对日志规范的防御式解读
另一个让我意外的点是,AI 对日志规范的理解也偏保守。我让它按“日志分级明确、便于排查”的标准生成日志代码,它反而把所有日志都降级了。原因很好理解:训练语料和网上博客反复强调“error 级别会触发告警,不要乱用”,AI 学到的教训是“少报错才安全”,所以它就尽量不用 error,甚至把该报 error 的场景也压成 warn 或 info。
这里需要澄清一个误区:日志级别不是越少越好,而是要和动作对应。error 表示“需要人马上看”,warn 表示“不需要马上看但值得注意”,info 表示“正常流程节点”。AI 没有我们团队具体的告警策略,它只能选择一个在所有团队里都不算错的方案——那就是少用 error,最好别用。
2.4 这不是坏事,但需要管理
我并不是说 AI 的保守一无是处。在快速原型、一次性脚本、异常影响面小的场景里,保守的异常处理反而能避免程序崩溃。但在核心业务链路里,这种保守就是隐患。所以真正的问题不是“AI 太笨”,而是“我们没告诉 AI 这条链路的异常策略是什么”。
想明白这一点之后,我就不再对着 AI 输出发脾气了,而是把调教重点放在提示词和规范约束上。让一个保守的初级工程师变成符合团队要求的工程师,靠的是清晰的规范和不断的 review,AI 也一样。
3. 调教 AI 的正确姿势:提示词、规范与护栏
3.1 提示词里写清楚异常边界
我试过无数次“直接让 AI 写一个健壮的下载模块”,结果都不理想。后来发现,不是提示词不够“宏大”,而是缺少可执行的边界。AI 需要知道:哪些异常是可恢复的,哪些是不可恢复的;可恢复的怎么处理,不可恢复的是抛出还是返回特定状态;日志打到什么级别。
我现在的提示词模板大致是这样:
请实现一个文件下载函数,满足以下异常处理要求: 1. FileNotFoundError 视为业务异常,记录 warning 日志,返回错误码 FILE_NOT_FOUND。 2. PermissionError 视为权限问题,记录 error 日志,抛出自定义 BusinessException(BIZ_PERMISSION_DENIED)。 3. OSError 视为系统异常,记录 error 日志并打印堆栈,原样抛出。 4. 所有日志必须包含参数 path、当前用户 userId、方法名。 5. 禁止 catch Exception 这种过宽捕获,禁止吞掉异常,禁止返回 None 表示失败。把边界写清楚之后,AI 生成的质量明显上升。它不再默认“全 catch 住”,而是知道要区分异常类型并采取不同动作。这里的关键在于,异常处理策略本来就是业务决策,不能指望 AI 替我们做决策。我们要做的是把决策告诉它。
3.2 把团队日志规范直接喂给 AI
如果你只是给 AI 一句话“按规范写日志”,它大概率还是按自己那套来。最好的办法是把团队的日志规范文档关键部分直接复制进提示词。我通常会把下面这几条放进去:
- 日志格式:
时间戳 [日志级别] traceId 类名 - 业务消息 上下文字段 - 级别定义:error 表示需要人工介入的故障;warn 表示可恢复但值得关注;info 表示关键流程节点;debug 表示调试细节。
- 禁止事项:禁止打日志不带异常堆栈;禁止使用
print;禁止在日志中拼接敏感个人信息。
有团队规范做锚点,AI 的“防御式保守”就会被约束到正确的方向上。它不再花心思猜该用 info 还是 error,因为你已经给了它判断标准。
3.3 给例子,不如给原则
我也试过在提示词里贴一大堆“正确示例”,比如优秀的异常处理代码片段。效果有,但不稳定。贴三个以上例子时,AI 容易“抄过头”,把示例里和当前场景无关的细节也搬过来。更稳的做法是给一两个正反例,同时把判断原则写清楚。
比如我常用的一对例子:
# 反例:吞掉异常,日志无堆栈,返回 None except Exception as e: logger.info("failed: %s", e) return None # 正例:区分可恢复与不可恢复,日志带上下文 except FileNotFoundError as e: logger.warning("file not found, use empty", extra={"path": path}) return []然后明确告诉 AI:“在其余场景里套用这个判断逻辑,而不是复制代码”。这样 AI 更可能学会“决策方式”,而不是“某个具体写法”。
3.4 启动前约定不可触碰的底线
最后这道护栏是审查清单。我会在提示词里要求 AI 在输出最后附上一段“自查说明”,逐条确认自己没有违反以下规则:
- 是否使用过宽的
except Exception? - 是否在捕获异常后没有记录日志?
- 是否在日志中丢失异常堆栈?
- 是否用 info 级别记录了 error 场景?
- 是否返回了容易让调用方混淆的默认值?
这一步强烈建议保留。AI 的自查说明不是给我们看的,是给 AI 自己看的。它会为了写出“没违反规则”的自查,主动修正前面的代码,相当于在生成阶段做了一轮自我约束。实测下来,加上自查要求后,AI 生成的异常处理代码出现“吞异常”的次数大幅下降。
4. 实操案例:从保守 AI 到合格初稿的完整流程
4.1 第一步:准备一份可复用的日志规范
这里我给一个简版规范,适用于大多数中大型后端项目。你可以直接抄进提示词或团队文档里:
| 字段 | 说明 | 示例 |
|---|---|---|
| ts | 毫秒级时间戳 | 2025-01-12 21:30:15.123 |
| level | 日志级别 | ERROR / WARN / INFO / DEBUG |
| traceId | 链路追踪 ID | a3f2b9c0d1 |
| module | 模块名 | file-download |
| bizCode | 业务错误码 | FILE_NOT_FOUND |
| message | 可读消息 | 文件不存在,使用默认列表 |
| stack | 异常堆栈 | 异常类 + 堆栈首行 |
| kv | 业务上下文 | path=/tmp/x.txt, userId=u_1024 |
规范里还要写清楚:ERROR 必须包含 stack;WARN 可以只带 message;INFO 用于流程节点,绝不携带异常堆栈;所有日志不记录身份证号、手机号等敏感字段。有了这个底子,AI 生成的日志代码才不至于各写各的。
4.2 第二步:设计错误码和上下文模型
很多团队日志乱,是因为错误码体系是乱的。我在项目里习惯给每个业务模块定义错误码前缀,比如文件模块是FILE_,权限模块是AUTH_,支付模块是PAY_。设计错误码之后,再把“异常 → 错误码 → 日志级别 → 用户可读信息”的映射关系做成提示词的一部分。
比如文件下载模块:
| 异常情况 | 错误码 | 日志级别 | 处理方式 |
|---|---|---|---|
| 文件不存在 | FILE_NOT_FOUND | WARN | 返回空列表 |
| 无读取权限 | FILE_PERMISSION_DENIED | ERROR | 抛出业务异常 |
| 磁盘 IO 故障 | FILE_IO_ERROR | ERROR | 抛出系统异常并告警 |
| 文件大小超限 | FILE_SIZE_EXCEEDED | WARN | 返回参数错误 |
有了这个映射表,AI 不再需要自己“猜测”每个异常该怎么处理,它的保守倾向也就失去了土壤。因为它一旦乱写,很容易违背映射表中的内容。
4.3 第三步:让 AI 按规范生成模块代码
把规范、错误码表和提示词模板组合到一起,生成效果会明显改观。我让同一个 AI 重新生成文件下载模块,这次它的输出已经是这个水准:
def download_file(source_path: str, user_id: str) -> list[bytes]: try: with open(source_path, "rb") as f: data = f.read() except FileNotFoundError as e: logger.warning( "file not found, use default list", extra={"error_code": "FILE_NOT_FOUND", "path": source_path, "user_id": user_id}, ) return [] except PermissionError as e: logger.error( "permission denied", extra={"error_code": "FILE_PERMISSION_DENIED", "path": source_path, "user_id": user_id}, exc_info=e, ) raise BusinessException("FILE_PERMISSION_DENIED") from e except OSError as e: logger.error( "unexpected io error", extra={"error_code": "FILE_IO_ERROR", "path": source_path, "user_id": user_id}, exc_info=e, ) raise return data注意几个细节:FileNotFoundError 用 WARN,PermissionError 和 OSError 用 ERROR;异常信息通过exc_info=e保留堆栈;业务异常用raise ... from e保留异常链;上下文里带了 path 和 user_id。这套写法放在生产环境里,至少能保证故障可感知、可定位、可追溯。虽然它还是有一些地方可以优化,比如文件大小校验没有做,但作为初稿已经足够合格。
4.4 第四步:用静态检查工具兜底
人不可能每次都逐行 review AI 代码,所以我把一部分检查交给了自动化工具。项目是 Python 的话,我会在 CI 里加 flake8 和 pylint 规则,强制检查except Exception、print、raise后无日志等常见问题。日志侧,我会写一个简单的自定义规则,利用 AST 解析源代码,检查 logger 调用是否带exc_info或stack_info。
在代码审查阶段,我还有一个习惯:让 AI 生成自己的代码评审意见。提示词里明确要求“指出异常处理中所有可能引发生产问题的地方,并给出修改建议”。听起来很绕,但效果不错——AI 在“挑刺模式”下并不保守,它会指出自己生成代码里日志级别不当、异常范围过宽的问题。我们只需要把这份评审意见当参考,再决定是否采纳。等于用 AI 的两种模式互相制衡。
5. 异常处理与日志规范 AI 化后的常见问题速查
5.1 AI 总是用 print 或 e.printStackTrace()
这个问题在 Java 项目里特别常见。AI 生成代码时冷不丁冒出一句e.printStackTrace(),通常发生在 catch 块里。原因很简单:训练语料里大量教材示例用 printStackTrace,AI 学了个正着。
我的处理办法是在提示词里加一句“禁止使用 print 或 printStackTrace,日志必须通过 logger 输出”,同时用静态检查工具兜底。如果项目有统一日志封装,可以直接把LoggerFactory.getLogger的用法贴给 AI,让它按封装类来。实测下来,加提示词之后出现 printStackTrace 的概率能降到几乎为零。
5.2 日志全是 info,看不到 error
如果你发现 AI 生成的代码里全是 info,连异常场景都用 info 记录,别急着怪它懒。它很可能是怕 error 触发告警。我之前处理过一段 AI 生成的回调处理代码,AI 把“验签失败”“重复回调”“数据库写入失败”全部打成了 info,原因写的是“不阻塞主流程”。
这种问题靠提示词就能拉回来:明确告诉 AI,error 级别用于“需要人工介入的故障”,并给出几个场景例子。同时要求它在输出前自查“是否把所有故障都压成了 info”。如果真的想让 AI 形成习惯,可以在日志规范里加一条:每段代码里至少包含一个 error 或 warn 级别的日志分支。反过来逼它思考异常路径。
5.3 异常链丢失:只见 message 不见堆栈
AI 特别习惯写logger.error("xxx" + e.getMessage())或者logger.error(str(e))。这在最坏情况下只能看到一句话,堆栈全部丢失,线上排查等于盲人摸象。我反复踩过这个坑,最后在提示词里固定了一段话:“所有 error 日志必须携带异常堆栈,Python 使用 exc_info=True,Java 使用 logger.error(msg, e) 而不是在 msg 里拼接 e.getMessage()。”
代码生成之后,我会快速扫一眼 catch 块里 logger 的第二个参数。只要发现白底黑字只有一个字符串参数,基本就是丢堆栈了,直接返工。
5.4 怎么让 AI 自己给自己挑毛病
最后分享一个我最常用的技巧:让 AI 双重输出。第一次生成代码,第二次让它对照异常处理和日志规范给代码挑毛病。挑毛病时 AI 会变得异常严格,有时候甚至比人类 review 还苛刻。它可能会指出“这里应该单独 catch ZeroDivisionError”,“这条日志缺少 traceId”,“这个返回值应该用 Optional 而不是 None”。虽然不一定每一条都正确,但能帮我们快速聚焦到高风险点。
为了这个流程更好用,我会在项目里维护一份“异常处理检查清单 Markdown 文件”,里面写着团队约定好的规则。每次调 AI 之前,把这份清单复制进提示词,要求它“先阅读清单,再写代码,最后对照清单自查”。这比每次重新描述规范省事得多,而且 AI 的输出稳定性会随着清单的完善持续提升。
回到开头那个话题。AI 在异常处理和日志规范上确实比我预想的保守,但保守本身不是缺陷,它是一种需要被管理的行为模式。我现在的态度是:把它当成一个特别认真、特别怕犯错但缺乏业务判断的初级工程师,给它足够清晰的规范和边界,它的产出就会从“可用但危险”变成“可靠且合规范”。如果你也在跟 AI 协作写业务代码,不妨从这个角度去调整提示词和规范文档,省下来的排查时间会非常可观。