Claude Code Agent Loop 研究系列(06)—— Streaming:从 SSE 事件到逐字显示
内容提要
本文介绍Claude Code如何通过SSE流式接收Anthropic API响应,实现文字逐字显示。一次调用包含6种事件(message_start、content_block_start/delta/stop、message_delta、message_stop),客户端用contentBlocks数组累积增量,text用追加、tool_use的JSON分片边接边解析。需处理SDK重复文本和消息mutate而非replace的坑。UI用34行手写store渲染,QueryGuard三状态防并发。
延伸解读
SSE 流式响应的核心机制
Anthropic API 通过 SSE 返回流式响应,一次调用包含 6 种事件:message_start、content_block_start、content_block_delta、content_block_stop、message_delta、message_stop。客户端按事件顺序维护 contentBlocks 数组,text 块用追加方式累积,tool_use 块则拼接 partial_json 字符串。理解这些事件类型是正确处理流式数据的基础。
增量解析与性能优化
tool_use 的 JSON 参数是分片发送的,Claude Code 采用边接边解析的策略,不等 content_block_stop 就尝试解析部分 JSON,从而支持 StreamingToolExecutor 在 LLM 还在输出时就开始执行工具。这要求 JSON parser 能容忍不完整输入,是一种典型的性能优化手段,但增加了实现复杂度。
SDK 的隐藏陷阱
Anthropic SDK 有两个反直觉行为:content_block_start 事件中 text 块会携带初始文本,且下一个 delta 会重复发送该文本,若直接追加会导致重复;另外,消息对象在追加到数组后仍需接收后续更新,Claude Code 选择直接 mutate 原对象而非 replace,以保证所有引用(如落盘队列)都能看到最新状态。这些细节是集成 SSE 时常见的坑。
UI 层与并发控制设计
UI 层使用一个 34 行手写 store 来消费流事件,通过订阅字段变化实现逐字渲染,避免引入重型状态管理库。同时,QueryGuard 用 idle、dispatching、running 三状态区分不同阶段的用户输入,支持 Ctrl-C 的不同语义,并防止并发查询,体现了 CLI 场景下简洁实用的设计思路。
Q&A
Claude Code 如何实现文字逐字显示?
Claude Code 通过 Anthropic API 的 SSE 流式接口接收分片数据,客户端用 contentBlocks 数组累积增量,UI 层订阅一个手写 store 中的当前流式消息字段,每当有新 delta 就更新并触发重新渲染,从而实现文字逐字显示。
Anthropic API 一次流式响应包含哪些事件?
一次响应包含 6 种事件:message_start(响应开始,含元信息)、content_block_start(内容块开始)、content_block_delta(增量内容)、content_block_stop(内容块结束)、message_delta(消息级元数据更新,如 stop_reason 和 usage)、message_stop(响应结束)。
Claude Code 如何处理 tool_use 的 JSON 分片?
tool_use 的 input 参数以 JSON 字符串分片通过 content_block_delta 的 partial_json 字段发送。Claude Code 采用边接边解析的策略,使用一个容忍不完整 JSON 的解析器,在流式输出过程中就能推断关键参数并启动工具执行,而不必等待 content_block_stop。
Anthropic SDK 有哪些已知的坑?
两个主要坑:1) content_block_start 事件中 text 类型块会带一段初始文本,但下一个 delta 会重复发送这段文本,如果直接拼接会导致重复,Claude Code 选择忽略 start 中的 text 字段,只用 delta 累积;2) 消息对象需要直接 mutate 而不是 replace,因为消息被多处引用(如落盘队列),replace 会导致引用不一致,mutate 能保证所有引用看到最新状态。
Claude Code 的 UI 层如何实现流式渲染?
UI 层使用一个 34 行的手写 store(提供 setState/getState/subscribe),每当有新的 delta 就更新 store 中当前流式消息的字段,UI 组件订阅该字段,有变化就重新渲染,从而实现逐字显示。
QueryGuard 的三状态是什么?为什么需要三个状态?
QueryGuard 的三状态是 idle(空闲)、dispatching(发送中)、running(流式返回中)。需要三个状态是因为 dispatching 和 running 对 Ctrl-C 的处理不同:dispatching 时 Ctrl-C 取消发送,running 时 Ctrl-C 打断当前 loop,boolean 无法表达这种区别。