Files
collection-claude-code-sour…/docs/cc_analysis/analysis_20260401/Claude_Code模型切换机制深度分析.md
2026-04-04 09:43:49 -07:00

15 KiB
Raw Blame History

Claude Code 是否偷换模型?源码级逆向分析

基于 Claude Code 源码2026-03-31 快照512K 行 TypeScript逆向分析 核心文件:src/utils/model/model.tssrc/services/api/withRetry.tssrc/query.tssrc/services/api/claude.ts


结论先行

不存在"偷换模型"。你的主对话模型完全由你控制,不会被偷偷降级。

但 Claude Code 确实在后台使用 Haiku弱模型执行辅助任务配额检查、摘要生成等这些 Haiku 调用不会出现在你的对话中,也不会替代你与 Opus 的交互。

唯一可能改变主对话模型的场景是 529 连续过载回退Opus → Sonnet此时会显示明确的系统消息通知用户。


1. 主对话模型选择:完全由用户控制

模型选择优先级(源码确认)

// src/utils/model/model.ts:92
function getMainLoopModel(): ModelName {
  const model = getUserSpecifiedModelSetting()
  if (model !== undefined && model !== null) {
    return parseUserSpecifiedModel(model)
  }
  return getDefaultMainLoopModel()
}

// src/utils/model/model.ts:61
function getUserSpecifiedModelSetting(): ModelSetting | undefined {
  // 优先级从高到低:
  // 1. /model 命令(会话内切换)— 最高优先级
  const modelOverride = getMainLoopModelOverride()
  if (modelOverride !== undefined) return modelOverride

  // 2. --model 启动参数 / ANTHROPIC_MODEL 环境变量 / settings.json
  return process.env.ANTHROPIC_MODEL || settings.model || undefined
}

你设了 Opus主对话就用 Opus。没有任何代码在你不知情的情况下把它换掉。

默认模型

// src/utils/model/model.ts:105
function getDefaultOpusModel(): ModelName {
  // 支持环境变量覆盖
  if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
    return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
  }
  return getModelStrings().opus46  // 默认 Opus 4.6
}

function getDefaultSonnetModel(): ModelName {
  if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
    return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
  }
  // 3P 提供商默认 Sonnet 4.5(可能尚未支持 4.6
  if (getAPIProvider() !== 'firstParty') return getModelStrings().sonnet45
  return getModelStrings().sonnet46
}

function getDefaultHaikuModel(): ModelName {
  if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
    return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
  }
  return getModelStrings().haiku45  // Haiku 4.5(所有平台通用)
}

2. 唯一会改变主对话模型的场景529 过载回退

触发条件(极其严格)

// src/services/api/withRetry.ts:326-350

// 必须同时满足以下所有条件:
// 1. 收到 529 错误(服务器过载)
// 2. 连续 529 次数 >= MAX_529_RETRIES
// 3. 配置了 fallbackModel
// 4. 以下二选一:
//    a. 设置了 FALLBACK_FOR_ALL_PRIMARY_MODELS 环境变量
//    b. 非 Claude.ai 订阅用户 且 使用非自定义 Opus 模型

if (is529Error(error) &&
    (process.env.FALLBACK_FOR_ALL_PRIMARY_MODELS ||
     (!isClaudeAISubscriber() && isNonCustomOpusModel(options.model)))) {
  consecutive529Errors++
  if (consecutive529Errors >= MAX_529_RETRIES) {
    if (options.fallbackModel) {
      // 触发回退
      throw new FallbackTriggeredError(options.model, options.fallbackModel)
    }
  }
}

关键限制:

条件 说明
!isClaudeAISubscriber() Claude.ai 订阅用户默认不触发回退
isNonCustomOpusModel() 仅对标准 Opus 模型生效(自定义模型 ID 不触发)
MAX_529_RETRIES 需要连续多次 529 才触发

回退执行过程(透明可见)

// src/query.ts:894-946

if (innerError instanceof FallbackTriggeredError && fallbackModel) {
  // 1. 切换模型
  currentModel = fallbackModel  // Opus → Sonnet不是 Haiku

  // 2. 清理孤立的 assistant 消息
  yield* yieldMissingToolResultBlocks(assistantMessages, 'Model fallback triggered')
  assistantMessages.length = 0

  // 3. 剥离 thinking 签名块(签名绑定原模型,发给回退模型会导致 400
  // 源码注释:
  // "Strip before retry so the fallback model gets clean history."

  // 4. 更新工具上下文中的模型引用
  toolUseContext.options.mainLoopModel = fallbackModel

  // 5. 记录分析事件
  logEvent('tengu_api_opus_fallback_triggered', {
    original_model: options.model,
    fallback_model: options.fallbackModel,
  })

  // 6. 显示系统消息通知用户(在 UI 中可见)
  `Switched to ${renderModelName(fallbackModel)} due to high demand
   for ${renderModelName(originalModel)}`
}

