流式管道

RAF 从设计上仅支持流式输出。`BoxStream` 类型是整个框架的数据传输载体——从 LLM 的 SSE 事件到最终的结构化响应,全部通过异步流传递。本章详解流式管道的类型系统、消费模式和 `AgentResponseConverter` 的工作原理。

BoxStream 类型

pub type BoxStream<'a, T> = Pin<Box<dyn Stream<Item = T> + Send + 'a>>;

这是 `Pin<Box<dyn Stream<Item = T> + Send>>` 的类型别名。使用时需要引入 `futures_util::StreamExt` 来获得流的组合能力。

**在框架中的使用**:

// LLM 客户端返回 SSE 流
pub trait IChatClient {
    async fn run(&self, messages: &[ChatMessage], options: ChatClientRunOptions)
        -> Result<BoxStream<'static, Result<AgentResponseUpdate>>>;
}

// Agent 返回结构化响应流
pub trait IAgent {
    async fn run(&self, messages: Vec<ChatMessage>, ...)
        -> Result<BoxStream<'static, Result<AgentResponseResult>>>;
}

注意生命周期 `'static`——流拥有其数据,不借用任何外部引用。

流式管道全景

flowchart LR
    subgraph "LLM API"
        SSE[SSE Events]
    end

    subgraph "Client 层"
        Transport[SseStream]
        Updates[Stream of AgentResponseUpdate]
    end

    subgraph "Framework 层"
        Conv[AgentResponseConverter]
        Results[Stream of AgentResponseResult]
    end

    subgraph "用户代码"
        Consume[StreamExt 消费]
    end

    SSE --> Transport
    Transport --> |parse| Updates
    Updates --> |consume| Conv
    Conv --> |yield| Results
    Results --> |next/collect| Consume

AgentResponseUpdate → AgentResponseResult 转换

AgentResponseConverter 内部状态

pub struct AgentResponseConverter {
    agent_id: AgentId,
    model_id: Option<String>,
    executor_id: String,
    properties: HashMap<String, serde_json::Value>,

    // 并行工具调用状态管理(合并为单一映射)
    tool_states: HashMap<String, ToolCallState>,  // call_id → 累加器 + 结束标记 + 实时解析器
    index_to_call_id: HashMap<usize, String>,      // 旧版 delta 的索引映射

    response_id: Option<String>,
    response_model: Option<String>,
}

/// 每个工具调用的完整状态,将累加器、结束追踪和流式 JSON 解析器合并在一起。
/// 相比旧版使用 4 个独立 HashMap(tool_accumulators、ended_calls、args_parsers、
/// index_to_call_id),合并后减少了状态管理复杂度。
#[derive(Debug, Default)]
struct ToolCallAccumulator {
    name: Option<String>,
    args: String,              // 累积的 JSON 参数字符串
    start_emitted: bool,       // 防止重复发出 ToolCallStart
}

#[derive(Default)]
struct ToolCallState {
    acc: ToolCallAccumulator,
    ended: bool,               // 调用是否已结束(防止重复 ToolCallEnd)
    parser: StreamingArgsParser, // 实时 JSON 解析器
}

AgentResponseConverter 工作流程

impl AgentResponseConverter {
    pub fn consume(&mut self, update: AgentResponseUpdate) -> AgentResponseResult {
        // 处理每个 SSE 更新,返回即时的 Content 块
    }

    pub fn finalize(&self, finish_reason: Option<FinishReason>, usage: Option<Usage>)
        -> AgentResponseResult {
        // 流结束时刷新缓冲状态
    }
}

`consume()` 是核心方法——它接收单个 `AgentResponseUpdate`,根据变体类型更新内部状态,并决定是否立即返回 `AgentResponseResult` 块:

