第 2 章:理解消息系统

和 Claude 对话就像收快递 —— 你得知道包裹里装的是什么,才能正确处理。

上一章我们用 query() 和 Claude 说了第一句话,也拿到了回复。但那些 message 到底是什么东西?里面又装了些啥?这一章我们把这个"黑箱"彻底拆开看看。


2.1 Claude 返回的"包裹"里有什么?

想象你网购了一箱东西。快递包裹(Message)到了,你拆开一看,里面可能有好几件商品(ContentBlock)。有的是衣服(文字回复),有的是说明书(思考过程),有的是提货单(工具调用请求)。

用代码说话:

import asyncio
from claude_agent_sdk import (
    query, ClaudeAgentOptions,
    AssistantMessage, ResultMessage,
    TextBlock, ToolUseBlock, ThinkingBlock
)

async def main():
    options = ClaudeAgentOptions(
        permission_mode="bypassPermissions",  # 自动批准工具使用
        max_turns=1,                          # 只让 Claude 回复一轮
    )

    async for message in query(prompt="你好,请做一下自我介绍", options=options):
        # 每个 message 就是一个"包裹"
        print(f"收到包裹类型: {type(message).__name__}")

        # 如果是 Claude 的回复,拆开看看里面有什么
        if isinstance(message, AssistantMessage):
            print(f"  里面有 {len(message.content)} 件物品:")
            for i, block in enumerate(message.content):
                print(f"  第 {i+1} 件: {type(block).__name__}")

asyncio.run(main())

运行后你会看到类似这样的输出:

收到包裹类型: AssistantMessage
  里面有 1 件物品:
  第 1 件: TextBlock
收到包裹类型: ResultMessage

两个核心概念:

概念 比喻 说明
Message 快递包裹 一次完整的消息,有 5 种类型
ContentBlock 包裹里的物品 消息的具体内容,有 4 种类型

一个 AssistantMessage(Claude 的回复)里可能同时包含多个 ContentBlock。比如 Claude 一边回答你的问题(TextBlock),一边决定调用某个工具(ToolUseBlock),这两个就会出现在同一个消息的 content 列表里。


2.2 五种消息类型

query() 返回的消息流里,你会遇到五种类型的消息。日常开发中最常打交道的是前两种。

2.2.1 AssistantMessage —— Claude 的回复(最常用)

这是你最常见到的消息类型。每当 Claude 说话、思考、或者想用工具,都会以 AssistantMessage 的形式发给你。

@dataclass
class AssistantMessage:
    content: list[ContentBlock]    # 内容块列表(文字、思考、工具调用等)
    model: str                     # 使用的模型,如 "claude-sonnet-4-5-20250514"
    parent_tool_use_id: str | None # 如果是工具调用的后续回复,这里会有对应的 ID
    error: str | None              # 如果出错了,这里会有错误类型

用法:

if isinstance(message, AssistantMessage):
    print(f"模型: {message.model}")
    for block in message.content:
        if isinstance(block, TextBlock):
            print(f"Claude 说: {block.text}")

2.2.2 ResultMessage —— 对话结束信号

这是每次对话的"收据",告诉你这次对话花了多少钱、用了多长时间。它永远是消息流中的最后一条消息

@dataclass
class ResultMessage:
    subtype: str                       # 结果子类型
    duration_ms: int                   # 总耗时(毫秒)
    duration_api_ms: int               # API 调用耗时(毫秒)
    is_error: bool                     # 是否出错
    num_turns: int                     # 对话轮数
    session_id: str                    # 会话 ID
    total_cost_usd: float | None       # 花了多少美元
    usage: dict[str, Any] | None       # Token 用量详情
    result: str | None                 # 最终结果文本

这就好比你去餐厅吃完饭,服务员递来的账单:

if isinstance(message, ResultMessage):
    print(f"耗时: {message.duration_ms / 1000:.1f} 秒")
    print(f"费用: ${message.total_cost_usd:.4f}" if message.total_cost_usd else "费用: 免费(订阅用户)")
    print(f"对话轮数: {message.num_turns}")
    print(f"是否出错: {message.is_error}")
    print(f"会话 ID: {message.session_id}")

