语音Agent如何在工具执行时持续回应
文章解释阻塞工具为何让语音Agent陷入静默,以及非阻塞和流式工具如何改善等待体验。其引用的基准中,首次发声时间由14.4秒降至1.2秒,底层查询仍耗时约13秒。
AI 深度解读
语音 Agent 的卡顿,有时不是查询太慢,而是等待期间一句话也说不出来。原文讨论的就是这个问题:后台任务照常运行,先把对话控制权交还模型,再决定何时播报进度和结果。
作者援引的 Google ADK 基准中,查询仍耗时 13 秒,首次开口却从 14.4 秒提前到 1.2 秒。对开发者有用的,是把「任务执行」「对话继续」「结果播报」拆开处理的思路,以及重复调用、取消和监控中的几个坑。
为什么加一句「请稍等」也救不了冷场
聊天界面可以用加载动画承接等待,电话里的长时间静音却容易让人以为连接断了。
原文中的普通工具 query_sales_database(region: str) -> dict,会先等待 run_expensive_query(region) 完成,再一次性返回结果。在它描述的实时双向会话里,模型正在等待这次函数响应,没有拿回控制权,提示词里的「告诉用户你正在处理」也就无法执行。
要让模型在等待时开口,先得让工具在任务完成前交还控制权。
原文援引 Google Developers Blog 于 2026 年 10 月 19 日发布的指南:在 gemini-3.8-live 的销售报告基准中,把一个工具从阻塞改成流式,首次开口时间从 14.4 秒降到 1.2 秒,数据库查询仍然需要 13 秒。
这里改善的是对话响应,不是查询性能。这个数字也属于原文引用的基准,不能当成作者自己的实测,更不能直接套到其他模型和项目。

有真实进度就流式返回,没有就后台等待
作者给出两种执行方式,区别在于等待过程有没有值得用户听的内容。
如果任务确实包含连接、扫描、聚合、格式化等阶段,可以把工具写成异步生成器(async generator)。它能多次产出结果,不必等全部结束才返回。
这段代码演示了如何让销售报告工具分阶段推送消息:
import asyncio
from typing import AsyncGenerator
async def query_sales_database(region: str) -> AsyncGenerator[str, None]:
"""Runs the quarterly sales report. Call this ONCE; it pushes updates."""
for message, delay in [
("Connecting to the sales warehouse", 1.5),
("Scanning order records", 3.5),
("Aggregating revenue by product line", 3.5),
("Formatting the report", 1.5),
]:
yield f"PROGRESS: {message}"
await asyncio.sleep(delay)
yield f"DONE: {region} revenue is up 12.4% year over year."
要看的是 yield:按原文描述,ADK 会识别生成器,将工具声明为非阻塞,并立即向模型发送确认。后续每次产出都作为部分函数响应送入会话,模型再把进度组织成口语。
不过,这段代码用 asyncio.sleep 模拟阶段,最终的同比增长 12.4% 也是写死的示例结果。实际接入时,进度应该来自真实任务状态。
如果等待期间没有有用的信息,就只在最后执行一次 yield await run_expensive_query(region)。工具仍是生成器,模型可以先收到待处理信号,说一句正在查询,然后等结果。用户中途问别的问题,也可以继续对话。
作者的选择标准很实用:有可讲的进展,就推送进展;只有等待,就让任务安静运行。不要为了填满静音,制造一串没有信息量的播报。
任务完成了,也不一定马上插话
任务怎么运行,与结果什么时候说出口,是两个独立决定。报告查完时,用户可能正在听另一个问题的回答。
ADK 通过 FunctionTool 的 response_scheduling 控制交付时机:
from google.adk.tools import FunctionTool
from google.genai import types
sales_tool = FunctionTool(query_sales_database)
sales_tool.response_scheduling = types.FunctionResponseScheduling.WHEN_IDLE
要注意最后一行:WHEN_IDLE 让结果等待当前发言结束。原文所述基准中,一份报告返回后,会等模型讲完另一段 20 秒的回答再播报;使用 INTERRUPT 则会中途打断模型。流式工具还可以逐条选择策略,例如中间进度设为 SILENT,最终结果设为 WHEN_IDLE。
真正上线,还要处理四件事。
第一,避免重复执行。用户重复询问时,模型可能再次调用尚未完成的工具。文档字符串里的 Call this ONCE 只是提示,作者示例用 _inflight: dict[str, asyncio.Task],以 customer_id:slot 为键拦住正在执行的预约。多实例部署则需要 Redis 加 TTL,或把幂等键传给下游 API。
第二,区分打断发言与取消任务。用户插话会停止音频并产生 interrupted 事件,后台工具仍会运行。若用户说「不用查了」,需要显式取消工具。原文给出的 ADK 接口是 stop_streaming(function_name),取消会在生成器内部触发 CancelledError,连接清理应放进 finally。
第三,控制进度密度。模型转述可能落后工具几秒,每 200 ms 推一次更新容易造成播报积压。作者建议选择三四个有意义的阶段,而非二十个零碎步骤。
第四,改监控指标。除了 p95 工具耗时,还应逐通话记录首次音频时间和最长静音时长,否则后台指标正常,也可能掩盖让用户挂断的体验问题。
我的判断
我认同把执行与播报分开。对语音交互来说,「多久有结果」和「多久能听到回应」确实是两件事。作者也没有声称这套方法能让数据库更快。
但那组 14.4 秒到 1.2 秒的数据,只能支持特定基准下的改善。原文没有提供样本量、网络条件、失败率或用户挂断率对照,「用户不再注意等待」仍是体验判断。三四个阶段更合适,也属于经验建议。
重复调用的防护还要再往前走一步。进程内 _inflight 能挡住执行期间的重复请求,任务结束后再次调用仍可能产生副作用;预约这类操作更需要下游幂等保障。取消生成器,也不能据此认定下游操作已经撤销。
作者把思路延伸到 OpenAI Realtime compatible endpoints、Pipecat 和 LiveKit Agents,这个方向成立;但原文没有给出对应实现。换框架时,需要核对确认消息、结果回注和取消的具体机制,不能照搬「改成 yield 就行」的结论。
能拿来做什么
- 做语音查询或报告助手时,先标出真正有意义的任务阶段,再选择逐步推送或最终一次返回,避免把每个内部步骤都播报给用户。
- 给实时会话增加独立的结果调度规则,明确哪些结果等待当前发言结束、哪些允许打断、哪些只更新内部状态。
- 在 Node 或其他服务端实现长任务时,把任务标识、重复请求防护、取消入口和资源清理一起设计;涉及预约等副作用时,优先落实下游幂等键。
- 在通话日志里同时记录首次音频时间、最长静音和工具耗时,先确定用户是在等计算,还是在等一句回应,再决定优化方向。