stateDiagram-v2
    [*] --> Waiting
    Waiting --> TextDelta: TextDelta
    Waiting --> ReasoningDelta: ReasoningDelta
    Waiting --> ToolCallStart: ToolCallStart
    Waiting --> ToolCallArgs: ToolCallArgs
    Waiting --> ToolCallDelta: ToolCallDelta (旧版)
    Waiting --> Usage: Usage
    Waiting --> Finish: Finish
    Waiting --> Error: Error
    
    TextDelta --> Waiting: 立即 yield TextContent
    ReasoningDelta --> Waiting: 立即 yield ReasoningContent
    
    ToolCallStart --> Accumulating: 创建累加器
    Accumulating --> ToolCallArgs: 追加参数 + 实时解析
    Accumulating --> ToolCallEnd: 标记完成
    ToolCallEnd --> Waiting: yield ToolCallEndContent
    
    ToolCallDelta --> StartEmit: 分解出 ToolCallStart
    StartEmit --> ArgsEmit: 分解出 ToolCallArgs
    ArgsEmit --> EndEmit: 分解出 ToolCallEnd
    
    Finish --> Waiting: 记录 finish_reason
    Finish --> Finalize: 流结束 → 调用 finalize()
    Finalize --> [*]: 刷新未完成的工具调用

旧版 ToolCallDelta 的分解

当 SSE 格式不区分 tool call 生命周期事件时,`Converter` 将 `ToolCallDelta` 分解为三步:

// 内部逻辑(简化版)
AgentResponseUpdate::ToolCallDelta { index, id, name, arguments_delta } => {
    // Step 1: 首次出现 → 发出 ToolCallStart
    if !accumulator.start_emitted {
        yield Content::ToolCallStart(...);
    }

    // Step 2: 累积参数 → 发出 ToolCallArgs
    accumulator.args.push_str(&arguments_delta);
    yield Content::ToolCallArgs(...);

    // Step 3: Finish 事件后 → finalize() 发出 ToolCallEnd + ToolCalling
}

流的消费模式

模式 1:逐块处理(实时 UI)

use futures_util::StreamExt;

let mut stream = agent.run(messages, Some(session), None).await?;
while let Some(result) = stream.next().await {
    match result {
        Ok(chunk) => {
            for content in chunk.contents {
                match content {
                    Content::Text(t) => print!("{}", t.delta),          // 实时打印
                    Content::Reasoning(r) => print!("[思考] {}", r.delta),
                    Content::ToolCallStart(s) => print!("[调用] {}", s.name),
                    Content::ToolCalling(c) => {
                        println!("\n[工具] {} args: {}", c.name, c.arguments);
                    }
                    Content::ToolCalled(c) => {
                        println!("[结果] {}", c.result.unwrap_or_default());
                    }
                    _ => {}
                }
            }
        }
        Err(e) => eprintln!("流错误: {}", e),
    }
}

模式 2:事件驱动的分类处理

let mut stream = agent.run(messages, Some(session), None).await?;
let mut display = String::new();
let mut tool_requests = Vec::new();

while let Some(result) = stream.next().await {
    let chunk = result?;
    for content in chunk.contents {
        match content {
            Content::Text(t) => {
                display.push_str(&t.delta);
                render_ui(&display);  // 实时渲染 UI
            }
            Content::ToolCalling(c) => {
                tool_requests.push((c.call_id, c.name, c.arguments));
                show_tool_progress(&c.name);
            }
            Content::ToolCalled(c) => {
                show_tool_result(c.result.as_deref().unwrap_or("failed"));
            }
            Content::Usage(u) => {
                update_token_counter(&u.usage);
            }
            _ => {}
        }
    }
    if let Some(reason) = chunk.finish_reason {
        finalize_response(reason);
    }
}

模式 3:使用 `collect_agent_response` 完整收集

use rust_agent_core::collect_agent_response;

let stream = agent.run(messages, Some(session), None).await?;
let response = collect_agent_response(stream).await?;

// 此时拥有完整的聚合响应
println!("回复: {}", response.text);
println!("工具调用: {:?}", response.tool_calls);
println!("Token: {:?}", response.usage);
println!("原因: {:?}", response.finish_reason);

`collect_agent_response` 内部遍历所有流块,聚合成一个 `AgentResponse`:

