用 Claude Code 修 bug:从一条报错到带回归测试的修复
一个启动就崩的应用,报错出在依赖包里,根因在自己的代码里。本站用同一个 bug 实测了两种问法:把现象、复现办法和做完的标准说清楚,和只说一句“修一下”。两次都修对了,差别在有没有回归测试、改动能不能回退。附一份提 bug 的提示词模板。
本站已实测,测试日期:2026-10-11。
本页目录(10 节)
这篇教程解决一个问题:手上有一条报错,怎么交给 Claude Code,才能拿回一个找到了根因、带着回归测试、自己验证过的修复,而不只是让报错消失。
第一个小时那篇里的 bug 偏简单,线索就在最近一次提交里。这一篇换一个更像日常的:应用一启动就崩,报错抛在依赖包里,git 历史里没有线索,现有的测试全是绿的。
我们用同一个 bug 试了两种问法,结果放在一起比。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-11 |
| 版本 | Claude Code 2.1.296 |
| 系统 | Ubuntu 24.04 |
| 模型 | Opus 5.5,auto 模式 |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
| 示例仓库 | Express 的源码,v5.3.0 |
场景:一个启动就崩的应用
应用的配置里有这样一行,逗号后面带一个空格:
app.set('trust proxy', 'loopback, 10.0.0.1');
一启动就报错:
/home/ccdemo/demo/b1-express/node_modules/proxy-addr/index.js:163
throw new TypeError('invalid IP address: ' + str)
^
TypeError: invalid IP address: 10.0.0.1
at parseipNotation (node_modules/proxy-addr/index.js:163:11)
at compileRangeSubnets (node_modules/proxy-addr/index.js:126:23)
at Function.compile (node_modules/proxy-addr/index.js:112:23)
at exports.compileTrust (lib/utils.js:212:20)
at Function.set (lib/application.js:371:34)
at Object.<anonymous> (repro.js:5:5)
这个报错有两处容易带偏:抛错的位置在 node_modules 里,看起来像依赖包的问题;IP 地址本身是对的。真正的线索是报错信息里冒号后面有两个空格。
想照着做:怎么布置这个 bug
这是我们故意埋的。Express 里有一行代码负责去掉每一项两边的空格,我们把它删了;又把唯一能发现这个问题的那个测试改成了不带空格的写法,模拟“测试没覆盖到”的真实情况。最后把这两处改动并进了原来的提交,git 历史里看不出来。
git clone --depth 1 --branch v5.3.0 https://github.com/expressjs/express.git
cd express
npm install
sed -i '210d' lib/utils.js
sed -i "s/'10.0.0.1, 10.0.0.2, 127.0.0.1, ::1'/'10.0.0.1,10.0.0.2,127.0.0.1,::1'/" test/req.ip.js
git commit -qa --amend --no-edit
macOS 自带的 sed 要写成 sed -i ''。再建一个复现脚本 repro.js:
var express = require('./');
var app = express();
app.set('trust proxy', 'loopback, 10.0.0.1');
app.get('/', function (req, res) {
res.json({ ip: req.ip, protocol: req.protocol });
});
console.log('ok');
这时 npm test 是 1261 个全过,node repro.js 会崩。
热身:把报错直接喂给它
不进交互界面也能问。官方文档提到可以用管道把数据送进去,我们把报错直接接给 claude -p:
node repro.js 2>&1 | claude -p "这是我们应用启动时的报错。用三句话以内说明:最可能的根因在哪个文件的哪个函数,为什么。先不要改任何东西。"
回答:
根因在 lib/utils.js 的 compileTrust 函数(约第 207–210 行):它处理 'trust proxy' 字符串时只做了
val.split(','),没有去掉每一项两边的空格。所以 repro.js 里的 'loopback, 10.0.0.1' 拆出来是 ' 10.0.0.1',
前面多了一个空格。这个值传给 proxy-addr 后被判为非法 IP,报错信息 invalid IP address: 10.0.0.1 里
冒号后面多出的那个空格就是这个前导空格。
它没有被 node_modules 带偏,直接指到了自己代码里的那个函数。先这样问一句、不让它动手,适合你想自己先判断一下的时候。
第一种问法:把现象、复现办法和做完的标准说清楚
进入交互界面,这样提:
我们的应用一启动就崩。运行 node repro.js 可以复现,报错是 TypeError: invalid IP address: 10.0.0.1。
先找到根本原因,不要改 node_modules 里的代码。然后先写一个能复现问题的失败测试,确认它失败,再修复,
最后跑相关的测试确认。不要提交。
1 分 6 秒,它做了这些(屏幕记录,有删节):
● Bash(cd /home/ccdemo/demo/b1-express; cat repro.js; node repro.js 2>&1 | head -30)
● Bash(… sed -n 190,220p lib/utils.js; git diff HEAD --stat; git log -3 --oneline -- lib/utils.js; …)
● Bash(… grep -n "version" node_modules/proxy-addr/package.json; sed -n 95,170p node_modules/proxy-addr/index.js; …)
● 我找到了根本原因:compileTrust 用 split(',') 拆分时没有去除空格,导致带前导空格的地址(如 " 10.0.0.1")
原样传给 proxy-addr,触发校验失败。接下来我会参考现有 req.ip 测试结构,编写一个失败测试来复现这个问题。
● Update(test/req.ip.js)
⎿ Added 15 lines
55 + it('should return the addr after trusted proxy based on list with spaces', function (done) {
56 + var app = express()
57 +
58 + app.set('trust proxy', ' 10.0.0.1, 10.0.0.2 , 127.0.0.1, ::1 ')
…
● Bash(… npx mocha --require test/support/env --reporter dot --check-leaks test/req.ip.js 2>&1 | tail -20)
⎿ 6 passing (91ms)
1 failing
● 测试按预期失败,报错与 repro 一致。现在修复:
● Update(lib/utils.js)
⎿ Added 1 line
209 val = val.split(',')
210 + .map(function (v) { return v.trim() })
● Bash(… node repro.js; npx mocha … test/req.ip.js test/req.ips.js test/req.host.j…)
⎿ ok
它的总结里写明了根因在 lib/utils.js 而不在依赖包里,新加的用例在修复前确实失败、报错和复现脚本一样,修复后和 trust proxy 有关的 9 个测试文件一起跑,103 个全过。
几处值得留意:
- 它先去看了依赖包的代码,但没有去改它。 读
proxy-addr是为了弄明白报错是怎么抛出来的,改的是上游传进去的值。 - 它查了 git 历史。 第二条命令里有
git log -3 --oneline -- lib/utils.js。这次历史里没有线索,它就接着读代码了。 - 测试不是随手补的。 它先读了现有的
test/req.ip.js,照着同样的结构写,而且用了比报错更刁钻的输入:两边有空格、连续两个空格、末尾有空格。 - 加回去的那一行,和我们删掉的一字不差。
做完之后我们自己跑了一遍全量测试:1262 个全过,比原来多的那一个就是它加的。
第二种问法:只说一句“修一下”
在另一份一模一样的仓库里,换成最省事的问法:
node repro.js 报错了,修一下。
34 秒:
● Bash(cat repro.js && node repro.js 2>&1 | head -40)
● Bash(sed -n 195,220p lib/utils.js)
● Bash(sed -i "s/ val = val.split(',')$/ val = val.split(',')\n .map(function (v) { return v.trim() })/" lib/utils.js && …)
● 修好了,node repro.js 现在输出 ok,全部 1261 个测试也都通过了。
……
这次没有加回归测试,改动也还没提交。需要的话我可以在 test/ 里补一个"逗号后带空格"的用例。
同样修对了,改的是同一行。
两种问法差在哪
| 说清楚的问法 | 一句话的问法 | |
|---|---|---|
| 找到的根因 | lib/utils.js 的 compileTrust |
一样 |
| 修复 | 加回 .map(...trim()) 那一行 |
一样 |
| 回归测试 | 加了,并且先确认它失败 | 没有加(它自己说明了) |
| 用什么改的文件 | 文件编辑工具 | sed -i 命令 |
| 修完跑了什么 | 复现脚本,加 9 个相关的测试文件 | 复现脚本,加全量测试 |
| 用时 | 1 分 6 秒 | 34 秒 |
| Claude Code 估算的花费 | 0.27 美元 | 0.20 美元 |
三点结论:
一、这种难度的 bug,问得再随意它也修得对。 差别不在修没修好,在修完之后留下了什么。
二、你不要求,就没有回归测试。 一句话的问法里它没有加,不过主动说了。没有测试,这个 bug 下次还会悄悄回来:我们埋 bug 的时候测试全是绿的,就是这个道理。
三、第二种问法的改动是用 sed 做的。 这有一个后果:按两下 Esc 的回退只管文件编辑工具做的修改,命令改的退不回来。/cost 里“Total code changes”那一行,第一次显示加了 16 行,第二次显示 0 行,也是同一个原因。细节见权限模式那篇的回退一节。
多花的 32 秒和 8 美分,换来的是一个测试和一份可以回退的改动。
官方的建议,和这次实测对得上的地方
官方文档里和修 bug 有关的建议,核心是这几条:
描述现象、可能的位置、“修好”是什么样。 文档举的例子是把“修一下登录的 bug”换成:
users report that login fails after session timeout. check the auth flow in src/auth/, especially token refresh.
write a failing test that reproduces the issue, then fix it
要根因,不要只压掉症状。 文档的另一个例子:把“构建失败了”换成“构建报这个错:(贴上报错)。修好并确认构建成功。解决根本原因,不要把报错压掉。”
给它一个能自己跑的检查。 文档说得很直接:没有能跑的检查,“看起来做完了”就是它唯一的信号,于是你成了那个验证环节,每个错都等着你去发现。给它一个会返回通过或失败的东西,它就会自己做、自己查、自己改到通过。
让它拿出证据。 测试的输出、它跑的命令和结果。看证据比你自己重跑一遍快。
另外三条提 bug 的小建议:告诉它复现问题的命令;说明复现的步骤;说明问题是每次都出现还是偶尔出现。
一份提 bug 的模板
把上面几条合起来,就是下面这个结构。这是本站按官方建议整理的,不是官方给的模板:
【现象】用户看到了什么,期望的是什么。
【复现】运行什么命令能复现;把完整的报错贴上来。
【范围】大概在哪个目录;哪些东西不要动(比如 node_modules、公开的接口)。
【做完的标准】
1. 先说明根本原因。
2. 写一个能复现问题的失败测试,确认它失败。
3. 修复。用文件编辑工具改,不要用 sed。
4. 跑这些测试确认:……
【不要做的】不要提交;不要顺手改别的。
第 3 条里“用文件编辑工具改”是这次实测之后加的。如果这条规矩你每次都要说,就把它写进 CLAUDE.md,写法见CLAUDE.md 怎么写。
修完之后,你自己要做的
官方文档把一种常见的失败叫作“信了再说”:它交出一个看起来没问题的实现,却没有处理边界情况。对应的办法只有一条:始终要有验证;验证不了的,就不要上线。
所以拿到修复之后:
- 看 diff。 这次只有 16 行,一眼看得完。改动越小,越容易确认它没有顺手动别的。
- 自己跑一遍全量测试。 它跑的是它认为相关的那部分。
- 看那个新测试测的是不是你关心的事。 把修复临时拿掉,测试应该失败。
走偏了怎么办
这次没有遇到,按官方文档的建议:
- 看到它方向不对,马上按
Esc打断,上下文还在,直接说该往哪走。 - 同一个问题纠正了两次还不对,说明上下文里已经堆满了失败的尝试。这时不要再纠正第三次,用
/clear清掉,把学到的东西写进一个更具体的提示词重新开始。 - 不确定该怎么修、或者要动好几个文件时,先用 plan 模式让它出方案。反过来,如果你一句话就能说清这个改动是什么,就不用先出方案。
- 排查范围很大时,不要让它无边际地“调查一下”,那会读几百个文件,把上下文占满。把范围说窄,或者让它派子代理去查。
这次实测没有覆盖到的
- 只测了一个 bug,每种问法只跑了一次。 模型每次的做法会不同,上面的对比说明的是倾向,不是定律。
- 这个 bug 仍然不算难。 它有稳定的复现脚本和完整的报错。偶发的问题、要看生产日志才能定位的问题、跨好几个服务的问题,没有测。
- bug 是我们埋的。 修复恰好等于还原被删掉的那一行,真实的 bug 往往没有这么干净的答案。