Claude Code 教程

调试复杂 Bug

概述

调试是开发者日常工作中最耗时的环节之一,尤其是面对那些难以复现、症状模糊的复杂 Bug。Claude Code 在调试方面有几个独特的优势:它能快速阅读大量相关代码、追踪调用链、分析错误模式,而且不会遗漏细节。

本篇将通过一系列真实的调试场景,展示如何利用 Claude Code 高效地定位和修复各类复杂问题。

你将学到:

  • 如何向 Claude Code 描述 Bug 以获得最有效的帮助
  • Claude Code 的调试思路和方法论
  • 处理不同类型的 Bug:运行时错误、内存泄漏、竞态条件、性能问题
  • 什么时候该给 Claude 更多上下文,什么时候该减少干扰
  • AI 调试的局限性及应对策略

调试方法论

向 Claude Code 有效地描述 Bug

描述 Bug 的质量直接影响 Claude Code 定位问题的效率。一个好的 Bug 描述应该包含以下要素:

好的描述方式:

用户报告了一个 Bug:在文章列表页面,当用户快速点击"加载更多"按钮时,
有时候会显示重复的文章。
复现步骤:
1. 打开 /posts 页面
2. 滚动到底部
3. 快速连续点击"加载更多"按钮 3-4 次
4. 观察列表中出现重复的文章条目
预期行为:每次点击应该加载不同的文章,不应有重复
实际行为:快速点击时,同一页的文章会出现多次
环境:生产环境,Chrome 最新版
频率:不是每次都能复现,大约 50% 的概率
相关代码在 src/components/PostList.tsx 和 src/hooks/usePagination.ts

不好的描述方式:

文章列表有 Bug,有时候会显示重复的,帮我修一下
💡

提示

给 Claude Code 的 Bug 描述越详细,它定位问题的速度就越快。关键要素包括:复现步骤、预期与实际行为的差异、发生频率、相关代码路径。如果有错误日志或截图,也一并提供。

Claude Code 的调试流程

当你给 Claude Code 描述一个 Bug 时,它通常会按以下步骤进行:

  1. 阅读相关代码:找到 Bug 涉及的文件并仔细阅读
  2. 追踪数据流:从用户操作开始,追踪数据如何在组件和函数间流动
  3. 提出假设:根据代码分析提出可能的原因
  4. 验证假设:通过添加日志、检查条件或阅读更多代码来验证
  5. 提出修复方案:给出具体的代码修改

场景一:运行时错误和堆栈追踪

分析错误信息

最常见的调试场景是运行时报错。把完整的错误信息和堆栈追踪提供给 Claude Code:

应用在生产环境中报了这个错误:
TypeError: Cannot read properties of undefined (reading 'map')
at UserDashboard (src/pages/Dashboard.tsx:42:28)
at renderWithHooks (node_modules/react-dom/...)
at mountIndeterminateComponent (node_modules/react-dom/...)
这个错误只在某些用户登录后才出现,大部分用户是正常的。
请阅读 Dashboard.tsx 并分析可能的原因。

Claude Code 会阅读相关文件,然后分析:

我看了 Dashboard.tsx 的代码。第 42 行是:
{user.projects.map(project => ...)}
问题在于 user.projects 在某些情况下可能是 undefined。
查看数据获取逻辑,我发现 API 返回的用户数据中,
当用户没有任何项目时,后端返回的是 null 而不是空数组 []。
修复方案有两个:
1. 前端防御(推荐作为快速修复):
{user.projects?.map(project => ...) }
或者
{(user.projects || []).map(project => ...)}
2. 后端修复(推荐作为根本解决方案):
确保 API 始终返回空数组而不是 null

处理隐蔽的错误

有些错误不会直接抛出异常,而是表现为行为异常:

