← 全部极客知识库

主流 Agent 入门教程 / CodeBuddy

6. 案例3-从报错到修复

原文备份 · 第 8 / 16 篇 · 查看极客时间原知识库 ↗

本篇目录 · 15 节

来源:CodeBuddy

场景

线上收到工单:有客户说账单金额跟合同对不上。日志里有零星几条 WARNING,但你本地跑一遍,结果看着挺正常。

偶发 bug 是最难搞的一类——复现不了就没法验证修没修好,改完还得担心是不是把别的弄坏了。

这一章我们完整走一遍:从日志定位 → 构造最小复现 → 先写失败测试 → 改代码 → 回归验证 → 评估影响范围。

现场

在 billkit 项目里(上一章做完的那个),素材包给了两份现场材料:

logs/app.log                  220 行,里面有 WARNING 和 ERROR
data/usage-2024-03.csv        3 月的用量数据

先自己看一眼日志,心里有个底:

macOS / Linux / WSL2

grep -E "WARNING|ERROR" logs/app.log | head -20

Windows PowerShell

Select-String -Path logs\app.log -Pattern "WARNING|ERROR" | Select-Object -First 20

你会看到类似:

2024-04-01 02:15:00,123 WARNING  billkit 客户 C1016 账单金额高于预期,用量档位疑似未生效
2024-04-01 03:21:00,456 ERROR    billkit 收到工单:账单金额与合同不符,请核查阶梯计价

注意那个词:"疑似未生效"。前人的猜测未必对——待会儿你会看到真正的根因不是档位问题。这很常见:日志里的判断经常是错的,别照着日志的结论去修。

第一步:先让它读现场,但别让它改

读一下 logs/app.log 和 billkit/pricing.py、billkit/processor.py。

工单说有客户账单金额跟合同对不上。请你:
1. 从日志里找出被点名的客户 ID
2. 列出你对根因的三个假设,按可能性排序,每个假设说明依据
3. 对每个假设,给出一个能证伪它的具体命令或代码

先不要改任何代码。

"给出能证伪它的具体方法"这句是关键。它会逼自己把假设变成可验证的东西,而不是停留在"可能是档位没生效"。

第二步:用真实数据复现

它多半会建议跑一遍真实数据,把可疑客户挑出来。我们就这么干:

在项目根目录写 find_anomaly.py(使本地 billkit 包可导入),
对 data/usage-2024-03.csv 跑一遍完整流程,对每个客户输出:
客户号、套餐、用量、小计、折扣率、折扣额、税额、应付。

然后对照 pricing.py 里文档字符串写的折扣规则("达到"门槛即享),
找出实际折扣与规则不符的客户。把结果做成表格打印出来。

这个脚本是临时排查用的,写完告诉我,我会决定要不要删。

跑:

python find_anomaly.py

你会得到这样的输出(数字以实际为准):

客户     套餐    用量      小计      折扣率  折扣额   应付
C1016    basic  70000    1000.00   0.00    0.00    1060.00   ← 规则说满1000应减3%
C1017    pro    200000   2000.00   0.05    100.00  2014.00   ← 规则说满2000应减10%

找到了。只有这两个客户有问题,其他 12 个都正常——这就是"偶发"的真相:不是随机出现,是只有金额恰好等于门槛时才出现。

这类 bug 靠"多跑几次碰运气"是永远复现不出来的。正确做法是找出触发条件的边界,而不是靠运气。

第三步:把根因挖出来

现在让它定位代码:

复现了:C1016(basic,小计恰好 1000.00)应该享受 3% 折扣但实际是 0;
C1017(pro,小计恰好 2000.00)应该是 10% 但实际是 5%。
其他客户都对。

看 pricing.py 的 volume_discount,告诉我:
1. 根因是哪一行
2. 为什么只有"恰好等于"时才触发
3. 这个写法在别的地方有没有同样的隐患(比如 tiered_price 里)
4. 改动会影响哪些调用方

它会指出:volume_discount 里用的是 subtotal > 1000 和 subtotal > 2000,而合同写的是"达到即享",应该是 >=。恰好等于时不满足 >,所以跳到了下一档。

顺手让它查了第 3 点——同一类错误往往不止一处。这比只修眼前这一个值钱得多。

第四步:先写失败测试,再改代码

上一章我们已经提交过一组失败的边界测试了(具体名称以你的 tests/test_pricing.py 为准)。如果你跳过了上一章,现在补一个:

给 pricing.py 的折扣门槛写一个 pytest 用例,放在 tests/test_pricing.py:
- basic 套餐,subtotal 恰好 1000.00,期望折扣率 0.03
- basic 套餐,subtotal 恰好 3000.00,期望折扣率 0.08
- pro 套餐,subtotal 恰好 2000.00,期望折扣率 0.10
- pro 套餐,subtotal 恰好 5000.00,期望折扣率 0.15

先跑一遍确认它是红的,把失败输出给我看。别改实现代码。
pytest tests/test_pricing.py -k discount -v

看到红色。FAILED ... assert 0.0 == 0.03。

为什么坚持"先写失败测试"? 三个理由:

  1. 它证明你真的复现了。 测试红 = bug 在,测试绿 = bug 没了。这是唯一可靠的判据。
  2. 它防止 bug 复发。 三个月后有人手滑把 >= 改回 >,这条测试会立刻报警。
  3. 它是最清晰的 bug 报告。 比截图和文字描述都精确。

第五步:改

现在才轮到改代码:

现在改。要求:
1. 把 volume_discount 里的门槛判断改成"达到即享"语义(>=)
2. 顺手检查 tiered_price 里有没有同类的边界问题,有就一起修,没有就明确告诉我没有
3. 不要改函数签名,不要改调用方
4. 改完把 tests/test_pricing.py 跑绿,再把全套测试跑一遍
5. 告诉我改了哪几行

