实时会话中的工具

实时 agent(相对于语音机器人)的决定性特性是,它可以在对话进行中执行真实操作:查找信息、处理退款、转交给人工,然后再说出结果。工具在服务端运行,因此你的业务逻辑和凭据永远不会接触到客户端。

工具回合如何流转

  1. 模型判断自己需要一个工具,并发出 FunctionCallDone { name, arguments, call_id }
  2. RealtimeRunner 查找 name 的处理器并运行它。
  3. 处理器的 JSON 结果会作为工具输出发送回模型。
  4. 运行器触发一次后续响应;模型基于该结果说出答案。

你不需要为此调用 create_response()——当 auto_respond_tools 开启时(默认开启),运行器会处理整个往返过程。

原生工具:ToolDefinition + FnToolHandler

这是轻量路径。ToolDefinition 是模型可见的 JSON schema;FnToolHandler 是一个在被调用时运行的同步闭包。

use adk_realtime::config::ToolDefinition;
use adk_realtime::events::ToolCall;
use adk_realtime::runner::FnToolHandler;
use serde_json::json;

fn process_refund_def() -> ToolDefinition {
    ToolDefinition {
        name: "process_refund".into(),
        description: Some("Issue a refund for an order. Only when clearly warranted.".into()),
        parameters: Some(json!({
            "type": "object",
            "properties": {
                "order_id": { "type": "string", "description": "e.g. 'A-10293'" },
                "reason":   { "type": "string", "description": "Short reason" }
            },
            "required": ["order_id", "reason"]
        })),
    }
}

fn process_refund_tool()
-> FnToolHandler<impl Fn(&ToolCall) -> adk_realtime::error::Result<serde_json::Value> + Send + Sync> {
    FnToolHandler::new(|call: &ToolCall| {
        let order = call.arguments.get("order_id").and_then(|v| v.as_str()).unwrap_or("unknown");
        // …do the work…
        Ok(json!({ "status": "approved", "order_id": order,
                   "message": format!("Refund approved for {order}.") }))
    })
}

使用 .tool(definition, handler) 在 builder 上注册它:

let runner = IntegratedRealtimeRunner::builder()
    .model(model)
    .config(config)
    .identity("support", "customer", &session_id)
    .session_service(sessions)
    .tool(process_refund_def(), process_refund_tool())
    .tool(connect_to_human_def(), connect_to_human_tool())
    .build()?;

处理器返回一个 serde_json::Value;你返回什么,模型就会看到什么,所以要包含一个可读的 message,方便 agent 转述。

处理器在事件循环内以服务端同步方式运行。请保持它们足够快;对于耗时工作,返回一个“已开始”状态,并在带外跟进。

桥接工具:任何 adk_core::Tool

如果你已经有 adk-core 工具(你自己的 FunctionTool,或者像知识图谱 remember/relate 这样的 adk-tool 内置工具),可以用 .adk_tool(...) 接入——无需重写。集成层会把每个工具包装成一个 ToolHandler,并生成一个作用域限定在会话 (app_name, user_id, session_id)ToolContext

use adk_tool::{RememberTool, RelateTool};

let runner = IntegratedRealtimeRunner::builder()
    .model(model).config(config).identity("app", "user", &sid)
    .memory_service(kg.clone())
    .adk_tool(Arc::new(RememberTool::new(kg.clone())))   // adk_core::Tool
    .adk_tool(Arc::new(RelateTool::new(kg)))
    .tool(get_weather_def(), get_weather())              // native handler — mix freely
    .build()?;

这就是 agent 组织自己的 memory 的方式。桥接方式很适合本地执行、与上下文无关的工具;而那些需要丰富 agent 状态的工具,更适合写成原生 FnToolHandler

并行工具调用

模型可以在一次响应中请求多个工具(例如:“伦敦的天气和时间分别是多少?”)。ADK-Rust 会正确处理这一点:它会在每个工具完成时发送该工具的输出,然后在分发响应结束后恰好一次触发一个 response.create

这一点很重要,因为朴素做法——每个工具都触发一次响应——会遇到 OpenAI 的 “conversation already has an active response in progress” 错误,并使会话停滞。运行器通过将“发送工具输出”(send_tool_output)与“触发响应”(respond_after_tools,在分发 ResponseDone 上调用一次)分离来避免这个问题。你会免费得到这一切;只是当读取事件时要注意,一个工具回合会跨越两个响应(见 Architecture)。

在 UI 中读取工具事件

要展示工具活动(例如“正在处理退款…”的 chip),请关注 FunctionCallDone

ServerEvent::FunctionCallDone { name, arguments, .. } => {
    // `arguments` is a JSON string of the call args
    ui_show_tool_activity(&name, &arguments);
}

随后,当工具结果被并入后续响应时,语音确认会作为 TranscriptDelta 到达。

看看它如何工作

customer_service 示例将 process_refundconnect_to_human 连接起来;realtime_tools 示例是一个无头探针,用来在两个提供方上测试单工具、并行工具以及计算器回合。

