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阶段
  • 方法无效?→ 提出新的假设
  • 不要再叠加额外的修复措施

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']
)

基于测试驱动开发的方法

在修复错误时:

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

实际应用效果

从调试过程来看:

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

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