第十篇:callModel.ts 深度解析 —— Claude Code 如何调用 Anthropic API

第十篇:callModel.ts 深度解析 ------ Claude Code 如何调用 Anthropic API

源码位置:src/services/api/callModel.ts(约800行核心逻辑)|难度:高级

一、引言:为什么 callModel.ts 是通信枢纽

前九篇我们拆解了 Claude Code 的架构、CLI、Handler、QueryEngine、query.ts,但一直有一个关键问题没回答:Claude Code 究竟是如何发起 HTTPS 请求到 https://api.anthropic.com/v1/messages 的?

答案就在 callModel.ts 里。这个文件虽然有约800行,却是整个系统的"通信枢纽"------负责:

  1. 构建 HTTP 请求:把内部消息格式转换成 Anthropic Messages API 需要的 JSON 格式
  2. 处理 SSE 流 :解析 text/event-stream,逐块提取 content_block_delta
  3. 限速重试:遇到 429 时指数退避,自动切换备用模型
  4. 错误分类 :把 API 错误转换成内部错误类型(FallbackTriggeredErrorPromptTooLongError 等)
  5. 性能追踪:记录首 token 时间、吞吐量、重试次数

💡 设计亮点callModel() 返回一个 AsyncGenerator ,逐块 yield 消息,而不是等整个响应完成。这使得 UI 可以实时渲染流式输出。


二、callModel.ts 在架构中的位置

复制代码
query.ts
   ↓
callModel()  ← 本篇主角,负责 API 通信
   ↓
Anthropic Messages API (https://api.anthropic.com/v1/messages)

核心职责划分

  • query.ts:纯业务逻辑(上下文压缩、工具执行编排、错误恢复)
  • callModel.ts:纯通信逻辑(HTTP 请求、SSE 解析、重试)

这种分离使得:

  • query.ts 可以专注业务(权限、成本追踪、会话持久化)
  • callModel.ts 可以专注通信(流式解析、重试、错误分类)

三、核心函数:callModel()

callModel() 是 callModel.ts 的唯一导出函数,签名如下:

typescript 复制代码
export async function* callModel(
  params: CallModelParams,
): AsyncGenerator<
  | AssistantMessage
  | StreamEvent
  | RequestStartEvent
  | RequestFailedEvent,
  void
>

CallModelParams:输入参数

字段 说明
messages 对话历史(Message\[\])
systemPrompt 系统提示词(SystemPrompt 类型)
tools 可用工具列表(Tool\[\])
thinkingConfig 思考配置(extended thinking 的开关)
signal AbortController.signal(用户取消)
options 请求选项(model、fallbackModel、querySource 等)

四、callModel 循环:一步步拆解

callModel() 内部是一个 无限循环while (true)),每次循环代表一次 API 调用。循环退出的条件:

  • 模型返回 stop_reason: "end_turn"(不需要调用工具)
  • 达到 maxRetries 上限
  • 用户取消(AbortController)

循环体的 10 个关键步骤

1. 构建请求体(Build Request Body)
typescript 复制代码
const requestBody: MessagesRequest = {
  model: currentModel,
  max_tokens: maxOutputTokensOverride ?? options.maxTokens ?? 8192,
  messages: messagesForRequest,
  system: systemPromptForRequest,
  tools: toolsForRequest,
  thinking: thinkingConfig,
  stream: true,  // 启用 SSE 流
}

关键转换

  • messages:内部 Message\[\] → Anthropic API 需要的格式(role/content/tool_calls)
  • system:SystemPrompt → string\[\](支持多段系统提示词)
  • tools:Tool\[\] → Anthropic Tool 格式(name/description/input_schema)
2. 发起 HTTPS 请求
typescript 复制代码
const response = await fetch(
  'https://api.anthropic.com/v1/messages',
  {
    method: 'POST',
    headers: {
      'x-api-key': apiKey,
      'anthropic-version': '2023-06-01',
      'content-type': 'application/json',
      'accept': 'text/event-stream',
      'x-app-name': 'claude-code',  // 标识来源
      ...(fallbackModel && {'x-fallback-model': fallbackModel}),
    },
    body: JSON.stringify(requestBody),
    signal: params.signal,  // 支持用户取消
  },
)