回退目标

// src/main.tsx:1336-1337
// 回退模型必须与主模型不同
if (fallbackModel && options.model && fallbackModel === options.model) {
  // 验证失败,不设置回退
}

// 回退目标是 Sonnet不是 Haiku
// 通过 getDefaultSonnetModel() 获取

用户如何知道发生了回退

回退时UI 中会显示一条系统消息:

Switched to Claude Sonnet 4.6 due to high demand for Claude Opus 4.6

同时在 AssistantTextMessage.tsx:178 中提供操作建议:

To continue immediately, use /model to switch to Sonnet and continue coding.

3. 后台辅助任务使用 Haiku — 不影响主对话

Claude Code 在后台使用 HaikugetSmallFastModel())执行多种轻量辅助任务。这些调用完全独立于你的对话,不会出现在消息流中。

所有 Haiku 使用场景(逐一列出)

3.1 配额检查

// src/services/claudeAiLimits.ts:199-218
async function makeTestQuery() {
  const model = getSmallFastModel()  // → Haiku
  const anthropic = await getAnthropicClient({ maxRetries: 0, model, source: 'quota_check' })
  // 发送 max_tokens: 1 的最小请求,仅读取响应头的配额信息
  return anthropic.beta.messages.create({
    model,
    max_tokens: 1,
    messages: [{ role: 'user', content: 'quota' }],
  }).asResponse()
}

用途: 检测用户是否已达配额限制。只读取 HTTP 响应头,不使用模型输出。

3.2 离开摘要Away Summary

// src/services/awaySummary.ts:44-52
// 当用户长时间离开后返回时,生成一个简短摘要
{
  thinkingConfig: { type: 'disabled' },  // 关闭思考
  tools: [],                              // 不给工具
  model: getSmallFastModel(),             // → Haiku
}

用途: 用户长时间不操作后返回时,快速生成"你离开期间发生了什么"的简要摘要。

3.3 Token 计数

// src/services/api/claude.ts:540-543
// WARNING: if you change this to use a non-Haiku model,
// this request will fail in 1P unless it uses getCLISyspromptPrefix.
const model = getSmallFastModel()  // → Haiku
// 调用 countTokens API 获取精确 token 数

用途: 调用 Anthropic 的 countTokens 端点获取精确 token 计数,用于自动压缩阈值判断。

3.4 工具使用摘要

// src/services/api/claude.ts:3273-3280
// 在主模型流式输出期间,并行用 Haiku 生成工具调用的简短描述
{
  model: getSmallFastModel(),             // → Haiku
  enablePromptCaching: false,
}

用途: 当模型连续调用多个工具时,生成简短的工具使用描述(如"读取了 config.ts 并修改了第 42 行")。

源码注释明确说明了并行关系:

// src/query.ts:1054
// Yield tool use summary from previous turn —
// haiku (~1s) resolved during model streaming (5-30s)

Haiku 在后台 1 秒内完成摘要,同时 Opus 还在处理你的主对话。

3.5 记忆相关性评分

// src/memdir/findRelevantMemories.ts
// 用 Sonnet非 Haiku评估记忆文件的相关性
// 从标题中选择最多 5 条与当前查询相关的记忆

3.6 Web 搜索预处理

// src/tools/WebSearchTool/WebSearchTool.ts:280
model: useHaiku ? getSmallFastModel() : context.options.mainLoopModel,
toolChoice: useHaiku ? { type: 'tool', name: 'web_search' } : undefined,

用途: 当条件满足时(useHaiku 标志),用 Haiku 预处理搜索查询。

3.7 其他辅助调用

调用者 模型 用途
tokenEstimation.ts Haiku Vertex 全局区域 token 计数回退
analyzeContext.ts Haiku 上下文分析的 countTokens 回退
claudeAiLimits.ts Haiku 配额状态检查
Bedrock client.ts Haiku 小模型可指定不同 AWS 区域

辅助任务 vs 主对话的隔离

你的对话:
  用户 → [Opus 4.6] → 助手响应 → [Opus 4.6] → 助手响应 → ...

