第十二篇:MessageBuilder 深度解析 —— Claude Code 如何构建对话消息

📚 系列文章 第12篇/共100篇 · 2026年7月 · ⏱️ 阅读时间约 13 分钟
源码位置:src/services/anthropic/MessageBuilder.ts


一、引言:为什么需要 MessageBuilder?

上一篇我们拆解了 AnthropicClient.ts,它负责"连接" Anthropic API。但在这之前还有一个关键问题:

API 需要的不是零散的字符串,而是结构化的消息对象。 这些消息从哪来?如何把用户的文本、系统提示、历史对话、工具调用结果、甚至图片,拼装成符合 Anthropic 规范的 messages 数组?

答案就是 MessageBuilder.ts

它是 Claude Code 对话流水线的第一道加工车间

  • 🧱 把多源输入(用户输入、系统提示、工具结果、图片)统一成标准 Message 结构
  • 🖼️ 处理多模态:把图片转成 base64 或 URL 引用的 image content block
  • 📐 管理对话上下文:裁剪过长历史、压缩旧消息、控制 token 预算
  • 🔗 串联工具调用:把 tool_use / tool_result 配对成合法对话轮次

可以说,没有 MessageBuilder,AnthropicClient 拿到的就是一堆无法发送的数据


二、MessageBuilder 架构概览

┌─────────────────────────────────────────────────────────────┐
│                    MessageBuilder                           │
├─────────────────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ Text Builder │  │ Image Builder│  │ System Builder   │  │
│  │ (纯文本块)    │  │ (多模态)     │  │ (系统提示)        │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ History Mgr  │  │ Token Budget │  │ Tool Round Mgr   │  │
│  │ (历史裁剪)    │  │ (预算控制)    │  │ (工具轮次配对)    │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
              AnthropicClient.sendMessage(messages)

数据流:

用户输入 + 系统提示 + 工具结果 + 图片
        │
        ▼
  MessageBuilder.build()
        │
        ├── 文本 → { type: 'text', text }
        ├── 图片 → { type: 'image', source }
        ├── 工具 → { type: 'tool_use' / 'tool_result' }
        ▼
  标准 Message[]  ──►  AnthropicClient  ──►  /v1/messages

三、核心类型定义

3.1 Anthropic 消息协议

MessageBuilder 的产物必须严格符合 Anthropic Messages API 的 schema:

interface Message {
  role: 'user' | 'assistant';
  content: ContentBlock[];   // 注意:content 是数组,不是字符串
}

type ContentBlock =
  | { type: 'text'; text: string }
  | { type: 'image'; source: ImageSource }
  | { type: 'tool_use'; id: string; name: string; input: unknown }
  | { type: 'tool_result'; tool_use_id: string; content: ContentBlock[] };

interface ImageSource {
  type: 'base64';
  media_type: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
  data: string;             // base64 编码的图片数据
}

关键认知: Anthropic 的 content 是一个块数组,而不是单个字符串。这正是多模态和工具调用的基础。MessageBuilder 的核心职责,就是正确地生成这个数组。

3.2 Builder 配置

interface MessageBuilderConfig {
  maxTokens: number;              // 总 token 预算(如 200000)
  systemPrompt: string;           // 系统提示
  reserveTokens: number;          // 为回复预留的 token 数
  imageCompression?: boolean;     // 是否压缩大图
  truncateStrategy: 'head' | 'tail' | 'smart';  // 裁剪策略
}

四、文本与系统提示构建

4.1 构建系统提示

系统提示(system prompt)在 Anthropic API 中是顶层参数,不是 messages 的一部分:

buildSystem(): string {
  const blocks: string[] = [];

  // 1. 基础身份提示
  blocks.push(this.config.systemPrompt);

  // 2. 注入当前环境信息(OS、shell、cwd、日期)
  blocks.push(this.buildEnvironmentContext());

  // 3. 注入工具定义摘要
  blocks.push(this.buildToolCatalog());

  return blocks.join('\n\n');
}

private buildEnvironmentContext(): string {
  const env = this.envCollector.collect();
  return [
    `<environment>`,
    `OS: ${env.os}`,
    `Shell: ${env.shell}`,
    `CWD: ${env.cwd}`,
    `Date: ${env.isoDate}`,
    `</environment>`
  ].join('\n');
}

