`AnthropicRuntimeClient::consume_stream` 详解

AnthropicRuntimeClient::consume_stream 详解

位置: main.rs:9493-9683

函数签名

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_timeouttrue 时,首事件等待 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)?;
    }
}

消息开始时,MessageStartcontent 字段可能已经携带了初始的 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 结束时,将之前累积的数据输出:

  1. 刷新 markdown 渲染器确保所有缓存的样式生效
  2. Thinking 块pending_thinking.take() → 生成 AssistantEvent::Thinking
  3. 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::Usagebuild_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

两次转换的原因:

  • StreamEventapi crate 的底层 SSE 协议类型(贴近 Anthropic/OpenAI 原始 schema)
  • AssistantEventruntime crate 的上层运行时类型(经过规范化,屏蔽 provider 差异)

2. 实时渲染与事件记录的分离

同一个事件同时处理两件事:

  • 渲染到终端:markdown 流式渲染 + ANSI 颜色(用户看得到)
  • 记录到 events:纯数据累积(给 build_assistant_message 用)

3. Thinking 块的延迟组装

Anthropic SSE 协议中,Thinking 块以 ContentBlockStart(type: thinking) 开始,主体由多个 ThinkingDelta 事件组成,ContentBlockStop 结束。consume_streampending_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
posted @ 2026-05-28 19:12  青山見我  阅读(49)  评论(0)    收藏  举报