
June 4, 2026 · 9:11 AM
Claude Code SDK #11:流式输出全解——一个参数打开 Agent 运行时的实时可观测性
默认模式下 Agent 运行像个黑盒,结果出来前什么都看不到。设置 include_partial_messages=True,SDK 开始向 async generator 插入 StreamEvent,文本逐字输出、工具调用实时显示状态。本篇完整拆解三层嵌套类型检查(StreamEvent→content_block_delta→text_delta)的设计逻辑、工具调用流的追踪方式、流式 UI 的构建模式,以及结构化输出在流式模式下的已知限制,附五条可落地的实践建议。
用 Claude Code SDK 跑一个 Agent 任务,默认体验是这样的:你发出请求,等待,等待,然后收到完整结果。对简单任务还好,但当 Agent 需要多轮工具调用、逐步推理时,这个「等待黑盒」体验会让开发者和用户都很难受——你不知道它在做什么,也不知道还要等多久。
**流式输出(Streaming Output)**是解法。一个参数
include_partial_messages=True,把 SDK 从「结果型 API」变成「进程可观测的运行时」——文本逐字输出、工具调用实时显示状态、Agent 每一步行为都变得可感知。本篇完整拆解这套机制的工作原理、三层嵌套类型检查的设计逻辑,以及如何用它构建流式 UI。开启流式输出只需一行
Python 中设置
include_partial_messages=True,TypeScript 中设置 includePartialMessages: true,就能开启流式模式。开启后,SDK 在原来返回
AssistantMessage / ResultMessage 的基础上,额外往 async generator 里插入 StreamEvent 消息——这些是从 Claude API 实时接收的原始流式事件,未经积累,逐个到达。from claude_agent_sdk import query, ClaudeAgentOptions
from claude_agent_sdk.types import StreamEvent
import asyncio
async def stream_response():
options = ClaudeAgentOptions(
include_partial_messages=True,
allowed_tools=["Bash", "Read"],
)
async for message in query(prompt="列出项目里的文件", options=options):
if isinstance(message, StreamEvent):
event = message.event
if event.get("type") == "content_block_delta":
delta = event.get("delta", {})
if delta.get("type") == "text_delta":
print(delta.get("text", ""), end="", flush=True)
asyncio.run(stream_response())Loading content card…
StreamEvent 的结构
Python 中的
StreamEvent 是一个 dataclass:@dataclass
class StreamEvent:
uuid: str # 事件唯一 ID
session_id: str # 会话 ID
event: dict[str, Any] # 原始 Claude API 流式事件
parent_tool_use_id: str | None # 来自子 Agent 时的父工具 IDTypeScript 里对应的类型名为
SDKPartialAssistantMessage,type 字段值为 'stream_event'。event 字段是原始的 Claude API 流式事件,不是 SDK 封装后的对象。这意味着两件事:一是你获得了最大灵活度,可以直接操作底层事件;二是 SDK 不会帮你积累文本——你需要自己把 text_delta 的碎片拼起来。event.type 有六种:1| 事件类型 | 含义 |
|---|---|
message_start | 新消息开始 |
content_block_start | 新内容块开始(文本块或工具调用块) |
content_block_delta | 内容块的增量更新 |
content_block_stop | 内容块结束 |
message_delta | 消息级更新(stop reason、usage 统计) |
message_stop | 消息结束 |
三层嵌套检查的逻辑
为什么一定要三层嵌套?这是因为 Claude API 的内容块有两种类型:文本块和工具调用块,而 delta 同样对应两种类型:
text_delta 和 input_json_delta。如果只检查到
content_block_delta 就直接读 delta.text,当这个 delta 实际上是工具调用的 JSON 输入片段时,你拿到的是 None 或错误数据。正确的读取路径:
- 先检查
message.type是StreamEvent(区分于AssistantMessage/ResultMessage) - 再检查
event["type"] == "content_block_delta"(过滤掉 start/stop/message 类事件) - 最后检查
delta["type"] == "text_delta"(区分文本片段与工具输入片段)
这套检查在处理工具调用流时会用到不同的分支——
input_json_delta 走的是另一条路。Loading stats card…
消息流的完整顺序
开启流式后,一次完整的 Agent 运行(假设 Claude 先生成文本、再调用工具、再回应)大概是这样的顺序:1
StreamEvent (message_start)
StreamEvent (content_block_start) ← 文本块开始
StreamEvent (content_block_delta) ← 文字逐字到达...
StreamEvent (content_block_stop)
StreamEvent (content_block_start) ← 工具调用块开始
StreamEvent (content_block_delta) ← 工具 JSON 输入逐步到达...
StreamEvent (content_block_stop)
StreamEvent (message_delta)
StreamEvent (message_stop)
AssistantMessage ← 完整消息
... 工具执行 ...
... 下一轮流式事件 ...
ResultMessage ← 最终结果AssistantMessage 和 ResultMessage 依然存在,流式事件只是「插在前面」。如果你的代码同时处理多种消息类型,只需要在 isinstance 分支里分别处理即可。Loading stats card…
流式工具调用追踪
工具调用在流式模式下有明确的生命周期,用三个事件标记:
content_block_start(工具开始调用)、input_json_delta(工具输入 JSON 逐步到达)、content_block_stop(工具调用完成)。async def stream_tool_calls():
options = ClaudeAgentOptions(
include_partial_messages=True,
allowed_tools=["Read", "Bash"],
)
current_tool = None
tool_input = ""
async for message in query(prompt="读取 README.md 文件", options=options):
if isinstance(message, StreamEvent):
event = message.event
event_type = event.get("type")
if event_type == "content_block_start":
content_block = event.get("content_block", {})
if content_block.get("type") == "tool_use":
current_tool = content_block.get("name")
tool_input = ""
print(f"开始调用工具: {current_tool}")
elif event_type == "content_block_delta":
delta = event.get("delta", {})
if delta.get("type") == "input_json_delta":
chunk = delta.get("partial_json", "")
tool_input += chunk
elif event_type == "content_block_stop":
if current_tool:
print(f"工具 {current_tool} 调用完成,参数: {tool_input}")
current_tool = None构建流式 UI
文本流和工具流结合起来,可以构建出「Agent 在做什么」的实时反馈界面。关键技巧是用一个
in_tool 标志区分「当前是否在执行工具」:工具执行期间暂停文本流,显示进度提示;工具完成后恢复文本流。async def streaming_ui():
options = ClaudeAgentOptions(
include_partial_messages=True,
allowed_tools=["Read", "Bash", "Grep"],
)
in_tool = False
async for message in query(
prompt="找出代码库里所有的 TODO 注释", options=options
):
if isinstance(message, StreamEvent):
event = message.event
event_type = event.get("type")
if event_type == "content_block_start":
content_block = event.get("content_block", {})
if content_block.get("type") == "tool_use":
tool_name = content_block.get("name")
print(f"\n[正在使用 {tool_name}...]", end="", flush=True)
in_tool = True
elif event_type == "content_block_delta":
delta = event.get("delta", {})
if delta.get("type") == "text_delta" and not in_tool:
# 只在工具未运行时输出文本
import sys
sys.stdout.write(delta.get("text", ""))
sys.stdout.flush()
elif event_type == "content_block_stop":
if in_tool:
print(" 完成", flush=True)
in_tool = False
elif isinstance(message, ResultMessage):
print("\n\n--- 全部完成 ---")这个模式在多步 Agent 任务里特别有用:用户能看到 Claude 在思考什么、用了哪些工具、什么时候完成——而不是对着一个转圈圈图标干等。
已知限制:结构化输出不流式
使用
outputFormat / output_format 开启结构化输出时,JSON 结果不会作为流式 delta 到达,而是等到任务完全结束,才出现在 ResultMessage.structured_output 里。1这意味着流式输出和结构化输出在「最终数据」上是互斥的:流式模式下,你能实时看到 Claude 的推理过程和工具调用,但结构化的最终结果仍然要等
ResultMessage。如果你的业务需要既展示进度又拿结构化输出,把两个逻辑分开处理即可——流式事件用于 UI 状态更新,ResultMessage 用于数据入库。五条实践建议
1. 不要用
break 提前退出 async for 循环。这是 SDK 文档和频道之前提到的一个坑:提前 break 可能触发 asyncio 的清理问题,导致连接状态异常。想在某条消息到达后停止处理,用标志位控制后续逻辑,而不是退出循环。2. 文本积累放在应用层,不放在事件处理层。SDK 不积累 delta,每个
text_delta 只包含当前片段。如果你需要完整文本(比如做 token 统计或日志),自己维护一个 full_text += delta["text"] 变量,而不是每次从头拼接。3. 工具状态和文本状态分开管理。如上面的
streaming_ui 示例,in_tool 标志是必要的——混在一起处理容易出现「工具输入 JSON 片段被当成用户可见文本输出」的问题。4. TypeScript 里类型守卫写法不同。Python 用
isinstance(message, StreamEvent) 检查,TypeScript 需要检查 message.type === 'stream_event',拿到事件后再用 message.event 读底层数据,语义一致但语法不同。5.
parent_tool_use_id 是子 Agent 追踪的锚点。当你的 Agent 编排里有子 Agent 时,子 Agent 产生的 StreamEvent 会带上 parent_tool_use_id,指向父 Agent 触发这个子 Agent 的工具调用 ID。多层编排下可以据此构建树状执行视图,区分哪些流式事件来自主 Agent、哪些来自子 Agent。下期 #12 预计拆解权限与安全系统——
permission_mode 的五档设置、allowed_tools / disallowed_tools 的精确控制,以及 Hooks 与权限评估的组合用法。References
More from this channel
- Claude Code SDK #15:Settings 配置体系全解——四层作用域 × 优先级链 × 热重载,精确控制 Agent 行为
- Claude Code SDK #14:IDE 集成全解——`--ide` 标志、内置 MCP 服务器、诊断信息流与多目录权限
- Claude Code SDK #13:自动化脚本全解——`-p` 开关背后的完整标志体系,把 Claude Code 嵌进任意流水线
- Claude Code SDK #12:权限与安全全解——五步评估链 × 五种模式 × Allow/Deny 规则,精准控制 Agent 的工具访问边界
- Claude Code SDK #10:结构化输出全解——JSON Schema × Zod/Pydantic,让 Agent 直接返回你要的数据结构
- Claude Code SDK #9:自定义工具全解——@tool 装饰器 × in-process MCP × 错误处理 × 非文本返回,把任意函数变成 Claude 的能力
- Claude Code SDK #8:MCP 集成全解——三种接入方式 × 工具命名规范 × Tool Search 懒加载,把 Agent 能力边界推到任意外部服务
- Claude Code SDK #7:子 Agent 编排全解——上下文隔离 × 工具沙盒 × 并行加速,让主 Agent 不再被子任务撑爆
Related content
- Sign in to comment.