跳转至

远程工具调用机制

概述

远程工具调用(Remote Tool Call)是 ClawSeed 的核心特性之一,允许移动客户端通过 WebSocket 注册和执行工具。Agent 无需区分本地工具和远程工具——两者都实现 Tool trait,调用方式完全相同。

注意: 远程工具注册在两个独立的注册表中——网关级 AppState.tool_registry(用于 /api/tools 可见性)和连接级 Agent.tool_registry(用于实际执行)。完整的三步注册流程见下方"连接生命周期"。

架构总览

┌──────────────────┐                          ┌──────────────────┐
│   移动客户端      │                          │   Gateway 服务端  │
│                  │                          │                  │
│  ClawseedClient  │   1. register_tools      │  WebSocket       │
│  (OkHttp WS)     │ ──────────────────────→  │  Handler         │
│                  │                          │       ↓          │
│                  │   2. tools_registered    │  RemoteTool      │
│                  │ ←──────────────────────  │  Registry        │
│                  │                          │       ↓          │
│                  │                          │  Agent           │
│                  │                          │  .tool_registry  │
│                  │                          │  (Arc<dyn        │
│                  │                          │   ToolRegistry>) │
│                  │                          │                  │
│  工具执行器      │   3. tool_call_request   │  Agent Loop      │
│  (ToolCall       │ ←──────────────────────  │  调用 RemoteTool │
│   Handler)       │                          │  .execute()      │
│                  │                          │                  │
│                  │   4. tool_result         │  等待响应        │
│                  │ ──────────────────────→  │  (30s 超时)     │
│                  │                          │       ↓          │
│                  │   5. result_acknowledged │  返回结果给      │
│                  │ ←──────────────────────  │  Agent Loop      │
└──────────────────┘                          └──────────────────┘

服务端实现

RemoteTool — 远程工具包装

RemoteTool 实现了 Tool trait,将工具执行桥接到 WebSocket 客户端:

pub struct RemoteTool {
    spec: ToolSpec,
    request_tx: mpsc::Sender<RemoteToolRequest>,
}

#[async_trait]
impl Tool for RemoteTool {
    fn name(&self) -> &str { &self.spec.name }
    fn description(&self) -> &str { &self.spec.description }
    fn parameters_schema(&self) -> Value { self.spec.parameters.clone() }

    async fn execute(&self, args: Value, _ctx: &dyn ToolContext) -> Result<ToolResult> {
        let (response_tx, response_rx) = oneshot::channel();
        let call_id = Uuid::new_v4().to_string();

        // 发送请求到 WebSocket 处理器
        self.request_tx.send(RemoteToolRequest {
            call_id: call_id.clone(),
            tool_name: self.spec.name.clone(),
            args,
            response_tx,
        }).await?;

        // 等待客户端响应(30 秒超时)
        match tokio::time::timeout(
            Duration::from_secs(30),
            response_rx,
        ).await {
            Ok(Ok(result)) => Ok(result),
            Ok(Err(_)) => Err(anyhow!("Channel closed")),
            Err(_) => Err(anyhow!("Remote tool timeout (30s)")),
        }
    }
}

关键设计: - 使用 mpsc::Sender 发送请求到 WebSocket handler - 使用 oneshot::channel 等待单次响应 - 30 秒超时防止无限等待 - 不使用 ToolContext(无法访问服务端能力)

RemoteToolRegistryHandle — 工具注册管理

pub struct RemoteToolRegistryHandle {
    tools: Vec<RemoteTool>,
    request_rx: mpsc::Receiver<RemoteToolRequest>,
}

管理从 WebSocket 客户端注册的工具,提供请求接收通道。

WebSocket 处理器

WebSocket handler 处理工具注册和请求转发:

async fn handle_ws(socket: WebSocket, agent: Agent) {
    let (registry_handle, request_rx) = RemoteToolRegistryHandle::new();
    let session_id = generate_session_id();

    while let Some(msg) = socket.next().await {
        match msg {
            // 工具注册
            Ok(Text(text)) if type == "register_tools" => {
                let remote_tools = registry_handle.build_tools();
                agent.add_remote_tools(remote_tools, session_id.clone());
                socket.send(tools_registered(count)).await;
            }

            // 工具结果返回
            Ok(Text(text)) if type == "tool_result" => {
                let result = ToolResult { success: true, output, error: None };
                response_tx.send(result);
            }

            // 工具错误返回
            Ok(Text(text)) if type == "tool_error" => {
                let result = ToolResult { success: false, output: String::new(), error: Some(err) };
                response_tx.send(result);
            }
        }
    }

    // WebSocket 断开时,通过 ToolSource::Remote { session } 批量移除
    // tool_registry.unregister_by_source() 自动清理该会话的所有远程工具
}

客户端实现

工具注册

// 构建工具规格
val toolSpec = ToolSpec(
    name = "device_info",
    description = "获取Android设备信息,包括型号、制造商、Android版本",
    parameters = """{"type":"object","properties":{},"required":[]}"""
)

