用途
这是一个面向疑难 Bug 和性能回归的诊断纪律(Skill)。当用户说“diagnose”“debug this”,或报告某处损坏、抛错、失败、变慢时使用。它强调:除非有明确理由,否则不要跳过任何阶段。
在探索代码库时,如果存在 GLOSSARY.md,应先阅读它以建立相关模块的心智模型,并查看所触及区域的 ADR(架构决策记录)。
脱敏要求
该技能会展示命令、输出和捕获的产物。展示前必须对每个密钥脱敏,用 <REDACTED> 替代。应基于环境变量构建循环,让凭据留在环境中,而不是出现在展示内容里。捕获的产物可能带有认证头,只引用携带信号的那几行。如果脱敏后的输出不足以诊断 Bug,应明确说明并向用户求助。
阶段一:构建反馈回路
这是整个技能的核心,其余都是机械操作。如果能针对该 Bug 获得一个紧凑的通过/失败信号(即在该 Bug 上会变红),就能找到原因;二分、假设检验和插桩都只是消费这个信号。如果没有,盯着代码看也无济于事。应在此投入不成比例的精力,要激进、有创意、拒绝放弃。
构建回路的方式(大致按顺序):
- 在能触达 Bug 的接缝处写失败测试:单元、集成、端到端。
- 针对运行中的开发服务器写 Curl / HTTP 脚本。
- 用固定输入调用 CLI,将 stdout 与已知良好快照做 diff。
- 无头浏览器脚本(Playwright / Puppeteer)驱动 UI,并对 DOM/控制台/网络做断言。
- 重放捕获的 trace:把真实网络请求 / 载荷 / 事件日志存盘,隔离地通过代码路径重放。
- 一次性测试台:启动系统的最小子集(单个服务、模拟依赖),用一次函数调用走通 Bug 代码路径。
- 属性 / 模糊循环:如果 Bug 是“有时输出错误”,跑 1000 个随机输入寻找失败模式。
- 二分测试台:如果 Bug 出现在两个已知状态之间(提交、数据集、版本),自动化“在状态 X 启动、检查、重复”,以便用 git bisect run。
- 差分循环:把同一输入分别跑旧版本与新版本(或两种配置),diff 输出。
- 人在回路(HITL)bash 脚本:最后手段。如果必须由人点击,用 scripts/hitl-loop.template.sh 驱动,使循环仍有结构,捕获的输出回传给你。
构建正确的反馈回路,Bug 就解决了 90%。
收紧回路
把回路当作产品来打磨:能否更快(缓存初始化、跳过无关初始化、缩小测试范围)?能否让信号更锐利(断言具体症状,而非“没崩溃”)?能否更确定(固定时间、设定随机种子、隔离文件系统、冻结网络)?30 秒的不稳定回路几乎不比没有好;2 秒的确定性回路是调试超能力。
非确定性 Bug
目标不是干净复现,而是提高复现率。把触发器循环 100 次、并行化、加压力、收窄时间窗口、注入 sleep。50% 概率的 Bug 可调试,1% 则不行,所以要持续提高复现率直到可调试。
确实无法构建回路时
停下来并明确说明。列出尝试过的方法。向用户请求:(a) 能复现问题的环境访问权限,(b) 脱敏后的捕获产物(HAR 文件、日志转储、核心转储、带时间戳的录屏),或 (c) 添加临时生产插桩的许可。没有回路就不要进入假设阶段。
完成标准:一个紧凑且能变红的回路。阶段一完成的标志是:你能说出一个已经至少运行过一次的命令(脚本路径、测试调用、curl),并展示其调用与输出(脱敏),且该命令满足:
- 能变红:驱动真实 Bug 代码路径并断言用户的确切症状,能在此 Bug 上变红、修复后变绿。不是“运行不报错”,而是必须能捕获这个具体 Bug。
- 确定性:每次运行结论一致(不稳定 Bug 则按上述要求固定高复现率)。
- 快速:以秒计,而非分钟。
- 智能体可运行:可无人值守运行;只有通过 scripts/hitl-loop.template.sh 才需要人在回路。
如果在该命令存在之前就发现自己读代码构建理论,请停下:直接跳到假设正是本技能要防止的失败。没有能变红的命令,就没有阶段二。
阶段二:复现 + 最小化
运行回路,看着它因 Bug 出现而变红。确认:回路产生的失败模式是用户描述的那个,而非附近碰巧出现的另一个失败(错误的 Bug = 错误的修复);失败在多次运行中可复现(非确定性 Bug 则复现率足够高);已捕获确切症状(错误消息、错误输出、慢的耗时),以便后续阶段验证修复确实解决了它。
最小化
一旦变红,把复现缩小到仍能变红的最小场景。逐一削减输入、调用方、配置、数据和步骤,每次削减后重跑回路,只保留对失败起关键作用的元素。这样做是因为最小复现能缩小阶段三的假设空间,并成为阶段五干净的回归测试。完成标准:剩余每个元素都是关键的,去掉任何一个都会让回路变绿。未复现并最小化之前不要继续。
阶段三:提出假设
在测试任何假设之前,先产生 3–5 个排序的假设。只生成单个假设会锚定在第一个看似合理的想法上。每个假设必须可证伪:陈述它做出的预测。格式:“如果 <X> 是原因,那么 <Y> 会让 Bug 消失 / <Z> 会让它更糟。”如果无法陈述预测,该假设只是感觉,应丢弃或锐化。在测试前把排序列表展示给用户,他们常有领域知识能立即重排(“我们刚部署了 #3 的改动”),或知道已排除的假设。这是廉价的检查点,能省大量时间。不要阻塞等待;用户不在时按自己的排序继续。
阶段四:插桩
每个探针必须对应阶段三的某个具体预测。一次只改变一个变量。工具偏好:环境支持时用调试器 / REPL 检查,一个断点胜过十条日志;在能区分假设的边界处打定向日志;绝不“记录一切再 grep”。给每条调试日志打唯一前缀,例如 [DEBUG-a4f2],结束时清理只需一次 grep。未打标签的日志会存活,打标签的会被清除。
性能分支:对性能回归,日志通常是错的。应建立基线测量(计时测试台、performance.now()、profiler、查询计划),然后二分。先测量,后修复。
阶段五:修复 + 回归测试
在修复前写回归测试,但仅当存在正确的接缝时。正确的接缝是指测试能在调用点按 Bug 实际发生的方式走通真实 Bug 模式。如果唯一可用的接缝太浅(Bug 需要多个调用方时只测单个调用方,或单元测试无法复现触发 Bug 的链路),在那里写回归测试会带来虚假信心。如果不存在正确接缝,这本身就是发现,应记录:代码库架构阻止了 Bug 被锁定,标记给下一阶段。
如果存在正确接缝:把最小复现变成该接缝处的失败测试;看着它失败;应用修复;看着它通过;针对原始(未最小化)场景重跑阶段一的反馈回路。
阶段六:清理
宣布完成前必须:原始复现不再复现(重跑阶段一回路);回归测试通过(或记录接缝缺失);所有 [DEBUG-...] 插桩已移除(grep 前缀);一次性原型已删除(或移到明确标记的调试位置);在提交 / PR 消息中说明最终正确的假设,让下一个调试者学习。