Skip to content
Published at:

第 23 章:场景四:调试与排错

场景定位:第五篇实战篇的第 4 个场景。前三个场景分别覆盖了理解陌生项目、从零搭建新功能、大规模重构。本章聚焦于开发工作中最让人头疼也最考验功力的环节——调试与排错

调试是开发者与代码之间的侦探游戏。你面对的是一个沉默的嫌疑人(Bug),手上只有零星的线索(日志、报错堆栈、用户反馈),需要在庞大的代码库中找出真相。传统调试依赖个人经验和运气——有人看一眼报错就知道问题在哪,有人加了一整天的 console.log 仍然一头雾水。

Claude Code 改变了这个游戏的规则。 它不会替代你的判断力,但它能在几分钟内完成你可能需要几小时才能做完的"地毯式搜索"——读取所有相关文件、追踪完整调用链、对比历史变更、提出系统性假设。本章将通过一个完整的生产环境 Bug 排查案例,展示如何将 Claude Code 作为你的调试搭档。

学习完本章后,你将能够:

  • 使用 Claude Code 从模糊报错中快速形成排查假设
  • 掌握假设驱动的系统化调试流程
  • 让 Claude 追踪跨文件的调用链和依赖关系
  • 验证修复并防止同类问题再次出现

23.1 场景描述

来自用户的模糊报错

周五下午 4:58,你正准备关电脑过周末。Slack 上弹出一条消息:

用户反馈:登录后有时会立刻被踢回登录页,要重新登录才行。不是每次都出现,大概每登录 10 次出现 2-3 次。Chrome 最新版,网络正常。

你打开日志平台,搜索该用户近一小时的记录。唯一相关的错误是:

2026-05-30 16:42:31 ERROR [auth.middleware] Token validation failed
  - userId: usr_8a3f2
  - ip: 203.0.113.45
  - userAgent: Mozilla/5.0 ... Chrome/132.0
  - traceId: 7b9c4-2e1f-4a8d

就这一行。没有堆栈信息,没有更详细的错误原因。Token validation failed 可能意味着任何事情——Token 过期、签名不匹配、解码失败、Redis 连接超时……

你叹了口气,重新打开终端。

为什么这类 Bug 最磨人

这个 Bug 具备所有"难啃的骨头"的特征:

特征为什么麻烦
不可稳定复现每 10 次出现 2-3 次,暗示可能是竞态条件或超时问题
错误信息模糊"Token validation failed" 覆盖了十几种可能的根因
涉及多个系统Auth 服务 → JWT 签发 → Redis Session → 中间件校验
没有堆栈日志只记录了错误描述,没有调用栈帮助定位

传统做法下,你可能会:在每个怀疑点加 console.log → 部署到测试环境 → 想办法复现 → 看日志 → 再加更多 log → 重复……这个过程可能持续几小时甚至一整天。

而有了 Claude Code,你的调试流程将完全不同。


23.2 Step 1:让 Claude Code 分析错误日志

从一行日志开始

你没有急着去翻代码,而是打开 Claude Code 侧边栏,把仅有的线索粘贴进去:

我在排查一个生产环境的 Bug。用户反馈登录后偶尔会被立即登出,
大概每 10 次出现 2-3 次。唯一的错误日志是:

"ERROR [auth.middleware] Token validation failed
  - userId: usr_8a3f2
  - ip: 203.0.113.45
  - traceId: 7b9c4-2e1f-4a8d"

帮我分析这个错误可能的原因。先不要改代码,只做分析。

Claude 的系统性分析

Claude 读完你的描述后,没有立刻下结论,而是提出了一个结构化的假设列表

根据你提供的信息,我分析了以下几种可能的原因,按可能性从高到低排列:

假设 1(高可能性):Token 过期时间过短
  如果 JWT 的 expiresIn 设置得太短(比如 60 秒),
  在登录流程中某些步骤耗时较长时,Token 可能在
  校验前就过期了——这解释了"偶尔"出现而非每次。