我们的搜索功能有问题:用户搜索 "react hooks" 时返回 0 条结果,
但数据库里明明有相关的文章。直接在数据库中查询是能找到的。
相关代码在:
- src/routes/search.ts(搜索接口)
- src/services/searchService.ts(搜索逻辑)
请帮我找出为什么搜索返回空结果

Claude Code 会逐步追踪搜索逻辑,可能发现类似这样的问题:

找到问题了。在 searchService.ts 的第 28 行:
const query = searchTerm.split(" ").join("%");
const results = await db.query(
"SELECT * FROM posts WHERE title LIKE ?",
[`%${query}%`]
);
当搜索词是 "react hooks" 时,query 变成了 "react%hooks",
然后 LIKE 模式变成 "%react%hooks%"。
问题是这个模式要求 "react" 和 "hooks" 必须紧挨着出现(中间只有一个任意字符),
但实际数据中标题可能是 "React Hooks 入门教程",中间有空格和其他文字。
修复方案:将每个搜索词单独做 LIKE 查询,用 AND 连接:
WHERE title LIKE '%react%' AND title LIKE '%hooks%'

场景二:内存泄漏

诊断内存问题

内存泄漏是最难调试的问题之一。Claude Code 可以帮你分析代码中潜在的内存泄漏点:

我们的 Node.js 服务运行一段时间后内存持续增长,
从启动时的 100MB 逐渐增长到 2GB 以上,最终 OOM 被 kill。
以下是一些观察到的现象:
- 重启后恢复正常,但几小时后又开始增长
- 内存增长与请求量大致成正比
- 没有明显的错误日志
请分析以下文件中可能存在的内存泄漏:
- src/middleware/cache.ts
- src/services/sessionManager.ts
- src/utils/eventBus.ts

Claude Code 会仔细分析这些文件中的内存管理模式:

我在三个文件中都发现了潜在的内存泄漏问题:
1. cache.ts — 缓存没有过期机制:
const cache = new Map();
export function getCached(key: string) {
return cache.get(key);
}
export function setCache(key: string, value: any) {
cache.set(key, value); // 只增不删,Map 会无限增长
}
修复:添加 TTL 过期机制,或使用 LRU Cache 库
2. sessionManager.ts — 事件监听器没有清理:
socket.on("connection", (client) => {
client.on("data", handleData);
// 缺少 client.on("close", ...) 来清理监听器
});
修复:在连接关闭时移除监听器并清理相关数据
3. eventBus.ts — 订阅后从未取消:
export function subscribeToUpdates(userId: string) {
eventEmitter.on(`update:${userId}`, handler);
// 没有对应的 off/removeListener
}
修复:返回取消订阅函数,在用户断开连接时调用

添加调试日志

让 Claude Code 帮你添加有针对性的日志来验证分析:

