🛠️ 第10章 场景二:修复 bug、补测试、做回归
📄 一句话概括

一份能用的 bug 报告有五个字段:现象、复现步骤、期望与实际、环境、影响面。 每条步骤都得能被一条命令执行——带「几下」「几次」「随便」的,都不算步骤。 日志从最后一个 Traceback 截到结尾,别从开头截,也别跳中间。

📝 本章和前面几章的关系

上一篇收尾在「先补验证」。这一篇开始修 bug。 修 bug 的第一步不是看代码,是把它变成一件能重复的事。 为什么这一步总被跳过? 因为「复现」感觉像准备工作,不像正事。 但没有复现,你连「修好了没有」都判断不了—— 你只能靠再试一次,而「再试一次」不是一个判据。 Demo 拿一段用户原话和一份结构化报告对照,再演示日志该怎么截: 三百行日志里,有用的只有最后那四十几行,剩下的全是心跳。

一、一份能用的 bug 报告有五个字段

先说清楚「能用」的标准:换个人拿着它,能不能在不问你的情况下复现。

字段 写什么 缺了会怎样
现象 一句话说发生了什么 不知道哪一步出的事
复现步骤 能逐条执行的命令 只能靠猜
期望与实际 该是什么、现在是什么 不知道算不算 bug
环境 版本、平台、数据形态 本地复现不出来
影响面 谁受影响、多久一次 不知道要不要连夜修

用户主动给的只有第一个字段。 「下单的时候老是报错」——这就是全部信息。 剩下四个都得问,而问的方式决定了你要问几轮。 有一种问法能在一轮内问完:不问「你能详细说说吗」,而是把你猜的版本写出来,让他确认或纠正。 「你是在点提交之后看到白页的,对吗?只填了姓名,没填备注,对吗?」 这种问法他只需要点头,不需要组织语言。 第五个字段最容易被漏,但它决定优先级。 「每天三次」和「上个月遇到一次」,同一个 bug 的处理方式完全不同。

二、每条步骤都得能被执行

判断一条步骤算不算步骤,有个很硬的尺子:能不能写成一条命令。

写着像步骤的 为什么不算
打开页面点几下 没说点哪、点什么
多试几次能复现 「几次」不是次数
随便一个订单都行 「随便」没有边界

这三句话里各有一个不能被程序执行的词:几下、几次、随便。 它们不是表达不清,是信息缺失——写的人自己也不知道是第几次、是哪个订单。 Demo 里那份结构化报告四条步骤,每条都配了「该看到什么」。 这一点比「怎么写」更重要: 有预期结果,才能判断这一步是「跑过去了」还是「跑出问题了」。 所以复现脚本的骨架是固定的: 准备环境、灌数据、触发、断言看到的东西。 四段,缺哪段都不算复现。 这也是为什么「复现」值得单独成篇: 它既是给人的报告,也是给 Agent 的输入。 你把四条命令交给它,它就能自己跑一遍;你给它一段吐槽,它只能猜。

三、日志要截哪一段

Demo 里那段日志三百行,有用的只有最后四十几行。 前面两百五十行是同构的心跳日志,每行只有时间戳和用户号。

截法 留下什么 错在哪
全量 三百行全带上 心跳把现场淹了
开头三十行 心跳的前三十行 恰好截掉了错误
Traceback 到结尾 异常栈与收尾 没有

第二行是很多人默认的做法,「先看开头」。 但日志是按时间写的,错误在结尾—— 从开头截,等于把最有用的部分扔掉,只留下了证明「服务在跑」的证据。 正确规则:从最后一个 Traceback 截到结尾,中间不跳。 因为栈是自下而上读的,跳一段就断了因果。 Demo 里三百行截到四十七行,token 从三千八降到四百二,九倍。 如果实在太长,留头五行加尾二十行,中间标一句「省略 N 行同构调用」。 但最后一个异常那几行不能省——那是唯一能直接告诉你「哪一行、什么错」的地方。

四、动手 Demo:一份能用的 bug 报告有五个字段

先看一段用户原话,逐字段标出它的信息缺口;再看一份结构化的四条步骤。 最后拿一段三百行的真日志,三种截法各截一遍,量行数和 token。 日志是程序生成的——两百五十行心跳加一段真 traceback,截法是脚本按规则切的,不是手工挑的。

raw_lines = RAW_LOG.split("\n")
idx = [i for i, x in enumerate(raw_lines) if x.startswith("Traceback")]
start = idx[0] if idx else 0      # 从最后一个 Traceback 开始
keep = raw_lines[start:]          # 截到结尾,中间不跳

print("全量   %d 行 %d token" % (len(raw_lines), tokens(RAW_LOG)))
print("开头30 %d 行 %d token" % (30, tokens("\n".join(raw_lines[:30]))))
print("截异常 %d 行 %d token" % (len(keep), tokens("\n".join(keep))))

实跑输出:

--------------------------------------------------------------------
一份能用的 bug 报告有五个字段
--------------------------------------------------------------------

  用户的原话是一段吐槽,工程上要的是能跑的东西。这两者中间隔着五个字段。

  ① 用户原话,和它的信息缺口
  --------------------------------------------------------------------

  [用户原话 · 未结构化]
  | 「下单的时候老是报错,点提交就跳一个白页,试了好几次都不行,你们赶紧看看。」
  这段话里没有一句能被直接执行。