假设 2(中可能性):服务器时钟不同步
  如果签发 Token 的服务器和校验 Token 的服务器
  之间存在时钟偏差(NTP 未同步),即使 Token 本身
  未过期,校验端也可能认为它已过期。

假设 3(中可能性):Redis 连接不稳定
  如果 Session 信息存储在 Redis 中,偶尔的连接
  超时或连接池耗尽可能导致 Token 校验时查不到
  Session,造成验证失败。

假设 4(低可能性):Token 签名密钥不一致
  如果多实例部署时,签发和校验使用了不同的密钥,
  则会出现间歇性的签名验证失败。

假设 5(低可能性):并发请求导致 Token 刷新竞争
  如果前端在 Token 即将过期时发起了多个并发请求,
  可能触发多次刷新,导致某次请求使用了被撤销的旧 Token。

要进一步缩小范围,我需要查看相关代码。
要我逐一检查这些假设吗?

这个分析的质量值得注意:

  • 按可能性排序:不是无差别罗列,而是基于经验给出优先级
  • 每个假设都解释了为什么能匹配症状:"偶尔出现"这个关键特征被反复关联
  • 主动提出下一步:不等你指令,Claude 已经准备好了执行计划

追踪 Token 的完整生命周期

你回复:

帮我查看 JWT 相关的代码,找出 token 生成和验证的完整逻辑。
从登录接口开始追踪,一直到中间件校验。

Claude Code 开始工作。它的执行步骤清晰可循:

1. Grep 搜索 "jwt" 关键词 → 找到 8 个相关文件
2. 读取 src/api/auth/login.ts — 登录接口入口
3. 读取 src/utils/jwt.ts — Token 生成工具函数
4. 读取 src/middleware/auth.ts — Token 校验中间件
5. 读取 src/config/auth.config.ts — 认证相关配置
6. 读取 src/services/session.ts — Session/Redis 管理

读完后,Claude 给出了完整的调用链梳理:

Token 生命周期追踪:

【签发阶段】src/api/auth/login.ts → src/utils/jwt.ts
  1. 用户提交用户名+密码
  2. login.ts 调用 verifyCredentials() 验证凭据
  3. 验证通过后,调用 jwt.signToken({ userId, role })
  4. signToken 内部配置:
     - payload: { userId, role }
     - secret: process.env.JWT_SECRET
     - options: { expiresIn: '60s' }    ← 这里!
  5. 生成的 Token 返回给前端,同时存入 Redis Session

【校验阶段】src/middleware/auth.ts
  1. 每个请求进入时,中间件从 Authorization header 提取 Token
  2. 调用 jwt.verifyToken(token) 验证签名和过期时间
  3. 如果 Token 有效,从 Redis 读取 Session 补充用户信息
  4. 如果 Redis 读取失败 → 返回 "Token validation failed"
  5. 如果 Token 过期 → jwt.verify 抛出 TokenExpiredError

我注意到的关键点:
- src/utils/jwt.ts 第 23 行:expiresIn: '60s'
- src/middleware/auth.ts 第 47 行:Redis 读取失败时只记录了
  "Token validation failed",没有区分"Token 本身无效" 
  和"Redis 读取失败"

第一条关键线索浮出水面expiresIn: '60s' —— Token 只有 60 秒的有效期。


23.3 Step 2:假设驱动的调试流程

逐一检验每个假设

有了 Token 生命周期的全景图,你开始让 Claude 逐个检验前面的假设。

检验假设 1:Token 过期时间

检查一下 jwt.ts 中 expiresIn 的设置,
60 秒对于生产环境来说正常吗?

Claude 的回答直接而明确:

生产环境中 JWT 的典型过期时间参考:

| 场景 | 推荐 expiresIn |
|------|---------------|
| Access Token | 15 分钟 ~ 1 小时 |
| Refresh Token | 7 天 ~ 30 天 |
| 一次性验证 Token(邮箱验证等) | 5 ~ 10 分钟 |
| API Key | 长期,通常不过期 |