关键请求头

  • x-app-name: claude-code:Anthropic 用来识别调用来源(用于配额管理)
  • x-fallback-model:告诉 API "如果限速,自动切换到这个模型"
  • accept: text/event-stream:要求返回 SSE 流
3. 检查响应状态
typescript 复制代码
if (!response.ok) {
  const errorBody = await response.text()
  
  if (response.status === 429) {
    throw new RateLimitError(errorBody)
  }
  
  if (response.status === 413) {
    throw new PromptTooLongError(errorBody)
  }
  
  if (response.status === 401) {
    throw new AuthenticationError(errorBody)
  }
  
  // 其他错误:500/502/503
  throw new APIError(response.status, errorBody)
}

错误分类

  • 429:限速错误 → 指数退避重试
  • 413:上下文过长 → 触发压缩
  • 401:认证失败 → 立即返回(不重试)
  • 500/502/503:服务端错误 → 指数退避重试
4. 解析 SSE 流
typescript 复制代码
const reader = response.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''

while (true) {
  const { done, value } = await reader.read()
  if (done) break
  
  buffer += decoder.decode(value, { stream: true })
  const lines = buffer.split('\n')
  buffer = lines.pop() || ''  // 保留最后一个不完整的行
  
  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const data = line.slice(6)
      if (data === '[DONE]') return  // 流结束
      
      const event = JSON.parse(data)
      yield* handleSSEEvent(event)  // 处理 SSE 事件
    }
  }
}

SSE 格式

复制代码
data: {"type":"message_start","message":{"id":"msg_..."}}

data: {"type":"content_block_delta","delta":{"text":"Hello"}}

data: [DONE]
5. 处理 SSE 事件(handleSSEEvent)
typescript 复制代码
function* handleSSEEvent(event: SSEEvent) {
  switch (event.type) {
    case 'message_start':
      // 初始化 assistant 消息
      yield createAssistantMessage(event.message)
      break
      
    case 'content_block_start':
      // 开始一个新的内容块(text/tool_use)
      break
      
    case 'content_block_delta':
      // 增量更新(text delta 或 input_json_delta)
      yield createStreamEvent(event.delta)
      break
      
    case 'content_block_stop':
      // 内容块结束
      break
      
    case 'message_delta':
      // 更新 message 级别字段(stop_reason、usage)
      break
      
    case 'message_stop':
      // 消息结束
      break
      
    case 'error':
      // API 返回错误(例如 429 在流中间)
      throw parseError(event.error)
  }
}

关键事件

  • content_block_delta:最重要的事件,包含 逐 token 的文本逐 key 的工具参数
  • message_start:包含 input_tokens 统计
  • message_delta:包含 stop_reasonoutput_tokens
6. 增量拼接工具参数(Tool Input Buffering)
typescript 复制代码
// 工具参数可能是流式返回的(input_json_delta)
// 需要增量拼接成完整 JSON
const toolInputBuffers = new Map<string, string>()

for (const delta of event.delta) {
  if (delta.type === 'input_json_delta') {
    const buffer = toolInputBuffers.get(toolUseId) || ''
    toolInputBuffers.set(toolUseId, buffer + delta.partial_json)
    
    // 尝试解析完整 JSON(可能还没完成)
    try {
      const fullInput = JSON.parse(toolInputBuffers.get(toolUseId))
      yield createToolUseMessage(toolUseId, fullInput)
    } catch {
      // JSON 还没完整,继续等待
    }
  }
}

为什么需要缓冲

  • 工具参数(input)可能很大(例如读取一个 10KB 的文件)
  • API 会把它拆成多个 input_json_delta 返回
  • 必须拼接完整后才能 JSON.parse()
7. 限速重试(Rate Limit Retry)
typescript 复制代码
catch (error) {
  if (error instanceof RateLimitError) {
    if (retryCount < maxRetries) {
      const delay = Math.pow(2, retryCount) * 1000  // 指数退避:1s → 2s → 4s
      await sleep(delay)
      retryCount++
      continue  // 重试
    }
    
    // 重试次数用尽,切换到 fallbackModel
    if (fallbackModel) {
      throw new FallbackTriggeredError(fallbackModel)
    }
    
    throw error  // 没有 fallback,抛出错误
  }
}