设计要点: 系统提示是动态拼装的,会实时注入环境上下文,让模型"知道自己在哪台机器上工作"。

4.2 构建用户文本消息

addUserText(text: string): this {
  this.pendingUserBlocks.push({ type: 'text', text });
  return this;  // 支持链式调用
}

// 用法
builder
  .addUserText('帮我重构这个函数')
  .addUserText('注意保留测试用例')
  .build();

五、多模态:图片处理

这是 MessageBuilder 最精彩的部分——把图片塞进对话。

5.1 从文件路径读取并编码

async addUserImage(filePath: string): Promise<this> {
  const buffer = await fs.readFile(filePath);
  const mediaType = this.detectMediaType(filePath);

  // 大图压缩(可选)
  const data = this.config.imageCompression
    ? await this.compressImage(buffer)
    : buffer;

  const base64 = data.toString('base64');

  this.pendingUserBlocks.push({
    type: 'image',
    source: {
      type: 'base64',
      media_type: mediaType,
      data: base64
    }
  });

  return this;
}

5.2 从 URL 引用图片

Claude 也支持直接传 URL(无需 base64):

addUserImageUrl(url: string): this {
  this.pendingUserBlocks.push({
    type: 'image',
    source: {
      type: 'url',
      url: url
    }
  });
  return this;
}

5.3 多模态消息示例

const message = await builder
  .addUserText('这张截图里有什么错误?')
  .addUserImage('./error-screenshot.png')
  .build();

// 生成的 content 块:
// [
//   { type: 'text', text: '这张截图里有什么错误?' },
//   { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'iVBOR...' } }
// ]

💡 实战价值: 这就是为什么 Claude Code 能"看"截图、读取设计稿、分析图表。MessageBuilder 把图片转成模型可消费的块,是多模态能力的根基。


六、工具调用轮次管理

工具调用在 Anthropic 协议里是一对配对块,必须严格交替:

assistant: { type: 'tool_use', id: 't1', name: 'Read', input: {...} }
user:      { type: 'tool_result', tool_use_id: 't1', content: [...] }

6.1 配对管理

addToolUse(block: ToolUseBlock): this {
  // tool_use 必须来自 assistant 消息
  if (this.lastRole !== 'assistant') {
    this.flush();  // 先结束当前轮次
  }
  this.pendingAssistantBlocks.push(block);
  this.lastRole = 'assistant';
  return this;
}

addToolResult(toolUseId: string, content: ContentBlock[]): this {
  // tool_result 必须来自 user 消息
  this.pendingUserBlocks.push({
    type: 'tool_result',
    tool_use_id: toolUseId,
    content
  });
  this.lastRole = 'user';
  return this;
}

6.2 关键约束校验

validateToolPairs(): void {
  // 每个 tool_use 必须有对应的 tool_result
  const useIds = this.collectToolUseIds();
  const resultIds = this.collectToolResultIds();

  for (const id of useIds) {
    if (!resultIds.has(id)) {
      throw new MessageBuildError(
        `工具调用 ${id} 缺少对应的 tool_result`
      );
    }
  }
}

⚠️ 血的教训: Anthropic API 对工具配对极其严格。如果 tool_use 没有配对的 tool_result,或反之,API 会直接返回 400。MessageBuilder 的校验逻辑正是为了拦截这类错误。


七、上下文与 Token 预算管理

这是 MessageBuilder 最复杂、也最能体现工程功力的部分。

7.1 Token 估算

private estimateTokens(blocks: ContentBlock[]): number {
  let total = 0;
  for (const block of blocks) {
    switch (block.type) {
      case 'text':
        total += this.tokenizer.count(block.text);
        break;
      case 'image':
        // 图片按像素估算 token(约 每 512x512 ≈ 85 token)
        total += this.estimateImageTokens(block.source);
        break;
      case 'tool_use':
      case 'tool_result':
        total += this.tokenizer.count(
          JSON.stringify(block.input ?? block.content)
        );
        break;
    }
  }
  return total;
}

7.2 智能裁剪

当历史超出预算时,按策略裁剪:

