在构建实时语音对话应用时,用户最直观的感受就是“反应慢”。这种慢并非单一模块算力不足,而是语音识别、大模型推理与语音合成三者之间采用了严格的串行阻塞模式。当一段五秒的问句说完后,系统才开始转写,转写完整后再调用语言模型,模型吐字结束才启动播报,累积延迟往往让交互变得像电话留言而非对话。要打破这种僵局,必须把三个环节全部改造为流式架构,并让它们在前端时间轴上重叠运行。

流式ASR的断句与增量回调机制
传统自动语音识别(ASR)接口要求上传完整音频文件,服务端做一次解码返回最终文本,这显然无法支撑低延迟场景。流式ASR的核心在于音频分块上传与临时结果回调:客户端按照一百到两百毫秒的帧长持续推送PCM数据,服务端基于神经网络声学模型实时输出带置信度的部分假设,并在检测到静音或语义边界时发出稳定文本片段。这里的关键在于端点检测(VAD)与解码器的状态保持,模型不需要每次从零开始,而是复用上一帧的隐状态,从而把单句首字延迟降到三百毫秒上下。
在工程实现上,我们通常使用WebSocket维持长连接,避免HTTP短连接的握手开销。下面这段Node.js伪代码展示了如何把麦克风流切分并发送,同时监听服务端下发的临时与终态结果。注意音频特殊字符在传输中无需转义,但服务协议里的标签字段要按接口文档处理。
const ws = new WebSocket('wss://api.ipipp.com/asr/stream');
let vad = createVad(); // 本地静音检测
navigator.mediaDevices.getUserMedia({audio:true}).then(stream => {
const ctx = new AudioContext();
const src = ctx.createMediaStreamSource(stream);
const proc = ctx.createScriptProcessor(4096, 1, 1);
proc.onaudioprocess = e => {
const pcm = e.inputBuffer.getChannelData(0);
if (!vad.isSilent(pcm)) {
ws.send(floatTo16LE(pcm));
}
};
src.connect(proc); proc.connect(ctx.destination);
});
ws.onmessage = evt => {
const msg = JSON.parse(evt.data);
if (msg.type === 'partial') showTemp(msg.text);
if (msg.type === 'final') commitSentence(msg.text);
};
采用这种结构后,用户刚说完半句话,界面上就已经出现转写草稿。但仅有ASR流式化还不够,因为大模型若等整句到位才启动,依然会浪费掉前段传输时间。因此下一环也必须接入流式推理。
LLM增量解码与边生成边下发的实践
大语言模型(LLM)自回归生成天然适合流式输出。多数推理框架支持SSE或WebSocket的token级推送,每生成一个词就立即发给客户端。问题在于如何与ASR的“终态句子”对齐:如果ASR每出临时结果就触发一次LLM请求,会产生大量无效计算。更优方案是维护一个滑动上下文,当ASR抛出final片段时,立即将其拼入对话历史并向LLM发起携带stream标志的请求;同时前端对LLM返回的token做缓冲,一旦凑够一个完整短句或达到标点边界,就提前交付给TTS。
以下Python片段演示了利用异步生成器消费流式LLM接口,并将分句后的文本通过队列传给合成模块。这里我们用简单的标点切分,真实系统可引入轻量语义分割模型。
import asyncio, aiohttp
async def stream_llm(history, tts_queue):
async with aiohttp.ClientSession() as sess:
async with sess.post('https://api.ipipp.com/llm/stream',
json={'messages': history}) as resp:
buf = ''
async for line in resp.content:
tok = line.decode().strip()
if not tok: continue
buf += tok
if buf.endswith(('。', '?', '!', ',', ',')):
await tts_queue.put(buf)
buf = ''
if buf: await tts_queue.put(buf)
async def asr_final_handler(text, tts_queue):
history.append({'role':'user','content':text})
asyncio.create_task(stream_llm(history, tts_queue))
这种增量解码让模型思考与用户收听几乎同步发生。不过若TTS仍采用整段合成,最后一段等待仍会拉长闭环。所以我们还要把语音合成改成流式。
TTS边接收边播放与三端时钟对齐
现代神经声码器支持基于前缀文本的实时合成,即输入少量字符就能预测并输出对应波形,随后随文本补充持续追加音频帧。客户端维护一个音频播放缓冲区,当TTS队列传来第一句文本,立刻请求合成并播放;后续句子在后台预合成,利用Web Audio的缓冲区调度避免爆音。三端协同的难点在时钟对齐:ASRfinal、LLM token、TTS音频帧各自带时间戳,前端需用单调时钟记录每个环节的到达与消费延迟,动态调节缓冲区长度,在网络抖动时优先保证流畅而非极低延迟。
下面HTML与JS混合示例展示了一个极简播放调度,其中<audio>标签仅作说明,真实项目多用AudioBufferSourceNode。
<audio id="out" autoplay></audio>
<script>
const queue = [];
let playing = false;
function pushTTS(chunk){
queue.push(chunk);
if(!playing) playNext();
}
async function playNext(){
if(queue.length === 0){ playing = false; return; }
playing = true;
const text = queue.shift();
const res = await fetch('https://api.ipipp.com/tts/stream',{
method:'POST', body: JSON.stringify({text})
});
const audioBlob = await res.blob();
document.getElementById('out').src = URL.createObjectURL(audioBlob);
document.getElementById('out').onended = playNext;
}
</script>
把上述三套流式机制组合,系统便能在用户讲话途中就开始准备回答,话音刚落,合成语音已流出。实践中还需考虑错误重试与降级:当LLM流式中断,可缓存已得token改用本地规则补完;ASR临时结果错误则在final到达时整体替换。只有把延迟当作跨模块指标而非单点优化,才能真正解决交互迟钝。
streaming_ASRTTSLLM_inference修改时间:2026-08-14 02:24:30