当前设置 60 秒 = 1 分钟。
这远低于行业标准的最小值(15 分钟)。

如果用户在登录后花 30 秒浏览首页,再点击一个
需要鉴权的链接,Token 已经只剩 30 秒。
如果网络稍有延迟或中间件处理耗时稍长——
Token 过期,用户被登出。

这能解释"偶尔出现"的症状:
- 快速操作的用户不会触发(Token 在过期前被使用)
- 稍慢的用户就会遇到(Token 已过期)
- 取决于用户操作速度,所以不可稳定复现

检验假设 2:时钟问题

检查一下代码中时间比较是用服务器时间还是客户端时间

Claude 检查了 jwt.tsauth.ts

Token 的签发和校验都使用 jsonwebtoken 库的标准行为:

- 签发时:jwt.sign() 使用服务器当前时间设置 iat(issued at)
- 校验时:jwt.verify() 使用服务器当前时间与 exp 比较
- 没有使用客户端时间

所以时钟问题只有在多服务器实例间不同步时才可能发生。
但从日志来看,traceId 7b9c4... 的所有请求都落在同一台
服务器上(通过 Pod 名称推断)。时钟问题的可能性较低。

检验假设 3:Redis 连接

检查 session 存储的 Redis 连接配置

Claude 读取了 src/services/session.ts 和 Redis 配置:

src/services/session.ts 第 15 行:
const redisClient = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  connectTimeout: 5000,       // 连接超时 5 秒
  maxRetriesPerRequest: 3,    // 单次请求最多重试 3 次
  retryStrategy: (times) => Math.min(times * 200, 2000),
});

src/middleware/auth.ts 第 42-50 行:
try {
  const session = await redisClient.get(`session:${userId}`);
  if (!session) {
    logger.error('Token validation failed', { userId, ip, traceId });
    return res.status(401).json({ error: 'Token validation failed' });
  }
} catch (redisError) {
  // 注意:这里捕获了 Redis 错误,但日志记录的仍是
  // 'Token validation failed'——和 session 不存在时完全一样
  logger.error('Token validation failed', { userId, ip, traceId });
  return res.status(401).json({ error: 'Token validation failed' });
}

Claude 指出了两个问题:

Redis 相关发现:

1. 连接配置看起来合理(5 秒超时,3 次重试),
   正常网络条件下不应频繁失败。

2. 但中间件的错误处理有严重问题:
   - Session 不存在 → 日志:"Token validation failed"
   - Redis 连接失败 → 日志:"Token validation failed"
   - 两者完全一样,无法从日志区分根因!
   
   建议修复:在 catch 块中记录不同的错误信息,
   比如 "Redis connection failed during token validation"。
   这样下次出现时可以立刻判断是否是 Redis 的问题。

锁定根因

三个假设检验完毕,证据指向非常明确:

假设 1(Token 过期):
  ✅ 发现 expiresIn: '60s',远低于生产环境标准
  ✅ 能完美解释"偶尔出现"的症状模式
  ✅ 修复最简单、影响最可控

假设 2(时钟问题):
  ❌ 无多实例部署(请求落单台服务器)
  ❌ 无 NTP 相关告警

假设 3(Redis 问题):
  ⚠️ Redis 配置本身合理,但错误日志区分度不够
  ⚠️ 即使 Redis 有问题,也不是当前 Bug 的主因
     (因为 Token 过期会先于 Redis 读取被 jwt.verify 拦截)

结论明确:expiresIn: '60s' 是根因。

追查历史:这个 60 是怎么来的

在动手修复之前,你还想确认一件事——这个 60 秒是设计如此还是手误。

这个 60 是故意的还是写错了?
查看 jwt.ts 的 git log,看这个值是谁改的、什么时候改的。

Claude Code 执行了 git log

$ git log --oneline --all -10 -- src/utils/jwt.ts

c4e8a2f feat: add JWT authentication (2026-05-15)
  +  expiresIn: '60s'

$ git show c4e8a2f -- src/utils/jwt.ts

