系统化调试
何时用
- 运行测试时出现
FAILED/ERROR,或 CI 红了。 - 程序行为与预期不符:输出值错误、接口返回异常状态码、UI 渲染出错。
- 改了一处代码后原本正常的功能突然挂掉(回归)。
- 看到报错信息、异常堆栈、日志中的
Exception/panic/Segfault,不知道从哪里下手。
核心规则
1. 先复现
规则: 在动任何代码之前,先稳定复现问题,写下复现步骤与"期望行为 vs 实际行为"。
为什么: AI 拿到 bug 描述后会立刻联想到"可能是 X 原因"并直接改代码——但如果问题根本无法稳定复现,改动就是在打空拳。更常见的事故是:AI 改了某处,恰好该次运行没触发 bug,就宣称"已修复",下次复现时问题依然存在。
怎么做:
- 明确记录触发条件:输入数据、环境变量、调用顺序、并发时序等。
- 写一个最小复现脚本或测试用例,能稳定触发问题再继续。
- 若问题无法稳定复现,先补充日志/断言,下一次触发时收集更多信息,而非盲猜。
2. 读真实报错
规则: 逐字阅读错误信息与完整堆栈,定位第一处出错的文件行号,不跳过、不脑补。
为什么: AI 经常只看错误的最后一行(如 NullPointerException),然后凭直觉猜"是不是哪个对象没初始化"并随意修改。真正的根因往往藏在堆栈中部——例如某个中间件吞掉了原始异常、某个工厂方法返回了错误类型——只读最后一行会让修复方向完全偏离。
怎么做:
- 从堆栈的最顶层(第一次抛出点)开始读,而不是从底层的框架代码开始。
- 遇到"Caused by"或"wrapped"链式异常,追到链条的根源。
- 把报错的关键词(函数名、行号、错误码)直接复制到搜索或代码跳转,不凭记忆定位。
3. 二分缩小范围
规则: 用打印/断点/注释二分法,把问题缩小到最小代码段,再下结论。
为什么: AI 倾向于在读了几十行代码后就"觉得问题在这里",跳过验证直接改。这种直觉经常错:真实 bug 往往在你以为不可能出错的地方。不二分就不改,是避免"修了半天发现改错位置"的唯一可靠方法。
怎么做:
- 把可疑范围一分为二:注释掉后半段,确认前半段输出正确,再检查后半段。
- 在关键中间点插入
print/console.log/assert,确认数据在此时的真实状态。 - 重复缩小,直到能用不超过 10 行代码稳定触发问题,再动手修复。
4. 找根因,不贴补丁
规则: 能解释"为什么会错"之后再动手改代码。禁止用 try/except 吞异常、随机调参、加 || null 等掩盖症状的做法。
为什么: AI 面对报错时最常见的逃生路线就是在外层套一个 try/except,让异常不再抛出,然后声称"问题解决了"。但根因没消除:数据仍然损坏、状态仍然不一致,只是沉默了。这类"修复"在生产环境会演变成更难排查的数据问题或静默错误。
怎么做:
- 改代码前用一句话写下根因假设:「变量
user在首次调用时为None,因为db.find()在记录不存在时返回None而非抛出异常。」 - 修复根因(加校验、修初始化逻辑),而不是在调用处加
try/except掩盖。 - 若确实需要捕获异常,必须在
except块里做有意义的处理(记录、回滚、向上重抛),绝不能空块或仅pass。
5. 改完验证
规则: 用最初的复现步骤逐一确认问题已修复,并确认没有引入新的失败。
为什么: AI 改完代码后习惯性地说"应该好了",但没有真正跑一遍。或者只跑了新加的测试,没跑已有的回归测试集,结果修了一个 bug、破了三个已有功能。
怎么做:
- 重新执行第 1 步写下的复现步骤,确认实际行为与期望行为一致。
- 跑完整测试套件(不只是相关测试),确认无新的
FAILED。 - 若引入了新失败,视为新 bug,回到第 1 步重新走流程,不要在同一次修改里叠加多个"顺手修复"。
正例 / 反例
反例:用 try/except 吞异常声称"已修复"
# 反例 — AI 用空 except 消灭报错,根因未解决
def get_user_age(user_id: int) -> int:
try:
user = db.find_user(user_id)
return user["age"] # user 为 None 时会 TypeError
except Exception:
pass # ❌ 吞掉异常,调用方收到 None,数据链路静默损坏
# 正例 — 定位根因:db.find_user 在记录不存在时返回 None,修复初始化逻辑
def get_user_age(user_id: int) -> int:
user = db.find_user(user_id)
if user is None:
raise ValueError(f"用户 {user_id} 不存在") # ✅ 明确报错,调用方能感知并处理
return user["age"]
反例:不读堆栈、凭直觉改错位置
# 错误日志:
# AttributeError: 'NoneType' object has no attribute 'email'
# File "app/views.py", line 42, in send_welcome
# msg = build_email(request.user)
# File "app/email.py", line 17, in build_email
# return Template(user.email) # ← 第一次出错点在这里
# 反例 — AI 只看到 AttributeError 就在视图层加了防御
def send_welcome(request):
if request.user is None: # ❌ request.user 其实从不为 None,
return # 根因是 build_email 接收了未登录状态下的匿名对象
...
# 正例 — 读完堆栈,定位到 build_email 第 17 行,找到根因:
# 匿名用户对象没有 email 属性,而非 request.user 为 None
def build_email(user):
if not hasattr(user, "email") or user.email is None:
raise ValueError("无法为匿名用户构建邮件") # ✅ 在真正出错点做校验
return Template(user.email)
自查清单
- 已写下稳定的复现步骤,并能在改代码前稳定触发问题。
- 已逐字读完完整堆栈,定位到第一处出错的文件与行号。
- 已通过二分或打印确认问题所在的最小代码段,而非凭直觉猜测。
- 能用一句话解释根因("因为 X,所以 Y"),而不只是描述症状。
- 修复方式针对根因,没有用空
except、|| null、if err != nil { return }等手段掩盖问题。 - 改完后重新执行了最初的复现步骤,确认实际行为与期望一致。
- 运行了完整测试套件,确认没有引入新的失败。