主流 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。
为什么坚持"先写失败测试"? 三个理由:
- 它证明你真的复现了。 测试红 = bug 在,测试绿 = bug 没了。这是唯一可靠的判据。
- 它防止 bug 复发。 三个月后有人手滑把
>=改回>,这条测试会立刻报警。 - 它是最清晰的 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 不等于事情结束 |
| 两步提交 | 测试一次、修复一次 |
延伸练习
- 让它把这次 debug 的全过程写成一份
docs/postmortem-2024-03-discount.md(事故复盘模板) - 让它在
tests/里加一个"合同一致性测试":把折扣规则写成数据驱动的用例表,改规则时只改表 - 让它给
processor.py加一个防御:账单金额与上月相比波动超过某个阈值时打 WARNING——让下一个 bug 早点被发现
常见坑
它一上来就改代码。 用 plan 模式(按 Shift+Tab 切到 plan),它在里面只能出方案。
它改的地方比你要求的多。 提示词里写"不要改函数签名,不要改调用方,改完告诉我改了哪几行"。事后一定 git diff。
它说修好了,但你不知道验证标准。 每次都要它给出"修复前后的具体数值对比"。修 bug 必须能看到数字变化。
日志里的结论是错的。 这次日志说"档位未生效",实际是折扣门槛判断问题。永远把日志当线索,不当结论。
偶发 bug 修完就没再管。 一定要跑影响范围评估——代码里的一个字符,对应的是真金白银。
下一章,我们对那个谁都不敢碰的胖子模块下手。