2.2.3 UserMessage —— 你发的消息

@dataclass
class UserMessage:
    content: str | list[ContentBlock]  # 消息内容
    uuid: str | None                   # 消息唯一标识
    parent_tool_use_id: str | None     # 关联的工具调用 ID

query() 模式下,你通常看不到 UserMessage,因为你的输入直接传给了 Claude,不会回显给你。

但在 ClaudeSDKClient(多轮对话模式)里,当 Claude 调用工具后,SDK 会自动把工具执行结果封装成 UserMessage 发回给 Claude。这时你就能在消息流里看到它了。

2.2.4 SystemMessage —— 系统级通知

@dataclass
class SystemMessage:
    subtype: str               # 通知子类型(如 "init" 等)
    data: dict[str, Any]       # 通知携带的数据

系统消息是 SDK 内部的通知,比如初始化完成、状态变更等。大多数情况下你可以直接忽略它。如果你好奇,可以打印出来看看:

if isinstance(message, SystemMessage):
    print(f"系统通知: {message.subtype}")
    print(f"通知数据: {message.data}")

2.2.5 StreamEvent —— 流式事件(高级)

@dataclass
class StreamEvent:
    uuid: str                          # 事件唯一标识
    session_id: str                    # 会话 ID
    event: dict[str, Any]              # 原始的 Anthropic API 流式事件
    parent_tool_use_id: str | None     # 关联的工具调用 ID

StreamEvent 是部分消息的实时更新 —— 就像你看直播打字,一个字一个字蹦出来。需要在 ClaudeAgentOptions 里设置 include_partial_messages=True 才能收到。初学阶段先跳过。

消息类型速查表

类型 出现频率 说明 你需要关注吗?
AssistantMessage 每次都有 Claude 的回复,包含文字、工具调用等 必须关注
ResultMessage 每次最后一条 对话结束的"收据" 建议关注
UserMessage ClaudeSDKClient 模式 你发的消息或工具执行结果 看场景
SystemMessage 偶尔 系统级通知 通常忽略
StreamEvent 需手动开启 流式部分更新 高级用法

2.3 四种内容块

AssistantMessagecontent 字段是一个列表,里面装的就是 ContentBlock。一共有四种。

2.3.1 TextBlock —— 最常见,Claude 的文字回复

@dataclass
class TextBlock:
    text: str  # Claude 说的话

这是你最常遇到的内容块,没什么花头,就是一段文字:

if isinstance(block, TextBlock):
    print(block.text)

2.3.2 ThinkingBlock —— Claude 的"内心独白"

@dataclass
class ThinkingBlock:
    thinking: str   # 思考过程的文字
    signature: str  # 签名(用于验证)

有时候我们希望 Claude 在回答之前先"想一想",特别是处理复杂数学题、逻辑推理的时候。开启扩展思考(Extended Thinking)后,你就能看到 ThinkingBlock —— 这就像是 Claude 的内心独白,让你知道它是怎么一步步推理出答案的。

什么时候会出现? 只有显式开启扩展思考配置后才会出现,默认不会有。后面 2.5 节会详细讲怎么开启。

2.3.3 ToolUseBlock —— Claude 想用某个工具

@dataclass
class ToolUseBlock:
    id: str                    # 这次工具调用的唯一 ID
    name: str                  # 工具名称,如 "Bash"、"Read"、"Write"
    input: dict[str, Any]      # 工具参数

Claude 不只会说话,它还会"动手"。当 Claude 决定要执行命令、读文件、写文件时,它会发出一个 ToolUseBlock,相当于跟你说:"我想用这个工具,参数是这些。"

if isinstance(block, ToolUseBlock):
    print(f"Claude 想使用工具: {block.name}")
    print(f"参数: {block.input}")
    # 例如:
    # Claude 想使用工具: Bash
    # 参数: {"command": "echo 'hello world'"}

常见的内置工具名:

工具名 用途
Bash 执行终端命令
Read 读取文件内容
Write 写入文件
Edit 编辑文件(精确替换)
Glob 按模式搜索文件
Grep 在文件中搜索内容