commit c4e8a2f
Author: dev@example.com
Date:   2026-05-15 14:22:00

+    const token = jwt.sign(payload, secret, { expiresIn: '60s' });

对比同文件中的注释:
+    // TODO: 上线前改为 3600s(1 小时)

所以 60 秒是开发阶段的临时设置,但TODO 没有被执行——
代码带着开发配置上线了。

真相大白。开发时为了方便测试(Token 快点过期,方便验证刷新逻辑),设置了 60 秒。注释里写了 TODO: 上线前改为 3600s,但这个 TODO 在代码审查和上线流程中被遗漏了。

经典的人为失误:开发配置残留到生产环境。这类问题靠人工 Code Review 很难抓干净——因为审查者会自然地假设"这行代码一定是有意为之的"。Claude Code 的价值在于它能不带假设地审视每一行代码,将"60s"和"行业标准 15 分钟 ~ 1 小时"之间的巨大差异标注出来。


23.4 Step 3:逐步缩小问题范围

修复根因

确定了根因,修复就很简单了:

帮我修复这个问题,把过期时间改为 3600 秒。
另外把那个 TODO 注释也删掉。

Claude Code 用 Edit 工具精确修改了 src/utils/jwt.ts

typescript
// 修改前:
const token = jwt.sign(payload, secret, { expiresIn: '60s' });  // TODO: 上线前改为 3600s

// 修改后:
const token = jwt.sign(payload, secret, { expiresIn: '3600s' });

搜索同类问题

你没有就此止步。一个地方出了问题,项目中其他地方很可能也会有同样的问题。

同时检查一下项目中是否还有其他地方用了同样的错误值。
搜索所有包含 "60s" 或 "expiresIn" 的代码。

Claude 执行了全项目搜索:

$ grep -rn "expiresIn" src/

src/utils/jwt.ts:23:    const token = jwt.sign(payload, secret, { expiresIn: '3600s' });
src/utils/emailToken.ts:12:  const token = jwt.sign({ email }, secret, { expiresIn: '60s' });
src/services/passwordReset.ts:35:  const resetToken = jwt.sign({ userId }, secret, { expiresIn: '60s' });
src/config/session.ts:8:    ttl: 60,  // 单位:秒

Claude 报告:

找到了 3 处相关的配置:

1. src/utils/jwt.ts — ✅ 已修复为 3600s

2. src/utils/emailToken.ts:12 — expiresIn: '60s'
   用途:邮箱验证 Token
   评估:邮箱验证链接通常在邮件中,用户需要时间查看邮件、
        点击链接。60 秒太短,建议改为 600s(10 分钟)

3. src/services/passwordReset.ts:35 — expiresIn: '60s'
   用途:密码重置 Token
   评估:同上,用户需要收邮件、点击链接、填写新密码。
        60 秒基本不可能完成,建议改为 600s

4. src/config/session.ts:8 — ttl: 60
   用途:Session 在 Redis 中的过期时间
   评估:60 秒的 Session TTL 极不正常。这意味着用户登录后
         如果 60 秒内没有操作,Session 就被清除了。
         建议至少 86400s(24 小时)

原来不止一处。全项目有 4 处使用了开发阶段的 60 秒配置,都是同一个模式——开发时为了方便测试设得很短,注释写了 TODO,然后被遗忘了。

你让 Claude 一次性修复所有问题:

把这三处也都修复:
- emailToken.ts → 600s
- passwordReset.ts → 600s
- session.ts → 86400s(24 小时)

Claude Code 用 Edit 工具逐一修改了每个文件。完成后,四处的配置都更新为合理的生产环境值。

改进错误日志

还记得中间件里Redis 错误和 Session 不存在被记录为同一个日志的问题吗?虽然它不是当前 Bug 的根因,但如果不修复,下一次排查会再次被误导。

之前你提到的中间件日志问题——Redis 错误和 Session 不存在的日志
完全一样。帮我把它们区分开来。

Claude Code 修改了 src/middleware/auth.ts 的错误处理:

