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, 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阶段
- 方法无效?→ 提出新的假设
- 不要再叠加额外的修复措施
4. 当你不确定时
- 直接说“我不明白X”
- 不要假装知道
- 向用户寻求帮助
- 进一步深入研究
第4阶段:实施
需解决根本原因,而非表面症状:
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次及以上修复均失败: 应重新审视系统架构(第4阶段第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%
- 新产生的错误:几乎为零 对比 非常常见
没有捷径,无需猜测。系统化方法始终最有效。