9.2 FunctionInvokingChatClient 工具调用循环

概述

`FunctionInvokingChatClient` 是 RAF 中最核心的装饰器。它实现了完整的工具调用循环——消费 LLM 的流式响应,检测工具调用,执行工具,将结果注入对话,然后循环调用 LLM 直到没有更多工具调用或达到轮次上限。

// crates/framework/src/decorators/invoke.rs

pub struct FunctionInvokingChatClient {
    inner: Arc<dyn IChatClient>,
    tools: Vec<Arc<dyn ITool>>,
    max_rounds: usize,  // 默认 10
}

状态机设计

`FunctionInvokingChatClient` 使用 `LoopState` 枚举管理工具调用循环的状态转换:

enum LoopState {
    Looping {
        messages: Vec<ChatMessage>,
        round: usize,
        options: ChatClientRunOptions,
    },
    Streaming {
        rx: mpsc::Receiver<Result<AgentResponseUpdate>>,
        on_done: Box<LoopState>,         // 流结束后的目标状态
        msg_rx: Option<mpsc::Receiver<Vec<ChatMessage>>>,  // 累积消息通道
    },
    Done,
}
stateDiagram-v2
    [*] --> Looping: 初始状态 (round=0)

    Looping --> Looping: 取消检查 → 发出 Error + Done
    Looping --> Looping: 审批恢复 → 执行/拒绝工具 → round+1
    Looping --> Looping: round >= max_rounds → MaxRounds + Done
    Looping --> Streaming: 调用 LLM 成功 → 发出第一个非 ToolCalls 事件

    Streaming --> Streaming: 消费流事件(非 Finish(ToolCalls))
    Streaming --> Looping: 消费 Finish(ToolCalls) → 合并累积消息 → round+1
    Streaming --> Done: 流错误或通道关闭

    Looping --> Done: 流错误
    Done --> [*]

核心循环实现

`FunctionInvokingChatClient::run()` 使用 `futures_util::stream::unfold` 实现异步状态机:

#[async_trait]
impl IChatClient for FunctionInvokingChatClient {
    async fn run(
        &self,
        messages: &[ChatMessage],
        options: ChatClientRunOptions,
    ) -> Result<BoxStream<'static, Result<AgentResponseUpdate>>> {
        let initial_state = LoopState::Looping {
            messages: messages.to_vec(),
            round: 0,
            options,
        };

        let stream = futures_util::stream::unfold(
            initial_state,
            move |state| {
                let inner = Arc::clone(&self.inner);
                let tools = Arc::clone(&self.tools);

                async move {
                    match state {
                        LoopState::Done => None,
                        LoopState::Streaming { .. } => { /* 消费流 */ }
                        LoopState::Looping { messages, round, options } => {
                            // ── 1. 取消检查 ──
                            if let Some(ref flag) = options.cancelled {
                                if flag.load(Ordering::Relaxed) {
                                    return Some((Ok(Error("cancelled")), Done));
                                }
                            }

                            // ── 2. 审批恢复 ──
                            if !options.tool_approval_responses.is_empty() {
                                // 重建消息 + 执行/拒绝工具
                            }

                            // ── 3. 轮次检查 ──
                            if round >= max_rounds {
                                return Some((Ok(Finish(MaxRounds)), Done));
                            }

                            // ── 4. 调用 LLM ──
                            let stream = inner.run(&messages, options).await?;

                            // ── 5. 创建通道,spawn 流消费任务 ──
                            let (tx, rx) = mpsc::channel(256);
                            let (msg_tx, msg_rx) = mpsc::channel(1);
                            tokio::spawn(async move {
                                // 在 spawned task 中消费 LLM 流
                            });

                            // ── 6. 返回第一个事件 ──
                            match rx.recv().await {
                                Some(Ok(update)) => {
                                    // 如果是 ToolCalls → Looping(round+1)
                                    // 否则 → Streaming(rx, on_done)
                                }
                                // ...
                            }
                        }
                    }
                }
            },
        );

        // 过滤内部 ToolCalls Finish 信号
        let stream = Box::pin(stream.filter(|r| {
            let keep = !matches!(r, Ok(Finish { finish_reason: ToolCalls, .. }));
            async move { keep }
        }));

        Ok(stream)
    }
}

Spawned Task:流消费

每次 LLM 调用返回后,`FunctionInvokingChatClient` 在 `tokio::spawn` 中启动一个异步任务来消费 LLM 的流:

graph TD
    A[LLM 流到达] --> B{Spawned Task}
    B --> C[消费 SSE 事件]
    C --> D{事件类型?}
    D -->|ToolCallStart| E[创建 AccumulatedToolCall]
    D -->|ToolCallArgs| F[追加参数片段到当前调用]
    D -->|ToolCallEnd| G[完成当前调用,加入 tool_calls 列表]
    D -->|TextDelta| H[转发文本增量]
    D -->|Finish| I[延迟处理]
    D -->|其他| J[直接转发]

    E --> K[通过 tx 发送事件给主循环]
    F --> K
    G --> K
    H --> K
    J --> K

    K --> L{流结束?}
    L -->|否| C
    L -->|是| M{has_tool_calls?}

    M -->|否| N[发送 Finish(Stop) → 结束]
    M -->|是| O{any_requires_approval?}

    O -->|是| P[发送 ToolApprovalRequest 事件]
    P --> Q[保存 assistant(tool_calls) 到 msg_tx]
    Q --> R[发送 Finish(AwaitingApproval)]

    O -->|否| S[并行执行工具 join_all]
    S --> T[保存 assistant + tool(result) 到 msg_tx]
    T --> U[发送 ToolCalled 事件]
    U --> V[发送 Finish(ToolCalls)]

AccumulatedToolCall

流消费过程中,工具调用参数通过 `AccumulatedToolCall` 进行累积:

