Diagnose 是一个面向疑难 Bug 和性能回退的诊断技能,强调以纪律化流程替代盲目翻代码。除非有明确理由,否则不应跳过任何阶段。在探索代码库时,建议借助项目的领域词汇表建立相关模块的心智模型,并查阅所触及领域的 ADR(架构决策记录)。
【第一阶段:构建反馈回路】 这是整个技能的核心,其余步骤都是机械性的。只要能为 Bug 建立快速、确定性、可由智能体运行的通过/失败信号,就一定能找到原因——二分查找、假设检验、埋点都只是在消费这个信号。没有信号,盯着代码也无济于事。应在这一步投入超常的努力,保持激进与创造性,不要放弃。
构建反馈回路的可选方式(大致按此顺序尝试):
- 在触达 Bug 的任意接缝处写失败测试——单元、集成或端到端。
- 针对运行中的开发服务器编写 curl / HTTP 脚本。
- 使用固定输入调用 CLI,将 stdout 与已知正确的快照做 diff。
- 无头浏览器脚本(Playwright / Puppeteer)——驱动 UI,对 DOM / 控制台 / 网络做断言。
- 重放捕获的 trace。把真实网络请求 / 载荷 / 事件日志存到磁盘,隔离地重放到代码路径中。
- 一次性测试脚手架。启动系统的最小子集(单个服务、模拟依赖),用单次函数调用触达 Bug 代码路径。
- 属性 / 模糊循环。若 Bug 是“有时输出错误”,运行 1000 个随机输入寻找失败模式。
- 二分查找脚手架。若 Bug 出现在两个已知状态之间(提交、数据集、版本),自动化“在状态 X 启动、检查、重复”,以便 git bisect run 使用。
- 差分循环。把同一输入分别跑旧版本与新版本(或两种配置),对比输出差异。
- HITL bash 脚本。最后手段。若必须人工点击,用 scripts/hitl-loop.template.sh 驱动,使循环仍保持结构化,捕获输出回馈给你。
构建正确的反馈回路后,Bug 已解决了 90%。还要迭代回路本身:把它当产品看待,问能否更快(缓存初始化、跳过无关初始化、缩小测试范围)、信号能否更锐利(针对具体症状断言,而非“没有崩溃”)、能否更确定(固定时间、设定 RNG 种子、隔离文件系统、冻结网络)。30 秒的脆弱回路几乎等同于没有回路;2 秒的确定性回路则是调试超能力。
对于非确定性 Bug,目标不是干净的复现,而是更高的复现率。把触发器循环 100 次、并行化、增加压力、收窄时间窗口、注入 sleep。50% 概率的 Bug 可调试,1% 则不行——持续提高复现率直到可调试。
当确实无法构建回路时,应停下来明确说明,列出尝试过的方法,并向用户请求:(a) 访问能复现问题的环境;(b) 捕获的产物(HAR 文件、日志转储、核心转储、带时间戳的屏幕录制);或 (c) 允许添加临时生产埋点。在没有回路之前,不要进入假设阶段。
【第二阶段:复现】 运行回路,观察 Bug 出现。确认:回路产生的失败模式与用户描述一致(不是附近碰巧出现的其他失败——错误的 Bug 会带来错误的修复);失败在多次运行中可复现(非确定性 Bug 则需达到足够高的复现率);已捕获确切症状(错误消息、错误输出、缓慢的耗时),供后续阶段验证修复确实解决了问题。在复现之前不要继续。
【第三阶段:假设】 在测试任何假设之前,先生成 3–5 个排好序的假设。只生成一个假设会锚定在第一个看似合理的想法上。每个假设必须可证伪,说明它做出的预测,格式为:“如果 X 是原因,那么 Y 将使 Bug 消失 / 使问题更严重。”如果无法陈述预测,该假设就只是一种感觉——丢弃或使其更精确。在测试前把排序列表展示给用户,他们常凭领域知识立刻重排(“我们刚给第 3 项部署了改动”),或已排除某些假设。这是低成本的检查点,能节省大量时间。不要因此阻塞——用户不在时按你的排序继续。
【第四阶段:埋点】 每个探针都必须对应第三阶段的一个具体预测,一次只改变一个变量。工具偏好:环境支持时优先用调试器 / REPL 检查,一个断点胜过十条日志;在能区分假设的边界处打定向日志;绝不要“全部打日志再 grep”。给每条调试日志打上唯一前缀,例如 [DEBUG-a4f2],结束时清理只需一次 grep。未打标签的日志会存活,打标签的日志会消亡。
性能分支:对于性能回退,日志通常是错的。应改为:先建立基线测量(计时脚手架、performance.now()、分析器、查询计划),再二分查找。先测量,后修复。
【第五阶段:修复 + 回归测试】 在修复前先写回归测试——但前提是存在正确的接缝。正确的接缝是指测试能在调用点按真实发生的方式复现 Bug 模式。如果唯一可用的接缝太浅(Bug 需要多个调用者时只测单个调用者、无法复现触发 Bug 的调用链的单元测试),在那里写回归测试只会带来虚假的信心。如果不存在正确接缝,这本身就是一项发现,应记录下来——代码库架构阻止了 Bug 被锁定,并标记给下一阶段。
若存在正确接缝:把最小化复现转成该接缝处的失败测试;看着它失败;应用修复;看着它通过;针对原始(未最小化)场景重新运行第一阶段的反馈回路。
【第六阶段:清理 + 复盘】 宣布完成前必须做到:原始复现不再出现(重跑第一阶段回路);回归测试通过(或记录缺少接缝的情况);所有 [DEBUG-...] 埋点已移除(用前缀 grep);一次性原型已删除(或移到明确标记的调试位置);在提交 / PR 消息中说明最终正确的假设——让下一位调试者也能学到。
然后问:什么本可以防止这个 Bug?若答案涉及架构变更(没有好的测试接缝、调用者纠缠、隐藏耦合),把具体情况交给 /improve-codebase-architecture 技能处理。修复完成之后再给出建议,而不是之前——此时你掌握的信息比刚开始时更多。