typescript
// 修改后:
try {
  const session = await redisClient.get(`session:${userId}`);
  if (!session) {
    logger.warn('Session not found in Redis', { userId, traceId });
    return res.status(401).json({ error: 'Session expired, please login again' });
  }
} catch (redisError) {
  logger.error('Redis connection error during token validation', {
    userId,
    traceId,
    error: redisError.message,
  });
  return res.status(503).json({ error: 'Service temporarily unavailable' });
}

关键改进:

改进点修改前修改后
Session 不存在记录 ERROR 级别降级为 WARN(正常业务流程)
Redis 连接失败与 Session 不存在混淆独立错误信息 + 记录原始错误
HTTP 状态码统一返回 401区分 401(认证失败)和 503(服务不可用)
用户提示模糊的 "Token validation failed"Session 过期有明确提示

防御性调试:好的错误日志不只是记录"出错了",而是要回答三个问题——(userId/traceId)、在哪(具体位置)、为什么(根因而非表象)。当你下一次排查问题时,清晰的日志能帮你省下几个小时。


23.5 Step 4:验证修复

编写测试用例

修复代码只是第一步。你需要确认修复是有效的,并且不会引入新的问题。

帮我写一个测试用例,验证 Token 过期时间是否正确。
测试应该:创建 Token → 检查 exp 字段 → 断言过期时间

Claude Code 读取了现有的测试文件结构和测试框架配置,然后创建了测试用例:

typescript
// src/utils/__tests__/jwt.test.ts
import { describe, it, expect } from 'vitest';
import jwt from 'jsonwebtoken';
import { signToken } from '../jwt';

describe('JWT Token 生成', () => {
  it('Token 过期时间应为 3600 秒(1 小时)', () => {
    const userId = 'test_user_001';
    const token = signToken({ userId, role: 'user' });

    // 解码 Token(不验证签名)以读取 payload
    const decoded = jwt.decode(token) as jwt.JwtPayload;

    expect(decoded).toBeDefined();
    expect(decoded.iat).toBeDefined();
    expect(decoded.exp).toBeDefined();

    // 验证过期时间 = 签发时间 + 3600 秒
    const expiresIn = decoded.exp! - decoded.iat!;
    expect(expiresIn).toBe(3600);

    // 验证 payload 中的 userId 正确传递
    expect(decoded.userId).toBe(userId);
    expect(decoded.role).toBe('user');
  });

  it('emailToken 过期时间应为 600 秒(10 分钟)', () => {
    const { signEmailToken } = require('../emailToken');
    const token = signEmailToken('user@example.com');
    const decoded = jwt.decode(token) as jwt.JwtPayload;

    const expiresIn = decoded.exp! - decoded.iat!;
    expect(expiresIn).toBe(600);
  });

  it('passwordReset Token 过期时间应为 600 秒(10 分钟)', () => {
    const { signPasswordResetToken } = require('../../services/passwordReset');
    const token = signPasswordResetToken('test_user');
    const decoded = jwt.decode(token) as jwt.JwtPayload;

    const expiresIn = decoded.exp! - decoded.iat!;
    expect(expiresIn).toBe(600);
  });
});

测试写好后,你运行了测试:

pnpm test -- jwt

三个测试全部通过。

手动验证登录流程

在部署到生产环境之前,你还想手动验证一下完整的登录流程。

帮我写一个 cURL 命令,手动验证完整的登录流程:
1. 登录获取 Token
2. 用 Token 访问受保护的接口
3. 确认返回 200 而不是 401

Claude 生成了一套可供手动执行的验证命令:

bash
# 1. 登录获取 Token
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"test_user","password":"test_pass_123"}' \
  | jq -r '.token')

echo "Token: ${TOKEN:0:20}..."

# 2. 用 Token 访问受保护接口
curl -s -X GET http://localhost:3000/api/user/profile \
  -H "Authorization: Bearer $TOKEN" \
  | jq .

# 3. 验证 Token 中的 exp 字段(获取过期时间)
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq '.exp - .iat'

