Claude的messages接口在开启流式输出后,会通过Server-Sent Events(SSE)协议持续返回增量数据。如果同时开启了extended thinking(扩展思考)功能,返回的事件流中还会夹杂thinking类型的增量,很多开发者在解析时容易把思考内容和正式回答混在一起,导致最终拼出来的文本里混杂了模型的内部推理过程。这篇文章系统地梳理Claude API流式事件的结构,重点讲清stream事件与thinking事件的处理方式,并给出可直接运行的代码示例。

一、流式响应的事件类型全解析
当请求参数中设置stream: true后,Claude API不再一次性返回完整响应,而是通过HTTP长连接分批推送事件。每个事件都有明确的类型标识,理解这些类型是正确解析的前提。整个事件流的骨架大致如下:
import httpx
import json
# 发起流式请求
payload = {
"model": "claude-sonnet-4-5",
"max_tokens": 4096,
"stream": True,
"thinking": {
"type": "enabled",
"budget_tokens": 10000
},
"messages": [
{"role": "user", "content": "解释一下快速排序的时间复杂度分析"}
]
}
with httpx.Client(timeout=120) as client:
with client.stream(
"POST",
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": "your-api-key",
"anthropic-version": "2023-06-01",
"content-type": "application/json"
},
json=payload
) as response:
for line in response.iter_lines():
if line.startswith("data:"):
data = json.loads(line[5:])
print(data["type"])
运行这段代码后,你会看到一系列事件类型依次出现。message_start是流的开端,携带完整的message对象骨架;随后是若干个content_block_start,每开启一个新的内容块就触发一次;接着是大量的content_block_delta,这是真正承载增量文本的事件;每个内容块结束时触发content_block_stop;最后由message_delta携带结束原因和用量统计,message_stop宣告整个流结束。
需要注意一个容易踩的坑:开启思考模式后,内容块通常至少有两个,第一个是thinking类型的块,第二个才是text类型的块。如果你只按块的索引顺序拼接文本,或者只判断delta类型而忽略了块类型,就会把思考过程误当成正式回答。正确的做法是在content_block_start时记录当前块的类型,再结合content_block_delta中的delta子类型进行分流。
二、thinking事件的区分与解析策略
思考模式下的增量事件有两种不同的delta子类型。thinking_delta携带的是模型的内部思考过程,其数据结构形如{"type":"thinking_delta","thinking":"..."};而text_delta携带的是正式回答,结构为{"type":"text_delta","text":"..."}。两者都出现在content_block_delta事件中,但内部的type字段不同,这是区分二者的关键。
此外,思考块开始时还可能出现signature_delta,它携带加密签名字段。这个签名用于验证思考内容的完整性,在后续请求中如果要回传思考块(多轮对话场景),必须原样带上这个签名,否则API会报错。很多开发者第一次遇到多轮思考对话时的invalid request error,根源就是丢掉了签名。
thinking_text = ""
answer_text = ""
current_block_type = None
signature = ""
for line in response.iter_lines():
if not line.startswith("data:"):
continue
event = json.loads(line[5:])
if event["type"] == "content_block_start":
block = event["content_block"]
current_block_type = block["type"] # thinking 或 text
elif event["type"] == "content_block_delta":
delta = event["delta"]
if delta["type"] == "thinking_delta":
thinking_text += delta["thinking"] # 思考过程,按需展示或隐藏
elif delta["type"] == "signature_delta":
signature += delta["signature"] # 保存签名,多轮对话回传时要用
elif delta["type"] == "text_delta":
answer_text += delta["text"] # 正式回答,直接拼接即可
print(delta["text"], end="", flush=True) # 实时输出给用户
elif event["type"] == "message_delta":
stop_reason = event["delta"].get("stop_reason")
usage = event.get("usage", {})
实际产品中,思考内容要不要展示给用户需要仔细权衡。展示思考过程能提升透明度,让用户理解模型是如何一步步推理的,特别适合数学、编程类任务;但思考过程往往很长且冗余,直接展示会干扰阅读体验。比较常见的折中方案是把思考内容折叠在一个可展开的区域里,默认只显示最终回答。无论是否展示,都建议把thinking文本单独存储,便于后续调试和问题排查。
三、多轮对话中回传思考块的注意事项
Claude在开启思考模式的流式请求中,遇到工具调用或人工接力(human in the loop)场景时,支持把上一个回合的thinking块原样回传,让模型从中断处继续推理。回传时必须满足两个条件:一是thinking块要包含签名字段,二是thinking块必须排在同一条assistant消息中所有其他内容块之前。换句话说,assistant消息的内容数组里,索引0的位置必须是thinking块,后面才能跟text块或tool_use块。
如果不打算让模型继续之前的思考,也可以直接省略thinking块,只回传text部分,这完全合法。唯一不允许的操作是修改thinking内容后回传,签名校验会直接失败。理解这一点后,多轮工具调用的流式处理流程就清晰了:收到tool_use块,执行工具,把工具结果作为user消息回传,同时把带签名的thinking块和tool_use块一并放回assistant消息中。
四、参数限制与用量统计
开启extended thinking后,请求参数有几条硬性限制需要记住。temperature必须设为1或直接省略,设成其他值会返回400错误;top_p和top_k同样不支持自定义;budget_tokens必须大于1023,且max_tokens必须大于thinking的预算值。这些限制的设计原因是思考过程需要较高的采样随机性来保证推理路径的多样性。
用量统计方面,流式模式下token计数分散在不同事件里。message_start中的usage包含input_tokens的初始值,而message_delta中的usage则包含output_tokens以及思考部分消耗的token。统计总消耗时,要把两处数据合并计算,尤其别忘了thinking部分也计入output_tokens,做成本核算时不能遗漏。下面是一个完整的统计示例:
total_input = 0
total_output = 0
if event["type"] == "message_start":
u = event["message"]["usage"]
total_input = u.get("input_tokens", 0)
# 开启了缓存或工具时,注意还有 cache_creation_input_tokens 等字段
elif event["type"] == "message_delta":
u = event.get("usage", {})
total_output = u.get("output_tokens", 0)
print(f"输入token: {total_input}, 输出token(含思考): {total_output}")
最后提醒一点,思考模式的流式响应首字延迟会明显变长,因为模型需要先完成一段思考才会输出正式回答。如果是面向C端的产品,建议在前端加一个明确的思考中状态提示,避免用户长时间等待白屏而误以为服务卡死。合理设置budget_tokens也能控制思考时长,简单问题给小预算,复杂推理任务再放大预算,在体验和成本之间取得平衡。
Claude API流式推理SSE事件解析修改时间:2026-09-02 09:18:36