语音 Agent 延迟评估先统一计时边界
同一语音平台的延迟指标可能因计时起止点不同而相差显著,后台数字也未必对应用户感知。文章拆解运营商传输、停顿判定、语音识别、模型生成与语音合成等环节,讨论如何建立有意义的测量口径。
AI 深度解读
语音 Agent 号称 500 毫秒就能回复,先别急着判断快慢:这个数字从用户说完话开始算,还是从系统确认用户说完后才算?终点是生成第一块音频,还是用户耳朵里真的响起声音?作者看到同一个语音平台的三个基准给出不同结果,问题就出在计时边界。
这篇文章的结论很实用:先把每一段等待测清楚,再讨论优化。对于正在接 STT、LLM、TTS 或电话链路的开发者,它提供了一套能落地的埋点与校准办法,也解释了为什么后台指标漂亮,通话仍然让人等得着急。
500 毫秒到底漏掉了哪段等待
作者讨论的是级联语音 Agent:语音转文字(STT),接大模型(LLM),再接文字转语音(TTS)。
用户说完「我想查订单状态」,声音先经过运营商网络到达媒体服务器。系统还要等一段静音,判断用户是否结束发言,这一步叫端点检测(endpointing),也叫轮次检测。随后才是最终转写、LLM 输出首批 token、TTS 生成首块音频,再经过出站媒体链路、网络与接收端的抖动缓冲区,最终播放。
抖动缓冲区用来吸收数据包到达时间的不均匀,因此会主动增加延迟。音频已经生成,不代表用户已经听见。
常见计时起点有三个:
- 从 endpoint 事件开始,埋点方便,却漏掉了此前等待用户结束发言的静音,可能已有数百毫秒。
- 从最终转写开始,适合观察后面的 LLM 与 TTS,却覆盖不到轮次检测和 STT。
- 从用户最后一个词结束开始,最接近用户感受,但需要事后分析音频,系统没有一个恰好对应的现成事件。
只有起点和终点一致,两个延迟数字才有比较价值。
还要看测试走的是电话、浏览器 WebRTC,还是没有网络的本地链路;统计的是中位数、平均值,还是单次最佳结果;这一轮有没有调用工具。短回答和等待工具返回的回答,也不能直接放在一起比。
用一套时钟,把等待拆成能排查的阶段
作者建议给每轮对话分配 ID,并在同一台机器上用同一个时钟记录阶段时间。这样,看到总耗时上升时,才能继续追问:慢在轮次判断、模型,还是音频发送?
下面这段 TypeScript 用来记录阶段首次发生的时刻,并计算相邻阶段的间隔:
type Stage =
| "speechEnd" // 从入站音频的 VAD 得到,尽量接近用户最后一个词的结束
| "endpoint" // 轮次检测确认用户已经说完
| "sttFinal" // 最终转写已就绪
| "llmFirstToken"
| "ttsFirstAudio" // TTS 返回第一块音频
| "firstPacketOut"; // 向用户发送第一个出站媒体包
class TurnTimer {
private marks = new Map<Stage, number>();
constructor(public readonly turnId: string) {}
mark(stage: Stage) {
if (!this.marks.has(stage)) {
this.marks.set(stage, performance.now());
}
}
between(from: Stage, to: Stage): number | undefined {
const a = this.marks.get(from);
const b = this.marks.get(to);
return a !== undefined && b !== undefined ? b - a : undefined;
}
summary() {
return {
turnId: this.turnId,
endpointWait: this.between("speechEnd", "endpoint"),
stt: this.between("endpoint", "sttFinal"),
llm: this.between("sttFinal", "llmFirstToken"),
tts: this.between("llmFirstToken", "ttsFirstAudio"),
sendOut: this.between("ttsFirstAudio", "firstPacketOut"),
serverSide: this.between("speechEnd", "firstPacketOut"),
};
}
}
要盯住 speechEnd 的来源:它应来自入站音频上的语音活动检测器(VAD),不能拿 endpoint 决策事件代替,否则最需要暴露的静音等待又消失了。
mark 只保留首次记录,避免流式系统重复触发事件时覆盖时间;performance.now() 使用单调时钟,不会因服务器墙上时间被校正而跳变。缺少任一阶段时,between 返回 undefined,也便于识别记录不完整的轮次。
不过,这些字段描述的是事件之间的间隔。例如 stt 是 endpoint 到最终转写就绪的时间,不宜直接理解为 STT 的全部计算耗时。
serverSide 也只是从服务端估计的语音结束,到首个出站包发出。它没有覆盖完整运营商链路与用户端抖动缓冲,不能直接当成用户实际等待时间。
用真实通话校准,再去看最慢的轮次
要知道用户究竟等了多久,作者建议从系统外部测量:录制测试通话,把用户和 Agent 的声音放在两个独立声道,再分别运行 VAD,计算用户语音结束到 Agent 语音开始的间隔。
录音位置决定测量边界。通过网络外部的测试电话或 SIP 客户端,在用户侧录音,才能把回程网络与接收端抖动缓冲的延迟纳入。媒体服务器上的录音覆盖不到这一段。
这套办法不必跑遍每一通生产电话。作者建议保留一组固定测试通话,每次更换模型、区域或 endpointing 设置,就沿真实电话链路重测,观察内部指标与用户侧录音之间的差距。
统计方式也要跟上。作者建议对每个阶段看中位数和第 95 百分位数(P95),再人工检查最慢的单独轮次。P95 可以理解为:约 95% 的观测值不超过这个耗时。
语音延迟可能有长尾,少数轮次会达到通常耗时的两三倍。原因可能是 LLM 请求慢、工具调用拖延,或者噪声让 STT 迟迟无法定稿;用户说话逐渐变轻,也可能让轮次检测等得太久。平均值把这些卡顿摊薄了,用户却可能已经开始说「喂?」并与 Agent 抢话。
我的判断
我认同这篇文章把计时边界放在优化前面。对于语音产品,漏掉静音等待和播放前的传输,再漂亮的模型耗时也解释不了用户为什么觉得慢。阶段埋点与外部录音各有用途,前者方便定位,后者负责校准体验。
但它提供的是测量方案和经验判断,没有展示三份基准的具体数据,也没有实测报告证明采用方案后能降低多少延迟。文中的「两三倍」长尾描述,不能当成某个平台的性能结论。
落地还有两个前提。首先,VAD 只是估计发言边界,原文没有交代检测参数及误差。其次,代码假设时间点能在同一台机器上记录;如果 Node 服务、转写服务和媒体服务分散部署,不能直接相减各进程的 performance.now()。跨服务如何统一计时,原文没有展开。
相比只盯 LLM 首 token 或 TTS 首音频,这套方法覆盖得更完整。不过,固定用例测出的内部与外部差距,也应随链路变化重新校准,不能长期当成一个恒定补偿值。
能拿来做什么
- 在 Node 语音编排服务中,以
turnId关联阶段记录,单独展示endpointWait,排查慢回复时先确认时间花在哪一段。 - 给监控面板写清计时边界,例如「入站 VAD 结束至首个出站媒体包」,并同时展示中位数和 P95,避免把服务端数字标成用户听到回复的耗时。
- 更换模型、部署区域或轮次检测参数时,跑同一组真实电话测试,用用户侧双声道录音核对等待间隔。
- 评估供应商时,把计时起止点、通话链路、统计口径及工具调用条件列在同一张表里,条件对齐后再比较数字。