# 预期输出:3600

你逐条执行了这些命令,返回结果符合预期——登录成功、接口返回 200、Token 过期时间显示 3600 秒。

部署前确认清单

在将修复推向生产之前,Claude 帮你整理了一份确认清单:

部署前检查清单:

□ Token 过期时间修复
  ✅ src/utils/jwt.ts: expiresIn 60s → 3600s
  ✅ src/utils/emailToken.ts: expiresIn 60s → 600s
  ✅ src/services/passwordReset.ts: expiresIn 60s → 600s

□ Session 配置修复
  ✅ src/config/session.ts: ttl 60 → 86400

□ 错误日志改进
  ✅ src/middleware/auth.ts: 区分 Session 过期和 Redis 故障

□ 测试覆盖
  ✅ jwt.test.ts: 三个过期时间验证用例全通过

□ 手动验证
  ✅ cURL 登录流程测试通过

□ 代码审查
  ✅ 所有变更仅涉及配置值修改和日志增强,无逻辑变更

□ 回滚方案
  git revert <commit-hash> 即可一键回滚

□ 监控关注点(上线后)
  - 观察 401 错误率是否下降
  - 关注 "Session not found" 警告频率(可能有用户需重新登录)
  - 关注 Redis 连接错误日志(区分后可以单独监控)

23.6 复盘与技巧总结

调试的系统化方法

这次排查之所以高效,不是因为你运气好,而是因为你遵循了一套系统化的调试流程

┌─────────────────────────────────────────────────────┐
│                系统化调试流程                          │
├─────────────────────────────────────────────────────┤
│                                                       │
│  1. 日志分析                                         │
│     ├─ 搜集所有可用的错误信息                         │
│     ├─ 让 Claude 从日志中提取关键字段                 │
│     └─ 识别症状模式(频率、时间分布、受影响用户)      │
│                                                       │
│  2. 假设驱动                                         │
│     ├─ 基于经验提出多个假设(按可能性排序)            │
│     ├─ 为每个假设设计验证方法                         │
│     └─ 让 Claude 并行检查多个假设                     │
│                                                       │
│  3. 逐一排除                                         │
│     ├─ 从可能性最高的假设开始验证                     │
│     ├─ 每个假设 → 检查代码 → 确认或排除               │
│     └─ 缩小排查范围,聚焦最可能的根因                  │
│                                                       │
│  4. 定位根因                                         │
│     ├─ 确认根因并追溯引入历史(git log)              │
│     ├─ 搜索同类问题(全项目扫描)                     │
│     └─ 识别关联问题(如日志区分度不够)               │
│                                                       │
│  5. 修复                                             │
│     ├─ 修复根因 + 所有同类问题                        │
│     └─ 修复过程中发现的次生问题                       │
│                                                       │
│  6. 验证                                             │
│     ├─ 单元测试(自动化验证)                         │
│     ├─ 手动测试(端到端验证)                         │
│     └─ 部署前确认清单                                 │
│                                                       │
└─────────────────────────────────────────────────────┘

Claude Code 在调试中擅长什么

擅长说明本章对应环节
大规模代码搜索几秒内扫描全项目,找出所有相关代码搜索 expiresIn 找到 4 处配置
追踪调用链从入口到出口完整追踪函数调用登录接口 → JWT 签发 → 中间件校验
对比差异找出代码中不一致的地方60s vs 行业标准 3600s
搜索相似问题在当前项目中搜索同类错误模式发现 3 处相同的遗留 TODO
生成测试针对 Bug 编写精准的测试用例过期时间断言测试
历史追溯通过 git log/blame 找到变更历史确认 60s 是开发配置残留
系统性分析基于有限信息提出结构化假设5 个假设按可能性排序

Claude Code 在调试中不擅长什么

