7.2 审批流完整链路
概述
审批流程涉及四个参与者:**调用方(Caller)**、**ChatClientAgent**、**FunctionInvokingChatClient(装饰器)** 和 **LLM 提供商**。当一个或多个工具调用需要审批时,装饰器会发出 `ToolApprovalRequest` 事件,然后用 `FinishReason::AwaitingApproval` 结束流。调用方收集审批响应后,通过新的 `agent.run()` 调用恢复执行。
完整时序图
sequenceDiagram
actor Caller as 调用方<br/>(前端/编排器)
participant Agent as ChatClientAgent
participant FICC as FunctionInvoking<br/>ChatClient
participant LLM as LLM 提供商
Note over Caller,LLM: ── 第一轮:触发审批 ──
Caller->>Agent: run(messages, session, options)
Agent->>Agent: 组装 system + history + user 消息
Agent->>Agent: 执行 on_invoking() Provider 链
Agent->>FICC: run(full_messages, client_opts)
loop 工具调用循环 (round 0)
FICC->>LLM: POST /chat/completions
LLM-->>FICC: SSE: ToolCallDelta(run_command, path="rm -rf /")
FICC->>FICC: 累积 ToolCallStart/ToolCallArgs/ToolCallEnd
LLM-->>FICC: SSE: finish_reason=tool_calls
end
FICC->>FICC: 检查 requires_approval()
Note right of FICC: RunCommand 被 ApprovalRequiredTool 包装<br/>requires_approval() → true
FICC-->>Caller: AgentResponseUpdate::ToolApprovalRequest {<br/> call_id: "call_1",<br/> name: "run_command",<br/> arguments: {command: "rm -rf /"},<br/> description: "执行系统命令"<br/>}
FICC-->>Caller: AgentResponseUpdate::Finish {<br/> finish_reason: AwaitingApproval<br/>}
Note over Caller: 流结束,等待用户决策
Note over Caller,LLM: ── 用户审批阶段 ──
Caller->>Caller: 展示审批请求给用户
Caller->>Caller: 用户点击"批准"或"拒绝"
Caller->>Caller: 构造 ToolApprovalResponse {<br/> call_id: "call_1",<br/> approved: true,<br/> reason: None<br/>}
Note over Caller,LLM: ── 第二轮:恢复执行 ──
Caller->>Agent: run(messages, session, options.with_tool_approval_responses([response]))
Agent->>FICC: run(full_messages, client_opts)
Note right of FICC: options.tool_approval_responses 非空<br/>→ 进入审批恢复路径
FICC->>FICC: 从 messages 中查找上轮 assistant(tool_calls)
FICC->>FICC: 匹配 pending tool_calls 与 approval responses
FICC->>FICC: call_1 被批准 → 执行 RunCommand
FICC->>FICC: 将 assistant + tool(result) 追加到 messages
FICC->>LLM: run(messages_with_tool_results)
LLM-->>FICC: SSE: TextDelta("命令执行完毕")
LLM-->>FICC: SSE: finish_reason=stop
FICC-->>Caller: TextDelta("命令执行完毕")
FICC-->>Caller: Finish(Stop)
Note over Caller: 正常结束
关键代码路径
1. 审批检测
审批检测位于 `FunctionInvokingChatClient` 内部流消费的 spawned task 中:
// crates/framework/src/decorators/invoke.rs
// ── Approval gate: if any tool requires approval, pause and wait ──
let any_requires_approval = tool_calls.iter().any(|tc| {
tools_for_execution
.iter()
.any(|t| t.name() == tc.name && t.requires_approval())
});
if any_requires_approval {
for tc in &tool_calls {
let tool = tools_for_execution.iter().find(|t| t.name() == tc.name);
let desc = tool.map(|t| t.description().to_string()).unwrap_or_default();
let args: serde_json::Value =
serde_json::from_str(&tc.arguments).unwrap_or(serde_json::Value::Null);
// 发出 ToolApprovalRequest
tx.send(Ok(AgentResponseUpdate::ToolApprovalRequest {
call_id: tc.id.clone(),
name: tc.name.clone(),
arguments: args,
description: desc,
})).await;
}
// 持久化 assistant(tool_calls) 消息
let mut next_messages = Vec::new();
next_messages.push(ChatMessage {
role: MessageRole::Assistant,
content: text_delta.clone(),
tool_calls: Some(/* tool_calls → ToolCall 列表 */),
// ...
});
msg_tx.send(next_messages).await;
// 结束流 — 等待调用方审批后恢复
tx.send(Ok(AgentResponseUpdate::Finish {
finish_reason: FinishReason::AwaitingApproval,
usage: None,
})).await;
return;
}
2. 审批恢复
当调用方带着 `tool_approval_responses` 重新调用 `run()` 时,`LoopState::Looping` 分支首先检查是否有待处理的审批响应:
LoopState::Looping { messages, round, options } => {
// 恢复入口:处理审批响应
if !options.tool_approval_responses.is_empty() {
let approval_map: HashMap<&str, &ToolApprovalResponse> = options
.tool_approval_responses
.iter()
.map(|r| (r.call_id.as_str(), r))
.collect();
// 从最后一条 assistant 消息中提取 pending tool_calls
let pending: Vec<ToolCall> = messages
.iter()
.rev()
.find(|m| m.role == MessageRole::Assistant && m.tool_calls.is_some())
.and_then(|m| m.tool_calls.clone())
.unwrap_or_default();
let mut next_messages = messages.clone();
for tc in &pending {
let approved = approval_map
.get(tc.id.as_str())
.map(|r| r.approved)
.unwrap_or(false);
if approved {
// 执行工具,追加 tool(result) 消息
let result = /* execute tool */;
next_messages.push(ChatMessage::tool(result, &tc.id));
} else {
// 追加拒绝消息
let reason = approval_map
.get(tc.id.as_str())
.and_then(|r| r.reason.as_deref())
.unwrap_or("User denied");
next_messages.push(ChatMessage::tool(
format!("Rejected: {}", reason),
&tc.id,
));
}
}
// 清空 approval_responses,继续循环
let mut opts = options.clone();
opts.tool_approval_responses.clear();
return Some((Ok(/* ... */), LoopState::Looping {
messages: next_messages,
round: round + 1,
options: opts,
}));
}
// ... 正常循环逻辑 ...
}
3. 流消费者体验
`FunctionInvokingChatClient::run()` 返回的最终流会过滤掉内部的 `Finish(ToolCalls)` 信号,因此外部消费者看到的结束原因只有 `Stop`、`AwaitingApproval` 或 `MaxRounds`:
// 过滤内部 ToolCalls Finish 信号
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 }
}),
);
多工具并行审批
当 LLM 一次返回多个工具调用(`parallel_tool_calls: true`),且其中包含审批工具时,审批流程会并行处理:
LLM 返回:ToolCallStart(echo, call_1) + ToolCallStart(run_command, call_2)
↑ 需审批
装饰器发出:
→ ToolApprovalRequest { call_id: "call_2", name: "run_command", ... }
→ Finish(AwaitingApproval)
调用方收集审批后重新调用:
→ 批准 run_command → 执行
→ call_1 (echo) 在消息上下文中已被保留,下一轮 LLM 可决定是否继续
注意:审批暂停只影响被标记为 requires_approval() 的工具。
未被 ApprovalRequiredTool 包装的工具在审批恢复后保持
在消息上下文中,但不会在审批恢复时自动执行——它们等待
LLM 在下一轮中重新发出工具调用。
归纳
审批流的核心是**暂停-收集-恢复**三步:
1. **暂停**:`FunctionInvokingChatClient` 检测到需要审批的工具调用时,发出 `ToolApprovalRequest` 事件并结束流,同时将 `assistant(tool_calls)` 消息通过 `msg_tx` 通道传递回循环状态。 2. **收集**:调用方(前端/编排器)从流中收集 `ToolApprovalRequest` 事件,向用户展示,收集决策,构造 `ToolApprovalResponse` 列表。 3. **恢复**:调用方将 `tool_approval_responses` 放入 `AgentRunOptions` 重新调用 `agent.run()`。`FunctionInvokingChatClient` 在循环开始时检测到待处理响应,执行批准的工具(追加 `tool(result)` 消息),拒绝的工具追加拒绝消息,然后继续下一轮 LLM 调用。