教程Claude Code系列第 1 / 6 篇

用 Claude Code 修 bug:从一条报错到带回归测试的修复

一个启动就崩的应用,报错出在依赖包里,根因在自己的代码里。本站用同一个 bug 实测了两种问法:把现象、复现办法和做完的标准说清楚,和只说一句“修一下”。两次都修对了,差别在有没有回归测试、改动能不能回退。附一份提 bug 的提示词模板。

发布 · 约 9 分钟读完 · 经人工审核()· 本站的内容怎么来的

本站已实测,测试日期:2026-10-11。

本页目录(10 节)
  1. 场景:一个启动就崩的应用
  2. 热身:把报错直接喂给它
  3. 第一种问法:把现象、复现办法和做完的标准说清楚
  4. 第二种问法:只说一句“修一下”
  5. 两种问法差在哪
  6. 官方的建议,和这次实测对得上的地方
  7. 一份提 bug 的模板
  8. 修完之后,你自己要做的
  9. 走偏了怎么办
  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 怎么写。

修完之后,你自己要做的

官方文档把一种常见的失败叫作“信了再说”:它交出一个看起来没问题的实现,却没有处理边界情况。对应的办法只有一条:始终要有验证;验证不了的,就不要上线。

所以拿到修复之后:

  1. 看 diff。 这次只有 16 行,一眼看得完。改动越小,越容易确认它没有顺手动别的。
  2. 自己跑一遍全量测试。 它跑的是它认为相关的那部分。
  3. 看那个新测试测的是不是你关心的事。 把修复临时拿掉,测试应该失败。

走偏了怎么办

这次没有遇到,按官方文档的建议:

  • 看到它方向不对,马上按 Esc 打断,上下文还在,直接说该往哪走。
  • 同一个问题纠正了两次还不对,说明上下文里已经堆满了失败的尝试。这时不要再纠正第三次,用 /clear 清掉,把学到的东西写进一个更具体的提示词重新开始。
  • 不确定该怎么修、或者要动好几个文件时,先用 plan 模式让它出方案。反过来,如果你一句话就能说清这个改动是什么,就不用先出方案。
  • 排查范围很大时,不要让它无边际地“调查一下”,那会读几百个文件,把上下文占满。把范围说窄,或者让它派子代理去查。

这次实测没有覆盖到的

  • 只测了一个 bug,每种问法只跑了一次。 模型每次的做法会不同,上面的对比说明的是倾向,不是定律。
  • 这个 bug 仍然不算难。 它有稳定的复现脚本和完整的报错。偶发的问题、要看生产日志才能定位的问题、跨好几个服务的问题,没有测。
  • bug 是我们埋的。 修复恰好等于还原被删掉的那一行,真实的 bug 往往没有这么干净的答案。

来源

通过 API 中转站使用这些模型?到 Veridrop 免费检测中转站给你的是不是它声称的模型,或者看看中转站排行榜前 100 名。

相关文章