网页里跑一个13秒的数据库查询,转个圈,没人抱怨。换成电话,同样13秒就是一片死寂——而死寂听起来像掉线。人会挂断。

打开网易新闻 查看精彩图片

谷歌ADK团队本周发布的一份指南给这件事标上了数字。在gemini-3.8-live的销售报告基准测试里,把一个工具从阻塞式改成流式,智能体开口说第一句话的时间从14.4秒降到1.2秒。查询本身还是花了13秒。只是打电话的人不再察觉了。

为什么在系统提示里加一句"告诉用户你正在处理"没用?原因很机械。普通工具是一个协程,执行完才返回:

  • 模型这一轮在等函数返回结果
  • 它没法说出"稍等"这句话,因为控制权还没交回来
  • 修复必须在架构层面:工具得在活干完之前先把控制权交出去

有过程就流式播报

如果任务有真实阶段——连接、扫描、聚合、格式化——就把工具写成异步生成器。在ADK里,把return改成yield就够了。框架会把声明标记为非阻塞,并立刻给模型发一个确认,于是智能体可以在活还在跑的时候说话。

每一次yield都以部分函数响应的形式到达模型,模型再转述出来:"好,我已经进到仓库里了,正在拉订单记录。"来电者听到的是进展,不是沉默。如果他在查询中途问了个不相关的问题,智能体也能回答,而且不会取消后台任务。

只是等待就放后台

用户已经走神了,还逐段播报就很烦。如果没什么值得说的,就只在最后yield一次。因为它仍然是生成器,模型会立刻收到一个挂起信号,说一句"我正在整理",然后安静到结果落地。

执行形态和交付时机是两个独立的旋钮。ADK把后者暴露为FunctionTool上的response_scheduling。在谷歌的基准测试里,一份在WHEN_IDLE下到达的报告会礼貌地等模型讲完另一段20秒的回答,再插进去;在INTERRUPT下它会打断模型说到一半的句子。流式工具甚至可以逐条更新做选择:中间进展保持SILENT,最终数字用WHEN_IDLE交付。

指南里那条经验法则值得记住:等待有故事,就流式播;等待只是等待,就放后台。

真实来电者出现后才会咬人的细节

  1. 重复调用。流式工具横跨好几轮,模型听到用户重复自己的话,可能再调一次。文档字符串里写"只调一次"有帮助,但真正兜底的是锁存,尤其当工具带副作用、比如订时段的时候。多实例部署时,把这个键挪到带TTL的Redis里,或者作为幂等键传给下游API。
  2. 插话不会取消工具。来电者抢话时,音频停了,你会收到一个interrupted事件,但正在跑的工具继续跑,结果照样送达。想让"算了不用了"真的停下工作,得给智能体一个显式的取消工具。ADK内置的stop_streaming(function_name)会取消生成器的任务,并在其中抛出CancelledError,所以把连接清理放进finally
  3. 播报会滞后。模型是边收边转述的,转述可能落后工具好几秒。别每200毫秒吐一行进展,三四个有意义的阶段胜过二十个碎步。
  4. 量对数字。这一整套里后端延迟一点没变。变的是首字音频时间和通话中最长静默时长。每次通话都记这两个。只盯工具延迟的p95,你永远看不到用户挂断的那个问题。

这套思路不限于ADK。在任何实时栈上——OpenAI Realtime兼容端点、Pipecat、LiveKit Agents——模式都一样:先给模型一个即时确认,把活当任务跑,结果到了就推回会话,再决定每一条该打断、该等待还是该沉默。

一个网页加载十秒,感觉是慢。一个语音智能体安静十秒,感觉是坏了。这个落差就是这套模式存在的全部理由。