2.3.4 ToolResultBlock —— 工具执行的结果

@dataclass
class ToolResultBlock:
    tool_use_id: str                                  # 对应哪次工具调用
    content: str | list[dict[str, Any]] | None        # 执行结果
    is_error: bool | None                             # 是否出错

工具执行完后,结果会以 ToolResultBlock 的形式出现。tool_use_idToolUseBlockid 对应,这样你就知道这是哪次调用的结果。

if isinstance(block, ToolResultBlock):
    print(f"工具调用 {block.tool_use_id} 的结果:")
    print(f"  内容: {block.content}")
    print(f"  是否出错: {block.is_error}")

内容块速查表

类型 比喻 出现条件
TextBlock Claude 说的话 几乎每次都有
ThinkingBlock Claude 的内心独白 需要开启扩展思考
ToolUseBlock "我想用这个工具" Claude 需要执行操作时
ToolResultBlock 工具执行的回执 工具执行完毕后

2.4 实战:写一个消息检查器

光看不练假把式。来写一个"消息检查器",让 Claude 执行一个会用到工具的任务,然后把每种消息和内容块的详细信息都打印出来。

import asyncio
from claude_agent_sdk import (
    query, ClaudeAgentOptions,
    AssistantMessage, UserMessage, SystemMessage, ResultMessage,
    TextBlock, ThinkingBlock, ToolUseBlock, ToolResultBlock,
)

async def main():
    options = ClaudeAgentOptions(
        permission_mode="bypassPermissions",  # 自动批准所有工具使用
        max_turns=2,                          # 允许 2 轮(调用工具 + 返回结果)
    )

    # 让 Claude 执行一个简单的命令,这样我们能看到工具调用相关的消息
    prompt = "请用 Bash 工具运行 echo 'Hello from Claude!',然后告诉我结果。"

    print("=" * 60)
    print("消息检查器 - 观察 Claude 返回的每一条消息")
    print("=" * 60)

    message_count = 0
    async for message in query(prompt=prompt, options=options):
        message_count += 1
        print(f"\n--- 消息 #{message_count} ---")
        print(f"类型: {type(message).__name__}")

        if isinstance(message, AssistantMessage):
            print(f"模型: {message.model}")
            if message.error:
                print(f"错误: {message.error}")
            print(f"内容块数量: {len(message.content)}")

            for i, block in enumerate(message.content):
                print(f"\n  [内容块 {i+1}] {type(block).__name__}")

                if isinstance(block, TextBlock):
                    # 文字太长就截断显示
                    text = block.text[:200] + "..." if len(block.text) > 200 else block.text
                    print(f"    文本: {text}")

                elif isinstance(block, ThinkingBlock):
                    thinking = block.thinking[:200] + "..." if len(block.thinking) > 200 else block.thinking
                    print(f"    思考过程: {thinking}")

                elif isinstance(block, ToolUseBlock):
                    print(f"    工具名: {block.name}")
                    print(f"    调用ID: {block.id}")
                    print(f"    参数: {block.input}")

                elif isinstance(block, ToolResultBlock):
                    print(f"    对应调用ID: {block.tool_use_id}")
                    print(f"    是否出错: {block.is_error}")
                    content = str(block.content)
                    content = content[:200] + "..." if len(content) > 200 else content
                    print(f"    结果: {content}")

        elif isinstance(message, UserMessage):
            content = str(message.content)
            content = content[:200] + "..." if len(content) > 200 else content
            print(f"内容: {content}")
            if message.parent_tool_use_id:
                print(f"关联工具调用ID: {message.parent_tool_use_id}")

        elif isinstance(message, SystemMessage):
            print(f"子类型: {message.subtype}")
            print(f"数据: {message.data}")

        elif isinstance(message, ResultMessage):
            print(f"是否出错: {message.is_error}")
            print(f"对话轮数: {message.num_turns}")
            print(f"总耗时: {message.duration_ms / 1000:.1f} 秒")
            print(f"API 耗时: {message.duration_api_ms / 1000:.1f} 秒")
            if message.total_cost_usd is not None:
                print(f"费用: ${message.total_cost_usd:.4f}")
            else:
                print("费用: 无(订阅用户)")
            print(f"会话ID: {message.session_id}")
            if message.result:
                result = message.result[:200] + "..." if len(message.result) > 200 else message.result
                print(f"结果: {result}")

    print(f"\n{'=' * 60}")
    print(f"总共收到 {message_count} 条消息")

