AI 语音实时对话如何降低延迟
约 4380 字大约 15 分钟
2026-06-01
一条语音对话是怎么完成的
在聊延迟治理之前,先快速对齐一下语音对话的完整流程和关键术语。
用户对着设备说一句话,到听见 AI 回复,整个链路大致是这样:
用户说话
→ 设备麦克风采集音频
→ VAD 判断"开始说话了"
→ 音频流推送到服务端
→ ASR 把音频转成文字("音量多少")
→ LLM 理解意图并决定怎么回复
→ 文本回复经过句子分割后送入 TTS
→ TTS 合成音频流
→ 音频推回设备播放("当前音量是55%哦~")这条链路上有几个关键名词:
| 术语 | 全称 | 作用 |
|---|---|---|
| VAD | Voice Activity Detection(语音活动检测) | 判断用户是否在说话,决定何时开始和结束语音识别;过滤环境噪音,避免无效音频占用带宽和 ASR 资源 |
| ASR | Automatic Speech Recognition(自动语音识别) | 把用户的语音音频转换成文本 |
| LLM | Large Language Model(大语言模型) | 理解用户意图、调用工具、生成回复文本 |
| TTS | Text-to-Speech(文本转语音) | 把 LLM 生成的文本回复转换成语音音频 |
| TTFT | Time To First Token | LLM 从收到输入到输出第一个文字 token 的耗时 |
| 首帧延迟 | TTS First Frame Latency | TTS 从收到文本到输出第一帧音频的耗时 |
端到端延迟通常指从用户说完话到听到 AI 第一句语音之间的总时间。它不是一个单点耗时,而是上面整条链路的叠加结果。
为什么需要 VAD,而不是直接把音频推给 ASR
一个常见的疑问是:为什么不能跳过 VAD,让设备直接把音频流实时推给 ASR?
原因是 ASR 本身只负责"把音频转成文字",它并不知道"这段话从哪里开始、到哪里结束"。如果没有 VAD,至少会带来三个问题:
- 带宽和计算浪费。没有 VAD 意味着设备需要持续推流——环境噪音、沉默、翻书声、走路声都会被送到服务端,既浪费网络带宽,也消耗 ASR 的计算资源。
- 识别边界不清。ASR 无法自主判断用户说完了,导致一段语音可能被切得太短(还没说完就提交了)或太长(把后面的噪音也识别进去)。
- 打断无法感知。在 AI 回复期间,VAD 负责检测用户是否插入了新的语音输入,从而触发打断。没有 VAD,系统根本不知道用户试图说话。
所以 VAD 不是"可有可无的优化项",而是语音链路中负责定义语音边界的关键组件。它的判断质量直接决定了 ASR 何时启动、何时收口、以及打断能否被正确感知。
在 AI 中台的前几篇文章里,我们聊了记忆召回、知识库检索和会话管理各自是怎么影响延迟的。但那些讨论主要停留在文本对话场景。一旦用户开口说话,整个延迟的体感会被放大很多倍——文本里等两秒还能接受,语音里等两秒就像面对一个反应迟钝的人。
联犀在把 AI 能力接入 IoT 设备语音交互的过程中,端到端延迟从最早的 5 秒以上逐步压到了 800 毫秒左右。这篇文章想分享的,不是某个单一技巧,而是整条语音链路(ASR → LLM → TTS)在各个阶段上的延迟治理思路。
语音延迟为什么比文本更难容忍
文本对话的延迟,用户感知的是"字有没有开始出来"。流式输出一旦启动,哪怕总时间较长,首字延迟如果够短,体感上就不会太差。
语音不一样。用户说话结束到听到 AI 回复第一句话之间,存在一个完全空白的等待窗口。这个窗口里系统其实在干很多事:收完音频、做语音识别、等模型推理、合成语音、推送音频。但用户感知不到这些中间过程,只能感受到一段"沉默"。
所以语音延迟治理的第一原则,不是让总回复变短,而是让首句语音尽快出现。
怎么定义和观测语音延迟
在优化之前,必须先能拆账。联犀把语音对话的端到端延迟拆成 4 段:
| 指标 | 含义 |
|---|---|
end→stt | 用户说话结束 → ASR 识别完成 |
stt→created | ASR 完成 → LLM 首 token 到达 |
created→audio | LLM 首 token → TTS 首帧音频 |
end→audio | 用户说话结束 → 听到 AI 第一句语音 |
优化前,典型场景的 end→audio 普遍在 5 秒以上,早期版本甚至超过 10 秒。拆细后可以看到,stt→created 和 created→audio 是两大主要瓶颈,真正花在语音识别上的只有几百毫秒,大头全在 LLM 和 TTS 启动阶段。
ASR 侧:让语音识别不再成为堵点
ASR 本身的识别速度其实不慢,但它周围的连接和等待很容易变成隐性延迟源。
异步建连与音频缓冲
早期 ASR 的 WebSocket 连接是在收到首帧音频时才建立的,建连期间的音频帧因为没有缓冲而被直接丢弃。改动后把 StreamingRecognize 移到独立 goroutine,同时将 audioChan 缓冲从 50 帧扩到 200 帧(约 4 秒),ASR 异步建连期间音频不再丢帧。
连接复用
每轮对话都重新走一次 TCP + TLS + WebSocket 握手,在设备场景下成本很高。通过 sync.Once 预热机制,首次识别时建立连接,后续轮次直接复用。实测冷启动约 1.08ms,热连接约 0.31ms,mock 环境下提升 3.5 倍,真实网络环境下收益更大。
收敛参数调优
豆包 ASR 的 end_window_size 从 600ms 逐步降到 200ms,缩短了服务端判断"用户说完"的时间。这个参数需要谨慎调整——降太低会误切自然停顿的从句,200ms 是在低延迟和准确率之间取的一个实测平衡点。
LLM 侧:推理路径的简化和 Prompt 的瘦身
stt→created 这一段曾经是最大瓶颈。拆细后发现,4 秒多的延迟里真正的大头是同步记忆检索(约 3 秒),其次是模型本身的首 token 延迟。
放弃原生 ReAct,改走单模型自决策
这里有一个容易被忽视的架构决策。联犀最初在设计上考虑过直接接入 eino 的 react.Agent,走标准的 ReAct 循环。但实际落地时放弃了这个方案,原因是:
- 不同模型的函数调用格式不完全一致(标准
tool_calls、豆包专有格式、JSON fallback) - 需要支持前端 tool 执行后回传结果再继续推理,标准 ReAct 的循环语义不够灵活
- 双模型(有工具 / 无工具)的设计带来额外的路由和切换开销,一次请求最多可能调两次 LLM(先无工具快排,再带工具正式调用)
- 涉及 tools 时默认难以开启流式输出,因为工具调用标记在流中分散出现,实时识别困难
最终改为基于 ToolCallingChatModel 的自定义单模型运行时。所有调用都带 tools schema,由 LLM 自己决定调不调工具,不再依赖关键词预判或快速路径路由。这个改动看似只是架构选择,实际上对延迟的影响很直接:消除了"先判断走哪条路"这层决策成本,也避免了双模型切换时的额外一次 LLM 调用。
后续进一步移除了 shouldUseVoiceFastPath、IsCasualChat 等关键词预判逻辑,以及独立的快捷回复路径。所有输入统一走 LLM,system prompt 中定义的身份、设备信息和物模型上下文才真正生效。
Prompt 瘦身
System Prompt 的长度直接影响首 token 延迟。LLM 在输出第一个 token 之前,必须先把所有输入 token(包括 system prompt、历史消息、工具描述)全部过一遍 attention 计算。prompt 越长,这个前置处理时间越长。
联犀实测过一组数据:某 clone 的 MemoryProfile 全文加载到 system prompt 后,prompt 膨胀到 5551 字符,对应的 LLM 首 token 延迟超过 10 秒。把 profile 压缩到 1000 字符、整体 prompt 控制在 2000-3000 字符后,同一场景的首 token 延迟降到了 3 秒以内。50 字和 800 字的 system prompt,TTFT 差距可达数百毫秒。
几轮压缩下来:
maxSkillKnowledgeLen从 15000 降到 6000,skill 注入量减少 60%- 硬编码的知识库指令(约 500 字符)改为从 KB 元数据动态生成,无 KB 时完全不出现
memoryArchiveRules从约 400 字符压缩到一行 80 字符- 增加 60 字长度约束,减少 LLM 生成时间和 TTS 数据量
同时调整 System Prompt 结构,增加对话类型判断分层指引(聊天 / 知识 / 设备状态 / 设备控制 / 实时数据),让模型先判断场景再决定是否调工具,减少无意义的工具调用轮次。
记忆检索分层
同步注入长期记忆曾是 stt→created 里 3 秒延迟的主要来源。早期的记忆检索基于 embedding 向量,每次召回都需要调用外部 embedding API 做向量化,再加上向量相似度计算,耗时往往在秒级。后来改为基于全文索引的文本检索(Jaccard + trust_score),直接从数据库做文本匹配,召回延迟从秒级降到了毫秒级。关闭动态记忆同步注入后,runtime ready 进一步从约 3 秒降到了亚毫秒级。记忆不是不用,而是改为按需、分层、异步召回,避免每轮都跑完整的检索链。
TTS 侧:连接预热与流式合成
created→audio 这一段的核心问题是"TTS 还没准备好"。
连接预热与 Session 复用
和 ASR 类似,TTS 的 WebSocket 连接也被预热了:sessionCreated 阶段后台异步建连,synthesize 结束时异步 preWarmSession 创建下一个 session。下次合成时若配置匹配直接复用 sessionID,跳过 StartSession 握手。
增量流式合成
早期 TTS 等 LLM 输出完整句子后才送入合成,句子积累延迟约 200-500ms。改为增量流式方案后,LLM token 直接增量送入 TTS,单 session 内多次 TaskRequest 合成,首句 TTS 延迟大幅降低。
当前代码中 TTS 管线优先使用 StreamingProvider(增量发送),仅在不可用或异常时回退到并行合成 + 顺序播放的方案。
句子分割器调优
voiceSentenceSplitter 的 minLen 从 4 降到 1,让第一个标点就能触发首句输出。比如 "嗯,灯已经关啦~",旧逻辑要积累 4 个字符才分割,新逻辑在 "嗯," 处就立即输出第一段,消除了约 100-200ms 的首句积累延迟。maxLen 从 30 降到 20,首句在逗号处即可分割,后续按完整句末标点分割。
VAD 与打断:静音判断和回声治理
VAD(语音活动检测)是整条链路的"守门人",它的判断直接影响 ASR 何时启动、何时结束。VAD 太松会误收噪音和回声,太严会切掉正常语音尾部。
参数调优
| 参数 | 调整方向 | 效果 |
|---|---|---|
threshold | 0.15 → 0.5 | 修复 threshold 过低导致检测器触发后无法退出的 bug |
release_after_frames | 20 → 50 | 约 3 秒词间停顿容忍度 |
min_silence_duration_ms | 320 → 250 | Silero 内部静音判定减少 70ms |
speech_pad_ms | 160 → 120 | 语音段前后填充减少 40ms |
| 滑动窗口进入阈值 | 3/10 → 2/10 | 改善安静语音的检测灵敏度 |
打断与回声
AI 回复期间 TTS 播放的回声容易被 VAD 误判为新语音,反复触发打断。修复方向有两个:一是把 initialReplyInputMuteWindow 从 500ms 提升到 2000ms,延长 TTS 播放后的输入屏蔽窗口;二是把 VAD 静音判断从基于 time.Since 改为仅基于 idleMs(按 frameDuration 累积),避免系统调度延迟导致的过早分段。
链路级优化:跨层协同
除了各层内部优化,还有一些需要跨层配合的改动:
首句 eager ASR 启动
把 startAsr() 从 lazy(首帧音频时才启动)提前到 voiceLoop 初始化时就启动,消除首句 WebSocket 建连延迟。后续回合仍通过 lazy 机制在 closeAsr 后重启。
音频预缓冲直通
sendAudioPaced 增加 preSendFrames 参数,前 5 帧直通不发 pacing,让首帧音频更快到达设备端。
respCreated 时机修复
respCreated 之前发送得过早,导致 LLM 延迟被测成 0ms。改为首个非空 token 到达时才发送,让观测数据真实反映模型首 token 延迟。
优化效果
同一套 devicesim-latency-breakdown 口径下,query 场景的对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
end→stt | ~300ms | ~250ms |
stt→created | ~4s | ~400ms |
created→audio | ~6s | ~150ms |
end→audio | 5s+ | ~800ms |
多轮压测(3 设备 × 10 轮)全部通过,fail_count = 0。短设备查询、短偏好表达的首响已稳定在 800ms 左右。
模型横向对比的意外发现
同一条语音链路上,不同模型的延迟表现并不完全符合直觉:
| 模型 | end→audio |
|---|---|
doubao-seed-2-0-lite-260215 | 1.167s |
doubao-1.5-pro-32k-250115 | 1.215s |
doubao-seed-2-0-code-preview-260215 | 1.364s |
doubao-seed-2-0-mini-260215 | 1.593s |
doubao-seed-2-0-pro-260215 | 1.669s |
Mini 和 Pro 反而更慢。这说明在语音延迟这条链上,模型参数规模不是唯一决定因素,模型的首 token 响应速度、工具调用格式的处理效率,以及和整套管线配合的流式行为,都会影响最终体感。
DeepSeek 的思考模式陷阱
DeepSeek 模型(如 deepseek-v4-flash)的 TTFT(首 token 时间)实测约 200ms,看起来比豆包更快。但它默认会开启 thinking/reasoning 模式——即使是一个简单的"音量多少"查询,模型也会先输出一段内部思考过程,再给出回答。这段思考过程对用户不可见,但会显著增加实际响应时间和 token 消耗。
联犀的处理方式是透传 reasoning_effort 或 extra_fields: {"thinking": {"type": "disabled"}} 来关闭思考模式。关闭后 DeepSeek 才能进入正常的低延迟对话节奏。但即使关闭思考,由于工具调用格式和流式行为与豆包存在差异,实际端到端延迟并未比豆包 lite 更优。最终在生产环境退选了 DeepSeek 的默认位,统一由豆包提供服务。
延迟治理的核心思路
回头看这整个过程,语音延迟治理最关键的三件事:
第一,拆账。如果不把 end→audio 拆成 end→stt→created→audio,团队只能笼统地说"AI 慢"。一旦拆开,问题就变成可执行的工程决策:是记忆检索太重、还是 TTS 首帧太慢、还是 ASR 建连卡了。
第二,按层治理,但不止于层内。ASR、LLM、TTS 每层都有独立优化空间,但更大的收益往往来自层间协同:LLM token 增量送入 TTS、ASR 连接提前预热、eager 启动和预缓冲直通,都是跨层设计。
第三,架构简化本身也是延迟优化。从双模型 ReAct 到单模型自决策,从关键词预判到统一 LLM 路径,从 embedding 向量检索到全文索引——这些架构层面的"减法"消除了原本以为是必要的路由和切换成本。很多时候延迟不是某个模块不够快,而是链路本身设计得太复杂。
写在最后
联犀把语音端到端延迟从 5 秒以上压到 800 毫秒左右,靠的不是某个黑科技,而是在整条链路上做了一遍"拆账、定位、精简、协同"的系统工程。
语音延迟治理有一个特点:它很难通过单点突破解决。ASR 再快,也救不了 LLM 10 秒的首 token 延迟;TTS 再优化,也弥补不了每轮都重建连接的浪费。真正有效的优化,必须站在整条链路的视角,找到当前阶段的真正瓶颈,再针对性动手。
还有一个容易被忽视的结论:模型选型和架构选择对延迟的影响,往往比工程调优更大。DeepSeek 的 TTFT 看起来比豆包快,但默认开启的思考模式让它在实际对话中反而更慢;双模型 ReAct 的设计初衷是提速,但路由切换的额外一次 LLM 调用反而增加了延迟。这些教训说明,在动手优化之前,先确认"当前的架构设计是否本身就在制造延迟",可能比调参数更重要。
语音对话的延迟不可能降到零——物理传输、模型推理、语音合成都有各自的硬边界。但当用户对着设备说"音量多少",能在不到一秒后听到"当前音量是55%哦~"的回复时,这种"对话感"就已经建立起来了。让首句语音在合理时间内出现,让用户感受到"我在和一台反应正常的设备对话"——这就是语音延迟治理的终极目标。
更新日志
2026/6/1 23:31
查看所有更新日志
521a0-docs(blog): 新增 AI 语音实时对话延迟优化技术分享文章于
