语音助手流水线:第 6 章毕业项目 本节摘要:把第 0111 节的全部内容缝起来——搭一个能听、能推理、能回话的语音助手。2026 年这已经是一个被解决的工程问题,不再是研究问题,但集成细节决定它能不能上线。本节毕业项目要求你搭一条端到端流水线:采集麦克风(16 kHz 单声道)、检测用户语音的起止、流式转录、把转录喂给能调工具(定时器、天气、日历)的 LLM、把 LLM 文本流式喂给 TTS、把音频回放给用户,并在用户中途打断时停止。延迟目标:笔记本 CPU 上,用户说完话后 800 毫秒内出第一个 TTS 字节。质量目标:不漏字、静音上不幻觉字幕、无声音克隆泄漏、提示注入不成功。
本节摘要:把第 01~11 节的全部内容缝起来——搭一个能听、能推理、能回话的语音助手。2026 年这已经是一个被解决的工程问题,不再是研究问题,但集成细节决定它能不能上线。本节毕业项目要求你搭一条端到端流水线:采集麦克风(16 kHz 单声道)、检测用户语音的起止、流式转录、把转录喂给能调工具(定时器、天气、日历)的 LLM、把 LLM 文本流式喂给 TTS、把音频回放给用户,并在用户中途打断时停止。延迟目标:笔记本 CPU 上,用户说完话后 800 毫秒内出第一个 TTS 字节。质量目标:不漏字、静音上不幻觉字幕、无声音克隆泄漏、提示注入不成功。本节会逐一拆解七个组件、三个你必将撞上的失败模式(首字被切、中途打断混乱、静音幻觉),以及 2026 年的四种生产参考栈(LiveKit 全商业、Pipecat 半开源、Moshi 全双工、Whisper.cpp+llama.cpp 全离线)。读完本节,你能为给定预算、规模、语种、合规约束产出一份完整的语音助手技术栈规格。
阅读完本节,你应当能够:
搭一个端到端助手:
延迟目标:笔记本 CPU 上,用户说完话后 800 毫秒内出第一个 TTS 字节。质量目标:不漏字、静音上不幻觉字幕、无声音克隆泄漏、提示注入不成功。
sounddevice,生产用原生 AudioUnit/ALSA/WASAPI。| 栈 | 延迟 | 许可 | 备注 |
|---|---|---|---|
| LiveKit + Deepgram + GPT-4o + Cartesia | 350~500 ms | 商业 API | 2026 行业默认 |
| Pipecat + Whisper-streaming + GPT-4o + Kokoro | 500~800 ms | 多为开源 | DIY 友好 |
| Moshi(全双工) | 200~300 ms | CC-BY 4.0 | 单模型,架构不同,见第 15 节 |
| Vapi / Retell(托管) | 300~500 ms | 商业 | 上线最快,定制有限 |
| Whisper.cpp + llama.cpp + Kokoro-ONNX | 离线 | 开源 | 隐私 / 边缘 |
💡 注意「延迟数字」与「延迟体感」的区别。一个标称 350 ms 的栈,如果首字被切、抢话不灵敏,体感会比标称 800 ms 但抢话灵敏的栈更差。所以衡量语音助手要把「首响应延迟」与「抢话响应延迟」分开测,后者往往决定用户是否觉得它「活着」。
import sounddevice as sd def mic_stream(chunk_ms=20, sr=16000): q = queue.Queue() def cb(indata, frames, time, status): q.put(indata.copy().flatten()) with sd.InputStream(channels=1, samplerate=sr, blocksize=int(sr * chunk_ms/1000), callback=cb): while True: yield q.get()
def capture_turn(stream, vad, pre_roll_ms=300, silence_ms=500): buf, pre, triggered = [], collections.deque(maxlen=pre_roll_ms // 20), False silent = 0 for chunk in stream: pre.append(chunk) if vad(chunk): if not triggered: buf = list(pre) # 预滚动:把 VAD 触发前的音频也带上 triggered = True buf.append(chunk) silent = 0 elif triggered: silent += 20 buf.append(chunk) if silent >= silence_ms: return b"".join(buf)
设计要点:
pre_roll_ms=300的预滚动缓冲是避免首字被切的关键——VAD 检测到语音时,用户的第一个字往往已经过去几十到几百毫秒。用一个固定长度的环形缓冲把 VAD 触发前的音频也保留下来,拼到正式缓冲前面,就能完整捕获「hey」「ok」这类短促的开头。
async def turn(audio_bytes): transcript = await stt.transcribe(audio_bytes) async for token in llm.stream(transcript): async for audio in tts.stream(token): await speaker.play(audio)
tools = [ {"name": "get_weather", "parameters": {"location": "string"}}, {"name": "set_timer", "parameters": {"seconds": "int"}}, ] async for chunk in llm.stream(user_text, tools=tools): if chunk.type == "tool_call": result = dispatch(chunk.name, chunk.args) continue_streaming(result) if chunk.type == "text": await tts.stream(chunk.text)
tts_task = asyncio.create_task(tts_loop()) while True: chunk = await mic.get() if vad(chunk): tts_task.cancel() await speaker.stop() await new_turn() break
code/main.py 提供一个可运行的模拟,用桩模型把七个组件接起来,即使没有硬件也能看到流水线形状。真实实现把桩换成:
silero-vad(pip install silero-vad)deepgram-sdk 或 openai-whisperopenai(gpt-4o)或 anthropickokoro 或 cartesiasounddevice 做输入输出四类栈的取舍:
⚠️ 六类坑:① 永远记录 PII——完整轮次音频在多数司法管辖区是 PII,30 天留存、静态加密;② 无抢话——用户一定会打断,助手必须停;③ TTS 阻塞——同步 TTS 阻塞事件循环,用 async 或独立线程;④ 工具调用无错误处理——工具会失败,LLM 必须收到错误 + 重试一次,然后优雅降级;⑤ 幻觉过滤过激——过滤过头助手只会复读「我帮不了这个」,过滤不够又会乱说,要在留出集上调;⑥ 无唤醒词选项——常听是隐私负债,加唤醒词门(Porcupine 或 openWakeWord)。
本节产出技能文档(原课程 outputs/skill-voice-assistant-architect.md),给定预算 + 规模 + 语种 + 合规约束,产出完整技术栈规格。模板:
基础:运行 code/main.py。它用桩模块端到端模拟一个完整轮次,打印逐级延迟。
进阶:把 STT 桩换成真实 Whisper 模型,对一段预录 .wav 跑,测 WER 与端到端延迟。
挑战:加工具调用:实现 get_weather(任意 API)和 set_timer。让 LLM 经工具路由,验证用户说「设个 5 分钟定时器」时正确的函数被触发,且回话确认。
下一节,我们下沉到流水线之下,讲神经音频编解码——EnCodec、SoundStream、DAC 如何把波形压成离散 token,这是 MusicGen、VALL-E、AudioLM 等所有「音频 token」模型的共同基座。