不擅长为什么替代方案
运行时状态调试Claude 无法连接调试器、设置断点、查看运行时的变量值传统断点调试、console.log、远程调试工具
环境特定问题无法访问你的生产/测试环境的实际状态提供日志文件、错误截图、环境信息描述
UI/视觉 Bug无法"看到"页面渲染效果提供截图、录制复现视频
需要反复交互的调试不能在 while 循环中"改一点 → 跑一下 → 看结果 → 再改"描述清楚复现步骤,Claude 一次性分析
性能分析无法运行 profiler、分析 CPU/内存快照提供 profiling 报告文本,Claude 辅助分析
第三方服务问题无法检查外部 API 状态、数据库实际数据提供 API 响应、数据库查询结果

理解边界很重要:Claude Code 最适合的是"静态分析型调试"——读代码、追踪逻辑、分析错误模式。对于需要运行时代码执行的调试场景,Claude 是辅助而非主力。

关键技巧

技巧 1:给 Claude 完整的错误信息

# ❌ 信息不完整
"登录有问题,帮我看看代码哪里错了"

# ✅ 信息完整
"用户反馈登录后偶尔被登出。错误日志:
'ERROR Token validation failed, userId: xxx'
相关文件在 src/api/auth/ 和 src/middleware/ 下。
先分析可能原因,不要直接改代码。"

技巧 2:让它先读代码再判断

Claude 有一个常见的倾向:基于你的描述直接给出修复建议,而不是先去读代码确认。你应该主动要求它先读后判

"帮我看看这个 Bug。先读取所有相关的文件,
理解逻辑后再给我分析。不要跳过读代码这一步。"

技巧 3:系统性排查,而非随机猜测

# ❌ 随机猜测
"可能是 Token 过期了吧,帮我改一下过期时间"

# ✅ 系统性排查
"列出所有可能导致 'Token validation failed' 的原因。
然后逐个检查代码,排除不可能的原因。"

技巧 4:并行检验多个假设

当 Claude 提出多个假设后,同时检查它们,而不是串行:

"你提了三个假设:Token 过期、Redis 连接、时钟不同步。
帮我同时检查这三个方向的相关代码,看看哪个最能解释
'偶尔出现'的症状模式。"

技巧 5:修 Bug 时顺手改进日志

每次修 Bug 都是一次改进可观测性的机会。如果你发现日志信息不足以定位问题——趁现在改掉它。下一次排查时你会感谢现在的自己。

技巧 6:修复一个,搜索同类

在修复一个 Bug 之后,永远让 Claude 做一次全项目搜索:
"这个 Bug 的模式是 [描述模式]。搜索项目中是否还有
其他地方有同样的问题。"

本章的案例就是最好的证明——一个 expiresIn: '60s' 后面藏着三个相同的定时炸弹。


23.7 本章小结

本章通过一个完整的生产环境调试案例,展示了 Claude Code 作为调试搭档的工作方式。

核心流程:

错误日志(一行模糊的报错)
  → 假设驱动分析(5 个可能原因,按可能性排序)
  → 代码追踪(Token 签发 → 校验 完整调用链)
  → 逐一验证(检验每个假设,排除不可能的)
  → 定位根因(expiresIn: '60s' — 开发配置残留)
  → 追溯历史(git log 确认 TODO 未执行)
  → 同类搜索(全项目发现 4 处相同问题)
  → 一并修复(根因 + 关联问题 + 日志改进)
  → 测试验证(单元测试 + 手动 cURL)
  → 部署确认(检查清单)

关键收获:

收获要点
系统化方法日志分析 → 假设驱动 → 逐一排除 → 定位根因 → 修复 → 验证
Claude 的强项代码搜索、调用链追踪、假设生成、历史追溯、测试生成
Claude 的弱项运行时调试、环境特定问题、UI 问题
核心习惯先读后判、并行验证、修一搜全、改进日志
思维转变从"自己读代码 → 自己猜 → 自己改"变成"Claude 读 → Claude 猜 → 你判断 → Claude 改"

下一章第 24 章:场景五:代码审查 —— 学习如何使用 Claude Code 进行系统化的代码审查,包括 PR Review 的最佳实践和审查视角。