它改完之后,先看 diff 再看测试:

git diff

确认改动只有那几行比较运算符,没有顺手"优化"别的东西。确认完再跑:

pytest -v

全绿。再验一次真实数据:

python -m billkit.cli --input data/usage-2024-03.csv --month 2024-03

现在 C1016 应该是 ¥1028.20(1000 - 30 折扣 = 970,加 6% 税 = 1028.20),C1017 是 ¥1908.00(2000 - 200 = 1800,加税 1908.00)。

算一遍这个数。 让 AI 改代码的风险不在于它改错,而在于它改对了但你不知道它对不对。手算一遍是最后一道防线。

第六步:评估影响范围

修完就完了吗?没有。这是个已经跑了几个月的计费 bug,得知道历史上影响了谁。

这个 bug 从 volume_discount 写出来的那天就存在了。帮我评估影响范围:

1. 查 git log,找出这个函数是何时引入的、之后改过几次
2. 写一个脚本,扫描 out/ 和 data/ 下所有历史账单数据,
   找出"小计恰好等于折扣门槛值"的记录,列出客户和月份
3. 估算这些客户被多收的总金额
4. 给我一份需要补差价的客户清单,格式是 CSV

脚本只读取、不修改任何现有文件。

第 4 点那句"脚本只读取、不修改"很重要。排查脚本一旦会写文件,风险就上了一个量级。

有了这份清单,你才敢去跟业务方说"影响 N 个客户、合计 M 元,需要补差价"。没有这个数,你只能说"好像有点问题"——那是完全不同的两回事。

Git:fix 分支的两步提交

git checkout -b fix/discount-boundary

# 第一步:提交失败测试(如果你上一章没提交过)
git add tests/
git commit -m "test: 折扣门槛按「达到即享」语义补充边界用例

合同规定月度金额达到门槛即享折扣,实现用了 > 而非 >=,
导致小计恰好等于门槛的客户不打折。当前用例失败,用于固化缺陷。"

# 第二步:提交修复
git add billkit/pricing.py
git commit -m "fix: 折扣门槛改为达到即享,修正边界客户不打折的问题

volume_discount 判断门槛时用了严格大于,小计恰好等于
1000/3000/2000/5000 的客户无法享受对应折扣。改为 >=。

影响范围:2024-03 账单中 C1016、C1017 两个客户,
分别多收 31.80 元和 106.00 元,需补差价。"

分两次提交的价值:将来看这段历史的人,能清楚看到"先有测试证明 bug 存在,再有修复"。顺序反了(先改再补测试),说服力就弱了——别人会怀疑测试是照着新代码写的。

提交信息让它生成:

用 Conventional Commits 格式给这次修复写提交信息。
标题不超过 50 字,正文说明:根因是什么、改了什么、
影响范围和金额、如何验证。

合并

git checkout main
git merge --no-ff fix/discount-boundary -m "Merge fix/discount-boundary

修复折扣门槛边界问题,影响 2 个客户共 137.80 元。"

用 --no-ff 保留分支历史。以后回看时能一眼看出"这是一次完整的 bug 修复"。

平台差异:排查命令对照

干什么 macOS / Linux Windows PowerShell WSL2
实时看日志 tail -f app.log Get-Content app.log -Wait tail -f app.log
过滤关键字 grep ERROR app.log Select-String -Path app.log -Pattern ERROR 同 Linux
统计出现次数 grep -c ERROR app.log (Select-String -Path app.log -Pattern ERROR).Count 同 Linux
看端口占用 lsof -i :8000 Get-NetTCPConnection -LocalPort 8000 同 Linux
看进程 `ps aux grep python` Get-Process python
环境变量 export X=1 $env:X = "1" 同 Linux

追日志时的高效用法:让终端里的日志直接流给它分析。

tail -f logs/app.log | codebuddy -p "实时分析这些日志,出现 ERROR 或异常模式时总结出来"

本章你多了什么

  • 一个修好的 bug,和一条防它复发的回归测试
  • 一份影响范围清单(哪些客户、多少钱、哪些月份)
  • 一次完整的 debug 流程经验:不是"看到报错→改代码",而是"定位→复现→固化→修复→评估"

本章新知识点

知识点 说明
要假设,也要证伪方法 逼它把猜测变成可验证的东西
找触发边界,不靠碰运气 偶发 bug 的真相通常是某个边界条件
先写失败测试 唯一可靠的"修好了"判据
排查脚本只读 定位阶段绝不让它写文件
评估影响范围 修完 bug 不等于事情结束
两步提交 测试一次、修复一次

延伸练习

  1. 让它把这次 debug 的全过程写成一份 docs/postmortem-2024-03-discount.md(事故复盘模板)
  2. 让它在 tests/ 里加一个"合同一致性测试":把折扣规则写成数据驱动的用例表,改规则时只改表
  3. 让它给 processor.py 加一个防御:账单金额与上月相比波动超过某个阈值时打 WARNING——让下一个 bug 早点被发现

常见坑

它一上来就改代码。 用 plan 模式(按 Shift+Tab 切到 plan),它在里面只能出方案。

它改的地方比你要求的多。 提示词里写"不要改函数签名,不要改调用方,改完告诉我改了哪几行"。事后一定 git diff。

它说修好了,但你不知道验证标准。 每次都要它给出"修复前后的具体数值对比"。修 bug 必须能看到数字变化。

日志里的结论是错的。 这次日志说"档位未生效",实际是折扣门槛判断问题。永远把日志当线索,不当结论。

偶发 bug 修完就没再管。 一定要跑影响范围评估——代码里的一个字符,对应的是真金白银。


下一章,我们对那个谁都不敢碰的胖子模块下手。