关于 debugging
debugging 是一项面向 AI Agent 的系统化调试技能,按复现、定位、诊断、修复和验证的流程处理软件缺陷。适用于多种语言与运行环境,并涵盖并发、内存和性能问题。
主要功能
- 01复现并缩小故障范围
- 02追踪根因并制定修复
- 03运行验证并添加回归测试
- 04分析并发、内存与性能问题
适用场景
- 01根据堆栈跟踪定位异常
- 02排查并发代码中的竞态条件
- 03诊断持续增长的内存占用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ seb1n/ awesome-ai-agent-skills --skill debugging安装后在 Claude Code 中输入/debugging确认已加载。
详细介绍
调试
此技能为 AI Agent 提供一套系统化方法,用于诊断和解决软件错误。Agent 不会猜测修复方案,而是遵循一套结构化流程——复现、隔离、诊断、修复、验证——以查明根本原因并给出可靠的修正方案。它可处理多种错误,包括逻辑错误、运行时异常、竞态条件、内存泄漏和性能回退,适用于多种语言和运行时环境。
工作流
-
复现问题。 确认错误确实可以观察到且能够重复出现。收集准确的错误消息、堆栈跟踪、日志输出,或对异常行为的描述。找出触发问题所需的最小输入或操作步骤。如果错误间歇性出现,请记录其出现频率,以及任何与其出现相关的环境条件(负载、时序、特定数据)。
-
隔离故障位置。 利用堆栈跟踪、错误消息和代码结构,缩小可能导致问题的代码范围。从故障点开始反向追踪数据流,找出值从何处开始偏离预期。检查将组件替换为存根或绕过组件后错误是否仍会出现,以排除无关代码路径。对于大型代码库,可使用二分查找策略——禁用系统的一半,检查错误是否仍会出现,然后重复此过程。
-
诊断根本原因。 确认故障代码范围后,准确找出代码行为异常的原因。常见根本原因包括:对输入的假设不正确(空值、空内容、超出范围)、并发线程修改状态、缓存或记忆化值过期、运算符优先级错误、异步调用缺少
await,或依赖项版本不兼容。区分根本原因与其症状:NullPointerException 是一种症状;根本原因可能是早在前面三个函数调用处就缺少了校验。 -
制定并应用修复方案。 编写能解决根本原因且不会引入副作用的最小改动。如果修复涉及更改共享接口,请追踪所有调用方,确保兼容性。优先采用能广泛处理此类错误的防护性修复(例如添加输入校验),而不是只针对单个已观察到的故障进行狭窄修补。
-
验证修复并防止回归。 再次执行复现步骤,确认错误已解决。编写或更新测试用例,记录此前失败的场景,防止错误悄然复发。检查现有测试是否仍能通过。如果错误出现在关键路径中,可考虑添加日志或监控,以便今后发现类似问题。
支持的技术
| 类别 | 工具和技术 |
|---|---|
| 堆栈跟踪 | Python 回溯、Java/JS 堆栈跟踪、Go panic 输出、Rust 回溯 |
| 日志 | Python logging、JavaScript console、结构化 JSON 日志 |
| 调试器 | pdb / ipdb、Chrome DevTools、gdb / lldb、dlv(Go) |
| 性能分析 | cProfile、py-spy、Chrome Performance 标签页、pprof |
| 内存分析 | tracemalloc、heapq、Valgrind、Chrome Heap Snapshots |
| 并发 | 线程转储、asyncio 调试模式、Go 竞态检测器(-race) |
用法
请提供以下一项或多项输入:
- 代码片段或文件路径,其中包含存在错误的代码。
- 错误消息或堆栈跟踪——请粘贴完整输出,而非摘要。
- 对异常行为的描述——说明预期结果与实际结果。
- 复现步骤——触发错误的确切命令、输入或用户操作。
- 日志——故障发生前及发生期间的相关日志输出。
你提供的上下文越多,Agent 就能越快、越准确地诊断问题。
示例
示例 1 — 调试 Python 异步代码中的竞态条件
Bug 报告:“有时响应中会缺少 user_profile 字段,但只在负载较高时出现。”
src/api/handler.py 中的错误代码:
import asyncio
import aiohttp
user_cache = {}
async def fetch_user(session, user_id):
if user_id in user_cache:
return user_cache[user_id]
async with session.get(f"https://api.example.com/users/{user_id}") as resp:
data = await resp.json()
user_cache[user_id] = data
return data
async def build_response(user_id):
async with aiohttp.ClientSession() as session:
profile, orders = await asyncio.gather(
fetch_user(session, user_id),
fetch_orders(session, user_id),
)
return {"user_profile": profile, "orders": orders}
Agent 诊断:
user_cache 字典是由并发协程访问的共享可变状态,访问时没有同步机制。当两个针对同一 user_id 的请求同时到达时,它们都会发现缓存未命中,都会发起 HTTP 请求,第二次写入可能会用一个部分不同的对象覆盖第一次写入。更严重的是,在并发迭代期间修改字典,在 Python 3 中可能会导致 RuntimeError。
根本原因: 并发上下文中的共享可变状态未同步。
修复:
import asyncio
import aiohttp
user_cache = {}
cache_locks = {}
global_lock = asyncio.Lock()
async def fetch_user(session, user_id):
async with global_lock:
if user_id not in cache_locks:
cache_locks[user_id] = asyncio.Lock()
lock = cache_locks[user_id]
async with lock:
if user_id in user_cache:
return user_cache[user_id]
async with session.get(f"https://api.example.com/users/{user_id}") as resp:
data = await resp.json()
user_cache[user_id] = data
return data
每个键对应的锁确保每次只有一个协程会获取指定用户的数据。其他协程会等待,然后从缓存中读取。
示例 2 — 调试 Node.js 服务中的内存泄漏
Bug 报告:“服务的内存使用量在 24 小时内从 150 MB 增长到 2 GB,随后因 OOM 崩溃。”
src/server.js 中的错误代码:
const express = require("express");
const app = express();
const requestLog = [];
app.use((req, res, next) => {
requestLog.push({
method: req.method,
url: req.url,
headers: { ...req.headers },
timestamp: new Date(),
});
next();
});
app.get("/health", (req, res) => {
res.json({ status: "ok", requestCount: requestLog.length });
});
app.listen(3000);
Agent 诊断:
requestLog 数组是一个模块级变量,会无限增长。每个传入请求都会向其中追加一个包含完整请求标头副本的对象。按每分钟 1000 个请求、平均标头大小为 2 KB 计算,这每天大约会累积 2.8 GB,与报告的 OOM 时间线相符。
根本原因: 无界增长且从未清理或轮换的内存数组。
修复:
const express = require("express");
const app = express();
const MAX_LOG_SIZE = 10000;
const requestLog = [];
app.use((req, res, next) => {
if (requestLog.length >= MAX_LOG_SIZE) {
requestLog.shift();
}
requestLog.push({
method: req.method,
url: req.url,
timestamp: new Date(),
});
next();
});
主要改动:(1)将数组限制在固定大小,并移除最旧的条目;(2)停止存储完整标头,只记录必要信息;(3)在生产环境中,使用适当的日志管道替换内存数组(例如,写入日志文件或发送到外部服务)。
验证: 使用 autocannon -d 60 http://localhost:3000/health 运行负载测试,并通过 process.memoryUsage() 监控内存。内存应在达到上限后趋于稳定,而不是线性增长。
最佳实践
- 从下往上阅读完整的堆栈跟踪。 根本原因通常位于最深层的应用程序调用帧中,而不是顶层异常。可以跳过框架调用帧,但应按顺序阅读代码中的调用帧。
- 一次只改一处。 验证假设时,只做一项修改,然后重新运行。若同时改动多处,就无法确定是哪项改动产生了效果。
- 有策略地使用日志。 在可疑函数的入口和出口插入日志语句,打印关键变量的值。修复 Bug 后,移除这些日志或降低日志详细程度。
- 先检查最近的改动。 如果 Bug 是在特定部署或提交后出现的,使用
git bisect或检查近期差异通常是找到根本原因最快的方法。 - 先复现,再修复。 切勿修复无法复现的 Bug。无法复现就无法验证修复是否有效,而且可能引入掩盖症状却未解决根本原因的改动。
- 编写回归测试。 每个已修复的 Bug 都应新增一个测试用例,确保它在修复前失败、修复后通过。这是防止同一 Bug 再次出现的最可靠方法。
边缘情况
- Heisenbug: 有些错误会在附加调试工具后消失(例如,断点导致的时序变化会掩盖竞态条件)。遇到这类问题时,请使用日志或跟踪,而不是交互式调试器;如果语言提供竞态检测器,也可以考虑运行它。
- 特定环境下的错误: 仅在生产环境中出现的错误,可能取决于操作系统版本、内存限制、网络延迟,或与开发环境不同的配置。Agent 会询问环境详情,并建议在匹配的约束条件下复现(例如,使用设置了内存限制的 Docker)。
- 第三方库错误: 如果根本原因在于依赖项,而非应用代码,修复方案可能包括升级库、应用变通方法,或固定到已知正常的版本。Agent 会检查更新日志和问题跟踪器,再给出建议。
- 编译器或运行时错误: 错误极少会出在语言运行时本身。Agent 会先彻底排查应用层面的解释;如果找不到应用层面的原因,再建议使用其他运行时版本进行测试。
- 状态损坏: 如果错误涉及数据损坏(例如,数据库行只写入了一半),诊断时需要同时检查数据和代码。Agent 会要求提供示例数据或数据库状态,以便与代码路径分析进行对照。