下一步:多模态 →

哪些工具受治理

IntegratedRealtimeRunner 会根据工具的注册方式来路由工具调用:

注册为分发已应用策略
adk_tool(...) — 一个 ADK Tool集成策略管道已配置的插件、转录记录、工具事件持久化
原生实时处理器RealtimeRunner 分发无 — 该处理器在构造上是可信的

一个 ADK 工具此前是通过 ToolBridgeAdapter 到达提供方的,这会创建一个上下文,并在没有插件、回调或确认的情况下调用 Tool::execute。因此,在标准 agent 循环中受治理的工具,实际上在 realtime 中是未受治理地运行的。现在,原生处理器绕过是针对一切的明确例外,而不再是默认行为。

插件失败会关闭失败

如果 before_tool_call 管道返回错误,该工具将被拒绝

{ "error": "tool guarded was refused: its before-tool plugin pipeline failed (...). Execution is refused rather than proceeding without policy." }

Important: 这条路径此前会将插件错误记录为非致命错误,然后执行该工具。授权、重写和策略都位于前置工具插件中,所以一个损坏的守卫就等同于没有守卫。

后置工具插件错误会保留工具自身的结果,因为工具已经运行完毕。

直接 agent 上的工具回调

RealtimeAgent 会以前置和后置工具回调应用相同的契约,与标准 agent 循环一致:

回调返回值影响
Ok(None)该工具运行
Ok(Some(content)) 来自 before 回调内容成为结果;该工具不会运行
Err(e) 来自 before 回调错误会成为结果,工具不会运行,并且会跳过 after 回调
Ok(Some(content)) 来自 after 回调内容会替换工具的结果
Err(e) 来自 after 回调错误会替换工具的结果

回调的 Content 会转换为提供方期望的 JSON 结果:FunctionResponse 部分提供其负载,其余内容则以 result 键提供其文本。

**重要:**在此契约被遵守之前,before-callback 的决定会被计算出来但随后被丢弃,因此工具无论如何都会运行——这就像一个报告拒绝但并不真正强制执行的门闩。after-callback 结果,包括错误,也会被丢弃。

实时工具上下文

RealtimeAgent 调用的工具所看到的能力,与在 Runner 下看到的相同:

能力来源
user_scopes()父级调用上下文
get_secret(name)父级调用上下文
shared_state()父级调用上下文
search_memory(query)父级的记忆服务
Identity (app_name, user_id, session_id, branch)父级调用上下文

**注意:**这些以前会落入 trait 默认值——一个空的 scope 列表,None 用于 secrets,以及 None 用于共享状态——因此,一个检查 scope 或 secret 的工具在实时环境中的行为会 与在 Runner 下不同,并且无法区分未经身份验证的调用方 与一个只是未能将 scopes 继续传递下来的上下文。

工具并发

RunnerConfig::max_concurrent_tools(默认 4)限制同时运行的工具处理器数量。 当一个响应分发多个调用时,runner 会将每个调用排入其事件循环, 并在有许可释放后允许其执行:

use adk_realtime::{RealtimeRunner, RunnerConfig};

let runner = RealtimeRunner::builder()
    .model(model)
    .runner_config(RunnerConfig {
        auto_execute_tools: true,
        auto_respond_tools: true,
        max_concurrent_tools: 3,
    })
    .build()?;

由此产生两个性质,并且两者都由测试覆盖:

  • 在工具执行期间,事件接收仍会继续。 音频增量、转录以及 中断会在工具运行时被处理。一个等待会话后续到达内容的处理器 不再会使会话死锁。
  • 最后一个输出之后,只跟随一个响应。 当工具输出被 自动发送时,模型只应收到一个 create_response。它会在 分发响应已关闭且每个已分发工具都已报告之后发出——两者顺序不限,因为现在响应可以在工具仍在运行时关闭。

**重要:**这个上限约束的是并发,而不是并行。处理器共享 runner 的任务,所以会阻塞线程的处理器——同步文件或网络 I/O、 大量计算——仍然会卡住循环。对此请使用 tokio::task::spawn_blocking

断开连接策略

runner 不会自动重连。在传输丢失时,它会让已分发的工具 完成,调用 EventHandler::on_disconnect,并从 run 返回:

use adk_realtime::{EventHandler, Result};

struct Reconnecting;

#[async_trait::async_trait]
impl EventHandler for Reconnecting {
    async fn on_disconnect(&self) -> Result<()> {
        tracing::warn!("realtime transport ended");
        Ok(())
    }
}

重连由调用方负责,因为这需要决定要重放哪些上下文, 并且在 Gemini 上还需要判断存储的恢复令牌是否仍然有效。on_disconnect 钩子 的存在是为了区分传输丢失和正常的 close——run 对两者都返回 Ok(())

实时会话中的工具 - ADK-Rust 文档 | ADK-Rust