pub async fn collect_agent_response(
    mut stream: BoxStream<'static, Result<AgentResponseResult>>,
) -> Result<AgentResponse> {
    let mut text = String::new();
    let mut reasoning_text = String::new();
    let mut tool_calls: Vec<ToolCall> = Vec::new();
    let mut finish_reason = None;
    let mut usage = None;

    while let Some(result) = stream.next().await {
        let chunk = result?;
        if chunk.finish_reason.is_some() {
            finish_reason = chunk.finish_reason;
        }
        for content in chunk.contents {
            match content {
                Content::Text(c) => text.push_str(&c.delta),
                Content::Reasoning(c) => reasoning_text.push_str(&c.delta),
                Content::ToolCalling(c) => tool_calls.push(ToolCall {
                    id: c.call_id, name: c.name, arguments: c.arguments,
                }),
                Content::Usage(c) => usage = Some(c.usage),
                _ => {}
            }
        }
    }

    Ok(AgentResponse {
        text, reasoning_text: if reasoning_text.is_empty() { None } else { Some(reasoning_text) },
        tool_calls, finish_reason, usage, ...
    })
}

模式 4:使用 `inspect` 实现副作用

let stream = agent.run(messages, Some(session), None).await?;
let logged = stream.inspect(|chunk| {
    if let Ok(c) = chunk {
        // 记录到日志但不影响流
        tracing::info!(contents = c.contents.len(), "Agent chunk");
    }
});

// 继续传递给下游消费者
let response = collect_agent_response(logged).await?;

模式 5:使用 `filter_map` 只关心特定内容

let tool_calls: Vec<String> = stream
    .filter_map(|result| async move {
        match result {
            Ok(chunk) => {
                let names: Vec<String> = chunk.contents.iter().filter_map(|c| {
                    if let Content::ToolCalling(tc) = c {
                        Some(tc.name.clone())
                    } else { None }
                }).collect();
                if names.is_empty() { None } else { Some(names) }
            }
            Err(_) => None,
        }
    })
    .flat_map(|names| futures_util::stream::iter(names))
    .collect()
    .await;

StreamingArgsParser: 实时 JSON 解析

当 `ToolCallArgs` 流式到达时,`StreamingArgsParser` 尝试实时解析 JSON 键值对,而不是等待整个 JSON 对象完成。

pub struct StreamingArgsParser;

pub enum ArgsEvent {
    Parsed { name: String, value: Value },   // → ToolCallArgsParsedContent
    Progress { name: String, received: usize, value: Value },  // → ToolCallArgsProgressContent
}

**示例**:当 LLM 流式输出 `{"path": "src/main.rs", "content": "fn main..."}` 时:

1. `{"path": "src/main.rs"` → 立即发出 `ToolCallArgsParsed{name:"path", value:"src/main.rs"}` 2. `, "content": "fn main` → 发出 `ToolCallArgsProgress{name:"content", received:10, value:"fn main"}` 3. `(...)` → 发出 `ToolCallArgsProgress{name:"content", received:17, value:"fn main() {\n...}"}` 4. `"}` → 发出 `ToolCallArgsParsed{name:"content", value:"fn main() {\n...}"}`

此机制使 UI 能实时展示工具调用的参数进度,无需等待整个 JSON 完成。

错误传播

流中的错误是 per-chunk 的:

- `stream.next().await` 返回 `Result<AgentResponseResult>` 是单个块可能失败 - 创建流时的 `?`(`agent.run(...).await?`)是流级错误(如网络不通)

// 流级错误:网络断开、API Key 无效
let mut stream = agent.run(messages, Some(session), None).await?;

// 块级错误:单个 SSE 事件解析失败
while let Some(item) = stream.next().await {
    match item {
        Ok(chunk) => { /* 正常 */ }
        Err(e) => {
            // 块错误——决定继续或退出
            if should_continue(&e) { continue; }
            else { break; }
        }
    }
}

下一步

掌握流式管道后,请阅读 **[压缩策略](./compression-strategies.md)**,了解如何在上下文窗口溢出时自动压缩消息列表。