重试策略

  • 指数退避:1s → 2s → 4s → 8s(最多 3 次)
  • 自动切换备用模型 :如果配置了 fallbackModel,重试失败后抛出 FallbackTriggeredError,由 query.ts 捕获并切换模型
8. 记录性能指标(Performance Metrics)
typescript 复制代码
const startTime = Date.now()
let firstTokenTime: number | null = null

for await (const event of handleSSE(response)) {
  if (!firstTokenTime && event.type === 'content_block_delta') {
    firstTokenTime = Date.now()  // 记录首 token 时间
  }
  
  yield event
}

const endTime = Date.now()
const totalLatency = endTime - startTime
const timeToFirstToken = firstTokenTime! - startTime

// 上报遥测(如果启用)
if (telemetryEnabled) {
  reportMetrics({
    model: currentModel,
    totalLatency,
    timeToFirstToken,
    outputTokens: event.usage?.output_tokens,
    retryCount,
  })
}

关键指标

  • Time to First Token (TTFT):从发起请求到收到第一个 token 的延迟(通常 200-500ms)
  • Total Latency:总延迟
  • Output Tokens/s:输出吞吐量
9. 处理流式工具执行(Streaming Tool Execution)
typescript 复制代码
// 如果启用了 streamingToolExecution
// 在收到 tool_use 块时立即开始执行(不必等整个模型响应完成)
if (event.type === 'content_block_start' && event.content_block.type === 'tool_use') {
  const toolUse = event.content_block
  
  // 立即启动工具执行(异步)
  streamingToolExecutor?.addTool(toolUse, currentAssistantMessage)
  
  // 同时继续处理流(模型可能还在返回其他内容)
}

好处

  • 工具执行和模型生成 并行
  • 用户能更早看到工具执行结果(减少感知延迟)
10. 清理资源
typescript 复制代码
finally {
  // 关闭 SSE 流
  reader.releaseLock()
  
  // 取消未完成的工具执行
  streamingToolExecutor?.cancel()
  
  // 清理缓冲区
  toolInputBuffers.clear()
}

五、高级特性

1. 请求去重(Request Deduplication)

typescript 复制代码
// 如果同一个请求(相同 messages/model/tools)正在发送
// 复用已有的响应流(而不是发起新请求)
const requestKey = JSON.stringify({
  messages: params.messages,
  model: currentModel,
  tools: params.tools,
})

const existingStream = activeStreams.get(requestKey)
if (existingStream) {
  yield* existingStream  // 复用流
  return
}

// 否则,发起新请求并缓存
const newStream = callModelImpl(params)
activeStreams.set(requestKey, newStream)
yield* newStream

为什么需要去重

  • 用户可能快速按两次 Enter(误触)
  • UI 可能渲染两次(React StrictMode)

2. 请求优先级(Request Priority)

typescript 复制代码
// 根据 querySource 设置请求优先级
const priority = {
  'repl': 'high',         // 用户直接输入,最高优先级
  'agent': 'normal',      // Agent 自动触发,普通优先级
  'compact': 'low',       // 压缩请求,最低优先级(后台任务)
}[params.options.querySource] ?? 'normal'

// 在遥测中上报优先级(Anthropic 可能用来优化调度)
requestHeaders['x-request-priority'] = priority

3. 响应验证(Response Validation)

typescript 复制代码
// 验证模型返回的工具调用是否合法
for (const toolUse of assistantMessage.message.content) {
  if (toolUse.type === 'tool_use') {
    const tool = params.tools.find(t => t.name === toolUse.name)
    if (!tool) {
      // 模型返回了不存在的工具 → 记录警告,但不阻断
      console.warn(`Model returned unknown tool: ${toolUse.name}`)
      continue
    }
    
    // 验证工具参数(根据 input_schema)
    const errors = validateToolInput(toolUse.input, tool.input_schema)
    if (errors.length > 0) {
      // 参数不合法 → 让模型修正(而不是报错)
      yield createUserMessage({
        content: `Tool input validation failed: ${errors.join(', ')}. Please fix and retry.`,
        isMeta: true,
      })
    }
  }
}

六、错误处理详解

错误分类

