深度拆解 Claude Code 源码系列(三):消息系统 —— 对话的语言与翻译器编者:这是系列文章的第三章。在前两章中,我们了解了 Claude Code 的整体架构和查询引擎。本章将深入消息系统 —— 理解 Claude Code 如何定义、创建、转换和渲染消息。引子:为什么消息系统如此重要?
如果你曾经看过 Claude Code 的源码,你可能会注意到一个令人惊讶的事实:`messages.ts` 文件有将近 6000 行代码。一个消息系统,为什么需要这么多代码?这难道不就是"用户发送消息,AI 回复消息"这么简单的事情吗?想象一下 Claude Code 在对话过程中需要处理的所有场景:- API 返回流式文本 —— Claude 逐字输出响应
- API 返回工具调用 —— Claude 需要读取文件或执行命令
- 工具执行失败 —— 错误信息需要传达给 Claude
- 权限重试 —— 用户改变主意,重新尝试被拒绝的工具调用
- Agent 生命周期 —— 子 Agent 被杀死的通知
每一种场景都需要定义特定的消息类型、创建函数、转换逻辑和渲染方式。而这仅仅是消息系统的冰山一角。消息系统本质上是一个"对话语言"的设计。 它定义了 Claude Code 内部各个模块之间如何通信,就像 TCP/IP 定义了互联网上的通信协议一样。在这一章中,我们将深入这个近 6000 行的消息系统,理解它是如何精密地组织和管理对话中的每一条信息的。第一部分:消息类型层次 —— 25+ 种消息的组织方式
在 Claude Code 中,消息不仅仅是"用户说的"和"AI 回复的"。它是一个丰富的类型系统,涵盖了对话的方方面面。1.1 消息类型的继承树
1. 类型安全 —— TypeScript 编译器可以在编译时检查消息格式3. 模式匹配 —— 通过 switch (message.type) 轻松处理不同类型的消息1.2 核心消息类型详解
UserMessage(用户消息)
isMeta —— 标记消息是否为"元消息"。元消息不发送到 API,只在本地使用(如斜杠命令输出)。
toolUseResult —— 当用户消息是工具调用的响应时,这个字段包含工具的输出。
isCompactSummary —— 标记消息是否为压缩摘要。压缩后的消息会有这个标志。
isSynthetic —— 标记消息是否为"合成"的(不是用户直接输入的,而是系统生成的)。
AssistantMessage(助手消息)
ContentBlock 是 Anthropic SDK 定义的内容块类型,包括:SystemMessage(系统消息)
这是消息系统中最大的类别。系统消息用于传达系统级别的信息。1.3 特殊消息类型
除了标准的用户、助手、系统消息外,还有一些特殊用途的消息类型:AttachmentMessage(附件消息)
ProgressMessage(进度消息)
TombstoneMessage(墓碑消息)
这是一个有趣的类型 —— 它用于标记消息为"已删除"。墓碑消息本身不渲染任何内容,它的作用是告诉 UI 层"这条消息应该被移除"。这是一种控制信号设计模式 —— 用数据而不是副作用来控制行为。GroupedToolUseMessage(分组工具使用消息)
当 Claude 在一次响应中调用多个工具时,这些工具调用可以被分组显示。CollapsibleMessage(可折叠消息)
用于 UI 折叠显示。例如,大量的文件读取结果可以被折叠为一个摘要。1.4 RenderableMessage —— UI 层的抽象
对于 UI 来说,不是所有消息类型都需要单独处理。RenderableMessage 定义了所有可以在 UI 中渲染的消息类型:CollapsedReadSearchGroup 是一个特殊的聚合类型,它将多个文件读取/搜索操作折叠为一个可折叠的 UI 元素:这种聚合显示的设计让 UI 不会信息过载 —— 10 次文件读取可以折叠为"读取了 10 个文件"的摘要,用户可以展开查看详情。*在下一部分中,我们将了解消息是如何被创建和转换的 —— Claude Code 有 32 个消息创建函数。*第二部分:消息创建与转换 —— 32 个工厂函数的精密协作
在 Claude Code 的消息系统中,创建一条消息从来不是简单地构造一个对象。每种消息类型都有自己的创建函数(factory function),这些函数负责填充必要的字段、生成 UUID、设置时间戳、处理格式转换。2.1 消息创建函数家族
在 messages.ts 中,有 32 个消息创建函数:每个创建函数都有明确的职责和参数列表,确保消息的格式一致性。2.2 createUserMessage 详解
1. 默认值策略 —— uuid 默认自动生成,timestamp 默认当前时间。调用方不需要关心这些细节。2. 可选参数 —— 所有额外标志都是可选的,调用方只设置需要的参数。3. 类型约束 —— content 可以是字符串或 ContentBlockParam 数组,支持简单和复杂两种使用场景。2.3 createCompactBoundaryMessage 详解
CompactMetadata 包含压缩的详细信息:·压缩的类型(自动/手动/snip/微/响应式)
·压缩前后的 Token 数变化
·哪些消息段被保留了
2.4 消息标准化(Normalization)
消息从不同来源到达时(API 响应、工具执行、用户输入),格式可能不一致。标准化函数将它们统一为标准格式:1. API 兼容 —— API 返回的消息格式可能与内部格式不同,需要转换2. 一致性保证 —— 不同来源的消息在处理后格式一致3. 类型收窄 —— 从宽类型收窄到窄类型,提高类型安全2.5 消息转换函数
2.6 消息处理的完整流程
让我们用一张流程图来展示消息从创建到渲染的完整路径:1. 消息从各种来源创建(API、工具、用户、系统事件)3. QueryEngine 管理(追加到消息历史、持久化)*在下一部分中,我们将了解 API 格式适配层 —— Claude Code 如何在 SDK 格式和内部格式之间桥接。*第三部分:API 格式适配层 —— SDK 格式与内部格式的桥接
Claude Code 使用 Anthropic SDK 与 Claude API 通信。但 API 返回的消息格式与 Claude Code 内部使用的消息格式并不相同。3.1 两种格式的区别
3.2 流事件到消息的转换
API 使用 Server-Sent Events (SSE) 返回响应。这意味着消息是逐步到达的,不是一次性返回的。1. message_start —— 创建消息的骨架,包含 UUID 和初始内容2. content_block_delta —— 累积文本片段,yield 给 UI 进行流式显示3. content_block_stop —— 标记一个内容块完成4. message_delta —— 更新消息的 usage 和 stop_reason5. message_stop —— 标记消息完成3.3 normalizeMessagesForAPI
在发送消息到 API 之前,需要将内部格式转换为 SDK 格式:3.4 SDK 消息映射
QueryEngine 中有一个工具函数用于 SDK 兼容的工具名称映射:这种映射确保 SDK 调用方可以正确识别工具名称。3.5 消息归一化(normalizeMessage)
normalizeMessage 函数将消息转换为 SDK 可识别的格式:这个函数的输出是 SDKMessage 类型,可以被 SDK 调用方(如远程控制服务器)正确解析。*在下一部分中,我们将了解消息渲染与 UI —— 消息如何在终端中显示。*第四部分:消息渲染与 UI —— 终端中的消息呈现
消息系统的最终目标是将消息呈现给用户。在 Claude Code 中,这通过 Ink(React 终端 UI 框架)实现。4.1 RenderableMessage 的渲染管线
在第一章中我们提到,RenderableMessage 是所有可以在 UI 中渲染的消息类型的并集:每种消息类型都有自己的渲染组件,最终统一到一个 Box 容器中。4.2 UserMessageRow 组件
用户消息用"你:"标签标识
支持字符串和内容块两种格式
使用 wrap="wrap" 自动换行
4.3 AssistantMessageRow 组件
支持文本、工具调用、思考过程多种内容块
每种内容块有自己的渲染方式
工具调用显示工具名称和参数
思考过程可能以折叠或灰色显示
4.4 SystemMessageRow 组件
4.5 CollapsedReadSearchGroup 渲染
这是最复杂的渲染组件之一。它将大量的读取/搜索操作折叠为一个可折叠的元素:这种聚合显示让 UI 保持简洁,用户可以展开查看详情。4.6 流式渲染
消息系统支持流式渲染 —— 在消息完成之前就显示部分内容。这种流式渲染让用户能够在 Claude 还在"思考"时就看到部分输出,极大地提升了交互体验。第五部分:特殊消息类型 —— 压缩边界、附件、权限重试
5.1 压缩边界消息
压缩边界消息(SystemCompactBoundaryMessage)是对话被压缩后插入的消息。2. 提供压缩的元数据(类型、Token 数变化)压缩边界通常以一条分隔线显示,让用户知道对话历史已经被压缩。5.2 附件消息
附件消息(AttachmentMessage)用于显示用户附加的文件、图片、PDF 等。当用户启用内存功能时,Claude Code 会自动附加 MEMORY.md 文件。附件消息会显示:5.3 权限重试消息
当用户拒绝了工具调用但后来改变主意时,系统会插入权限重试消息:🔄 重试工具调用:FILE_READ(src/utils/messages.ts)这个消息告诉 Claude 重新尝试之前被拒绝的工具调用。5.4 进度消息
进度消息(ProgressMessage)用于显示长时间运行工具的进度。⏳ Bash 命令执行中:npm install...进度消息通常在工具开始执行时插入,在工具完成时被替换为结果消息。5.5 墓碑消息
墓碑消息(TombstoneMessage)是最特殊的消息类型 —— 它不包含任何内容,只是一个控制信号。消息被删除(如用户删除了一条消息)
消息被压缩移除
消息被编辑(旧版本被墓碑标记)
第六部分:消息持久化 —— 转录文件与会话恢复
消息系统的最后一个重要职责是持久化 —— 将消息历史保存到磁盘,以便下次恢复会话。6.1 转录文件格式
6.2 recordTranscript 函数
消息持久化通过 recordTranscript 函数完成:recordTranscript 将消息历史追加到转录文件中。6.3 会话恢复
当用户通过 --resume 命令恢复会话时,系统会读取转录文件:这使得 Claude Code 可以在上次中断的地方继续对话。6.4 压缩后的转录处理
总结与预告
本章回顾
在这一章中,我们深入了 Claude Code 近 6000 行的消息系统,理解了它是如何组织和管理对话中的每一条信息的:1. 消息类型层次
30+ 种消息类型,分为用户消息、助手消息、系统消息、附件消息、进度消息等多个类别。类型安全的设计让编译器可以在编译时检查消息格式。2. 消息创建与转换
32 个消息创建函数,每种类型都有自己的工厂函数。标准化函数确保不同来源的消息格式一致。3. API 格式适配层
SDK 格式与内部格式的桥接。流事件到消息的转换,消息归一化为 SDK 可识别的格式。4. 消息渲染与 UI
RenderableMessage 渲染管线,每种消息类型有自己的渲染组件。流式渲染让用户体验更流畅。5. 特殊消息类型
压缩边界、附件、权限重试、进度、墓碑 —— 这些特殊消息各有特定的职责和渲染方式。6. 消息持久化
转录文件格式、会话恢复、压缩后的转录处理,让对话可以跨会话延续。这些设计你可以借鉴什么?
1. 工厂函数模式 —— 每种类型有自己的创建函数,确保格式一致性2. 适配器模式 —— SDK 格式与内部格式的桥接,下游代码不关心外部格式3. 类型并集 —— RenderableMessage 用类型并集定义所有可渲染消息4. 控制信号模式 —— 墓碑消息用数据而不是副作用控制行为5. 流式渲染 —— 在数据到达时就渲染,而不是等待完成下一章预告
在理解了消息系统之后,下一章我们将深入 **流式处理** —— 理解 Claude Code 如何处理 SSE 事件流、如何实现流式工具调用、以及流式响应的错误处理和重试机制。
如果你对"实时数据流"的处理感兴趣,下一章会给你带来深入的理解。