语音 Agent 的延迟藏在三段流水线里
文章拆解语音识别、语言模型与语音合成的衔接,说明串行等待如何累积通话延迟。内容包含可运行的 Python 骨架,并讨论流式处理、轮次判断、电话传输和业务工具接入。
AI 深度解读
电话里的 AI 听起来像一个助手,背后却是语音转文字(STT)、大语言模型(LLM)和文字转语音(TTS)在接力。作者的核心判断是:通话是否自然,往往取决于三段系统怎么连接,不能只看模型回答得好不好。
这篇最有用的部分,是把「回答太慢」拆成了可以定位的工程问题:哪些等待可以重叠,用户插话时该取消什么,以及每轮对话该记录哪些时间。不过,原文给的是结构示意,没有真实通话的性能对照,示例与生产实现之间也有距离。
为什么三个 API 都很快,电话还是会冷场
一次语音交互的基本路径是:STT 把来电者的音频转成文字,LLM 结合文字和对话上下文生成回复,TTS 再把回复变成音频。
流式 STT 会在用户说话期间持续给出临时识别结果,结束后给出最终结果;LLM 则可以逐个 token 输出内容。电话线路负责传输音频,轮次检测负责判断用户是否说完,业务逻辑还要处理工具调用、CRM 查询和转人工。
最直接的实现,是等最终转写完成,再等模型生成完整回答,最后合成整段语音。这条路径实现容易,代价是等待逐段累加。用户听到的只有沉默,很容易怀疑电话断了。
作者建议:第一句话完整生成后,就交给 TTS,后面的回答继续生成。
降低开口前的等待,比单独追求某个模型的速度更接近这个问题的要害。
但原文没有给出延迟数据,因此我们只能认可这个调度方向,不能据此推断能快多少。
按句交给 TTS,示例究竟做到了哪一步
下面这段核心代码解决的是:把 LLM 的流式回复积累成句子,每完成一句就合成并送入音频输出队列。transcribe、llm_stream 和 synthesize 在原文中都是模拟函数。
import asyncio
import re
SENTENCE_END = re.compile(r"(?<=[.!?])\s+")
async def handle_turn(audio_chunks, audio_out):
final_text = ""
async for text, is_final in transcribe(audio_chunks):
if is_final:
final_text = text
buffer = ""
async for token in llm_stream(final_text):
buffer += token
parts = SENTENCE_END.split(buffer)
# every part except the last is a complete sentence
for sentence in parts[:-1]:
await speak(audio_out, await synthesize(sentence))
buffer = parts[-1]
if buffer.strip():
await speak(audio_out, await synthesize(buffer.strip()))
重点看 await speak(audio_out, await synthesize(sentence)):它让完整句子及时进入语音合成,但也会等这句合成、入队结束后,才继续消费 LLM 流。这里没有独立的生成任务和合成任务,不能把它理解成已经完成了充分并发的流水线。
另外,STT 的临时结果虽然持续到达,这段代码仍然只在 is_final 为真时保存文字,并在转写迭代结束后启动 LLM。它主要改善的是「必须等整段回答生成完才开始合成」的问题,没有展示 STT 与 LLM 的重叠执行。
原文用 asyncio.sleep(0.01) 模拟转写和合成,用 asyncio.sleep(0.005) 模拟 LLM 输出。这些是演示用的等待时间,不是真实服务测量值。
还有两处容易误读。首先,分句正则只匹配英文 . ! ? 后面的空白,直接用于中文回复并不合适。其次,原文称运行后会逐句打印,但所给骨架实际把字节放入队列,没有提供启动入口和消费队列的打印逻辑;synthesize 也只是把文字编码成字节,并未生成真实语音。
真正影响通话体验的,是打断与异常处理
按句播报只是开始。作者列出的生产设计选择,更接近语音产品实际会遇到的问题。
轮次检测需要结合 STT 的语音结束信号与一小段静音窗口,避免用户停顿一下就被抢话。作者认为,打断用户比稍微多等一会儿更糟,但没有给出通用窗口长度,建议用真实通话调节。
插话处理(Barge-in)则是反方向的问题:用户已经开口,助手还在继续说。此时不仅要停止播放,还要取消待完成的 LLM 和 TTS 工作,再开始新一轮。否则上一轮的内容可能继续消耗资源,甚至混入后续播放。
业务数据通过工具查询,提示词保持精简。遇到较慢的工具,作者建议先说一句简短的等待提示。低识别置信度、API 超时和用户要求人工,也都应提前定义处理路径。
最后是观测:每轮对话都记录各阶段时间戳,才能知道慢在转写、生成还是合成。只记录请求总耗时,很难支持具体优化。
我的判断
我认同作者把注意力从「换个更强模型」转向系统连接。语音交互既要回答正确,也要在合适的时机开口、停口,这些无法只靠模型能力解决。
但「大部分质量问题来自连接方式」是作者的经验判断,原文没有提供故障统计或对照实验。串行方案可以作为原型起点,按句播报则增加了分句、取消和状态管理的要求,不能只替换一个循环就视为生产方案。
代码尤其应该当作调度思路阅读。它没有展示真实电话接入、可听音频、插话取消和异常恢复,也没有证明端到端体验改善了多少。真正落地时,仍需用实际通话验证。
能拿来做什么
- 做语音助手原型时,可以先沿用「最终转写、流式生成、按句合成」的结构,明确每个接口收什么、返回什么,再接入真实服务。
- 做中文语音场景时,应先检查分句规则,至少覆盖中文句末标点,并验证句子切分是否影响播报的连贯性。
- 实现插话功能时,把停止播放、取消 LLM、取消 TTS 作为同一轮对话的结束动作,避免只静音却让旧任务继续运行。
- 排查响应慢时,给 STT、LLM、TTS 和播放环节记录时间戳,用实际等待位置决定下一步优化哪一段。