asyncio.run(main())

完整代码见 examples/message_inspector.py

运行后你大概会看到这样的消息序列:

--- 消息 #1 ---
类型: AssistantMessage
模型: claude-sonnet-4-5-20250514
内容块数量: 2

  [内容块 1] TextBlock
    文本: 我来帮你运行这个命令。

  [内容块 2] ToolUseBlock
    工具名: Bash
    调用ID: toolu_01XYZ...
    参数: {'command': "echo 'Hello from Claude!'"}

--- 消息 #2 ---
类型: AssistantMessage
模型: claude-sonnet-4-5-20250514
内容块数量: 1

  [内容块 1] TextBlock
    文本: 命令已成功运行,输出结果是:Hello from Claude!

--- 消息 #3 ---
类型: ResultMessage
是否出错: False
对话轮数: 2
总耗时: 3.2 秒
...

注意观察消息序列的规律: 1. Claude 先说一句话(TextBlock)+ 发起工具调用(ToolUseBlock) 2. SDK 自动执行工具,把结果反馈给 Claude 3. Claude 根据工具结果给出最终回答(TextBlock) 4. 最后收到 ResultMessage,对话结束


2.5 扩展思考模式

有些问题需要 Claude "深思熟虑" —— 比如复杂的数学比较、逻辑推理、代码分析。这时候可以开启扩展思考,让 Claude 先在"脑子里"想清楚,再给你答案。

怎么开启?

有两种方式:

方式一:精确控制思考预算

options = ClaudeAgentOptions(
    thinking={"type": "enabled", "budget_tokens": 10000},  # 最多用 10000 个 token 来思考
    max_turns=1,
)

方式二:用 effort 参数(更简单)

options = ClaudeAgentOptions(
    effort="high",    # 可选 "low"、"medium"、"high"、"max"
    max_turns=1,
)

effort 更直观 —— 就像告诉 Claude "这道题你多想想"。thinking 则给你精确控制思考预算的能力。

看到 ThinkingBlock 时怎么处理?

import asyncio
from claude_agent_sdk import (
    query, ClaudeAgentOptions,
    AssistantMessage, TextBlock, ThinkingBlock,
)

async def main():
    options = ClaudeAgentOptions(
        thinking={"type": "enabled", "budget_tokens": 10000},
        max_turns=1,
    )

    async for message in query(prompt="9.11 和 9.8 哪个大?请仔细想想。", options=options):
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if isinstance(block, ThinkingBlock):
                    print("Claude 的思考过程:")
                    print("-" * 40)
                    print(block.thinking)
                    print("-" * 40)
                    print()
                elif isinstance(block, TextBlock):
                    print(f"Claude 的最终回答: {block.text}")

asyncio.run(main())

完整代码见 examples/thinking_example.py

运行后你会看到类似这样的输出:

Claude 的思考过程:
----------------------------------------
用户问的是 9.11 和 9.8 哪个大。
这是在比较两个小数。
9.11 = 9 + 0.11
9.8 = 9 + 0.8
0.8 > 0.11
所以 9.8 > 9.11
----------------------------------------

Claude 的最终回答: 9.8 比 9.11 大。虽然 11 比 8 大,但在小数中...

小贴士:很多人(包括某些 AI)会被 "9.11 vs 9.8" 这个问题绊倒,因为直觉上 11 > 8。但开启扩展思考后,Claude 会像做数学题一样一步步推理,大大降低出错概率。


2.6 小结

这一章我们彻底搞清楚了 Claude 消息系统的结构:

掌握了消息系统,你就能准确地从 Claude 的回复中提取出你想要的信息。下一章我们来聊聊 ClaudeAgentOptions —— 也就是怎么"指挥"Claude 按你的意思来工作。