| 字段 | 原话里有没有 | 缺了会怎样 |
|---|---|---|
| 现象 | 有(白页) | 知道出事了,不知道哪一步 |
| 复现步骤 | 没有 | 只能靠猜 |
| 期望 vs 实际 | 没有 | 不知道算不算 bug |
| 环境 | 没有 | 本地复现不出来 |
| 影响面 | 没有(「老是」) | 不知道要不要连夜修 |

  五个字段里,只有第一个是用户会主动给的。剩下四个都得问,而问的方式决定了你问几轮。

  ② 结构化版:四条复现步骤每条都能执行
  --------------------------------------------------------------------
| # | 做什么 | 怎么执行 | 该看到什么 |
|---|---|---|---|
| 1 | 起一个干净的服务 | python -m app.main | 服务在 8000 就绪 |
| 2 | 灌一条最小数据 | python tools/seed.py --one | 打印 order_id |
| 3 | 用空名字下单 | python -m cli POST /order name='  ' | 返回 500 |
| 4 | 复现三次确认稳定 | 重复第 3 步 3 次 | 3 次都 500 |

  四条,全部可执行。这不是「写得更详细」,是「每条都能被一条命令验证」。

  ③ 可执行化的判据
  --------------------------------------------------------------------
| 写着像步骤的 | 为什么不算 |
|---|---|
| 打开页面点几下就行 | 「点几下」没有说清点哪、点什么 |
| 多试几次能复现 | 「几次」不是次数,是不可复现的自白 |
| 随便一个订单都能触发 | 「随便」不是范围,是没有边界 |

  三条的共同毛病:里面都有一个不能被程序执行的词——几下、几次、随便。
  ! 一个简单的自查:把每条步骤抄给一个没参与的人,他能不能不问你就照做。能,就是可执行;
  要问你一句,就不算。

  ④ 日志要截哪一段
  --------------------------------------------------------------------
| 截法 | 行数 | token | 留下什么 |
|---|---:|---:|---|
| 全量 | 299 | 3,880 | 250 行心跳 + 一段 traceback |
| 只留开头 30 行 | 30 | 412 | 250 行心跳的前 30 行 |
| 只留 traceback 到结尾 | 47 | 420 | 异常栈和之后的收尾日志 |

  三种截法里,只有第三种把「出错的现场」留下了。前两种截掉的恰好是有用的那段。
  ! 规则很简单:从最后一个 Traceback 开始截到结尾,中间不跳。
  因为栈帧是自下而上读的,跳一段就断因果。
  如果确实太长,留头五行加尾二十行,中间标一句「省略 N 行同构调用」——但别省掉最后一个异常那几行。

那张字段表要看第二列。 五个字段里只有一个(现象)是用户会主动给的——所以「问 bug」这件事,本质是补齐另外四个。 四条步骤那一段,重点是最后一列「该看到什么」。 没有预期结果,你就判断不了这一步是通过还是失败——而「判断不了」会一路带到修完之后的验证里。 日志那张表是这一篇最实用的部分。 三种截法里,只有第三种把现场留下了。 第二种「只留开头」是最常见的错,而且错得很自然—— 因为人读东西的习惯是从头读,但日志不是给人从头读的。 九倍这个数不是重点,重点是从三百行到四十七行之后,你还会真的去看它。 三百行没人看,四十七行会看——这中间的差别不是压缩率,是「会不会被读」。

30 秒实验

  1. 翻一个你最近修的 bug,试着补全那五个字段。 补不出来的那一个,就是你当时漏掉的那一步。
  2. 把你项目里最近一次的报错日志按「Traceback 到结尾」截一遍。 数一下截掉多少行,其中有没有一行是有用的。
  3. 找一条你写过的复现步骤,检查里面有没有「几下」「几次」「随便」。 有就说明它还不能被执行。

常见误区

  1. 「用户说得越详细越好。」 用户说不详细,这是常态。 你的活是把他那句话补成五个字段,不是要求他先学会写 bug 报告。
  2. 「日志带上全部信息最保险。」 带全了没人看。 三百行和四十七行的区别不是完整度,是会不会被真的读一遍。
  3. 「能手动点出来就算复现了。」 手动点是一次性的。 复现的意义在于「能重复」,不能重复的复现,修完之后你还是不知道好没好。
自测三问(点开对答案) 1. 五个字段里哪几个是用户不会主动给的? —— **除了「现象」,其余四个都得你问。** **所以问 bug 的本质是补齐这四个字段。** 2. 一条步骤算不算步骤,尺子是什么? —— **能不能写成一条命令。** **带「几下」「几次」「随便」的,都还不能执行。** 3. 日志为什么要从 Traceback 截到结尾? —— **因为错误在结尾,而栈是自下而上读的。** **从开头截恰好扔掉现场,跳中间就断了因果。**

下一篇讲范围:把「只准改这两个文件」写进任务,再拿它当尺子量 diff。

相关笔记