// 通过 Builder 注册
val client = ClawseedClient.builder(url)
    .registerTool(toolSpec)
    .toolCallHandler { request ->
        when (request.name) {
            "device_info" -> ToolCallResult.Success(queryDeviceInfo())
            else -> ToolCallResult.Failure("unknown tool")
        }
    }
    .build()

工具调用处理

// 收到 tool_call_request 时的处理
private fun dispatchToolCall(request: ToolCallRequest) {
    val handler = toolCallHandler ?: run {
        // 没有注册处理器,返回错误
        webSocket?.send(ToolCallResult.Failure("No handler").toJson(request.id).toString())
        return
    }
    // 在单线程 executor 中执行,避免竞态
    executor.execute {
        val result = runCatching { handler.handleToolCall(request) }
            .getOrElse { ToolCallResult.Failure(it.message ?: "Exception") }
        // 立即通过 WebSocket 返回结果
        webSocket?.send(result.toJson(request.id).toString())
    }
}

消息协议详解

工具注册阶段

// 客户端 → 服务端
{
    "type": "register_tools",
    "tools": [
        {
            "name": "device_info",
            "description": "获取设备信息",
            "parameters": {"type": "object", "properties": {}, "required": []}
        },
        {
            "name": "camera",
            "description": "拍摄照片",
            "parameters": {
                "type": "object",
                "properties": {
                    "quality": {"type": "string", "enum": ["low", "medium", "high"]}
                }
            }
        }
    ]
}

// 服务端 → 客户端
{
    "type": "tools_registered",
    "count": 2,
    "registered": 2
}

工具调用阶段

// 服务端 → 客户端(请求执行工具)
{
    "type": "tool_call_request",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "device_info",
    "args": {}
}

// 客户端 → 服务端(成功结果)
{
    "type": "tool_result",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "output": "{\"model\":\"Pixel 8\",\"manufacturer\":\"Google\",\"android_version\":\"14\"}",
    "success": true
}

// 客户端 → 服务端(错误结果)
{
    "type": "tool_error",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "error": "Camera permission denied",
    "success": false
}

// 服务端 → 客户端(确认结果已收到)
{
    "type": "result_acknowledged",
    "id": "550e8400-e29b-41d4-a716-446655440000"
}

远程工具 vs 本地工具

特性 本地工具 远程工具
注册方式 all_tools()(CLI)或 shared_builtin_tools(网关),注册为 ToolSource::BuiltIn WebSocket register_tools 消息,注册为 ToolSource::Remote { session }
执行位置 Gateway 服务端 客户端设备
ToolContext 完整访问(Memory、SecurityPolicy 等) 不使用
超时 无限制 30 秒
生命周期 随 Gateway 进程 随 WebSocket 连接
典型用途 文件操作、Shell、Web 请求 设备能力(相机、传感器、联系人)
错误处理 ToolResult::error tool_error 消息或超时

连接生命周期

WebSocket 连接建立
客户端发送 register_tools
Gateway 创建 RemoteTool 实例:
  1. 注册到共享 AppState.tool_registry,通过 register_or_replace()(ToolSource::Remote { session })
     → 使工具在 /api/tools 端点可见
  2. 注入到当前连接 Agent,通过 agent.add_remote_tools(tools, session)
     → 使 Agent 可实际调用这些工具
正常对话和工具调用
WebSocket 断开
Gateway 通过 tool_registry.unregister_by_source() 从共享注册表中移除远程工具
(Agent 侧的工具随 Agent 被丢弃而清理)
后续对话不再调用已断开客户端的工具

重要:远程工具的生命周期与 WebSocket 连接绑定。连接断开后,相关工具自动从共享注册表和 Agent 中移除。

双重注册表影响: 共享 AppState.tool_registry 和每个 Agent.tool_registry 是独立的。/api/tools 可能显示来自其他连接但当前 Agent 无法调用的工具。在单连接场景下(当前 Android Demo),两个注册表实际上保持同步。

典型应用场景

设备信息查询

ToolSpec("device_info", "获取设备信息",
    """{"type":"object","properties":{},"required":[]}""")

相机操作

ToolSpec("camera", "拍摄照片",
    """{"type":"object","properties":{"quality":{"type":"string","enum":["low","high"]}},"required":[]}""")

联系人查询

ToolSpec("contacts", "查询手机联系人",
    """{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}""")

传感器数据

ToolSpec("sensors", "读取传感器数据",
    """{"type":"object","properties":{"type":{"type":"string","enum":["accelerometer","gyroscope","gps"]}},"required":["type"]}""")

错误处理

场景 处理方式
工具处理器未注册 返回 tool_error,消息:"No handler registered"
工具执行抛异常 捕获异常,返回 tool_error,包含异常消息
客户端未在 30s 内响应 Gateway 返回超时错误给 Agent
WebSocket 断开 移除所有远程工具,Agent 不再调用
call_id 不匹配 丢弃无法关联的响应

安全考虑

  • 远程工具无法访问服务端能力(Memory、SecurityPolicy、Provider)
  • 工具参数由客户端自行验证
  • Gateway 仍然通过 Hook 管线拦截工具调用
  • before_tool_call Hook 可以取消远程工具调用
  • 建议:在 SecurityPolicy 中限制可注册的工具名称范围