调试与排障
你写了一个 code-review Skill,security.md 里写得清清楚楚要检查 SQL 注入,结果真跑起来,一段明显的字符串拼接 SQL 就在眼皮底下,它愣是没提。
你的第一反应是在 security.md 里加粗、加 MUST、加 ALWAYS ALWAYS ALWAYS。
别急。先搞清楚它为什么没按预期工作。
第一步:读 transcript
transcript 是 AI 的执行日志——它看到了什么、想了什么、调用了哪些工具、输出了什么。在 Claude Code 中可以查看每次执行的详细过程。
读 transcript 时关注三件事:
- AI 是否加载了你的 SKILL.md? 有时候 Skill 根本没被触发,用户说的话没命中 description 里的关键词,AI 就当普通对话处理了。
- AI 是否读了你的规则文件? 你有 5 个 references 文件,但 AI 可能只加载了 3 个。看看 security.md 在不在其中。
- AI 对指令是怎么理解的? 有时候你写的”检查 SQL 注入风险”,AI 理解成”提醒开发者注意 SQL 注入”而不是”逐行扫描拼接 SQL 的代码”。
大部分问题在这一步就能定位。不看 transcript 就改 prompt,等于蒙眼调参。
问题定位决策树
Skill 没按预期工作
├── 完全没反应
│ ├── description 缺少关键词 → 补充用户的常用表述
│ ├── 被其他 Skill 抢先匹配 → 检查 description 是否有重叠
│ └── 手动用 /skill-name 强制触发,确认 Skill 本身没问题
│
├── 触发了,但指令没被遵循
│ ├── 指令太抽象 → 加具体的代码示例
│ ├── 指令被对话历史冲淡 → 精简 SKILL.md,删掉没用的内容
│ └── 上下文中有矛盾信息 → 检查 references 之间是否打架
│
├── 遵循了,但结果不对
│ ├── 领域知识不足 → 在 references/ 中补充背景
│ ├── 规则太笼统 → 用正反代码示例替代文字描述
│ └── AI 对术语理解有偏差 → 加定义和上下文解释
│
└── 时好时坏
├── SKILL.md 太长,尾部内容被 compaction 截断 → 重要指令前移
├── 指令有歧义,AI 每次解读不同 → 改写为无歧义表述
└── 随机性导致 → 用 evals 多次运行,确认是概率问题还是确定性 bug从上往下排查,先确认 Skill 被触发了,再看指令有没有被读到,最后才是调指令的措辞。顺序搞反了会浪费大量时间。
上下文窗口用量分析
Skill 的所有内容——SKILL.md 正文、references 文件、动态注入的命令输出——都要占用上下文窗口。如果你的 SKILL.md 加上 5 个 references 文件已经占了上下文的 30%,留给实际代码分析的空间就不够了。AI 被迫在有限空间里做取舍,你的某些规则自然会被”忽略”。
诊断方法很粗暴但有效:把 references 文件删掉一半,看审查效果是否反而更好。如果是,说明你的 Skill 内容超载了。
Compaction 的影响更隐蔽。当上下文快满时,Claude Code 会压缩对话历史来腾出空间。压缩时会保留最近触发的 Skill 内容,但如果 Skill 本身就很大,它的尾部也可能被截断。
这就是为什么我们反复强调:重要的指令放在 SKILL.md 的前半部分。放在末尾的”务必检查 SQL 注入”可能正好在 compaction 的裁剪线上。
实战:定位一个漏掉的 SQL 注入
场景:code-review 审查一个 Node.js 后端 PR,其中有这样一行:
const result = await db.query(`SELECT * FROM orders WHERE user_id = ${req.params.id}`);教科书级的 SQL 注入。但 code-review 没报。
排查过程:
- 看 transcript,确认 security.md 被加载了——确实加载了
- 找到 security.md 中关于 SQL 注入的描述:
### SQL 注入
注意检查 SQL 注入风险。一句话,没了。
问题找到了。“注意检查 SQL 注入风险”这句话对 AI 来说太抽象,它不知道要找什么具体模式。它可能扫了一眼觉得”用了 db.query 看起来是 ORM 的写法,应该没问题”。
修复——把抽象规则改成具体的代码模式匹配:
### SQL 注入
检查以下模式:
- 模板字符串拼接 SQL:`` `SELECT ... ${variable}` ``
- 字符串拼接 SQL:`"SELECT ... " + variable`
- 未使用参数化查询的裸 SQL 调用
❌ 错误示例:
`db.query(\`SELECT * FROM orders WHERE user_id = ${userId}\`)`
✅ 正确写法:
`db.query('SELECT * FROM orders WHERE user_id = ?', [userId])`修改后重跑,这次立刻报了 Critical。
教训:AI 不是不能发现 SQL 注入,而是你没告诉它要找的具体”形状”是什么。第七章讲的”正反示例”原则,在调试阶段体现得最明显。
错误恢复模式
Skill 在运行时会依赖外部资源——脚本执行、子代理调用、文件读取、CLI 工具。这些环节都可能挂掉。与其祈祷一切顺利,不如在 SKILL.md 中预设兜底策略。
| 场景 | 表现 | 应对 |
|---|---|---|
| 脚本执行失败 | npx tsx 报错,输出一堆 stack trace | 在 SKILL.md 中加兜底指令:“如果脚本执行失败,跳过后处理步骤,直接输出审查结果” |
| 子代理超时 | context: fork 的子代理长时间无响应 | 设置合理的超时预期;SKILL.md 中说明”如果子代理未返回结果,用主对话完成剩余审查” |
| references 文件缺失 | Read 工具报 file not found | 在 SKILL.md 中加容错:“如果参考文件不存在,用你的通用知识替代,并在输出中标注’未加载团队规则‘“ |
| 动态注入命令失败 | !`gh pr diff` 返回错误信息 | 在命令中用 2>/dev/null || echo "FALLBACK" 做兜底(v3 快照已展示此模式) |
关键原则:优雅降级,而不是静默失败。 兜底策略执行后,必须在输出中告知用户哪个环节降级了、结果可能缺少什么。用户看到”未加载团队规则,以下审查基于通用标准”,至少知道要多看一眼。看到一份看似正常但实际少了一半检查项的报告,才是真正危险的。
调试速查表
| 症状 | 最可能的原因 | 快速验证 |
|---|---|---|
| 完全没反应 | description 不匹配 | 用 /skill-name 手动触发 |
| 格式不对 | 输出模板被忽略或截断 | 检查模板位置是否在 SKILL.md 后半段 |
| 部分规则不生效 | 规则文件没被加载 | 在 transcript 中搜索文件名 |
| 结果时好时坏 | 指令有歧义 | 多跑几次 eval 对比结果差异 |
| 审查太浅 | 上下文超载 | 删减 references 后对比效果 |
| 子代理结果为空 | 传参不足 | 检查 $ARGUMENTS 是否包含必要信息 |
调试 Skill 和调试代码一个道理:先复现,再定位,最后修复。别跳过前两步直接改 prompt——那叫碰运气,不叫调试。