4.3 ApprovalRequiredTool 审批包装器
`ApprovalRequiredTool` 是 RAF 工具系统的人机协同(HITL)入口。它通过装饰器模式包装任意 `ITool`,在运行时标记该工具需要人工审批,由 `FunctionInvokingChatClient` 在执行前拦截并发出审批事件。
设计动机
在生产环境中,某些操作(如删除文件、执行 shell 命令、修改数据库)不应由 Agent 自动完成。RAF 提供两层审批机制:
1. **工具级审批**:通过 `ApprovalRequiredTool` 包装特定工具,该工具的每次调用都需要审批 2. **策略级审批**:通过 `ScopePolicy::ApproveOutside`,当工具操作超出工作区边界时触发审批
`ApprovalRequiredTool` 是工具级审批的载体,它不改动被包装工具的任何逻辑,仅重写 `requires_approval()`。
完整定义
/// 包装任意 [`ITool`],标记为需要人工审批才能执行。
///
/// 对应 MAF 的 `ApprovalRequiredAIFunction`。
///
/// `FunctionInvokingChatClient` 在运行时检查 `requires_approval()`,
/// 当返回 `true` 时,发出 [`ToolApprovalRequest`] 事件而非立即执行。
#[derive(Clone)]
pub struct ApprovalRequiredTool {
pub inner: Arc<dyn ITool>,
}
impl ApprovalRequiredTool {
pub fn new(tool: Arc<dyn ITool>) -> Self {
Self { inner: tool }
}
}
委托模式
`ApprovalRequiredTool` 实现 `ITool` 时采用完全委托——所有方法都透传到内部工具,仅重写 `requires_approval()`:
#[async_trait]
impl ITool for ApprovalRequiredTool {
fn name(&self) -> &str {
self.inner.name()
}
fn description(&self) -> &str {
self.inner.description()
}
fn parameters(&self) -> serde_json::Value {
self.inner.parameters()
}
async fn execute(&self, arguments: serde_json::Value) -> Result<ToolResult> {
self.inner.execute(arguments).await
}
fn requires_approval(&self) -> bool {
true // ← 唯一的行为差异
}
fn kind(&self) -> &str {
self.inner.kind() // ← 透传分类
}
}
**设计精妙之处:**
- `name()`、`description()`、`parameters()` 全部透传——LLM 看到的工具签名完全相同 - `execute()` 透传——被批准后,执行逻辑和原始工具完全一致 - **仅 `requires_approval()` 返回 `true`**——这是框架判断是否需要审批的唯一依据
审批响应数据结构
当 `FunctionInvokingChatClient` 检测到 `requires_approval() == true` 时,它不立即执行工具,而是发出 `ToolApprovalRequest` 事件。调用者需要回复 `ToolApprovalResponse`:
/// 调用者对 ToolApprovalRequest 的响应。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolApprovalResponse {
/// 匹配对应 ToolApprovalRequest 中的 call_id
pub call_id: String,
/// true = 批准执行,false = 拒绝
pub approved: bool,
/// 拒绝原因(可选,会反馈给 LLM)
pub reason: Option<String>,
}
审批流程
sequenceDiagram
participant LLM as LLM 服务
participant FICC as FunctionInvokingChatClient
participant ART as ApprovalRequiredTool
participant Inner as 实际工具
participant User as 用户/调用者
LLM->>FICC: tool_calls ["RunCommand: rm -rf /important"]
FICC->>ART: requires_approval()
ART-->>FICC: true
Note over FICC: 发出 ToolApprovalRequest 事件
FICC-->>User: [ToolApprovalRequest] call_id=1, tool=RunCommand, args={...}
alt 用户批准
User-->>FICC: [ToolApprovalResponse] approved=true
FICC->>ART: execute(arguments)
ART->>Inner: execute(arguments)
Inner-->>ART: ToolResult
ART-->>FICC: ToolResult
FICC->>LLM: 注入 tool result message
else 用户拒绝
User-->>FICC: [ToolApprovalResponse] approved=false, reason="危险操作"
FICC->>LLM: 注入拒绝消息(含 reason)
end
使用场景
场景一:生产环境命令执行
use rust_agent_core::ApprovalRequiredTool;
// Agent A:开发环境,自动执行
AgentBuilder::new().with_tool(RunCommand {
scope: None,
timeout_secs: None,
});
// Agent B:生产环境,需要审批
AgentBuilder::new().with_tool(ApprovalRequiredTool::new(Arc::new(RunCommand {
scope: None,
timeout_secs: None,
})));
场景二:结合 WorkspaceContextProvider 策略审批
`WorkspaceContextProvider` 在 `add_tool()` 中检测 `ScopePolicy`,自动为 `ApproveOutside` 策略包裹 `ApprovalRequiredTool`:
let scope = WorkspaceScope::new("/safe/project", "my-project")
.with_policy(ScopePolicy::ApproveOutside);
let provider = WorkspaceContextProvider::new(Arc::new(scope))
.add_tool(WriteFile { scope: None }) // 自动包裹 ApprovalRequiredTool
.add_tool(RemovePath { scope: None }); // 自动包裹 ApprovalRequiredTool
场景三:仅特定工具需要审批
let mut registry = ToolRegistry::new();
// 读取文件:始终自动
registry.register(ReadFile { scope: None });
// 写入文件:需要审批
registry.register_arc(Arc::new(ApprovalRequiredTool::new(Arc::new(WriteFile {
scope: None,
}))));
// 删除文件:需要审批
registry.register_arc(Arc::new(ApprovalRequiredTool::new(Arc::new(RemovePath {
scope: None,
}))));
ToolApprovalRequest 事件
当需要审批时,`FunctionInvokingChatClient` 发出的事件结构(定义于 `AgentResponseUpdate`):
pub struct ToolApprovalRequest {
pub call_id: String, // 唯一调用 ID,必须匹配审批响应
pub tool_name: String, // 工具名称
pub arguments: Value, // 工具参数(用于用户决策)
}
调用者通常监听 `AgentResponseUpdate::ToolApprovalRequest(approval_req)` 变体,向用户展示工具名和参数,收集批准/拒绝决策后发送 `ToolApprovalResponse`。
关键要点
1. **装饰器模式,零侵入**——`ApprovalRequiredTool` 不修改被包装工具的任何代码,仅添加审批标记 2. **框架层拦截**——审批检查发生在 `FunctionInvokingChatClient` 的工具调用循环中,工具自身无需感知 3. **call_id 匹配**——`ToolApprovalRequest` 和 `ToolApprovalResponse` 通过 `call_id` 关联,支持并发审批 4. **拒绝原因反馈 LLM**——`reason` 字段注入对话,让模型了解为什么操作被拒绝,持续优化决策