Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


系统化调试方法

概述

随意的修复不仅会浪费时间,还可能引发新的错误。仓促的修补措施只能掩盖根本问题。

核心原则: 在尝试任何修复之前,务必先找到问题的根源。仅解决表面症状等同于失败。

违背这一流程的条文,便是背离了调试的本质。

不可违背的铁律

NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST

如果您尚未完成第一阶段,就无法提出修复方案。

反馈循环规则

反馈循环即调试过程。在通过阅读代码形成理论之前,应先创建或确定一个精准的指令:该指令能在出现用户描述的确切症状时显示红色,而在故障修复后则变为绿色。一个精准的循环具备快速执行、结果可预测、可由智能体运行等特性,并且足够具体以便定位特定故障——而不仅仅是“不再崩溃”。

当难以稳定复现问题时,应投入更多精力来构建这样的循环。没有能够触发红色提示的循环就盲目猜测,正是这项技能旨在避免的错误方式。

适用场景

适用于所有技术问题:

  • 测试失败
  • 生产环境中的故障
  • 意外行为
  • 性能问题
  • 构建失败
  • 集成问题

尤其建议在以下情况使用:

  • 时间紧迫(紧急情况容易让人倾向于盲目猜测)
  • 似乎“只需快速修复一下”
  • 已经尝试过多种修复方法
  • 之前的修复方案无效
  • 对问题尚未完全理解

以下情况切勿跳过此步骤:

  • 问题看似简单(简单的故障也有其根本原因)
  • 时间仓促(匆忙行事只会导致返工)
  • 有人要求立即解决问题(系统化方法比盲目尝试更高效)

四个阶段

在进入下一阶段之前,必须完成当前阶段的全部工作。


第一阶段:根本原因分析

在尝试任何修复方案之前:

1. 仔细阅读错误信息

  • 不要跳过错误或警告信息
  • 它们往往包含解决问题的关键线索
  • 完整阅读堆栈跟踪信息
  • 记下行号、文件路径及错误代码

操作方法: 对相关源文件使用 read_file 命令。可使用 search_files 在代码库中查找错误字符串。

2. 构建精准的反馈循环

  • 是否能用一个指令触发用户描述的确切症状?
  • 该指令在故障存在时是否会失败,而仅在故障修复后才正常运行?
  • 其执行速度是否足够快,可支持反复运行?
  • 结果是否可预测?对于那些偶尔才会出现的故障,能否提高其复现频率以便调试?
  • 如果无法复现 → 应收集更多数据,而非盲目猜测。

构建循环的方法——可按以下顺序尝试:

  1. 在接近故障点的位置运行的失败测试:单元测试、集成测试或端到端测试。
  2. 对正在运行的开发服务器发起请求的HTTP 脚本 / curl 命令
  3. 使用测试数据通过CLI 调用相关程序,并将标准输出/错误输出与预期结果进行对比。
  4. 使用无头浏览器脚本(如 Playwright/Puppeteer),对 DOM、控制台或网络请求进行验证。
  5. 回放已捕获的日志信息:HAR 文件、请求载荷、事件日志、队列消息或 webhook 内容。
  6. 编写一个简易测试框架,启动系统中最小且能触发故障的那部分功能。
  7. 当故障表现为在较大输入范围内出现间歇性错误输出时,可使用属性/模糊测试循环
  8. 若故障是在两个已知状态之间出现的,可使用适合 git bisect run二分查找测试框架
  9. 通过对比测试循环,比较旧版本与新版本、两种配置、两种服务提供商或两组数据集之间的差异。
  10. 仅作为最后手段使用人工干预脚本:将人工操作步骤自动化,并记录其结果,以保持调试流程的条理性。

在循环构建完成后,还需进一步优化:

  • 提高执行速度:缓存相关设置、缩小测试范围、跳过无关的初始化步骤。
  • 使触发信号更精准:明确验证具体症状,而非仅判断是否“成功”。
  • 增强结果可预测性:固定时间点、控制随机性、隔离文件系统、冻结网络连接。

对于那些结果不可预测的故障,首要目标是提高其复现频率,而非追求完美。可以重复执行触发操作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']
)

基于测试驱动开发的方法

在修复错误时:

  1. 编写能够复现该错误的测试用例(RED阶段)
  2. 通过系统化的方法定位根本原因
  3. 解决根本问题(GREEN阶段)
  4. 通过测试验证修复效果并防止问题再次出现

实际应用效果

通过对比不同的调试方式:

  • 系统化方法:仅需15-30分钟即可解决问题
  • 随机尝试方法:需花费2-3小时反复折腾
  • 首次修复成功率:95% 对比 40%
  • 引入新错误的情况:几乎为零 对比 非常常见

没有捷径,无需猜测。系统化方法始终最有效。