一份能用的 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 秒实验
- 翻一个你最近修的 bug,试着补全那五个字段。 补不出来的那一个,就是你当时漏掉的那一步。
- 把你项目里最近一次的报错日志按「Traceback 到结尾」截一遍。 数一下截掉多少行,其中有没有一行是有用的。
- 找一条你写过的复现步骤,检查里面有没有「几下」「几次」「随便」。 有就说明它还不能被执行。
常见误区
- 「用户说得越详细越好。」 用户说不详细,这是常态。 你的活是把他那句话补成五个字段,不是要求他先学会写 bug 报告。
- 「日志带上全部信息最保险。」 带全了没人看。 三百行和四十七行的区别不是完整度,是会不会被真的读一遍。
- 「能手动点出来就算复现了。」 手动点是一次性的。 复现的意义在于「能重复」,不能重复的复现,修完之后你还是不知道好没好。
自测三问(点开对答案)
1. 五个字段里哪几个是用户不会主动给的? —— **除了「现象」,其余四个都得你问。** **所以问 bug 的本质是补齐这四个字段。** 2. 一条步骤算不算步骤,尺子是什么? —— **能不能写成一条命令。** **带「几下」「几次」「随便」的,都还不能执行。** 3. 日志为什么要从 Traceback 截到结尾? —— **因为错误在结尾,而栈是自下而上读的。** **从开头截恰好扔掉现场,跳中间就断了因果。**下一篇讲范围:把「只准改这两个文件」写进任务,再拿它当尺子量 diff。
- ☐ (本篇完,下一篇 10.2 约束修改范围:防止「顺手」重构)