后台并行:
  [Haiku] → 配额检查1 token
  [Haiku] → 工具使用摘要(~1s
  [Haiku] → token 计数

两条线完全隔离Haiku 的输出不进入你的对话消息流。

4. Fast Mode同模型加速不换模型

这是一个常见误解,源码中有明确声明:

// src/constants/prompts.ts:702
`Fast mode for Claude Code uses the same ${FRONTIER_MODEL_NAME} model
 with faster output. It does NOT switch to a different model.
 It can be toggled with /fast.`

Fast Mode 通过 API 的 speed 参数请求更快的输出,模型不变。如果 API 拒绝 fast mode如组织未启用会自动降回标准速度

// src/services/api/withRetry.ts:310-313
if (wasFastModeActive && isFastModeNotEnabledError(error)) {
  handleFastModeRejectedByAPI()
  retryContext.fastMode = false
  continue  // 重试,同一模型,标准速度
}

5. Plan Mode 临时模型切换 — 透明可见

// src/commands/model/model.tsx:214
// Do not update fast mode in settings since this is an automatic downgrade

// src/commands/model/model.tsx:256
onDone(`Current model: ${chalk.bold(renderModelLabel(mainLoopModelForSession))}
  (session override from plan mode)\nBase model: ${displayModel}`)

Plan Mode 可能临时使用 Sonnet 执行实现步骤,但:

  • UI 显示 "session override from plan mode"
  • 显示 Base model 让你知道原始模型
  • 不修改 settings 中的模型设置

6. Effort 降级 — 不换模型,换参数

// src/utils/effort.ts:162
// API rejects 'max' on non-Opus-4.6 models — downgrade to 'high'.

如果设置了 effort: max 但模型不支持,会降级 effort 参数max → high不换模型。


7. 全局搜索:不存在的场景

通过对 512K 行源码的全局搜索,以下场景被确认不存在

假设的偷换场景 搜索结果 证据
根据订阅层级偷换弱模型 不存在 getMainLoopModel() 不检查订阅类型
根据额度用量偷换弱模型 不存在 额度用完返回 429 错误,不换模型
随机/概率性降级到弱模型 不存在 Math.random() 与模型选择相关的代码
A/B 测试使用弱模型回答 不存在 GrowthBook 控制功能开关,不控制模型
静默替换后不通知用户 不存在 529 回退有系统消息,其他场景不换主模型
把 Opus 对话路由到 Haiku 不存在 Haiku 仅用于辅助任务
根据问题复杂度选择弱模型 不存在 主循环始终使用用户指定的模型
根据时间段降级(如高峰期) 不存在 无时间相关的模型选择逻辑
首次用户用弱模型 不存在 默认模型是 Opus 4.6

8. 模型使用全景图

┌─────────────────────────────────────────────────────────┐
│                   你的主对话                              │
│                                                         │
│  模型:你指定的(默认 Opus 4.6                          │
│  控制:/model 命令 > --model 参数 > 环境变量 > 设置       │
│  唯一例外529 连续过载 → Sonnet 回退(有通知)            │
│                                                         │
├─────────────────────────────────────────────────────────┤
│                   后台辅助任务Haiku                    │
│                                                         │
│  配额检查      → 1 token 请求,只读响应头                 │
│  Token 计数    → countTokens API 调用                    │
│  工具摘要      → 并行于主对话,~1s 完成                    │
│  离开摘要      → 用户回来时显示简要信息                    │
│  Web 搜索      → 搜索查询预处理(条件触发)                │
│                                                         │
│  这些不进入你的对话消息流                                  │
│                                                         │
├─────────────────────────────────────────────────────────┤
│                   Fork Agent 任务                        │
│                                                         │
│  上下文压缩    → 使用你的主模型(共享 prompt cache       │
│  记忆提取      → 使用你的主模型                           │
│  权限分类器    → Haikuauto 模式下的命令安全分类)        │
│                                                         │
│  压缩结果进入上下文,但不替代你的对话                      │
└─────────────────────────────────────────────────────────┘

9. 如何验证你正在使用的模型

方法 1查看 API 响应

每条助手消息的内部结构中包含模型信息:

// AssistantMessage.message.model 字段包含实际使用的模型 ID
// 例如:"claude-opus-4-6-20260301"

方法 2使用 /cost 命令

/cost 命令显示按模型分类的 token 使用量,你可以看到每个模型消耗了多少 token。

方法 3检查 status line

状态栏显示当前模型名称。如果发生回退,会更新为回退后的模型。

方法 4环境变量调试

# 查看所有 API 调用的模型信息
export CLAUDE_CODE_DEBUG=1

# 强制禁用回退
unset FALLBACK_FOR_ALL_PRIMARY_MODELS

10. 总结

问题 答案 依据
Opus 会被偷偷换成 Haiku 吗? 不会 Haiku 仅用于辅助任务,不进入主对话
Opus 会被偷偷换成 Sonnet 吗? 仅在 529 连续过载时,且会通知 withRetry.ts:347 + query.ts:946
Claude.ai 订阅用户会被降级吗? 默认不会 !isClaudeAISubscriber() 条件排除
后台有 Haiku 调用吗? 有,但不影响你的对话 6 种辅助任务,全部隔离
Fast Mode 换模型吗? 不换 源码明确声明 "does NOT switch to a different model"
有没有根据问题难度选模型? 没有 主循环始终使用用户指定模型

一句话:你付费用 Opus对话就用 Opus。Haiku 只在幕后干杂活。


来自AI超元域 | B站频道https://space.bilibili.com/3493277319825652

基于 Claude Code 源码逆向分析2026-03-31