做语音交互系统时,延迟是最核心的体验指标。用户说完话,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 的表现比简单的能量检测好很多,推荐直接用。


各组件实际选型

组件我的选择备注
ASRFunASR(本地)中文效果好,支持流式,需要 GPU
ASR(备选)阿里云 ASR不想维护本地服务时用
LLMGPT-4o / Claude按场景切换,用统一路由层
TTSCosyVoice音色自然,支持克隆;慢,需优化
TTS(快速版)Edge TTS微软,免费,延迟低,音色一般
VADSilero VAD准确率高,CPU 可用

总结

降低语音交互延迟的核心是两点:

  1. 流水线并发:ASR 出句子就送 LLM,LLM 出句子就送 TTS,三段并行而不是串行
  2. 句子级处理:不要等全文,句子是最小有效单元

优化后的首字节音频延迟(用户说完到听到第一个字)稳定在 800ms 以内,整体流畅度从”明显卡顿”变成”基本自然”。

下一步准备测试端侧 ASR(Whisper.cpp 量化版)和更快的 TTS 方案,进一步压缩链路延迟。