private fitToBudget(messages: Message[]): Message[] {
  const budget = this.config.maxTokens - this.config.reserveTokens;
  let used = this.estimateMessagesTokens(messages);

  if (used <= budget) return messages;

  // 从最旧的消息开始裁剪(保留最近对话)
  const result = [...messages];
  while (used > budget && result.length > 1) {
    const removed = result.shift()!;   // 移除最旧的一条
    used -= this.estimateMessagesTokens([removed]);
  }

  // 在开头插入截断标记
  result.unshift(this.buildTruncationNotice());
  return result;
}

7.3 三种裁剪策略对比

策略 行为 适用场景
head 丢弃最旧消息 长对话,最近内容最重要
tail 丢弃最新消息 罕见,保留早期上下文
smart 保留首尾,压缩中间 需要全局上下文的复杂任务

💡 设计哲学: Claude Code 默认用 head 策略——因为编程对话中,最近的指令和上下文通常最关键。但 smart 策略会在处理超大文件/跨文件重构时启用,避免丢失早期的重要约束。


八、完整构建流程

build(): BuiltMessages {
  // 1. 收尾当前 pending 的块
  this.flush();

  // 2. 组装 messages 数组
  const messages = this.assembleMessages();

  // 3. 校验工具配对
  this.validateToolPairs();

  // 4. Token 预算裁剪
  const fitted = this.fitToBudget(messages);

  // 5. 构建系统提示
  const system = this.buildSystem();

  return { messages: fitted, system };
}

// 最终交给 AnthropicClient
const { messages, system } = builder.build();
const response = await client.sendMessage(messages, { system });

完整 ASCII 时序:

用户/工具 ──► MessageBuilder
                │
                ├─ addUserText()      ─┐
                ├─ addUserImage()     ├─ 累积 pending 块
                ├─ addToolUse()       │
                ├─ addToolResult()    ─┘
                │
                ▼
            build()
                ├─ flush()            收尾轮次
                ├─ assemble()         组装 messages
                ├─ validateToolPairs() 校验
                ├─ fitToBudget()      token 裁剪
                └─ buildSystem()      系统提示
                │
                ▼
        { messages, system } ──► AnthropicClient

九、使用示例

9.1 最简用法

const builder = new MessageBuilder({
  maxTokens: 200000,
  systemPrompt: '你是一个编程助手',
  reserveTokens: 4096,
  truncateStrategy: 'head'
});

const { messages, system } = builder
  .addUserText('用 TypeScript 写一个快速排序')
  .build();

client.sendMessage(messages, { system });

9.2 多模态 + 工具混合

const { messages, system } = await builder
  .addUserText('这个 UI 有问题,看图')
  .addUserImage('./ui-bug.png')            // 多模态
  .addToolUse({                            // 工具调用
    type: 'tool_use',
    id: 't1',
    name: 'Read',
    input: { file_path: './App.tsx' }
  })
  // (tool_result 会在下一轮由工具执行结果填入)
  .build();

9.3 长对话自动裁剪

// 假设已经累积了 300 轮对话,远超 200k token 预算
const { messages } = builder
  .addUserText('继续')
  .build();   // 内部自动裁剪最旧的消息,保留最近上下文

十、源码亮点总结

特性 实现方式 价值
多模态块 content 块数组 + image source 支持图片输入
动态系统提示 实时注入环境/工具上下文 模型"身临其境"
工具配对校验 validateToolPairs() 避免 400 错误
Token 预算 fitToBudget() 智能裁剪 永不超窗口
链式 API addX().addY().build() 调用优雅
策略可配 head/tail/smart 灵活适配场景

十一、下一篇预告

第13篇我们将深入 流式编排(StreamProcessor / query.ts),看看 Claude Code 如何把消息推送到 API、处理流式响应中的 tool_use 增量块、并实时渲染终端 UI。敬请期待!


📚 Claude Code 源码解析系列

  1. 源码泄露事件深度解析
  2. 开发环境搭建指南
  3. CLI入口与命令系统
  4. AI对话引擎全解析
  5. 工具系统深度解析
  6. CLI命令行解析核心
  7. Handler处理器链
  8. QueryEngine查询引擎
  9. query.ts API对话层
  10. callModel.ts API调用层
  11. AnthropicClient API客户端
  12. MessageBuilder 消息构建器 ← 本文
  13. StreamProcessor 流式编排(待续)

💡 如果这篇文章对你有帮助,欢迎点赞、收藏、关注!

有问题欢迎在评论区讨论!

#ClaudeCode #源码分析 #MessageBuilder #多模态 #TypeScript #AI编程

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