一句话带走:语音体验的分水岭不在识别准不准,而在你敢不敢把整条链路拆开流式。
先说一个让我脸红的数字
上个月我给自己的AI助手加语音功能,跑通那天还挺得意:识别有了,合成有了,能对话了。
结果朋友试了一句,扭头跟我说:「这玩意儿像在跟客服排队。」
我看了眼日志——用户说完话,2.1秒之后AI才开始出声。
2.1秒。人跟人面对面聊天,对话间隔超过1秒就会觉得尴尬,这是心理学里老掉牙的结论。我做了一大堆prompt优化、RAG调优,结果栽在一个200毫秒和2000毫秒的区别上。
不服。花了一周时间把整条语音链路拆开重做,最后压到了300毫秒左右。今天就把选型、踩坑、方案全摊开讲讲。
选型:流式是唯一解,但流式也分三六九等
先交代背景:
我的场景是实时语音对话,中文为主,部署在国内,预算有限——不烧钱买顶级闭源方案。
▪ ASR怎么选
一开始我用的是讯飞和阿里云的标准版识别接口,准确率没得说,95%上下。但它们是「整句识别」:你说完,停顿一下,服务端判定句子结束,才返回结果。这个「停顿判定」本身就要吃掉500到800毫秒。
后来换了流式ASR,对比下来:
我最后用了FunASR自部署。省钱的副产物是延迟反而更低——数据不出机房,WebSocket直连,没有公网绕路。
顺带说一句,开源的Paraformer流式版中文效果比我预想好得多,日常对话场景和商用云服务差距不大,专业术语差一些。

▪ TTS更卷
TTS(Text-to-Speech,文本转语音,语音合成)的坑比ASR(Automatic Speech Recognition,自动语音识别,语音转文字)多。
市面上有名有姓的我测了一圈:
-
商用云服务
阿里、火山、讯飞,音质好,但普通接口是「整段合成」——给它一段话,它合成完整音频再返回。300字的回答,合成加传输轻松超过3秒。 -
流式TTS接口
火山和阿里都有流式版本,首包延迟200到300毫秒,边合成边推流,这个可用。 -
开源方案
CosyVoice、ChatTTS、GPT-SoVITS,自部署首包能做到300毫秒内,音质看运气,部署是真费劲。
我选了流式TTS云服务起步,音质和省心优先。后面想把音色做成产品特色,再考虑CosyVoice自部署。
优化三板斧:从2.1秒到300毫秒
选型只是地基,真正的延迟大头在我的pipeline设计上。三板斧,每板都有账可算。
▪ 第一板:分句合成,别等LLM说完
最早的架构蠢得可笑:LLM流式输出 → 等全部生成完 → 整段丢给TTS → 等合成完 → 播放。
每一个「等」都是白给的。改成分句合成:LLM每输出到一个句子边界(句号、问号、换行),立刻把这一句丢给TTS开始合成播放,LLM继续往下生成。
buffer = ""
async for chunk in llm_stream:
buffer += chunk
# 遇到句末标点就切一句出去合成
if buffer and buffer[-1] in "。!?;\n":
asyncio.create_task(tts_speak(buffer.strip()))
buffer = ""
注意这里用的是「句子边界」而不是标点全切。逗号太碎,一句「其实吧,我觉得」被切成两段音频,拼接处会有诡异的停顿感,像结巴。
这一刀下去,用户听到第一个字的时间从2.1秒降到约800毫秒。大头是LLM首token延迟加TTS首包。
▪ 第二板:连接预热,杀掉冷启动
压到800毫秒后卡住了。翻日志发现,每次对话开始,ASR建连300毫秒、TTS建连200毫秒——全是握手开销。
解法简单粗暴:保持长连接,会话开始前就预建好ASR和TTS的WebSocket,用心跳保活。TTS那边再狠一点,预合成一个十几毫秒的静音片段,让播放器提前进入「推流就绪」状态。
class WarmPool:
def __init__(self):
self.tts_ws = None
async def preheat(self):
#会话空闲时就建好连接,不等到用户开口
self.tts_ws = await connect_tts()
await self.tts_ws.send_silence(duration_ms=20)#预热合成引擎
又砍掉400毫秒。剩400毫秒左右,这里面LLM首token占了大头——这个没法绕过,只能靠模型侧或者边缘部署优化。
▪ 第三板:音频参数抠到字节
最后一步是抠细节:TTS采样率用24kHz而不是48kHz(传输字节减半,人耳听不出差别),音频分片用200毫秒一个chunk而不是1秒(起播更快,缓冲更平滑),WebSocket开压缩但只压协议头不压音频(音频本身已经是压缩格式,再压纯浪费CPU)。
到这里,用户开口到听到回应,稳定在300毫秒上下。朋友再试,说了句:「欸,这回像打电话了。」
最难的其实是打断:barge-in
延迟解决之后,我以为完事了。然后用户说:「我想中途插话,它不理我。」
对。AI在说话的时候,麦克风还在收音,把AI自己的声音都识别进去了,用户插一句「停」,AI充耳不闻继续念。
这就是barge-in(插话打断 / 智能打断),语音对话里最恶心的工程问题。我的方案分三层:
第一层,回声消除。
最朴素的做法:AI说话时暂停ASR。问题是用户必须等AI闭嘴才能说话,体验像对讲机,按了才能说。真正要做半双工以上的体验,得让ASR知道「这段声音是音箱放出来的」——硬件上靠回声消除(AEC),软件上简单版可以自己做「回声抑制」:把当前正在播放的音频文本记下来,ASR识别结果和播放中的内容高度重合时,判定为回声,丢弃。
第二层,语义判停。
用户插话分两种:「嗯」「对」这种附和,和「等等,你说错了」这种真打断。前者不该停,后者必须立刻停。我用一个小分类器(几百毫秒内出结果)判断插话意图,真打断就立刻:停TTS推流、清空播放缓冲、把用户插话内容追加进对话上下文。
第三层,状态机兜底。
打断瞬间最容易出竞态——TTS还在推流、播放队列里还压着三句话、ASR刚收到半截音频。我把整条链路收拢成一个状态机,任何打断都先走统一的取消例程,中止TTS推流、清空播放队列、挂起ASR会话。宁可丢掉半句合成好的音频,也不让两个声音叠在一起。
说实话,做barge-in那一周,我天天在跟竞态bug搏斗。有一个bug折磨了我两天——打断后偶尔会冒出半秒前一句的尾巴,幽灵一样。最后发现是播放队列没清干净。语音链路里,时序问题的阴间程度比纯文本服务高一个量级。
⚠️ 踩坑提醒:
不要用「AI说话时干脆关掉麦克风」来回避barge-in。短期省事,长期你的产品会被钉死在对讲机体验上,用户插不上话的语音助手,留存率很难看。
账算下来
最后汇总一下这条链路的账:
如果你的AI产品也想加语音,我的建议就三条:
流式ASR加流式TTS是入场券,一分句预热是性价比最高的两刀,barge-in预留至少一周——它是整个项目里最不像「语音」、最像「分布式系统」的部分。
你给自己的AI产品接语音时,卡在哪个环节了?是TTS音色选不出来,还是打断处理把你折磨疯了?评论区聊聊,踩过的坑互相报个信。
💡 一句话带走:语音体验的分水岭不在识别准不准,而在你敢不敢把整条链路拆开流式。
