做语音交互系统时,延迟是最核心的体验指标。用户说完话,2 秒内没有响应就会觉得系统卡顿;超过 3 秒基本就是不可用。
但 ASR(语音识别)+ LLM(生成回复)+ TTS(语音合成)三段链路叠加,每段都有延迟,不做优化轻松超过 5 秒。
这篇文章记录我在优化这条链路时的思路和实际效果。
先搞清楚延迟在哪里
最直观的方式是把每段时间单独测出来:
import time
t0 = time.time()
# ASR:用户说完话到识别出文字
asr_result = asr_recognize(audio_chunk)
t1 = time.time()
# LLM:文字送进去到开始出第一个 token
first_token = next(llm_stream(asr_result))
t2 = time.time()
# TTS:第一句话合成完成到音频可播放
audio = tts_synthesize(first_sentence)
t3 = time.time()
print(f"ASR: {(t1-t0)*1000:.0f}ms")
print(f"LLM TTFT: {(t2-t1)*1000:.0f}ms") # Time To First Token
print(f"TTS: {(t3-t2)*1000:.0f}ms")
我的测试环境(本地 GPU,调用云端 LLM API):
| 阶段 | 延迟 |
|---|---|
| ASR(FunASR 本地) | 300-500ms |
| LLM TTFT(GPT-4o) | 500-1200ms |
| TTS 首句(CosyVoice) | 400-800ms |
| 串行总计 | 1200-2500ms |
如果三段串行,最快也要 1.2 秒。这还是理想情况,实际网络抖动、LLM 负载高的时候更慢。
核心优化:流水线并发
串行是最大的浪费。优化的核心思路是:不等上一段全部完成,拿到足够的输出就立刻送给下一段。
ASR 流式识别
大多数 ASR 服务支持流式模式——边说边识别,识别到一个句子就输出,不用等整段话说完。
async def stream_asr(audio_stream):
async for chunk in audio_stream:
result = await asr_client.recognize_streaming(chunk)
if result.is_final: # 识别到完整句子
yield result.text
这样用户还在说话时,第一句就已经在处理了。
LLM 流式输出 + 句子切分
LLM 的流式输出是 token 级的,但 TTS 需要完整的句子。所以要做句子切分——攒够一个句子就送给 TTS,不用等全文生成完。
async def stream_llm_sentences(prompt: str):
buffer = ""
async for token in llm_client.stream(prompt):
buffer += token
# 检测句子边界:句号、问号、感叹号后面跟空格或换行
sentences = re.split(r'(?<=[。!?.!?])\s*', buffer)
if len(sentences) > 1:
# 至少有一个完整句子
for sentence in sentences[:-1]:
if sentence.strip():
yield sentence.strip()
buffer = sentences[-1] # 保留未完成的句子
if buffer.strip():
yield buffer.strip()
TTS 异步合成
每拿到一个句子就异步启动合成,合成完成就加入播放队列:
import asyncio
async def pipeline(user_audio):
play_queue = asyncio.Queue()
async def synthesize_and_enqueue(sentence):
audio = await tts_client.synthesize(sentence)
await play_queue.put(audio)
# ASR → LLM → TTS 全程流水线
async for asr_text in stream_asr(user_audio):
async for sentence in stream_llm_sentences(asr_text):
asyncio.create_task(synthesize_and_enqueue(sentence))
return play_queue
效果: 流水线并发后,用户说完话到听到第一句回复的时间从 2-5 秒降到 800ms-1.5 秒。
VAD 端点检测的坑
VAD(Voice Activity Detection)决定”用户什么时候说完了”,这个判断直接影响响应速度和体验。
问题: VAD 的静音判定阈值很难调。
- 太短(200ms):用户说话中间停顿一下就被切断,句子残缺
- 太长(800ms):用户说完要等很久才开始响应,感觉卡顿
我的方案:动态阈值
class AdaptiveVAD:
def __init__(self):
self.silence_threshold = 400 # 初始阈值 ms
self.speech_duration = 0
def on_speech_end(self, duration_ms):
# 说话越长,允许的停顿也越长
if duration_ms > 5000:
self.silence_threshold = 600
elif duration_ms > 2000:
self.silence_threshold = 500
else:
self.silence_threshold = 350
短问题(“几点了”)停顿短就切,长段叙述停顿可以稍微长一些。
另一个坑:背景噪音。噪音环境下 VAD 误判很多,Silero VAD 的表现比简单的能量检测好很多,推荐直接用。
各组件实际选型
| 组件 | 我的选择 | 备注 |
|---|---|---|
| ASR | FunASR(本地) | 中文效果好,支持流式,需要 GPU |
| ASR(备选) | 阿里云 ASR | 不想维护本地服务时用 |
| LLM | GPT-4o / Claude | 按场景切换,用统一路由层 |
| TTS | CosyVoice | 音色自然,支持克隆;慢,需优化 |
| TTS(快速版) | Edge TTS | 微软,免费,延迟低,音色一般 |
| VAD | Silero VAD | 准确率高,CPU 可用 |
总结
降低语音交互延迟的核心是两点:
- 流水线并发:ASR 出句子就送 LLM,LLM 出句子就送 TTS,三段并行而不是串行
- 句子级处理:不要等全文,句子是最小有效单元
优化后的首字节音频延迟(用户说完到听到第一个字)稳定在 800ms 以内,整体流畅度从”明显卡顿”变成”基本自然”。
下一步准备测试端侧 ASR(Whisper.cpp 量化版)和更快的 TTS 方案,进一步压缩链路延迟。