9.4 流式处理与中间件
概述
RAF 的流式处理链路将 LLM 的 SSE 字节流逐层转换为结构化的 `AgentResponseResult`,为下游消费者(前端 UI、编排引擎、日志系统)提供丰富的事件驱动 API。本章追踪从 HTTP 字节到最终 `AgentResponseResult` 的完整转换链路,并详解中间各层的数据结构和处理逻辑。
流式处理全链路
LLM API (HTTP/SSE)
│
▼
SseStream (字节 → SseChunk → AgentResponseUpdate)
│
▼
FunctionInvokingChatClient (消费 AgentResponseUpdate 流,工具循环)
│
▼
AgentResponseConverter (AgentResponseUpdate → AgentResponseResult)
│
▼
ChatClientAgent (输出 AgentResponseResult 流)
│
▼
collect_agent_response() (聚合流 → AgentResponse)
graph LR
subgraph "传输层"
A[bytes_stream]
end
subgraph "解析层"
B[SseStream]
C[SseChunk 反序列化]
end
subgraph "内部事件层"
D[AgentResponseUpdate]
end
subgraph "公共 API 层"
E[AgentResponseConverter]
F[AgentResponseResult]
end
subgraph "聚合层"
G[collect_agent_response]
H[AgentResponse]
end
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
第一层:SseStream — SSE 字节 → AgentResponseUpdate
`SseStream` 是流式处理的最底层。它实现了 `futures_core::Stream` trait,将 `reqwest` 的 HTTP 字节流转换为 `AgentResponseUpdate` 事件:
// crates/client/src/transport.rs
impl<S> Stream for SseStream<S>
where
S: Stream<Item = reqwest::Result<Bytes>> + Send + Unpin + 'static,
{
type Item = Result<AgentResponseUpdate, AgentError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// 1. 优先从 pending 队列返回事件(一个 chunk 可能产生多个事件)
if let Some(update) = self.pending.next() {
return Poll::Ready(Some(Ok(update)));
}
loop {
// 2. 从缓冲中提取完整行(按 \n 分割)
if let Some(pos) = self.buffer.iter().position(|&b| b == b'\n') {
let line = /* drain buffer */;
let trimmed = line.trim();
// 3. 解析 "data: " 行
if let Some(data) = trimmed.strip_prefix("data: ") {
if data == "[DONE]" {
self.done = true;
return Poll::Ready(None);
}
// 4. 反序列化 JSON → SseChunk → Vec<AgentResponseUpdate>
match serde_json::from_str::<SseChunk>(data) {
Ok(chunk) => {
let mut events = map_chunk(chunk, self.usage_format);
// 返回第一个事件,其余排队
let first = events.remove(0);
self.pending = events.into_iter();
return Poll::Ready(Some(Ok(first)));
}
Err(e) => return Poll::Ready(Some(Err(/* parse error */))),
}
}
continue;
}
// 5. 需要更多数据 → 从 inner stream 读取
match Pin::new(&mut self.inner).poll_next(cx) {
Poll::Ready(Some(Ok(bytes))) => {
self.buffer.extend_from_slice(&bytes);
continue;
}
// ...
}
}
}
}
SseChunk 结构
#[derive(Debug, Deserialize)]
struct SseChunk {
id: Option<String>,
object: Option<String>,
model: Option<String>,
choices: Vec<SseChoice>,
usage: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
struct SseChoice {
finish_reason: Option<String>,
delta: SseDelta,
}
#[derive(Debug, Deserialize, Default)]
struct SseDelta {
content: Option<String>, // 文本增量
reasoning_content: Option<String>, // 推理内容增量(DeepSeek)
tool_calls: Option<Vec<SseToolCall>>, // 工具调用增量
}
map_chunk() — SseChunk → Vec<AgentResponseUpdate>
fn map_chunk(sse: SseChunk, usage_format: UsageFormat) -> Vec<AgentResponseUpdate> {
let mut events = Vec::new();
// 响应元数据(首个 chunk 通常携带 id/model)
if sse.id.is_some() || sse.model.is_some() {
events.push(AgentResponseUpdate::ResponseMetadata {
id: sse.id.clone(),
model: sse.model.clone(),
});
}
for choice in &sse.choices {
// 文本增量
if let Some(ref content) = choice.delta.content {
if !content.is_empty() {
events.push(AgentResponseUpdate::TextDelta { delta: content.clone() });
}
}
// 推理增量
if let Some(ref reasoning) = choice.delta.reasoning_content {
if !reasoning.is_empty() {
events.push(AgentResponseUpdate::ReasoningDelta { delta: reasoning.clone() });
}
}
// 工具调用增量
if let Some(ref tool_calls) = choice.delta.tool_calls {
for tc in tool_calls {
events.push(AgentResponseUpdate::ToolCallDelta {
index: tc.index,
id: tc.id.clone(),
name: tc.function.as_ref().and_then(|f| f.name.clone()),
arguments_delta: tc.function.as_ref()
.and_then(|f| f.arguments.clone())
.unwrap_or_default(),
});
}
}
// 完成原因 + 用量
if let Some(ref reason) = choice.finish_reason {
let finish_reason = match reason.as_str() {
"stop" => FinishReason::Stop,
"length" => FinishReason::Length,
"tool_calls" => FinishReason::ToolCalls,
"content_filter" => FinishReason::ContentFilter,
other => FinishReason::Other(other.to_string()),
};
let usage = sse.usage.as_ref().and_then(|v| usage_format.parse(v));
events.push(AgentResponseUpdate::Finish { finish_reason, usage });
}
}
events
}
第二层:AgentResponseUpdate 枚举
`AgentResponseUpdate` 是内部事件类型。它是 SSE 解析的直接产物,由装饰器消费:
// crates/core/src/message.rs
#[derive(Debug, Clone)]
pub enum AgentResponseUpdate {
TextDelta { delta: String },
ReasoningDelta { delta: String },
/// 旧版工具调用增量 — 分解为 ToolCallStart/ToolCallArgs/ToolCallEnd
ToolCallDelta { index: usize, id: Option<String>, name: Option<String>, arguments_delta: String },
/// 新工具调用开始
ToolCallStart { id: String, name: String },
/// 工具调用参数增量
ToolCallArgs { id: String, args_delta: String },
/// 工具调用参数完毕
ToolCallEnd { id: String },
/// 工具执行结果
ToolCalled { id: String, result: Option<String>, error: Option<String> },
/// 工具调用需要审批
ToolApprovalRequest { call_id: String, name: String, arguments: serde_json::Value, description: String },
Usage { usage: Usage },
Finish { finish_reason: FinishReason, usage: Option<Usage> },
Error { message: String },
ResponseMetadata { id: Option<String>, model: Option<String> },
}
第三层:AgentResponseConverter — AgentResponseUpdate → AgentResponseResult
`AgentResponseConverter` 将内部的 `AgentResponseUpdate` 转换为公共 API 的 `AgentResponseResult`。它同时处理旧版 `ToolCallDelta` 到新版生命周期事件(`ToolCallStart`/`ToolCallArgs`/`ToolCallEnd`)的分解:
// crates/framework/src/converter.rs
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>, // 旧版 ToolCallDelta 索引映射
response_id: Option<String>,
response_model: Option<String>,
}
/// 每个工具调用的聚合状态。
#[derive(Default)]
struct ToolCallState {
acc: ToolCallAccumulator, // 名称 + 参数累积 + start 去重
ended: bool, // 防止重复 ToolCallEnd
parser: StreamingArgsParser, // 实时 JSON 解析
}
impl AgentResponseConverter {
/// 消费单个 AgentResponseUpdate,生成 Content 和 Event 向量
pub fn consume(&mut self, update: AgentResponseUpdate) -> ConvertOutput;
/// 生成带有 finish_reason 的最终 AgentResponseResult
pub fn finalize(&mut self, finish_reason: Option<FinishReason>, usage: Option<Usage>) -> AgentResponseResult;
}
consume() 映射关系
| AgentResponseUpdate | Content 变体(可多个) | |---------------------|----------------------| | `TextDelta` | `Content::Text` | | `ReasoningDelta` | `Content::Reasoning` | | `ToolCallStart` | `Content::ToolCallStart` | | `ToolCallArgs` | `Content::ToolCallArgs` + 可选的 `ToolCallArgsParsed` / `ToolCallArgsProgress` | | `ToolCallEnd` | `Content::ToolCallEnd` | | `ToolCallDelta` | 分解为 `ToolCallStart` + `ToolCallArgs`(+ 解析进度) | | `ToolCalled` | `Content::ToolCalled` | | `ToolApprovalRequest` | `Content::Text`(格式化为 `[Approval required: XXX]`) | | `Usage` | `Content::Usage` | | `Finish` | 无直接 Content(finish_reason 在 finalize 中处理) | | `Error` | `Content::Error` | | `ResponseMetadata` | 不产生 Content(更新内部 response_id/model) |
工具调用生命周期转换
LLM SSE 流(旧版格式):
ToolCallDelta { index: 0, name: "read_file", arguments_delta: "{\"pa" }
ToolCallDelta { index: 0, arguments_delta: "th\": \"/etc/hosts\"}" }
AgentResponseConverter 分解:
Content::ToolCallStart { call_id: "__tc_0", name: "read_file" }
Content::ToolCallArgs { call_id: "__tc_0", args_delta: "{\"pa" }
Content::ToolCallArgs { call_id: "__tc_0", args_delta: "th\": \"/etc/hosts\"}" }
finalize() 中:
Content::ToolCallEnd { call_id: "__tc_0" } // 自动补充
Content::ToolCalling { call_id: "__tc_0", ... } // 完整汇总
第四层:AgentResponseResult — 公共 API
// crates/core/src/message.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResponseResult {
pub id: Option<String>,
pub model: Option<String>,
pub finish_reason: Option<FinishReason>,
pub contents: Vec<Content>,
pub events: Vec<Event>,
}
Content 枚举 — 12 个变体
// crates/core/src/message.rs
pub enum Content {
Text(TextContent), // 文本增量
Reasoning(ReasoningContent), // 推理文本增量
Uri(UriContent), // URI 资源
ToolCallStart(ToolCallStartContent), // ① 工具调用开始
ToolCallArgs(ToolCallArgsContent), // ② 参数增量
ToolCallArgsParsed(ToolCallArgsParsedContent), // ②b 参数已解析
ToolCallArgsProgress(ToolCallArgsProgressContent), // ②c 参数接收中
ToolCallEnd(ToolCallEndContent), // ③ 参数完毕
ToolCalling(ToolCallingContent), // ④ 完整调用(可直接执行)
ToolCalled(ToolCalledContent), // ⑤ 执行结果
Usage(UsageContent), // 用量统计
Error(ErrorContent), // 错误
}
工具调用生命周期(5 阶段):
ToolCallStart → ToolCallArgs(×N) → ToolCallEnd → ToolCalling → ToolCalled
①开始 ②参数流式到达 ③参数完毕 ④完整调用 ⑤执行结果
- ①~③ 是**流式阶段**:SSE 数据到达时实时发出,UI 可据此展示进度 - ④ 是**汇总阶段**:流结束时一次性发出,携带完整解析的参数结构体 - ⑤ 是**执行阶段**:工具执行完成后发出,携带结果或错误
FinishReason 枚举
// crates/core/src/types.rs
pub enum FinishReason {
Stop, // 正常结束
Length, // 达到长度限制
ToolCalls, // 包含工具调用(内部使用,对消费者过滤)
ContentFilter, // 被内容过滤器拦截
AwaitingApproval, // 审批暂停
MaxRounds, // 达到最大轮次
Other(String), // 其他
}
第五层:collect_agent_response() — 流聚合
`collect_agent_response()` 将完整的 `AgentResponseResult` 流聚合为单个 `AgentResponse`:
// crates/core/src/stream.rs
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?;
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),
_ => {}
}
}
if chunk.finish_reason.is_some() {
finish_reason = chunk.finish_reason;
}
}
Ok(AgentResponse { text, tool_calls, finish_reason, usage, ... })
}
在 ChatClientAgent 中的集成
// crates/framework/src/agent/run.rs
// Phase 2: LLM 调用
let stream = self.chat_client.run(&full_messages, client_opts).await?;
let converter = AgentResponseConverter::new(agent_id, executor_id, &run_options);
let converted = futures_util::stream::unfold(
(stream, converter, None::<FinishReason>, None::<Usage>, false),
|(mut stream, mut converter, mut pf, mut pu, done)| async move {
if done { return None; }
loop {
match stream.next().await {
Some(Ok(update)) => {
// 捕获 FinishReason 和 Usage
if let AgentResponseUpdate::Finish { ref finish_reason, ref usage } = update {
pf = Some(finish_reason.clone());
if usage.is_some() { pu = usage.clone(); }
}
// 转换并输出
let output = converter.consume(update);
if !output.contents.is_empty() || !output.events.is_empty() {
return Some((Ok(AgentResponseResult {
// ...
contents: output.contents,
events: output.events,
}), (stream, converter, pf, pu, false)));
}
}
Some(Err(e)) => return Some((Err(e), ...)),
None => {
// 流结束:生成最终 AgentResponseResult
let fr = converter.finalize(pf.clone(), pu.clone());
return Some((Ok(fr), (stream, converter, pf, pu, true)));
}
}
}
},
);
完整数据流示例
以下展示 Agent 读取文件时的完整数据流:
HTTP SSE:
data: {"id":"resp_1","model":"deepseek-chat","choices":[{"delta":{"content":"我来"}}]}
data: {"choices":[{"delta":{"content":"读取文件"}}]}
data: {"choices":[{"finish_reason":"stop"}],"usage":{...}}
SseStream → AgentResponseUpdate:
ResponseMetadata { id: "resp_1", model: "deepseek-chat" }
TextDelta { delta: "我来" }
TextDelta { delta: "读取文件" }
Finish { finish_reason: Stop, usage: {...} }
AgentResponseConverter → AgentResponseResult:
{ contents: [
Content::Text { delta: "我来" },
Content::Text { delta: "读取文件" },
Content::Usage { ... },
],
finish_reason: Some(Stop),
}
collect_agent_response → AgentResponse:
{ text: "我来读取文件", tool_calls: [], finish_reason: Some(Stop), ... }
归纳
RAF 的流式处理链路通过 5 层转换实现了从原始字节到结构化 API 的完整映射:
| 层 | 输入 → 输出 | 关键类型 | |----|-----------|---------| | 1. SSE 传输 | HTTP bytes → AgentResponseUpdate | `SseStream`, `SseChunk` | | 2. 工具循环 | AgentResponseUpdate 消费/转发 | `FunctionInvokingChatClient` | | 3. 内容转换 | AgentResponseUpdate → Content/Event | `AgentResponseConverter` | | 4. 结果输出 | Content/Event → AgentResponseResult | `AgentResponseResult` | | 5. 流聚合 | AgentResponseResult 流 → AgentResponse | `collect_agent_response()` |
工具调用的 5 阶段生命周期(`Start → Args → End → Calling → Called`)提供了丰富的进度反馈,使 UI 能够实时展示工具调用状态,同时保持向后兼容的 `ToolCallDelta` 旧版格式支持。