`AnthropicRuntimeClient::consume_stream` 详解
AnthropicRuntimeClient::consume_stream 详解
函数签名
async fn consume_stream(
&self,
message_request: &MessageRequest,
apply_stall_timeout: bool, // 是否对首事件应用 stall 超时(post-tool 续接场景)
) -> Result<Vec<AssistantEvent>, RuntimeError>
负责消费一次流式 API 响应,将底层 SSE 事件流处理为 Vec<AssistantEvent> 供上层 ConversationRuntime 使用。
整体流程
consume_stream()
│
├── Phase 1: 初始化
│ ├── 调用 client.stream_message() 发起 HTTP POST,拿到 MessageStream
│ ├── 选择输出目标(stdout / sink)
│ └── 初始化渲染器、缓冲区、状态变量
│
├── Phase 2: 事件循环 (loop)
│ ├── 读取下一个 StreamEvent(可选 stall 超时)
│ ├── 按 8 种事件类型分发处理
│ ├── 渲染到终端 + 累积到 events Vec
│ └── 流结束(next_event 返回 None)→ break
│
└── Phase 3: 后处理
├── 提取 prompt cache 记录
├── 补全缺失的 MessageStop
├── 判断是否有 MessageStop
├── 有 → 返回 events
└── 无 → 降级为非流式请求兜底
Phase 1 — 初始化 (main.rs:9498-9520)
let mut stream = self.client.stream_message(message_request).await?;
调用 ProviderClient::stream_message,经由 ApiProviderClient 路由到具体 provider:
- Anthropic →
AnthropicClient::stream_message→ POST/v1/messages→ 拿到MessageStream - xAI/OpenAI →
OpenAiCompatClient::stream_message→ POST/chat/completions→ 拿到MessageStream
返回的 MessageStream 只等到了 HTTP 响应头(200 OK),body 尚未读取。
状态变量
| 变量 | 类型 | 用途 |
|---|---|---|
stream |
MessageStream |
流式事件的迭代器 |
out |
&mut dyn Write |
输出目标 — stdout(emit_output=true)或 io::sink()(静默模式) |
renderer |
TerminalRenderer |
ANSI 终端渲染器,将 markdown 转为带颜色的终端文本 |
markdown_stream |
MarkdownStreamState |
增量 markdown 渲染状态(处理跨 chunk 的 markdown 边界) |
events |
Vec<AssistantEvent> |
累积的运行时事件,最终返回给 build_assistant_message |
pending_tool |
Option<(id, name, input)> |
正在累积的 ToolUse(input 通过 InputJsonDelta 逐步拼接) |
pending_thinking |
Option<(thinking, signature)> |
正在累积的 Thinking 块(DeepSeek V4 特殊处理) |
block_has_thinking_summary |
bool |
当前 content block 是否已打印 "Thinking hidden" 摘要 |
saw_stop |
bool |
是否收到过 MessageStop 事件 |
received_any_event |
bool |
是否收到过任何事件(用于 stall 超时判断) |
Phase 2 — 事件主循环 (main.rs:9522-9650)
2a. 读取事件(带可选的 stall 超时)
let next = if apply_stall_timeout && !received_any_event {
match tokio::time::timeout(POST_TOOL_STALL_TIMEOUT, stream.next_event()).await {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 10秒(常量定义在第175行)
Ok(inner) => inner?,
Err(_elapsed) => return Err("post-tool stall: ..."),
}
} else {
stream.next_event().await?
};
stall 超时机制:在 tool 调用后的续接请求(post-tool)场景,模型有时会卡住不响应。apply_stall_timeout 为 true 时,首事件等待 10 秒(POST_TOOL_STALL_TIMEOUT),超时则报错。AnthropicRuntimeClient::stream 捕获此错误后会重试一次。
正常模式或已收到过事件则直接调用 stream.next_event(),挂起等待下一个 chunk 到达。
2b. 8 种事件类型的分发处理
收到 Some(event) 后,按 StreamEvent 类型匹配:
MessageStart (main.rs:9546)
ApiStreamEvent::MessageStart(start) => {
for block in start.message.content {
push_output_block(block, out, &mut events, &mut pending_tool, true,
&mut block_has_thinking_summary)?;
}
}
消息开始时,MessageStart 的 content 字段可能已经携带了初始的 content block(如初始 Text 块、Thinking 块头部)。遍历这些初始 block,交给 push_output_block 处理。
ContentBlockStart (main.rs:9558)
ApiStreamEvent::ContentBlockStart(start) => {
if let OutputContentBlock::Thinking { thinking, signature } = &start.content_block {
pending_thinking = Some((thinking.clone(), signature.clone()));
}
push_output_block(start.content_block, out, &mut events, &mut pending_tool, true,
&mut block_has_thinking_summary)?;
}
新的 content block 开始:
- Thinking 块特殊处理:初始化
pending_thinking,用于累积后续ThinkingDelta/SignatureDelta事件的内容。这是为了兼容 DeepSeek V4 等模型的reasoning_content协议差异。 - ToolUse 块:
push_output_block创建pending_tool = Some((id, name, initial_input))。流式模式下初始 input 为空对象{},被替换为空字符串,后续通过InputJsonDelta累积。
ContentBlockDelta (main.rs:9576)
ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta {
ContentBlockDelta::TextDelta { text } => {
// 每个文本片段:实时渲染 markdown + 记录事件
if !text.is_empty() {
// 通知进度
if let Some(progress_reporter) = &self.progress_reporter {
progress_reporter.mark_text_phase(&text);
}
// markdown 增量渲染
if let Some(rendered) = markdown_stream.push(&renderer, &text) {
write!(out, "{rendered}")?;
}
events.push(AssistantEvent::TextDelta(text));
}
}
ContentBlockDelta::InputJsonDelta { partial_json } => {
// 累积 tool input JSON 片段
if let Some((_, _, input)) = &mut pending_tool {
input.push_str(&partial_json);
}
}
ContentBlockDelta::ThinkingDelta { thinking } => {
// 首次 ThinkingDelta:打印 "▶ Thinking hidden" 摘要
if !block_has_thinking_summary {
render_thinking_block_summary(out, None, false)?;
block_has_thinking_summary = true;
}
// 累积 thinking 文本到 pending_thinking → 最终以 AssistantEvent::Thinking 写入 session
if let Some((t, _)) = &mut pending_thinking {
t.push_str(&thinking);
}
}
ContentBlockDelta::SignatureDelta { signature } => {
// 累积 signature 到 pending_thinking
if let Some((_, sig)) = &mut pending_thinking {
sig.get_or_insert_with(String::new).push_str(&signature);
}
}
}
4 种 delta 子类型:
| Delta 类型 | 来源 | 处理 |
|---|---|---|
TextDelta |
普通文本生成(所有 provider) | 进度报告 + markdown 增量渲染 + 写入 events |
InputJsonDelta |
ToolUse 的输入参数 JSON | 逐步拼接 JSON 字符串到 pending_tool.input |
ThinkingDelta |
Thinking 块的内容(Anthropic extended thinking / DeepSeek reasoning) | 打印"Thinking hidden",累积到 pending_thinking |
SignatureDelta |
Thinking 块的签名 | 累积到 pending_thinking.signature |
ContentBlockStop (main.rs:9612)
ApiStreamEvent::ContentBlockStop(_) => {
// 刷新 markdown 流
if let Some(rendered) = markdown_stream.flush(&renderer) {
write!(out, "{rendered}")?;
}
// 把累积的 thinking 转成 AssistantEvent::Thinking
if let Some((thinking, signature)) = pending_thinking.take() {
events.push(AssistantEvent::Thinking { thinking, signature });
}
// 把累积的 tool call 转成 AssistantEvent::ToolUse
if let Some((id, name, input)) = pending_tool.take() {
// 通知进度
if let Some(progress_reporter) = &self.progress_reporter {
progress_reporter.mark_tool_phase(&name, &input);
}
// 在终端上显示 tool call
writeln!(out, "\n{}", format_tool_call_start(&name, &input))?;
events.push(AssistantEvent::ToolUse { id, name, input });
}
}
ContentBlock 结束时,将之前累积的数据输出:
- 刷新 markdown 渲染器确保所有缓存的样式生效
- Thinking 块:
pending_thinking.take()→ 生成AssistantEvent::Thinking - ToolUse 块:
pending_tool.take()→ 在终端打印工具调用摘要(如📄 Reading file…/✏️ Writing file (42 lines))→ 生成AssistantEvent::ToolUse
MessageDelta (main.rs:9637)
ApiStreamEvent::MessageDelta(delta) => {
events.push(AssistantEvent::Usage(delta.usage.token_usage()));
}
消息结尾的用量信息(input_tokens, output_tokens 等),转换为 AssistantEvent::Usage 供 build_assistant_message 记录到 session。
MessageStop (main.rs:9640)
ApiStreamEvent::MessageStop(_) => {
saw_stop = true;
// 刷新 markdown 流
if let Some(rendered) = markdown_stream.flush(&renderer) {
write!(out, "{rendered}")?;
}
events.push(AssistantEvent::MessageStop);
}
流式响应正常结束。标记 saw_stop = true,刷新 markdown 后写入终止事件。
2c. 流结束
let Some(event) = next else { break; };
MessageStream::next_event() 返回 None → TCP 连接关闭,所有事件已读完 → 退出循环。
Phase 3 — 后处理 (main.rs:9652-9683)
3a. 提取 prompt cache 记录
push_prompt_cache_record(&self.client, &mut events);
从 ApiProviderClient 中取出缓存的 prompt cache 记录,转换为 AssistantEvent::PromptCache 事件。仅 Anthropic 原生 API 支持(xAI/OpenAI 无 prompt cache,返回 None)。
3b. 补全缺失的 MessageStop
if !saw_stop
&& events.iter().any(|event| {
matches!(event, AssistantEvent::TextDelta(text) if !text.is_empty())
|| matches!(event, AssistantEvent::ToolUse { .. })
})
{
events.push(AssistantEvent::MessageStop);
}
如果流中没有正常的 MessageStop,但确实收到了有效内容(有文本或 tool call),手动补一个 MessageStop。这处理了某些 provider 在非正常关闭连接但内容已完整的情况。
3c. 判断是否有 MessageStop → 正常返回
if events.iter().any(|event| matches!(event, AssistantEvent::MessageStop)) {
return Ok(events);
}
只要有 MessageStop(无论来自服务端还是补的),返回事件列表。
3d. 降级:非流式兜底
let response = self
.client
.send_message(&MessageRequest { stream: false, ..message_request.clone() })
.await?;
let mut events = response_to_events(response, out)?;
push_prompt_cache_record(&self.client, &mut events);
Ok(events)
如果没有 MessageStop 也没有有效内容(流在收到任何有用数据前断开了),降级为非流式请求重新发送:
- 设置
stream: false response_to_events将完整的MessageResponse一步解析为事件列表
辅助函数
push_output_block (main.rs:10507)
处理单个 OutputContentBlock:
| Block 类型 | 处理 |
|---|---|
Text { text } |
渲染为 ANSI 文本 + 记录 TextDelta 事件 |
ToolUse { id, name, input } |
存入 pending_tool(流式模式下清空初始空 JSON) |
Thinking { thinking } |
打印 "▶ Thinking (N chars hidden)" 摘要 |
RedactedThinking { .. } |
打印 "▶ Thinking block hidden by provider" |
response_to_events (main.rs:10551)
将完整的非流式 MessageResponse 转换为 Vec<AssistantEvent>:
遍历 content 中的每个 OutputContentBlock
→ push_output_block(非流式模式,streaming_tool_input=false)处理 ToolUse 时从 pending 立即弹出
→ 确认有 Usage 和 MessageStop
format_tool_call_start (main.rs:10057)
格式化 tool call 的终端显示。按工具类型定制展示:
| 工具名 | 显示格式 |
|---|---|
bash / Bash |
格式化的 shell 命令 |
Read |
📄 Reading {path}… |
Write |
✏️ Writing {path} ({lines} lines) |
Edit |
带 old/new 差异的格式化输出 |
| 其他 | 工具名 + JSON 输入 |
render_thinking_block_summary (main.rs:10490)
在终端打印 Thinking 块的摘要信息:
// 非首次 → 不打印额外信息
// 有字符数 → "▶ Thinking (N chars hidden)"
// redacted → "▶ Thinking block hidden by provider"
// 无信息 → "▶ Thinking hidden"
push_prompt_cache_record (main.rs:10578)
从 provider client 提取最后一次 prompt cache 记录,转换为 PromptCacheEvent。仅 Anthropic 有效。
关键设计要点
1. 双重事件转换
网络字节 → SseParser → StreamEvent → consume_stream → AssistantEvent
两次转换的原因:
StreamEvent是apicrate 的底层 SSE 协议类型(贴近 Anthropic/OpenAI 原始 schema)AssistantEvent是runtimecrate 的上层运行时类型(经过规范化,屏蔽 provider 差异)
2. 实时渲染与事件记录的分离
同一个事件同时处理两件事:
- 渲染到终端:markdown 流式渲染 + ANSI 颜色(用户看得到)
- 记录到 events:纯数据累积(给
build_assistant_message用)
3. Thinking 块的延迟组装
Anthropic SSE 协议中,Thinking 块以 ContentBlockStart(type: thinking) 开始,主体由多个 ThinkingDelta 事件组成,ContentBlockStop 结束。consume_stream 用 pending_thinking 暂存 Thinking 内容,在 ContentBlockStop 才统一释放为 AssistantEvent::Thinking。
4. ToolUse 输入的逐步拼接
ToolUse 的 JSON 参数通过 InputJsonDelta 事件分片到达。pending_tool 暂存 (id, name, input_string),每个 delta 往 input 追加片段,到 ContentBlockStop 时 JSON 才完整。
5. Stalled continuation 检测
设 apply_stall_timeout=true 时用 10 秒超时等待首事件。AnthropicRuntimeClient::stream 捕获 stall 错误后重试一次(最多 2 次),解决模型在 tool 调用后偶尔卡住不生成续接内容的问题。
6. 三阶段降级
正常流式完成 → Ok(events)
│
▼ 有内容但没 MessageStop
手动补 MessageStop → Ok(events)
│
▼ 什么都没收到
降级为非流式重新请求 → Ok(fallback_events)
流程图
client.stream_message(request)
│
▼
MessageStream (HTTP body 尚未读取)
│
▼ 循环
┌───────────────────────────────────────────────────┐
│ next_event() │
│ │ │
│ ├─ None → break │
│ │ │
│ └─ Some(event) ─── match event type ──────┐ │
│ ┌──────────┬─────────────┐ │ │
│ │ 事件类型 │ 处理 │ │ │
│ ├──────────┼─────────────┤ │ │
│ │MessageSt.│ push_output │ │ │
│ │ContentBl.│ Thinking/Tool │ │ │
│ │BlockStart│ 初始化累积 │ │ │
│ │TextDelta │ 渲染+记录 │ │ │
│ │InputJson │ 拼接 tool │ │ │
│ │Thinking │ 累积 thinking │ │ │
│ │BlockStop │ 释放累积 │ │ │
│ │MessageD. │ 记录用量 │ │ │
│ │MessageSt.│ saw_stop=true │ │ │
│ └──────────┴─────────────┘ │ │
└───────────────────────────────────────────────────┘
│
▼ break
后处理
│
├─ push_prompt_cache_record
├─ 补全 MessageStop(如有内容但无 stop)
├─ 有 MessageStop → return Ok(events)
└─ 无 MessageStop → 降级非流式
→ return Ok(fallback_events)
完整调用链上下文
ConversationRuntime::run_turn (conversation.rs:361)
└─ self.api_client.stream(request) ← ApiClient trait
└─ AnthropicRuntimeClient::stream ← main.rs:9441
├─ 构建 MessageRequest
├─ 重试循环(max 2 次,处理 stall)
└─ AnthropicRuntimeClient::consume_stream ← main.rs:9493 ★ 本函数
├─ ProviderClient::stream_message ← client.rs:92
│ ├─ AnthropicClient::stream_message → POST /v1/messages
│ └─ OpenAiCompatClient::stream_msg → POST /chat/completions
├─ 事件循环 + 渲染
└─ 降级:ProviderClient::send_message(非流式兜底)
→ response_to_events

浙公网安备 33010602011771号