实时会话中的工具
实时 agent(相对于语音机器人)的决定性特性是,它可以在对话进行中执行真实操作:查找信息、处理退款、转交给人工,然后再说出结果。工具在服务端运行,因此你的业务逻辑和凭据永远不会接触到客户端。
工具回合如何流转
- 模型判断自己需要一个工具,并发出
FunctionCallDone { name, arguments, call_id }。 RealtimeRunner查找name的处理器并运行它。- 处理器的 JSON 结果会作为工具输出发送回模型。
- 运行器触发一次后续响应;模型基于该结果说出答案。
你不需要为此调用 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_refund 和 connect_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(())。