
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 输出发脾气了而是把调教重点放在提示词和规范约束上。让一个保守的初级工程师变成符合团队要求的工程师靠的是清晰的规范和不断的 reviewAI 也一样。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.123level日志级别ERROR / WARN / INFO / DEBUGtraceId链路追踪 IDa3f2b9c0d1module模块名file-downloadbizCode业务错误码FILE_NOT_FOUNDmessage可读消息文件不存在使用默认列表stack异常堆栈异常类 堆栈首行kv业务上下文path/tmp/x.txt, userIdu_1024规范里还要写清楚ERROR 必须包含 stackWARN 可以只带 messageINFO 用于流程节点绝不携带异常堆栈所有日志不记录身份证号、手机号等敏感字段。有了这个底子AI 生成的日志代码才不至于各写各的。4.2 第二步设计错误码和上下文模型很多团队日志乱是因为错误码体系是乱的。我在项目里习惯给每个业务模块定义错误码前缀比如文件模块是FILE_权限模块是AUTH_支付模块是PAY_。设计错误码之后再把“异常 → 错误码 → 日志级别 → 用户可读信息”的映射关系做成提示词的一部分。比如文件下载模块异常情况错误码日志级别处理方式文件不存在FILE_NOT_FOUNDWARN返回空列表无读取权限FILE_PERMISSION_DENIEDERROR抛出业务异常磁盘 IO 故障FILE_IO_ERRORERROR抛出系统异常并告警文件大小超限FILE_SIZE_EXCEEDEDWARN返回参数错误有了这个映射表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_infoe, ) 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_infoe, ) raise return data注意几个细节FileNotFoundError 用 WARNPermissionError 和 OSError 用 ERROR异常信息通过exc_infoe保留堆栈业务异常用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 块里。原因很简单训练语料里大量教材示例用 printStackTraceAI 学了个正着。我的处理办法是在提示词里加一句“禁止使用 print 或 printStackTrace日志必须通过 logger 输出”同时用静态检查工具兜底。如果项目有统一日志封装可以直接把LoggerFactory.getLogger的用法贴给 AI让它按封装类来。实测下来加提示词之后出现 printStackTrace 的概率能降到几乎为零。5.2 日志全是 info看不到 error如果你发现 AI 生成的代码里全是 info连异常场景都用 info 记录别急着怪它懒。它很可能是怕 error 触发告警。我之前处理过一段 AI 生成的回调处理代码AI 把“验签失败”“重复回调”“数据库写入失败”全部打成了 info原因写的是“不阻塞主流程”。这种问题靠提示词就能拉回来明确告诉 AIerror 级别用于“需要人工介入的故障”并给出几个场景例子。同时要求它在输出前自查“是否把所有故障都压成了 info”。如果真的想让 AI 形成习惯可以在日志规范里加一条每段代码里至少包含一个 error 或 warn 级别的日志分支。反过来逼它思考异常路径。5.3 异常链丢失只见 message 不见堆栈AI 特别习惯写logger.error(xxx e.getMessage())或者logger.error(str(e))。这在最坏情况下只能看到一句话堆栈全部丢失线上排查等于盲人摸象。我反复踩过这个坑最后在提示词里固定了一段话“所有 error 日志必须携带异常堆栈Python 使用 exc_infoTrueJava 使用 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 协作写业务代码不妨从这个角度去调整提示词和规范文档省下来的排查时间会非常可观。