name: diagnosing-bugs description: "针对疑难缺陷和性能回归的诊断循环。用户说“诊断”“调试”,或报告损坏、抛错、失败、变慢时使用。"
先建立能抓住故障的反馈,再分析原因
这篇最值得反复精读。核心不是列很多原因,而是获得一条能对特定故障判定失败或成功的检查路径。
还不清楚 Skill、Agent、安装和调用?先读 从零开始的6节入门课。
在关系图中查看 diagnosing-bugs 与其他技能的关联 →
先把必要的概念讲清楚
本课把排错变成可以不断检验的推理过程。关键能力是先让问题以可观察方式出现,再用证据区分原因;不能因为某个解释听起来专业就直接改代码。
下面是老师补充的入门说明;原作者的要求保留在中英对照正文中。所有例子均为帮助理解而构造的教学情境。
fixture / harness|测试样本与运行装置
fixture 是为复现或测试准备的已知输入和初始数据,例如固定课程清单。harness 是把代码、输入和检查串起来的运行装置,例如一次命令启动最小服务并断言结果。它们帮助每次在相同条件下比较,而不是凭上次页面看起来怎样判断。
regression|原来正常,修改后退化
已有能力在后续修改中变坏,可以是功能错误或性能变慢。回归测试把曾经的故障场景固定为检查,帮助以后发现同类问题。但测试只覆盖写明的输入和边界,单条回归测试不是绝对不会再出错的保证。
seam|测试接入点
作者在测试语境中指测试进入系统、触发行为并观察结果的公共边界。例如调用“收藏课程”接口,再通过“我的收藏”接口检查结果。入口可以是模块公开函数、服务接口或页面,并非一定是浏览器。选择高层稳定入口能覆盖内部多个步骤;入口少不等于测试场景少。
interface / API|接口
使用者与一项能力交互时遵循的约定,包括可调用什么、传入什么、得到什么、失败怎样表示。接口可以是程序函数,也可以是网络请求。创建收藏接口接收用户和课程信息、返回收藏结果;它不需要向调用者暴露数据库表结构。这里的接口通常不是指页面外观。 本教材涉及更广义的“接口”时,还包括错误、调用顺序和约束等使用约定;不能把接口一概等同于网络 API。
mock|替代真实依赖的测试对象
测试时用可控制的对象替代真实服务,例如让模型调用固定返回一句答案。这样可以稳定检查程序如何处理响应,但无法据此证明真实模型总能生成好答案。过度替代内部组件还会让测试和内部写法绑定,一重构就要改测试。
CI|持续集成检查
把修改提交到共享流程时,自动运行测试、类型检查等约定检查,尽早发现集成问题。绿灯表示配置的检查通过,不代表所有需求都正确;红灯也可能由环境故障导致,应看具体证据。需要先检查“配置了什么”,才能解释绿灯的意义。
读原文,理解每一步为什么这样做
左右内容按小节对应;窄屏先中文、后英文。两种语言均完整展示,对应讲解紧接在小节之后。译文传达原文要求;老师讲解补充概念、原因、例子与适用边界。
原文中的复现率、耗时和工作量表述用于表达作者的调试取向,不是本教材的测量结果。
name: diagnosing-bugs description: Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.
排查棘手的程序问题
这是一套处理难以定位的 bug 的工作纪律。只有明确说明理由时,才可以跳过阶段。
探索代码库时,如果有 CONTEXT.md,先阅读它,理解相关模块;同时检查涉及区域的架构决策记录。
Diagnosing Bugs
A discipline for hard bugs. Skip phases only when explicitly justified.
When exploring the codebase, read CONTEXT.md (if it exists) to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.
展示之前先隐藏敏感信息
本技能要求展示命令、输出和捕获材料。先把秘密信息替换成 <REDACTED>。脚本通过环境变量读取凭据,避免让秘密出现在展示的命令中。捕获材料可能含身份验证请求头,只引用真正提供诊断线索的行。
如果脱敏后信息不足以诊断,说明情况并询问用户。
Redact
This skill has you show commands, outputs and captured artifacts. Redact every secret first: write <REDACTED> in its place. Build loops against env vars, so the credential stays in the environment rather than in what you show. Captured artifacts carry auth headers: quote only the lines that carry the signal.
If the redacted output is not enough to diagnose the bug, say so and ask the user.
先处理资料中的敏感值,不破坏诊断所需结构
日志、网络请求和截图可能带有密钥、用户标识或真实内容。脱敏是替换不必要的敏感值,同时保留能判断问题的结构,例如请求字段、错误码、事件顺序。
例子:“某用户第二次收藏返回冲突”需要保留重复操作关系,却不一定需要真实邮箱。可以用固定假用户表示同一身份。若把所有 ID 随机改成不同值,反而可能抹掉 bug 的因果关系。
原文要求在传播调试材料前考虑这一点。它不意味着看到敏感值就不分析,而是让可用证据与不必要暴露分开。
阶段 1:建立可反复运行的验证流程
这是本技能的核心。 先得到一个快速、明确的通过或失败信号,而且它必须会在这个具体 bug 出现时失败。二分定位、假设验证和诊断记录都依靠它。作者强调:缺少这个信号,只盯代码看不能代替验证。
在这里投入比通常更多的精力,积极尝试,灵活换方法,不轻易放弃。
Phase 1: Build a feedback loop
This is the skill. Everything else is mechanical. If you have a tight pass/fail signal for the bug (one that goes red on this bug), you will find the cause; bisection, hypothesis-testing, and instrumentation all just consume it. If you don't have one, no amount of staring at code will save you.
Spend disproportionate effort here. Be aggressive. Be creative. Refuse to give up.
反馈循环为什么比猜原因更优先
循环就是能够反复运行的输入、操作和检查。收藏偶尔丢失时,“我昨天见过一次”不能指导修复;“固定输入连续执行收藏和刷新,检查记录是否仍存在”才开始形成可检验信号。
测试、HTTP 脚本、CLI、浏览器、轨迹重放和最小测试装置,是不同构造方法。选择能触达症状且可重复的入口,而不是把所有方法都跑一遍。旧新版本差分适合比较同输入输出,二分适合缩小问题首次出现的版本范围。
**所谓 tight,**是执行快、结果较稳定、启动负担低。每次循环耗半小时且需要你手工点十步,就很难连续检验假设。可以缓存无关准备、固定随机种子、隔离测试数据,提高信号质量。
用一次重复收藏故障,理解什么叫能抓住问题的检查
假设用户报告:快速点两次收藏,会出现两条记录。 正确验证必须触发两次请求,再检查同一用户和课程只有一条记录。只打开页面确认没报错,抓不住这个问题。
把触发与检查写成一条命令,是为了每次修改后都在相同条件下比较。先看到它失败,证明检查确实能识别这个 bug,而不是一个永远通过的装饰。
然后再列候选原因:前端重复发送、后端未防重复、并发请求同时通过检查。每种都带预测,例如“顺序发送不重复,同时发送容易重复”。接下来观察能区分它们的证据,而不是一次乱改多处。
修复后既运行小测试,也回到用户原来的完整操作。小场景通过,不自动证明原始问题完全消失。最后清理临时日志并记录原因,后续才知道为什么这样改。
原文的“解决九成”以及复现概率数字是强调方法的经验表达,不是保证成功率。历史 diagnose 还在后文使用了最小复现,却没有像新版一样给出完整缩小阶段;阅读时需要看到这个版本差别。
构建方法:大致按以下顺序尝试
- 从能触发问题的入口写失败测试,可以是单元、集成或端到端测试。
- 用 Curl 或 HTTP 脚本请求正在运行的开发服务器。
- 用固定测试输入调用命令行程序,将标准输出与已知正确的快照比较。
- 用 Playwright 或 Puppeteer 编写无界面浏览器脚本,操作页面,并检查页面结构、控制台或网络行为。
- 回放捕获记录:将实际请求、数据或事件日志保存成文件,再沿相关代码路径单独重放。
- 搭建一次性诊断运行环境:只启动必要的一小部分系统,例如一个服务与模拟依赖,用一次函数调用触发问题路径。
- 使用属性或随机输入循环:对于有时输出错误的问题,运行例如 1000 组随机输入,寻找失败模式。
- 建立二分定位流程:已知问题在两个提交、数据集或版本间出现时,自动启动某个状态、检查、再换状态,使
git bisect run能帮助定位。 - 建立差异比较:同一输入分别经过旧版和新版,或两套配置,对照输出。
- 最后才采用需要人参与的 Bash 流程。若确实必须人工点击,使用
scripts/hitl-loop.template.sh组织步骤,将捕获的输出反馈给 Agent。
作者用“找到正确验证流程,问题就解决了九成”强调这一步的价值。
Ways to construct one, in roughly this order
- Failing test at whatever seam reaches the bug: unit, integration, e2e.
- Curl / HTTP script against a running dev server.
- CLI invocation with a fixture input, diffing stdout against a known-good snapshot.
- Headless browser script (Playwright / Puppeteer) that drives the UI and asserts on DOM/console/network.
- Replay a captured trace. Save a real network request / payload / event log to disk; replay it through the code path in isolation.
- Throwaway harness. Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.
- Property / fuzz loop. If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode.
- Bisection harness. If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can
git bisect runit. - Differential loop. Run the same input through old-version vs new-version (or two configs) and diff outputs.
- HITL bash script. Last resort. If a human must click, drive them with
scripts/hitl-loop.template.shso the loop is still structured. Captured output feeds back to you.
Build the right feedback loop, and the bug is 90% fixed.
继续打磨验证流程
把验证流程本身当成产品。得到可运行版本后,继续问:
- 能否更快?例如缓存准备结果、跳过无关初始化、缩小测试范围。
- 判断能否更准确?检查用户的具体症状,而不只是“没有崩溃”。
- 结果能否更稳定?例如固定时间、随机种子、文件系统状态或网络条件。
等待 30 秒却时好时坏的流程,帮助有限;两秒得到稳定结论的流程,可以显著提高排查效率。
Tighten the loop
Treat the loop as a product. Once you have a loop, tighten it:
- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)
- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".)
- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)
A 30-second flaky loop is barely better than no loop; a 2-second deterministic one is tight, a debugging superpower.
偶发问题怎样处理
先提高复现概率,不要求一开始就每次百分之百出现。可以连续触发 100 次、并行执行、增加压力、缩小关键时序范围或加入等待。
作者以 50% 与 1% 的复现率比较,强调应持续提高出现频率,直到足够便于诊断。
Non-deterministic bugs
The goal is not a clean repro but a higher reproduction rate. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not, so keep raising the rate until it's debuggable.
确实无法建立流程时
停下来,明确说明,并列出尝试过的方法。请用户提供可以复现的环境访问方式;或脱敏材料,如 HAR 网络记录、日志、内存转储、带时间戳录屏;或允许临时增加生产环境诊断记录。
没有验证流程,不继续猜原因。
When you genuinely cannot build a loop
Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a redacted captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do not proceed to hypothesise without a loop.
完成标准:一条已经运行、确实能抓住此问题的命令
你应能明确指出一条脚本、测试或 curl 命令,且至少已运行一次,展示脱敏后的调用及输出。它必须满足:
- [ ] 能触发实际问题路径,检查用户的准确症状,有 bug 时失败、修复后通过。只证明不报错不够。
- [ ] 结果稳定;偶发问题则有固定且足够高的复现率。
- [ ] 以秒为单位完成,而不是等几分钟。
- [ ] Agent 可以无人值守运行;必须人工参与时采用上述 HITL 模板。
如果命令还没有建立,你已开始读代码形成原因理论,就停下来。这正是技能要防止的跳步:没有能发现此 bug 的命令,就不能进入下一阶段。
Completion criterion: a tight loop that goes red
Phase 1 is done when the loop is tight and red-capable: you can name one command (a script path, a test invocation, a curl) that you have already run at least once (show the invocation and its output, redacted), and that is:
- [ ] Red-capable: it drives the actual bug code path and asserts the user's exact symptom, so it can go red on this bug and green once fixed. Not "runs without erroring"; it must be able to catch this specific bug.
- [ ] Deterministic: same verdict every run (flaky bugs: a pinned, high reproduction rate, per above).
- [ ] Fast: seconds, not minutes.
- [ ] Agent-runnable: you can run it unattended; a human in the loop only via
scripts/hitl-loop.template.sh.
If you catch yourself reading code to build a theory before this command exists, stop: jumping straight to a hypothesis is the exact failure this skill prevents. No red-capable command, no Phase 2.
红灯必须是用户报告的那个问题
一条命令运行失败,不一定算建立了复现。如果程序因缺少依赖无法启动,和“刷新后收藏丢失”是两个问题。完成标准要求循环已经针对目标症状报错,并真实运行观察过。
不稳定问题可以提高出现概率:重复触发、施加并发压力、控制时间窗口。原文的概率数字是经验化表达,不是统计学定理。应报告尝试次数与实际失败次数,而不只写“稳定复现”。
确实无法复现时,要交代已试方法和缺少什么材料,例如真实请求样本或目标环境访问。没有循环就继续猜原因,会让后面的修改失去验证依据。
阶段 2:重现,并缩小复现场景
运行流程,看到问题出现,检查失败。确认:
- [ ] 失败现象就是用户报告的那个,而不是附近另一个问题。
- [ ] 多次可复现;偶发问题也有足够高的出现频率。
- [ ] 已记录准确错误消息、输出或耗时,供后面比较。
Phase 2: Reproduce + minimise
Run the loop. Watch it go red as the bug appears.
Confirm:
- [ ] The loop produces the failure mode the user described, not a different failure that happens to be nearby. Wrong bug = wrong fix.
- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).
- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.
最小化:删掉无关部分,保留同一种失败
最小复现不是把代码剪得越短越好,而是在失败仍然相同的前提下移除无关因素。课程收藏问题可能只需一个用户、一门课程、两次请求,不必启动整套推荐系统。
每删一部分都重新运行。若删掉并发后 bug 不再出现,并发可能就是必要条件;不能为了得到一个简单示例,把真正原因也删掉。
保存原始场景很重要。最小样本有助于找原因,最终修复还要回到最初较完整的场景检验,以免只修好人工简化后的情况。
缩小复现范围
在仍然失败的前提下,逐渐减少输入、调用方、配置、数据和步骤。一次只移除一个因素,每次重新运行。只保留对触发错误有实际作用的部分。
这样可以减少后续需要怀疑的变量,也更容易形成干净的回归测试。按作者的完成标准,剩下每个元素都应有必要作用:去掉任何一个,这个复现就不再失败。
既复现又缩小之后,才进入下一阶段。
Minimise
Once it's red, shrink the repro to the smallest scenario that still goes red. Cut inputs, callers, config, data, and steps one at a time, re-running the loop after each cut, and keep only what's load-bearing for the failure.
Why bother: a minimal repro shrinks the hypothesis space in Phase 3 (fewer moving parts left to suspect) and becomes the clean regression test in Phase 5.
Done when every remaining element is load-bearing: removing any one of them makes the loop go green.
Do not proceed until you have reproduced and minimised.
阶段 3:提出可以推翻的假设
验证前先列出 3—5 个候选原因,并按可能性排序。只想到一个解释,容易被第一个看似合理的想法固定住。
每个假设必须给出可检验预测,例如:“如果 X 是原因,改变 Y 应让问题消失;改变 Z 应让问题加重。”说不出预测的只是直觉,应删除或进一步明确。
验证前将排序展示给用户。他可能知道刚上线了什么改动,或哪些原因已经排除。这个交流能节省时间;用户暂时不在时,不必一直等待,按当前排序继续。
Phase 3: Hypothesise
Generate 3–5 ranked hypotheses before testing any of them. Single-hypothesis generation anchors on the first plausible idea.
Each hypothesis must be falsifiable: state the prediction it makes.
Format: "If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse."
If you cannot state the prediction, the hypothesis is a vibe: discard or sharpen it.
Show the ranked list to the user before testing. They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it; proceed with your ranking if the user is AFK.
多个可证伪假设,怎样避免第一印象绑架
假设应给出不同的可观察预测。例如收藏丢失可能来自未保存、查询缓存未刷新、当前用户身份变化。若记录确实已在存储但查询仍旧,单纯“保存失败”的解释就被削弱。
先列 3–5 项并排序,可以暴露竞争解释。你有业务背景时,可以补充“昨天刚换过缓存策略”,帮助重排,而不是替 AI 在没有资料时盲猜。
好假设形如:“如果缓存未更新是原因,绕过缓存查询应能看到记录。”坏假设只是“可能是缓存”。试验要能让不同解释给出不同结果,才值得执行。
阶段 4:加入针对性的观察
每个观察点都要对应前面的具体预测,一次只改变一个变量。
优先使用环境支持的调试器或 REPL;一个合适断点可能胜过十条日志。其次在能区分不同假设的位置记录日志。不要把所有东西都记下来,再全文搜索碰运气。
所有临时日志加独特前缀,如 [DEBUG-a4f2],便于最后一次搜索找齐并清理。
如果是性能退化,通常不先堆日志。建立基准计时,使用计时脚本、performance.now()、性能分析器或查询计划,再二分定位。先测量,再修复。
Phase 4: Instrument
Each probe must map to a specific prediction from Phase 3. Change one variable at a time.
Tool preference:
- Debugger / REPL inspection if the env supports it. One breakpoint beats ten logs.
- Targeted logs at the boundaries that distinguish hypotheses.
- Never "log everything and grep".
Tag every debug log with a unique prefix, e.g. [DEBUG-a4f2]. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.
Perf branch. For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, performance.now(), profiler, query plan), then bisect. Measure first, fix second.
插桩:在能区分假设的位置观察
插桩是在程序中临时增加观测,例如断点、针对性日志或计时。一次只改变一个变量,是为了能把结果变化归因到刚做的操作。
例如在写入完成和读取返回两个边界记录同一个请求标识,可以判断丢失发生在哪段。把所有变量都打印出来,可能淹没关键线索,还增加信息暴露和清理成本。
性能问题要先建立耗时基线、分析调用或查询,再改实现。日志说“运行完成”不能证明速度是否退化。临时日志用唯一标记,是为了最后能够完整找到并删除。
阶段 5:回归测试与修复
先写回归测试,再改代码,但前提是有适合验证真实问题模式的测试接入点。
如果问题需要多个调用方配合才能出现,只测一个调用方就太浅;如果触发依赖一连串调用,单元测试无法重现这条链,也可能给出虚假的安心感。
没有合适入口,本身就是架构方面的发现。记录这个限制,说明为什么现在不能用恰当测试防止复发,并带到下一阶段。
有合适入口时:
- 将最小复现变成失败测试。
- 实际运行,看到失败。
- 应用修复。
- 重新运行,看到通过。
- 回到最初未缩小的完整场景,再运行阶段 1 的验证流程。
Phase 5: Fix + regression test
Write the regression test before the fix, but only if there is a correct seam for it.
A correct seam is one where the test exercises the real bug pattern as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.
If no correct seam exists, that itself is the finding. Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.
If a correct seam exists:
- Turn the minimised repro into a failing test at that seam.
- Watch it fail.
- Apply the fix.
- Watch it pass.
- Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.
回归测试要复现真实故障模式
如果问题必须由两个并发调用触发,只写一个串行单调用测试,即使通过也没有锁住问题。正确的测试接入点要能包含造成失败的真实交互。
有合适入口时,把最小复现先变成失败测试,再修复、看它通过,最后重跑原始场景。没有合适入口时,记录架构妨碍验证这一事实;不要造一个无关测试来满足“有测试”字段。
清理包括临时日志、试验代码和相关说明。完成报告应说清成立的原因、修复方式、已验证场景和尚未覆盖范围,让下一个人有可靠起点。
阶段 6:清理与收尾
宣布完成前确认:
- [ ] 原始复现已不再失败,阶段 1 流程已重跑。
- [ ] 回归测试通过;没有适合入口时,限制已记录。
- [ ] 带
[DEBUG-...]前缀的临时诊断代码已全部清理。 - [ ] 一次性原型已删除,或移到明确标注用途的调试位置。
- [ ] 提交或 PR 说明写明最终得到验证的原因,供下一位排查者学习。
Phase 6: Cleanup
Required before declaring done:
- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)
- [ ] Regression test passes (or absence of seam is documented)
- [ ] All
[DEBUG-...]instrumentation removed (grepthe prefix) - [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)
- [ ] The hypothesis that turned out correct is stated in the commit / PR message, so the next debugger learns
先作答,再看参考思路
用自己的话说明:它解决什么问题,完成后会留下什么?
请各用一句话回答。若它只做规划或解释,不要把“已开发”“已部署”写成产物。
为什么修复后既要跑最小测试,又要跑原始场景?
我已思考,查看参考思路
最小测试利于定位,但可能遗漏原场景中的交互条件。重跑原场景才能检查缩小问题时是否丢掉了真实故障的一部分。
原文中哪条要求在你的环境下可能不成立?
说出具体一句及其前提,例如工具不可用、资料缺失、已有项目约定冲突,或它只是作者偏好。把你的答案带回课堂,我们据此继续讨论。
把方法放进一个具体情境
教学案例:Agent 报告“资料已保存”却找不到文件。先固定工作目录与文件名,复现写入和读取;区别未写入、写到别处、保存失败未报告。增加重试不能替代这三个原因的核验。
边界与容易误读的地方
无法建立反馈时要明确缺失的访问或材料,不得把猜测写成根因。小故障可合理简化,但不能省略证明修复命中原问题。
讨论后再实践:先判断上述情境是否适用,再选择真实任务。现在无需安装、运行命令或修改现有项目。