关于 systematic-debugging
面向开发者的系统化调试技能,用于调查故障、缺陷、性能问题和集成问题。它要求先查明根因并收集证据,再验证最小修复,避免只处理表面症状。
主要功能
- 01复现问题并检查错误证据
- 02追踪跨组件的数据流
- 03比较正常与故障模式
- 04逐一验证根因假设
- 05修复前添加回归测试
适用场景
- 01调查失败的测试或构建
- 02追踪跨服务的集成故障
- 03诊断不一致的 API 或检索结果
- 04避免只修补表象以处理生产缺陷
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ magnus919/ agent-skills --skill systematic-debugging安装后在 Claude Code 中输入/systematic-debugging确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdevals/evals.jsonreferences/dependency-source-example.mdmacos-sandbox-debug-example.md详细介绍
系统化调试
概述
随机修复会浪费时间并引入新 bug。临时补丁会掩盖根本问题。
**核心原则:**尝试修复前,务必找到根本原因。只修复症状,意味着失败。
铁律
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
如果尚未完成阶段 1,就不能提出修复方案。
适用场景
适用于任何技术问题:
- 测试失败
- 生产环境中的 bug
- 行为异常
- 性能问题
- 构建失败
- 集成问题
尤其适用于:
- 时间紧迫时(紧急情况很容易让人忍不住猜测)
- “只做一个快速修复”看起来显而易见时
- 已经尝试过多种修复方案时
- 之前的修复无效时
- 尚未完全理解问题时
不要在以下情况下跳过:
- 问题看起来很简单时(简单的 bug 也有根本原因)
- 赶时间时(匆忙行事必然导致返工)
- 有人要求立即修复时(系统化处理比反复试错更快)
四个阶段
完成当前阶段后,再进入下一阶段。
阶段 1:调查根本原因
在尝试任何修复之前:
1. 仔细阅读错误消息
- 不要跳过错误或警告——其中往往包含确切的解决方案
- 完整阅读堆栈跟踪。记下行号、文件路径和错误代码
- **操作:**阅读错误位置的相关源文件,并在代码库中搜索错误字符串,找出所有相关代码路径。
2. 稳定复现
- 能否可靠地触发问题?具体步骤是什么?
- 如果无法复现 → 收集更多数据,不要猜测
- **操作:**运行失败的测试或触发 bug:
pytest tests/test_module.py::test_name -v --tb=long
3. 检查近期更改
- 哪些更改可能导致问题?Git 差异、近期提交、新依赖、配置更改
- 操作:
git log --oneline -10
git diff
git log -p --follow src/problematic_file.py | head -100
4. 在多组件系统中收集证据
当系统包含多个组件时(API → 服务 → 数据库、CI → 构建 → 部署):
提出修复方案前,先添加诊断埋点。对于每个组件边界:
- 记录进入组件的数据
- 记录离开组件的数据
- 验证环境和配置的传递情况
- 检查每一层的状态
运行一次以收集证据,确定问题出在哪里。然后分析证据,找出故障组件。
5a. 检查架构和环境是否存在差异
当 bug 在生产环境中可以复现,但在测试中无法复现时:
- 比较测试夹具架构与生产环境架构——使用
PRAGMA table_info()、\\.schema或同类命令 - 检查是否缺少 NOT NULL 列、外键、唯一约束或默认值
- 检查 SQLite 日志模式、连接标志或 PRAGMA 设置是否存在差异
- 验证测试数据的结构与生产环境一致——不仅要检查列名,还要检查约束
简化的测试架构是隐藏 bug 的常见来源。如果生产表包含 model TEXT NOT NULL,而测试表只有 vector BLOB,那么只有在违反 NOT NULL 约束时才会触发的 bug,就会顺利通过测试。
**操作:**分别对两个数据库运行 PRAGMA table_info(table_name),并对比输出。
5b. 检查回退链中的异常类型是否足够具体
当 try/except 回退逻辑未能捕获日志中出现的错误时:
try:
# Primary path — can raise OperationalError
conn.execute("INSERT INTO t (model_name) VALUES (?)", ...)
except sqlite3.OperationalError:
# Fallback — can raise IntegrityError (sibling, not child)
conn.execute("INSERT INTO t (vector) VALUES (?)", ...)
OperationalError 和 IntegrityError 是同级异常——二者都继承自 DatabaseError,而 DatabaseError 继承自 Error。捕获其中一种不会捕获另一种。
可采用以下修复方案:
- 如果两条路径可能因不同子类型而失败,则捕获父类(
except sqlite3.DatabaseError) - 将
Exception作为最后的兜底(范围更广,但比漏掉异常类型更安全) - 显式处理每种预期的错误类型
5c. 渐进式特征分析 — 工具与 API 行为
当你调查的检索系统、API 或知识图谱返回空结果或不一致的结果时:
从有效的情况开始,逐步扩大范围,直到出现问题。这样可以隔离导致故障的变量。
| 输入 | 预期 | 实际 | 诊断 |
|---|---|---|---|
单个已知术语(例如 python) | 有命中 | 有命中 | 工具正常,连接正常 |
来自同一文档的双词短语(type safety) | 有命中 | 有命中 | 短语检索正常 |
相关的双词短语(generic types) | 有命中 | 0 次命中 | 找到边界 — 问题仅与该短语有关 |
| 包含相同术语的更长查询 | 0 次命中 | 0 次命中 | 确认:并非偶然 |
不要跳过特征分析: 直接断定“嵌入模型坏了”只是猜测。这个表格可以逐一排除变量。
5d. 修复库代码前检查依赖项来源
当你追踪到第三方依赖项中的 Bug 时:
修补库代码前,先确认它是从哪里安装的:
pip show <package-name>
关键字段:
- 可编辑项目位置 — 如果存在,说明这是通过
pip install -e安装的开发副本。这是有意安装的吗? - 位置 — 它位于 site-packages(生产环境)还是临时/开发目录中?
- 版本 — 与 PyPI 上的最新版本进行比较:
pip index versions <package-name>
规则: 如果该软件包是以可编辑开发副本的形式安装的,而你并非有意这样安装,立即停止并询问。症状可能是代码与上游版本不一致导致的,正确的修复方式可能是切换到生产软件包,而不是修补 fork。
示例: 一个后台工作进程因 table X has no column named Y 而崩溃。调查发现,这是从 /private/tmp/some-fork/ 安装的可编辑版本。某个开发 fork 在 INSERT 语句中添加了列名,却从未添加对应的架构迁移。正确的修复并非给 fork 添加迁移,而是切换到 PyPI 上的生产软件包,并删除开发副本。
6a. 先研究再猜测 — 系统化网页搜索
当你收集了所有本地证据,但仍不清楚根本原因时:
不要猜测解决方案。进行结构化的网页研究:
- 使用确切的错误消息搜索 — 引用错误消息,并包含错误代码和函数名
- 结合症状与平台上下文搜索 — 组合搜索故障现象和操作系统/工具版本
- 使用组件/服务名称搜索 — XPC 服务、守护进程和子系统通常有已记录的 Bug
- 优先查看近期讨论串 — 筛选当前版本相关内容。旧方案可能不适用
- 提出解决方案前阅读完整讨论串 — 只读部分内容会导致修复不完整
- 查看讨论串末尾是否有已确认的解决方法,不要只看最初的诊断
6b. macOS 应用故障排查 — 沙盒应用
当你调试 macOS 应用时(尤其是 Books、Music 或 App Store 等沙盒应用):
应用被限制在 ~/Library/Containers/<bundle-id>/ 下的沙盒容器中。
定位容器
ls ~/Library/Containers/<bundle-id>/
# Data/Library/ — preferences, caches, databases
# Data/Documents/ — user-visible content, import queues
检查配套的 XPC 服务
许多 Apple 应用会使用后台 XPC 服务执行文件操作:
# XPC services live in the framework bundle or app bundle:
/System/Library/PrivateFrameworks/<Framework>.framework/XPCServices/
/System/Applications/<App>.app/Contents/XPCServices/
ps aux | grep -i "<service-name>"
直接读取数据库
沙盒应用通常使用 SQLite/CoreData:
sqlite3 ~/Library/Containers/<bundle-id>/Data/Documents/<path>.sqlite ".tables"
sqlite3 ~/Library/Containers/<bundle-id>/Data/Documents/<path>.sqlite "SELECT * FROM ZTABLE LIMIT 10;"
检查系统日志
log show --predicate 'process == "AppName"' --last 10m --style compact
log stream --predicate 'process == "AppName"' --style compact
重置 TCC 权限
如果应用无法访问沙盒外的文件(导入失败但没有提示):
tccutil reset All com.apple.bundle-id
了解 I/O 边界
- 安全作用域书签(通过拖放或
open命令创建)仅能使用一次。如果导入失败,书签会被消耗,后续尝试也会静默失败。 - NSOpenPanel(“文件” > “导入”对话框)会创建新的书签,用于测试时更可靠。
- 如果从“下载”文件夹运行
open -b bundle-id file.ext有效,但从“桌面”运行无效,很可能是 TCC/分层访问问题(macOS 会向“下载”文件夹授予更宽松的访问权限)。
区分本地状态损坏与云同步损坏
重置容器可以修复本地状态,但无法修复 iCloud 同步损坏。云端问题的迹象:
- 完全删除容器并重新安装后,问题仍然存在
- 重启后导入一次成功,随后问题逐渐加重
- 多台设备上都出现相同问题
**操作:**如果重置本地状态无法解决问题,iCloud 同步状态可能已损坏。系统设置 → Apple ID → iCloud → 管理储存空间 → [App] → 删除所有数据是最后的手段。
恢复选项(按升级顺序排列)
- 结束并重启 XPC 服务(
kill -9 <PID>)——暂时有效,XPC 会自动重启 - 重置 App 容器(
rm -rf ~/Library/Containers/<bundle-id>/) - 重置 TCC 权限(
tccutil reset All <bundle-id>) - 重启 Mac
- 删除该 App 的 iCloud 数据
- 创建一个 macOS 测试用户——如果 App 在那里能正常工作,问题出在你的用户资源库,而不是系统
6c. 跟踪数据流
当错误出现在调用栈深处时:
- 错误值是从哪里来的?
- 是什么以错误值调用了这个函数?
- 持续向上追踪,直到找到源头
- 从源头修复,而不是处理症状
**操作:**在代码库中搜索函数引用和变量赋值,以跟踪数据路径。
阶段 1 完成检查清单
- 已完整阅读并理解错误消息
- 已稳定复现问题
- 已识别并检查近期更改
- 已收集证据(日志、状态、数据流)
- 已将问题定位到具体组件或代码
- 已形成根因假设
**停止:**在弄清楚问题发生的原因之前,不要进入阶段 2。
阶段 2:模式分析
先找到模式,再修复:
1. 查找正常工作的示例
- 在同一代码库中找到类似且正常工作的代码
- 类似的正常代码与出问题的代码有何不同?
2. 对照参考实现
- 如果要实现某种模式,请完整阅读参考实现——不要略读
- 完全理解该模式后再应用
3. 找出差异
- 正常工作的部分与出问题的部分有何不同?
- 列出所有差异,无论多小
- 不要想当然地认为“这不可能有影响”
- **操作:**在代码库中搜索类似模式以进行比较。
4. 理解依赖项
- 这需要哪些其他组件?
- 需要哪些设置、配置和环境?
- 它基于哪些假设?
阶段 3:提出假设并测试
科学方法:
1. 提出一个假设
- 清楚地表述:“我认为 X 是根因,因为 Y”
- 把它写下来。具体明确,不要含糊其辞。
2. 进行最小化测试
- 做出尽可能小的更改来测试假设
- 一次只改一个变量
- 不要一次修复多个问题
3. 继续之前先验证
- 有效了吗?→ 阶段 4
- 没有效?→ 提出新假设
- 不要在现有修复上叠加更多修复
4. 不确定时
- 说“我不理解 X”——不要假装懂
- 寻求帮助,进一步研究
阶段 4:实施
修复根因,而不是症状:
1. 创建失败测试用例
- 尽可能简单地复现问题。如果可行,编写自动化测试。
- 修复前必须有测试用例。
- 测试环境必须与生产环境一致——检查测试夹具是否符合真实架构。
2. 实施单一修复
- 解决已确定的根因。一次只做一项更改。
- 不要顺手做改进,也不要捆绑重构。
3. 验证修复
# Run the specific regression test
pytest tests/test_module.py::test_name -v
# Run full suite — no regressions
pytest tests/ -q
4. 如果修复无效——三次原则
- 停止。
- 统计:你尝试过多少次修复?
- 如果少于 3 次:返回阶段 1,根据新信息重新分析
- 如果达到或超过 3 次:停止,并质疑架构(见下方第 5 步)
- 未进行架构讨论前,不要尝试第 4 次修复
5. 如果 3 次以上修复均失败:质疑架构
表明存在架构问题的模式:
- 每次修复都会在不同位置发现新的共享状态或耦合
- 实施修复需要“大规模重构”
- 每次修复都会在其他地方引发新的症状
停止并质疑基本假设:
- 这种模式从根本上是否合理?
- 你是否在“纯粹出于惯性而坚持使用它”?
- 你应该重构架构,还是继续修复症状?
尝试更多修复之前先讨论。 这并非假设失败——而是架构错误。
危险信号——停止并遵循流程
如果你发现自己在想:
- “先快速修复,之后再调查”
- “只要试着改一下 X,看看是否有效”
- “做出多项更改,然后运行测试”
- “跳过测试,我会手动验证”
- “问题大概出在 X,我来修一下”
- “我不完全理解,但这也许行得通”
- “以下是主要问题:[列出修复方案,但没有调查]”
- 尚未追踪数据流就提出解决方案
- “再尝试修复一次”(已经尝试过 2 次以上时)
- 每次修复都会在不同位置发现新问题
这些情况都意味着:停止。返回阶段 1。
如果 3 次以上修复均失败: 质疑架构(阶段 4,第 5 步)。
调查流程——保持推进势头
当调查正在进行,而用户说“我们有所进展,但还没完成”之类的话时:
不要通过提出澄清问题打断流程。继续推进——收集更多证据,尝试下一个诊断步骤,换个角度检查。暂停调查并询问“你尝试 X 后发生了什么?”会浪费用户的注意力。
应继续推进的信号:
- “我们有所进展,但还没完成”——继续调查
- 用户不理会澄清问题——他们想要的是行动,而不是问题
- “好”或“还是不行”——尝试下一步,不要请求许可
应该怎么做: 从日志、数据库或文件状态中推断信息——不要让用户充当你的检测工具。只有当你确实无法在没有输入的情况下继续,且已用尽所有自助选项时,才提出问题。
常见的自我辩解
| 借口 | 事实 |
|---|---|
| “问题很简单,不需要流程” | 简单的问题也有根本原因。处理简单 Bug 时,流程很快。 |
| “情况紧急,没时间走流程” | 系统化调试比反复猜测和试错更快。 |
| “先试试这个,再调查” | 第一次修复会定下处理模式。一开始就要做对。 |
| “确认修复有效后再写测试” | 未经测试的修复无法持久。先写测试才能验证修复。 |
| “一次做多项修复可以节省时间” | 无法确定哪些修复有效,还会引入新 Bug。 |
| “我看出问题了,让我来修” | 看到症状 ≠ 理解根本原因。 |
| “再尝试修复一次”(失败 2 次以上后) | 失败 3 次以上 = 架构问题。质疑这种模式。 |
快速参考
| 阶段 | 主要活动 | 成功标准 |
|---|---|---|
| 1. 根本原因 | 阅读错误信息、复现问题、检查更改、收集证据、追踪数据流 | 理解是什么问题以及为什么发生 |
| 2. 模式 | 查找可用的示例、进行比较、找出差异 | 知道哪些地方不同 |
| 3. 假设 | 提出理论、进行最小化测试、每次只改变一个变量 | 得到确认或形成新假设 |
| 4. 实施 | 创建回归测试、修复根本原因、验证 | Bug 已解决,所有测试均通过 |
参考资料
- references/dependency-source-example.md — 一个完整示例,说明如何发现可编辑的开发分支导致库依赖项的架构发生偏移。阶段 1 的第 5d 步若让你怀疑 Bug 源于第三方软件包,请阅读此文。
- references/macos-sandbox-debug-example.md — 调试 Apple Books.app 导入失败问题的完整演练,展示阶段 1 第 6b 步中的 macOS 沙盒技术。调试 macOS 沙盒应用程序时请阅读此文。