别再给 CI 加 sleep 了:用 systematic-debugging 把偶发失败定位到根因
CI 偶尔报一次 NOT_READY,重跑又绿了。最常见的“修复”是什么?把 sleep(50) 改成 sleep(200)。
这通常只是把失败概率压低,并没有消除根因:消费者依赖的是“服务已就绪”这个条件,却用“时间大概过去了”代替条件。机器负载、进程启动、网络抖动一变,问题还会回来;而快机器每次都要白等。
今天从《AI Skills 每日精选》的 5 个候选中选择 systematic-debugging 做实战,因为它不绑定某种框架,解决的又是 Agent 最常见的坏习惯:看到症状就猜修复。本文用一个完全本地、无第三方依赖的最小案例,走完“取证—对比—假设—修复—验证”的闭环。
本文没有把 Superpowers 安装进正式 Codex 环境。源码核验来自维护者官方仓库;示例仅在当前项目的隔离临时目录运行,未读取 Cookie、凭据、客户数据或其他项目源码。
这个 Skill 到底约束了什么
systematic-debugging 是 Superpowers 中的调试流程 Skill。它的核心不是多提供几条命令,而是规定修复前必须先完成根因调查。官方 SKILL.md 把流程拆成四个阶段:
- 根因调查:读完整错误、稳定复现、检查最近变更,在组件边界收集证据并反向追踪数据;
- 模式分析:寻找同仓库里的正常样本,完整阅读参考实现,逐项列出差异与依赖;
- 假设与实验:一次只提出一个具体假设,用最小改动验证一个变量;
- 实现与验证:先保留失败用例,再做单一修复,运行相关测试与回归测试。
如果连续三次修复都失败,Skill 要求停止继续叠补丁,转而讨论架构是否存在共享状态或错误耦合。这一点很重要:第四次“再试一下”往往不是坚持,而是在制造更难解释的新状态。
安装或接入
官方 README 给出的 Codex 安装入口是插件市场:
- Codex App:打开侧栏
Plugins,在 Coding 分类找到Superpowers,点击+并按提示确认; - Codex CLI:输入
/plugins,搜索superpowers,选择Install Plugin; - Claude Code:使用官方市场命令
/plugin install superpowers@claude-plugins-official。
安装前建议先打开 维护者仓库 和 OpenAI 插件市场仓库 核对来源、许可证和当前版本。不要从搜索结果复制不明安装脚本,也不要为了试用把第三方 Skill 直接写进用户级目录。
安装后可以这样明确触发:
使用 Superpowers 的 systematic-debugging 调查这个 CI 偶发失败。
先稳定复现并记录组件边界证据;在根因得到验证前不要改代码。
一次只测试一个假设,修复后运行失败用例和相关回归测试。
如果只想评估它,先让 Agent 读取官方 SKILL.md,在临时工作树或隔离副本中做只读调查;确认工作方式和权限范围后,再决定是否安装。
最小可复现实例:固定等待为什么会“时好时坏”
下面用一个生产者模拟服务启动,用消费者模拟 CI 测试。生产者会在指定延迟后进入 ready;错误版本无论实际状态如何都只等 50ms。
保存为 repro.mjs,只需要 Node.js 18 或更高版本:
const [mode = "buggy", rawDelay = "80"] = process.argv.slice(2);
const delayMs = Number(rawDelay);
const state = { ready: false };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function startProducer() {
await sleep(delayMs);
state.ready = true;
}
async function waitFor(predicate, { timeoutMs = 500, intervalMs = 10 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) return;
await sleep(intervalMs);
}
throw new Error(`not ready within ${timeoutMs}ms`);
}
const startedAt = Date.now();
const producerDone = startProducer();
if (mode === "buggy") {
await sleep(50);
} else if (mode === "fixed") {
await waitFor(() => state.ready);
} else {
throw new Error("mode must be buggy or fixed");
}
if (!state.ready) {
console.error(
`[${mode}] NOT_READY at +${Date.now() - startedAt}ms; producer=${delayMs}ms`,
);
process.exitCode = 1;
} else {
console.log(
`[${mode}] READY at +${Date.now() - startedAt}ms; producer=${delayMs}ms`,
);
}
await producerDone;
先复现症状:
node repro.mjs buggy 30
node repro.mjs buggy 90
第一条通常通过,第二条会在约 50ms 时输出:
[buggy] NOT_READY at +50ms; producer=90ms
这已经比“CI 偶尔红”多了一条关键证据:失败边界发生在消费者读取状态时,且生产者的完成时间晚于固定等待。
按四阶段走一遍,而不是直接改成 sleep(200)
阶段一:根因调查
先把失败变成可控输入。分别使用 10、30、45、60、90、120ms 的生产者延迟,不改其他变量。结果是:前三档通过,后三档失败,分界线恰好落在固定等待的 50ms 附近。
此时可以把数据流写成:
启动生产者 -> 尚未 ready -> 消费者固定等待 50ms -> 读取 ready -> 失败
真实系统里,证据应放在边界,而不是把所有环境变量和响应体全部打印出来。例如记录启动时间、健康检查状态码、重试次数和耗时即可;Secret 只记录 SET/UNSET,Token、Cookie 和数据库连接串不能原样进入日志。
阶段二:模式分析
正常模式依赖的是条件:健康检查返回成功、端口开始监听、文件原子落盘或任务状态变为 ready。错误模式依赖的是猜测:50ms 后“应该好了”。
两者的关键差异不是等待时长,而是退出条件。官方配套的 condition-based-waiting 也强调用条件轮询替代任意超时。
阶段三:提出单一假设
只写一个可证伪的假设:
根因是消费者把固定时长误当成就绪条件;如果改为轮询
ready并设置总超时,那么 50ms 之后才就绪的用例也应通过,且快速用例无需等满 50ms。
注意,这一步没有同时增加重试库、扩大全局超时、重构生产者或改 CI 机器。一次只改变一个变量,才能知道到底什么起作用。
阶段四:单一修复与验证
修复就是示例里的 waitFor:每 10ms 检查一次真实条件,总等待上限为 500ms。上限仍然必要,否则真正无法启动时测试会永久挂起。
我在 Windows 上用 Node.js 22.20.0 实际运行六档延迟,结果如下:
| 版本 | 10/30/45/60/90/120ms | 通过率 | 快路径表现 |
|---|---|---|---|
固定 sleep(50) | 3 通过、3 失败 | 3/6 | 即使 10ms 就绪也等约 50ms |
| 条件等待 + 500ms 上限 | 6 通过、0 失败 | 6/6 | 10ms 档约 12ms 返回 |
这次修复同时得到两个实际效果:慢启动不再因为越过 50ms 而误报,快启动也不再支付固定等待成本。它没有证明所有 CI 偶发失败都来自时间竞争,只证明了当前这个失败用例的假设。
常见坑
1. 把 sleep 加长当成根因修复
50ms 改成 500ms 只是在移动概率边界。除非协议本身明确规定必须等待固定时长,否则优先等待可观测条件。
2. 一次改三处再看是否变绿
同时加重试、改超时、换依赖版本,即使测试通过也无法归因。每轮只验证一个假设;失败就撤销实验,带着新证据回到阶段一。
3. 只跑一次成功用例
偶发问题需要控制变量、重复运行,并覆盖临界值两侧。先留下能失败的脚本或自动化测试,再改生产代码。
4. 只修最深层的报错位置
NOT_READY 发生在消费者,不代表错误起源也在那里。真实调用链应按 root-cause-tracing 从坏值向上追:谁传入了这个状态,它又从哪里产生,直到找到最早的错误假设。
5. Agent 说“已修复”就结束
Skill 是流程约束,不是正确性证明。必须看到原失败用例由红变绿、相关回归测试通过,并确认没有改变超时、缓存或并发语义。
权限与安全边界
安装 Skill 相当于给 Agent 增加一套会继承现有工具权限的操作说明,不是添加一篇无害文档。调试流程可能读取 Git 历史、搜索整个仓库、运行测试、写入诊断日志和修改代码,因此应明确边界:
- 第三方 PR 先在只读副本或隔离工作树调查,不执行分支自带的未知脚本;
- 生产环境默认只读,不主动扫描端口、调用真实支付/邮件接口或修改数据库;
- 日志只保留定位所需的结构化信号,凭据、Cookie、客户数据和私有响应体必须脱敏;
- 把可写目录、允许执行的测试命令和可访问域名写进任务约束;
- 需要联网、提升权限、安装依赖或触达外部系统时,先让 Agent 说明目的和影响范围,再单独授权;
- 固定并审阅插件版本,升级时查看 Skill、hooks、scripts 和权限声明的 diff。
Superpowers 使用 MIT 许可证。官方 README 还说明:可选的 brainstorming 可视化伙伴默认会从维护者站点加载 Logo 并携带 Superpowers 版本;不需要该流量时可设置 SUPERPOWERS_DISABLE_TELEMETRY=true。这不意味着调试 Skill 会上传项目内容,但在受限网络环境中仍应显式关闭非必要外联,并用出口策略验证。
适合与不适合的场景
适合:测试偶发失败、构建错误、集成故障、性能退化、多组件数据传递异常,以及已经试过一两次但仍未解决的 bug。问题越跨进程、跨服务、跨 CI 阶段,边界取证越有价值。
不适合:已经有明确根因和机械修复方式的批量格式化;没有可复现环境、日志和观测权限却要求它“自动猜中”;需要立即止血的生产事故中,也不能把完整调查误用成阻止回滚或流量切换。正确做法是先执行已授权、可逆的止血措施,同时保留证据,随后对根因修复走完整流程。
最后记住一句话:调试速度不取决于你改代码有多快,而取决于你多早把“猜测”变成了可证伪的假设。systematic-debugging 最有价值的地方,就是让 Agent 在动手前先把这一步做完。