请在 cache.ts 中添加调试日志,帮我确认缓存确实在无限增长:
1. 在 setCache 函数中记录当前 Map 的大小
2. 每 60 秒打印一次缓存的总条目数和估算的内存占用
3. 记录哪些 key 被添加但从未被读取
// 添加调试日志
let cacheHits = 0;
let cacheMisses = 0;
export function setCache(key: string, value: any) {
cache.set(key, value);
if (cache.size % 100 === 0) {
console.log(`[Cache Debug] 缓存条目数: ${cache.size}`);
}
}
// 定时报告
setInterval(() => {
const memUsage = process.memoryUsage();
console.log(`[Cache Debug] 报告:`, {
cacheSize: cache.size,
heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`,
cacheHits,
cacheMisses,
});
}, 60000);

场景三:竞态条件和异步问题

诊断竞态条件

回到开篇提到的文章重复问题,这是一个典型的竞态条件:

文章列表在快速点击"加载更多"时出现重复条目。
请分析 src/hooks/usePagination.ts 中的竞态条件问题

Claude Code 会分析异步逻辑:

找到竞态条件了。在 usePagination.ts 中:
const loadMore = async () => {
const nextPage = page + 1;
const newItems = await fetchPosts(nextPage);
setPage(nextPage);
setItems(prev => [...prev, ...newItems]);
};
问题是:当用户快速点击时,多个 loadMore 调用同时执行。
假设当前 page = 1:
- 第一次点击:nextPage = 2,发请求
- 第二次点击(请求还没返回):nextPage 还是 2,又发了同样的请求
- 两个请求都返回 page 2 的数据,被追加到列表中
修复方案:
// 修复:添加加载状态锁和请求取消
export function usePagination<T>(fetcher: (page: number) => Promise<T[]>) {
const [items, setItems] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const loadMore = useCallback(async () => {
// 方案1:加载状态锁
if (loading) return;
setLoading(true);
// 方案2:取消前一个请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const controller = new AbortController();
abortControllerRef.current = controller;
try {
const nextPage = page + 1;
const newItems = await fetcher(nextPage);
// 确保请求没有被取消
if (!controller.signal.aborted) {
setPage(nextPage);
setItems((prev) => [...prev, ...newItems]);
}
} catch (error) {
if (error instanceof Error && error.name !== "AbortError") {
console.error("加载失败:", error);
}
} finally {
setLoading(false);
}
}, [page, loading, fetcher]);
return { items, loading, loadMore };
}

处理 Promise 相关的 Bug

我们有一个批量处理函数,偶尔会漏处理一些记录,但没有报错。
请分析 src/services/batchProcessor.ts 中是否有 Promise 处理的问题

Claude Code 可能会发现这样的问题:

在 batchProcessor.ts 中发现了两个 Promise 相关的问题:
问题 1 — forEach 中使用 async 函数(不会等待完成):
items.forEach(async (item) => {
await processItem(item); // 这些 Promise 没有被等待
});
console.log("全部处理完成"); // 实际上还没处理完
修复:使用 for...of 或 Promise.all
for (const item of items) {
await processItem(item);
}
问题 2 — Promise.all 中的错误被吞掉:
const results = await Promise.all(
items.map(item => processItem(item).catch(() => null))
);
// catch 吞掉了所有错误,失败的记录默默被忽略
修复:使用 Promise.allSettled 并记录失败的任务
const results = await Promise.allSettled(
items.map(item => processItem(item))
);
const failed = results.filter(r => r.status === "rejected");
if (failed.length > 0) {
console.error(`${failed.length} 个任务失败:`, failed);
}

场景四:性能调试

诊断慢查询

我们的 /api/dashboard 接口响应时间超过 5 秒,影响用户体验。
请分析 src/routes/dashboard.ts 中的性能问题

Claude Code 会分析代码并找出性能瓶颈:

发现了几个性能问题:
1. N+1 查询问题:
const users = await User.find();
for (const user of users) {
// 对每个用户单独查询 — 如果有 100 个用户就是 100 次查询
user.postCount = await Post.countDocuments({ authorId: user.id });
user.lastPost = await Post.findOne({ authorId: user.id }).sort({ createdAt: -1 });
}
修复:使用聚合查询一次获取所有数据
const stats = await Post.aggregate([
{ $group: {
_id: "$authorId",
postCount: { $sum: 1 },
lastPostDate: { $max: "$createdAt" }
}}
]);
2. 串行查询可以并行:
const totalUsers = await User.countDocuments();
const totalPosts = await Post.countDocuments();
const recentActivity = await Activity.find().sort(...).limit(10);
修复:使用 Promise.all 并行执行
const [totalUsers, totalPosts, recentActivity] = await Promise.all([
User.countDocuments(),
Post.countDocuments(),
Activity.find().sort(...).limit(10),
]);
3. 缺少数据库索引:
Post.find({ authorId: userId }).sort({ createdAt: -1 })
这个查询需要 authorId 和 createdAt 的复合索引
修复:添加索引
postSchema.index({ authorId: 1, createdAt: -1 });

添加性能监控

请在 dashboard 路由中添加性能监控,记录每个数据库查询的耗时:
1. 记录总的接口响应时间
2. 记录每个数据库查询的单独耗时
3. 如果任何查询超过 1 秒就打印警告
// 简单的性能计时工具
function createTimer(label: string) {
const start = performance.now();
return {
end() {
const duration = performance.now() - start;
if (duration > 1000) {
console.warn(`[SLOW] ${label}: ${duration.toFixed(0)}ms`);
} else {
console.log(`[Perf] ${label}: ${duration.toFixed(0)}ms`);
}
return duration;
},
};
}
// 使用示例
router.get("/dashboard", async (req, res) => {
const total = createTimer("Dashboard 总耗时");
const t1 = createTimer("查询用户统计");
const userStats = await getUserStats();
t1.end();
const t2 = createTimer("查询文章统计");
const postStats = await getPostStats();
t2.end();
total.end();
res.json({ userStats, postStats });
});

场景五:分析测试失败

让 Claude Code 分析测试失败

测试失败是日常开发中最常见的调试场景之一:

CI 中有个测试突然开始失败了,之前一直是通过的。失败信息:
FAIL tests/services/orderService.test.ts
● OrderService › calculateTotal › should apply discount for orders over 100
expect(received).toBe(expected)
Expected: 90
Received: 95
at Object.<anonymous> (tests/services/orderService.test.ts:45:32)
最近修改了 src/services/pricingService.ts 中的折扣计算逻辑。
请分析为什么测试失败了

Claude Code 会阅读测试代码和最近修改的源代码,然后分析:

我看了最近对 pricingService.ts 的修改。之前的折扣逻辑是:
- 订单满 100 打九折(10% 折扣)
最近的修改加入了一个新规则:
- 订单满 100 但不足 200 打九五折(5% 折扣)
- 订单满 200 打九折(10% 折扣)
测试期望订单金额 100 时获得 10% 折扣(结果 90),
但新的折扣规则只给了 5% 折扣(结果 95)。
这不是 Bug,而是业务规则变更后测试没有同步更新。
建议:
1. 更新测试用例以反映新的折扣规则
2. 添加测试覆盖新的折扣梯度
3. 在 commit message 中注明业务规则变更

何时给更多上下文 vs 何时给更少

需要更多上下文的情况

  • 跨文件的数据流问题:告诉 Claude 数据从哪里来、经过哪些处理、最终到哪里去
  • 业务逻辑相关的 Bug:解释业务规则,否则 Claude 无法判断什么是正确行为
  • 环境特定的问题:说明运行环境(Docker、Kubernetes、特定版本的运行时等)
  • 间歇性问题:提供出现问题的时间模式、频率、关联因素
// 提供充分的上下文
这个 Bug 只在 Docker 环境中出现,本地开发环境正常。
Docker 使用的是 Alpine Linux 基础镜像。
文件路径中有中文字符时会触发这个问题。

可以减少上下文的情况

  • 明确的错误和堆栈追踪:错误信息本身已经提供了足够的线索
  • 单文件的逻辑错误:问题局限在一个函数或文件内
  • TypeScript 类型错误:编译器的错误信息通常很明确
// 错误信息本身已足够
TypeScript 编译报错:
src/utils/format.ts:15:3 - error TS2345:
Argument of type 'string' is not assignable to parameter of type 'number'.
请修复这个类型错误

AI 调试的局限性

Claude Code 不擅长的调试场景

虽然 Claude Code 是强大的调试助手,但它也有局限性,了解这些局限可以帮你更好地利用它:

1. 需要实际运行环境的问题

Claude Code 无法真正「运行」代码来观察行为。对于需要实际执行才能发现的问题(如特定数据触发的 Bug),你需要提供运行时的信息(日志、状态截图等)。

2. 硬件和操作系统相关的问题

文件系统权限、网络配置、操作系统差异等问题,Claude Code 只能根据经验推测,无法直接检查你的环境。

3. 高度依赖上下文的问题

如果 Bug 涉及外部服务(第三方 API、消息队列、缓存服务)的行为,Claude Code 可能无法完全理解问题的全貌。你需要提供外部服务的日志和状态。

4. 时序敏感的问题

微妙的竞态条件、定时器精度问题、事件循环的微妙行为 —— 这些问题需要对运行时行为的精确观察,Claude Code 只能通过代码分析来推测。

最佳协作模式

面对这些局限,最有效的方式是将 Claude Code 作为你的「分析搭档」:

// 你负责收集信息
我在服务器上运行了 strace,发现进程在打开文件时有大量的 EMFILE 错误。
以下是 strace 的输出片段:
[粘贴 strace 输出]
同时 lsof 显示进程打开了超过 65000 个文件描述符。
以下是 lsof 的统计:
[粘贴统计信息]
// Claude Code 负责分析
请根据这些信息分析是什么导致了文件描述符泄漏
ℹ️

信息

把 Claude Code 想象成一个非常聪明但只能通过屏幕看到信息的同事。你是它的「眼睛」和「手脚」—— 你在实际环境中收集信息和执行操作,它帮你分析代码和制定策略。这种人机协作的模式能发挥各自的最大优势。

调试工作流总结

以下是使用 Claude Code 调试复杂 Bug 的推荐工作流:

步骤一:收集信息

在向 Claude Code 求助之前,先收集以下信息:

  • 完整的错误信息和堆栈追踪
  • 复现步骤(越详细越好)
  • 最近的代码变更(git loggit diff
  • 运行环境信息
  • 相关的日志输出

步骤二:描述问题

用结构化的方式向 Claude Code 描述问题:

## 问题描述
[一句话描述症状]
## 复现步骤
1. ...
2. ...
## 预期行为
[应该怎样]
## 实际行为
[实际怎样]
## 相关信息
- 错误日志:[粘贴]
- 环境:[说明]
- 最近变更:[说明]
- 相关文件:[列出]

步骤三:协作分析

让 Claude Code 阅读代码、分析原因、提出修复方案。如果第一次分析没有找到根因,提供更多信息或让它换个角度思考:

你的分析有道理,但我验证了一下,添加了空值检查后问题依然存在。
这说明问题不在这里。请从其他角度分析,重点看一下中间件的执行顺序

步骤四:验证修复

让 Claude Code 帮你实施修复并验证:

请实施你提出的修复方案,然后:
1. 运行现有的测试确认没有引入新问题
2. 为这个 Bug 添加一个专门的回归测试
3. 检查是否有其他地方存在类似的问题
💡

提示

第 3 点特别重要 —— 当发现一个 Bug 时,同一类型的问题很可能在其他地方也存在。让 Claude Code 搜索整个代码库中是否有类似的模式,可以防止同类 Bug 再次出现。

经验总结

使用 Claude Code 调试复杂 Bug 的关键要点:

  1. 信息质量决定效率:提供详细的错误信息、复现步骤和上下文,Claude Code 就能更快地定位问题
  2. 结构化描述问题:用「预期行为 vs 实际行为」的格式描述 Bug,帮助 Claude Code 理解问题的本质
  3. 善用 Claude 的阅读能力:让它阅读大量相关代码、追踪调用链,这是 Claude Code 最擅长的
  4. 你收集信息,Claude 分析:你负责在实际环境中收集日志、运行诊断工具,Claude Code 负责分析数据和代码
  5. 不要只修 Bug,要防 Bug:让 Claude Code 搜索代码库中是否存在同类问题,添加回归测试防止复发
  6. 了解局限性:AI 不能替代真实环境的调试工具(debugger、profiler、strace 等),但它能帮你更快地理解这些工具的输出
  7. 迭代式调试:如果第一次分析没有找到根因,提供更多信息或让 Claude Code 换个思路,调试本身就是一个迭代过程

评论与讨论