#[derive(Clone, Default)]
struct AccumulatedToolCall {
    id: String,       // 工具调用 ID
    name: String,     // 工具名称
    arguments: String, // 累积的 JSON 参数字符串
}

工具调用累积过程

ToolCallStart { id: "call_1", name: "read_file" }
  → AccumulatedToolCall { id: "call_1", name: "read_file", arguments: "" }

ToolCallArgs { id: "call_1", args_delta: "{\"path\": \"" }
  → AccumulatedToolCall { ..., arguments: "{\"path\": \"" }

ToolCallArgs { id: "call_1", args_delta: "/etc/hosts\"}" }
  → AccumulatedToolCall { ..., arguments: "{\"path\": \"/etc/hosts\"}" }

ToolCallEnd { id: "call_1" }
  → tool_calls.push(AccumulatedToolCall { ... })

工具执行流程

1. Schema 验证

工具执行前,参数会经过 `validate_against_schema()` 验证:

fn validate_against_schema(
    args: &serde_json::Value,
    schema: &serde_json::Value,
) -> std::result::Result<(), String> {
    // 1. 参数必须是 JSON 对象
    let obj = match args {
        serde_json::Value::Object(o) => o,
        other => return Err("Expected a JSON object...".into()),
    };

    // 2. 所有 required 字段必须存在
    if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
        for field in required {
            if !obj.contains_key(field.as_str().unwrap_or("")) {
                errors.push(format!("Missing required field: {}", field));
            }
        }
    }

    // 3. 每个字段的值类型必须与 schema 中声明的类型兼容
    if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
        for (field_name, field_value) in obj {
            let expected_type = properties[field_name]
                .get("type")
                .and_then(|t| t.as_str());
            // 检查 string/number/boolean/object/array 类型匹配
        }
    }
}

2. 并行工具执行

所有工具调用通过 `futures_util::future::join_all` 并行执行:

let tool_futures: Vec<_> = tool_calls.iter().map(|tc| {
    async move {
        // 参数解析
        let args_value = serde_json::from_str(&tc.arguments)?;

        // Schema 验证
        if let Some(tool) = tools.iter().find(|t| t.name() == tc.name) {
            validate_against_schema(&args_value, &tool.parameters())?;
        }

        // 执行工具
        match tools.iter().find(|t| t.name() == tc.name) {
            Some(tool) => tool.execute(args_value).await,
            None => /* "Tool not found" error */,
        }
    }
}).collect();

let results = futures_util::future::join_all(tool_futures).await;

3. 消息累积

每个工具执行后,`assistant(tool_calls)` 和 `tool(result)` 消息对被添加到下一轮的消息列表中:

// assistant 消息
next_messages.push(ChatMessage {
    role: MessageRole::Assistant,
    content: text_delta.clone(),
    tool_calls: Some(tool_calls.iter().map(|tc| ToolCall { ... }).collect()),
    ...
});

// tool(result) 消息(每组一个)
for (i, result) in results.iter().enumerate() {
    next_messages.push(ChatMessage {
        role: MessageRole::Tool,
        content: result.result.unwrap_or(/* error */),
        name: Some(tool_calls[i].name.clone()),
        tool_call_id: Some(tool_calls[i].id.clone()),
        ...
    });
}

Provider Tools 合并

`FunctionInvokingChatClient` 支持将 ContextProvider 注入的工具与静态注册的工具合并:

// 合并 provider 工具与静态工具(去重)
let mut combined: Vec<Arc<dyn ITool>> = (*tools).clone();
let mut seen: HashSet<String> = combined
    .iter().map(|t| t.name().to_string()).collect();

for pt in &options.provider_tools {
    if seen.contains(pt.name()) {
        // ApprovalRequiredTool 替换非审批版本
        if pt.requires_approval() {
            combined.retain(|t| t.name() != pt.name());
            combined.push(Arc::clone(pt));
        }
    } else {
        seen.insert(pt.name().to_string());
        combined.push(Arc::clone(pt));
    }
}

max_rounds 限制

为防止无限循环,`FunctionInvokingChatClient` 有最大轮次限制(默认 10):

if round >= max_rounds {
    return Some((Ok(AgentResponseUpdate::Finish {
        finish_reason: FinishReason::MaxRounds,
        usage: None,
    }), LoopState::Done));
}

流过滤

`FunctionInvokingChatClient` 返回的最终流过滤了内部的 `Finish(ToolCalls)` 信号,因为外部消费者不需要知道内部循环的轮次边界:

let stream: BoxStream<'static, Result<AgentResponseUpdate>> = Box::pin(
    stream.filter(|r| {
        let keep = !matches!(r, Ok(AgentResponseUpdate::Finish {
            finish_reason: FinishReason::ToolCalls, ..
        }));
        async move { keep }
    }),
);

外部消费者看到的结束原因只有 `Stop`、`AwaitingApproval` 或 `MaxRounds`。

归纳

`FunctionInvokingChatClient` 通过状态机 + spawned task 的设计实现了完整的工具调用循环:

| 组件 | 职责 | |------|------| | `LoopState` 枚举 | 管理状态转换:`Looping → Streaming → Looping → ... → Done` | | `unfold` 流 | 异步状态机驱动整个循环 | | `mpsc::channel` | 连接 spawned task(流消费)和主循环 | | Spawned task | 消费 LLM 流,累积工具调用,执行工具或发出审批请求 | | `validate_against_schema()` | 工具执行前的参数校验 | | `join_all` | 并行执行多个工具调用 | | 流过滤 | 隐藏内部 `Finish(ToolCalls)`,保持外部 API 简洁 |

这个设计确保了尽管内部有多轮 LLM 调用和工具执行,外部调用者始终看到的是一个单一的、连续的流。