错误类型 是否可重试 恢复策略
429(限速) 指数退避重试 → 切换备用模型
500/502/503(服务端错误) 指数退避重试
413(prompt-too-long) ⚠️ 触发上下文压缩 → 返回错误
max_output_tokens ⚠️ 升级输出上限 → 注入恢复提示词 → 返回错误
401(认证失败) 立即返回错误
400(请求格式错误) 立即返回错误
网络超时 重试(最多 3 次)

错误转换

typescript 复制代码
// 把 API 错误转换成内部错误类型
function parseAPIError(status: number, body: string): ClaudeCodeError {
  if (status === 429) {
    const retryAfter = extractRetryAfter(body)
    return new RateLimitError(retryAfter)
  }
  
  if (status === 413) {
    return new PromptTooLongError()
  }
  
  if (body.includes('max_output_tokens')) {
    return new MaxOutputTokensError()
  }
  
  return new APIError(status, body)
}

七、性能优化

1. 连接复用(Connection Reuse)

typescript 复制代码
// 使用 global fetch(浏览器/Node.js)会自动复用 TCP 连接
// 不需要手动管理连接池
const response = await fetch(...)

效果

  • 同一会话的多次 API 调用复用同一个 TCP 连接
  • 减少 TLS 握手延迟(~50ms)

2. 请求体压缩(Request Compression)

typescript 复制代码
// 如果请求体很大(例如包含长上下文),启用 gzip
if (JSON.stringify(requestBody).length > 100_000) {
  headers['content-encoding'] = 'gzip'
  body = gzipSync(JSON.stringify(requestBody))
}

效果

  • 100KB 上下文 → 10KB gzip(节省 90% 带宽)

3. 流式解析优化(Streaming Parse Optimization)

typescript 复制代码
// 避免每次都 JSON.parse() 整个事件
// 只解析需要的字段
const event = {
  type: data.type,
  delta: data.delta,
  // 其他字段按需解析
}

// 如果事件是 content_block_delta(最常见)
// 直接传递 delta,不解析整个事件
if (data.type === 'content_block_delta') {
  yield { type: 'delta', delta: data.delta }
}

效果

  • 减少 30% CPU 占用(在长对话中)

八、总结:callModel.ts 的设计哲学

读完 src/services/api/callModel.ts 的约 800 行代码,有三个设计决策令人印象深刻:

  1. AsyncGenerator 而非回调

    • 通过异步生成器逐步 yield 事件,使得 REPL UI 可以实时渲染流式输出
    • SDK 消费者也能灵活处理中间事件(例如,记录日志、取消请求)
  2. 多层错误恢复

    • 限速:指数退避 → 切换备用模型
    • 上下文过长:触发压缩 → 返回错误
    • 网络错误:重试 → 返回错误
    • 每层恢复都是独立的,互不相干,且可以按任意顺序组合
  3. 特性门控(Feature Gating)

    • 所有实验性特性(请求去重、优先级、响应验证)都用 feature() 包裹
    • 在生产构建中,未启用的特性代码会被 bun:bundle 完全移除(tree-shaking)
    • 这使得 callModel.ts 能同时服务:
      • 稳定版(只有核心功能)
      • 内测版(启用所有实验性特性)
      • Ant 内部版(额外的调试和遥测)

下一篇预告 :我们将深入 src/services/api/streamProcessor.ts ------ 真正解析 SSE 流、处理 content_block_delta、拼接工具参数的那一层,看看"200ms 首 token 延迟"是怎么做到的。


Claude Code 源码分析系列 · 第十篇


附录:callModel.ts 完整调用流程图

复制代码
用户按 Enter
   ↓
query.ts: query()
   ↓
callModel.ts: callModel()
   ↓
[构建请求体]
   ↓
[发起 HTTPS 请求]
   ↓
[检查响应状态]
   ↓  (如果 429)
[指数退避重试]
   ↓  (如果重试失败)
[抛出 FallbackTriggeredError]
   ↓  (由 query.ts 捕获)
[切换到 fallbackModel]
   ↓
[重新调用 callModel()]
   ↓  (如果 200)
[解析 SSE 流]
   ↓
[逐块 yield 事件]
   ↓
[UI 实时渲染]
   ↓
[工具执行]
   ↓
[返回工具结果]
   ↓
[下一轮循环]