name: systematic-debugging description: “4-phase root cause debugging: understand bugs before fixing.” version: 1.1.0 author: Hermes Agent (adapted from obra/superpowers) license: MIT platforms: [linux, macos, windows] metadata: hermes: tags: [debugging, troubleshooting, problem-solving, root-cause, investigation] related_skills: [test-driven-development, plan, subagent-driven-development]
系统化调试方法
概述
随意的修复不仅会浪费时间,还可能引发新的错误。仓促的修补措施只能掩盖根本问题。
核心原则: 在尝试任何修复之前,务必先找到问题的根源。仅解决表面症状等同于失败。
违背这一流程的条文,便是背离了调试的本质。
不可违背的铁律
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
如果您尚未完成第一阶段,就无法提出修复方案。
反馈循环规则
反馈循环即调试过程。在通过阅读代码形成理论之前,应先创建或确定一个精准的指令:该指令能在出现用户描述的确切症状时显示红色,而在故障修复后则变为绿色。一个精准的循环具备快速执行、结果可预测、可由智能体运行等特性,并且足够具体以便定位特定故障——而不仅仅是“不再崩溃”。
当难以稳定复现问题时,应投入更多精力来构建这样的循环。没有能够触发红色提示的循环就盲目猜测,正是这项技能旨在避免的错误方式。
适用场景
适用于所有技术问题:
- 测试失败
- 生产环境中的故障
- 意外行为
- 性能问题
- 构建失败
- 集成问题
尤其建议在以下情况使用:
- 时间紧迫(紧急情况容易让人倾向于盲目猜测)
- 似乎“只需快速修复一下”
- 已经尝试过多种修复方法
- 之前的修复方案无效
- 对问题尚未完全理解
以下情况切勿跳过此步骤:
- 问题看似简单(简单的故障也有其根本原因)
- 时间仓促(匆忙行事只会导致返工)
- 有人要求立即解决问题(系统化方法比盲目尝试更高效)
四个阶段
在进入下一阶段之前,必须完成当前阶段的全部工作。
第一阶段:根本原因分析
在尝试任何修复方案之前:
1. 仔细阅读错误信息
- 不要跳过错误或警告信息
- 它们往往包含解决问题的关键线索
- 完整阅读堆栈跟踪信息
- 记下行号、文件路径及错误代码
操作方法: 对相关源文件使用 read_file 命令。可使用 search_files 在代码库中查找错误字符串。
2. 构建精准的反馈循环
- 是否能用一个指令触发用户描述的确切症状?
- 该指令在故障存在时是否会失败,而仅在故障修复后才正常运行?
- 其执行速度是否足够快,可支持反复运行?
- 结果是否可预测?对于那些偶尔才会出现的故障,能否提高其复现频率以便调试?
- 如果无法复现 → 应收集更多数据,而非盲目猜测。
构建循环的方法——可按以下顺序尝试:
- 在接近故障点的位置运行的失败测试:单元测试、集成测试或端到端测试。
- 对正在运行的开发服务器发起请求的HTTP 脚本 / curl 命令。
- 使用测试数据通过CLI 调用相关程序,并将标准输出/错误输出与预期结果进行对比。
- 使用无头浏览器脚本(如 Playwright/Puppeteer),对 DOM、控制台或网络请求进行验证。
- 回放已捕获的日志信息:HAR 文件、请求载荷、事件日志、队列消息或 webhook 内容。
- 编写一个简易测试框架,启动系统中最小且能触发故障的那部分功能。
- 当故障表现为在较大输入范围内出现间歇性错误输出时,可使用属性/模糊测试循环。
- 若故障是在两个已知状态之间出现的,可使用适合
git bisect run的二分查找测试框架。 - 通过对比测试循环,比较旧版本与新版本、两种配置、两种服务提供商或两组数据集之间的差异。
- 仅作为最后手段使用人工干预脚本:将人工操作步骤自动化,并记录其结果,以保持调试流程的条理性。
在循环构建完成后,还需进一步优化:
- 提高执行速度:缓存相关设置、缩小测试范围、跳过无关的初始化步骤。
- 使触发信号更精准:明确验证具体症状,而非仅判断是否“成功”。
- 增强结果可预测性:固定时间点、控制随机性、隔离文件系统、冻结网络连接。
对于那些结果不可预测的故障,首要目标是提高其复现频率,而非追求完美。可以重复执行触发操作100次,采用并行测试、施加压力、缩小时间窗口或插入休眠时间。如果故障复现率为50%,则仍有希望调试;而若仅为1%,通常则难以解决。
操作方法: 使用 terminal 工具来运行这个精准的反馈循环。
# Run a specific failing test
pytest tests/test_module.py::test_name -v
# Or run a scripted repro
python scripts/repro_bug.py
# Or run a high-repetition flaky repro
for i in {1..100}; do pytest tests/test_flake.py::test_name -q || break; done
3. 检查近期变更
- 哪些变更可能导致了此问题?
- Git差异对比、最新提交记录
- 新增的依赖项、配置更改
操作步骤:
# Recent commits
git log --oneline -10
# Uncommitted changes
git diff
# Changes in specific file
git log -p --follow src/problematic_file.py | head -100
4. 在多组件系统中收集故障证据
当系统包含多个组件时(API → 服务 → 数据库,CI → 构建 → 部署):
在提出修复方案之前,先添加诊断工具:
针对每个组件边界:
- 记录进入该组件的数据
- 记录离开该组件的数据
- 验证环境/配置是否正确传递
- 检查各层级的状态
运行一次以收集能显示故障发生位置的证据。 随后分析这些证据,确定出出现问题的组件。 最后针对该特定组件进行深入排查。
5. 追踪数据流
当错误位于调用栈的深层时:
- 有问题的值源自何处?
- 是哪个函数传入了有问题的值?
- 持续向上追踪直至找到源头
- 应从根源解决问题,而非仅处理表面症状
操作建议: 使用 search_files 功能来追踪相关引用。
# Find where the function is called
search_files("function_name(", path="src/", file_glob="*.py")
# Find where the variable is set
search_files("variable_name\\s*=", path="src/", file_glob="*.py")
第一阶段完成检查清单
- 已完整阅读并理解错误信息
- 已存在循环指令且至少运行过一次
- 该循环具备精准定位能力:能准确反映用户的实际问题,而非类似故障
- 该循环结果具有确定性,或偶发故障的出现频率足够高以便调试
- 已识别并审查近期所做的修改
- 已收集相关证据(日志、系统状态、数据流)
- 已将问题定位到具体组件或代码上
- 能够提出并验证根本原因假设
停止: 在弄清楚问题产生的原因之前,切勿进入第二阶段。
第二阶段:模式分析
在修复前先找出规律:
0. 最小化问题重现场景
一旦循环触发红色状态,需将问题重现范围缩小到仍会出现异常的最简场景。逐一削减输入参数、调用方、配置项、数据以及操作步骤,每次调整后都重新运行循环。仅保留对故障起关键作用的元素。
当移除所有剩余元素后循环仍能恢复正常时,即表示已完成此步骤。最小化的问题重现场景能够缩小假设范围,通常也能作为最简洁的回归测试用例。
1. 寻找正常运行的示例
- 在同一代码库中查找类似且能正常运行的代码
- 哪些部分是正常的,且与出现故障的部分具有相似性?
操作: 使用 search_files 功能来查找类似的模式:
search_files("similar_pattern", path="src/", file_glob="*.py")
2. 与参考实现进行对比
- 若要实现某种模式,务必完整阅读参考实现
- 不要略读——需逐行细读
- 在应用之前要彻底理解该模式的工作原理
3. 确定差异点
- 正常运行与出错状态之间有何不同?
- 无论差异多么微小,都需一一列出
- 切勿认为“那无关紧要”
4. 了解依赖关系
- 它还需要哪些其他组件?
- 需要哪些设置、配置或环境条件?
- 它基于哪些假设运行?
第三阶段:假设提出与测试
科学方法:
1. 制定可排序且可证伪的假设
- 在对任何假设进行测试之前,先提出3–5个合理的假设。
- 按其成立的可能性以及被证伪的难度对其进行排序。
- 明确每个假设的预测内容:“如果X是原因,那么改变或观察Y就应该导致Z发生。”
- 对那些无法做出可验证预测的假设予以舍弃或进一步细化。
如果有用户在场,应在测试前先展示这些排序后的假设列表。他们可能具备相关领域知识,能够立即对假设的优先级进行重新评估。如果用户不在场,则直接按照自己的排序进行操作。
2. 最小化测试范围
- 使用最简单的测试方法来验证排名最高的假设。
- 每次只修改一个变量。
- 不要同时修复多个问题。
- 若有条件,优先使用调试器或REPL进行排查;一个断点往往比大量日志更有效。
- 如果需要添加日志,请为每条临时日志加上唯一的前缀,如
[DEBUG-a4f2],这样后续清理只需一次搜索即可完成。
3. 在继续之前进行验证
- 测试成功?→ 进入第四阶段
- 测试失败?→ 提出新的假设
- 绝对不要在原有问题上再叠加更多修复措施
4. 当你不确定时
- 直接说“我不明白X”
- 不要假装知晓
- 向用户寻求帮助
- 进一步深入研究
第四阶段:实现方案
要解决根本原因,而非表面症状:
1. 创建失败的测试用例
- 实现最简单的复现方式
- 如有可能,将其转化为自动化测试
- 在开始修复之前必须先准备好该测试用例
- 可运用“测试驱动开发”相关技能来完成此步骤
2. 仅实施一项修复措施
- 针对已确定的根本原因进行修正
- 每次只做一次更改
- 不要趁机顺便进行其他改进
- 也不要一次性进行大规模重构
3. 验证修复效果
# Run the specific regression test
pytest tests/test_module.py::test_regression -v
# Run full suite — no regressions
pytest tests/ -q
4. 若修复无效——“三次原则”
- 暂停。
- 统计:你已尝试过多少种修复方法?
- 若少于3种:返回第一阶段,利用新信息重新分析
- 若达到3种及以上:立即暂停,并审视架构问题(见下文第5步)
- 未经架构层面讨论,切勿尝试第4种修复方法
5. 若3种及以上修复均失败:审视架构
表明存在架构问题的迹象:
- 每次修复都会在不同位置暴露出新的共享状态或耦合问题
- 实施修复需要大规模重构
- 每次修复都会在其他地方引发新的问题
暂停并反思基本原则:
- 这种设计模式从根本上是否合理?
- 我们是否只是出于惯性而坚持使用它?
- 是应该重构架构,还是继续仅解决表面症状?
在尝试更多修复之前,务必与用户进行沟通。
这并非假设错误——而是架构本身存在问题。
警示信号——立即暂停并遵循流程
如果你发现自己有如下想法:
- “先快速解决,稍后再深入调查”
- “试试修改X,看看是否有效”
- “同时进行多项修改,然后运行测试”
- “跳过测试,我手动验证即可”
- “问题很可能出在X上,让我去修复它”
- “虽然不太明白,但这样或许能行”
- “虽然模式表明是X,但我打算稍作调整”
- “主要问题如下:[未经过调查便列出修复方案]”
- 在未追踪数据流之前就提出解决方案
- “再试一次修复”(尽管已经尝试过2次及以上)
- 每次修复都在不同位置引发新问题
以上所有情况都意味着:立即暂停,返回第一阶段。
若3种及以上修复均失败: 审视架构问题(第四阶段第5步)。
常见借口与事实
| 借口 | 实际情况 |
|---|---|
| “问题很简单,无需遵循流程” | 简单问题也有根本原因。对于简单错误,规范流程反而能更快解决问题。 |
| “情况紧急,没时间按流程来” | 系统化的调试方式比盲目试错的效率更高。 |
| “先试试这个,然后再调查” | 第一次修复就会定下后续解决问题的模式,应从一开始就做好。 |
| “等确认修复有效后再写测试” | 未经测试的修复无法长期稳定运行,先测试才能证明其有效性。 |
| “同时进行多项修复可以节省时间” | 这样无法确定究竟是哪项修改起了作用,还可能引发新错误。 |
| “参考资料太长,我就稍作调整” | 不完全理解内容就动手修改必然会导致错误,务必完整阅读文档。 |
| “我看到问题了,让我来修复它” | 看到症状并不等同于理解了根本原因。 |
| “再试一次修复”(在2次及以上失败后) | 3次及以上失败意味着存在架构问题,应重新审视问题模式,而非继续尝试修复。 |
快速参考指南
| 阶段 | 关键任务 | 成功标准 |
|---|---|---|
| 1. 查明根本原因 | 查看错误信息、重现问题、检查变更内容、收集证据、追踪数据流 | 明确问题的“是什么”以及“为何发生” |
| 2. 分析模式 | 寻找有效的解决方案案例、进行对比、找出差异点 | 清楚了解哪些部分存在不同 |
| 3. 建立假设 | 形成理论假设、逐一测试单一变量 | 确认假设成立或形成新的假设 |
| 4. 实施修复 | 编写回归测试、解决根本原因、进行验证 | 错误已修复,所有测试均通过 |
Hermes Agent集成
调查工具
在第一阶段可使用以下Hermes工具:
search_files— 查找错误字符串、追踪函数调用、定位问题模式read_file— 读取带行号的源代码,以便精确分析terminal— 运行测试、查看git历史记录、重现错误web_search/web_extract— 查找错误信息相关的资料及库文档
结合delegate_task使用
对于复杂的多组件调试场景,可调度子代理来执行调查任务:
delegate_task(
goal="Investigate why [specific test/behavior] fails",
context="""
Follow systematic-debugging skill:
1. Read the error message carefully
2. Reproduce the issue
3. Trace the data flow to find root cause
4. Report findings — do NOT fix yet
Error: [paste full error]
File: [path to failing code]
Test command: [exact command]
""",
toolsets=['terminal', 'file']
)
基于测试驱动开发的方法
在修复错误时:
- 编写能够复现该错误的测试用例(RED阶段)
- 通过系统化的方法定位根本原因
- 解决根本问题(GREEN阶段)
- 通过测试验证修复效果并防止问题再次出现
实际应用效果
通过对比不同的调试方式:
- 系统化方法:仅需15-30分钟即可解决问题
- 随机尝试方法:需花费2-3小时反复折腾
- 首次修复成功率:95% 对比 40%
- 引入新错误的情况:几乎为零 对比 非常常见
没有捷径,无需猜测。系统化方法始终最有效。