Easy-Vibe第5次:从零搭建本地语音对话助手全链路实践 Datawhale的Easy-Vibe项目跟到第5次说实话前面几期都在“看懂样例”这期终于进入“自己动手改”的阶段了。任务围绕一个语音交互Demo的完整链路展开把语音识别、大模型对话、语音合成三段串在一起目标是让一台普通电脑上的AI应用能听、能说、能连续对话。如果你也在跟Easy-Vibe或者手头正好想做类似的语言交互小项目这篇笔记记录了从环境搭建到链路调优的完整过程适合有一定Python基础、但没怎么碰过音频处理和模型部署的同学参考。我做这套东西之前最大的困惑是网上教程很多但要么只讲调用一个现成API要么直接扔一堆模型训练代码中间那段“怎么把语音变成文字、再把文字变成语音”一直没讲透。Easy-Vibe这期恰好补上了这个缺口它不要求你有GPU也不要求你懂深度学习训练只要愿意动手就能把一个“能对话的AI”跑在本地。1. 这期Easy-Vibe到底在做什么1.1 Datawhale和Easy-Vibe是什么Datawhale是国内一个专注于开源学习社区的组织经常发起“一群人一起学某个技术”的打卡活动特点是资料开源、任务循序渐进、有社区答疑。Easy-Vibe是这个体系下的一个偏体验型AI应用系列任务目的是让参与者通过亲手复现AI交互Demo理解当前AI能做什么、怎么接入自己的日常工具。它跟看文档最大的区别就是“每期都有实际可玩的东西”。前几期我跟着做了图片理解、文本生成一类的小实验整体感受是门槛被刻意压低了不逼你背概念先让你跑起来。而到了第5次任务强度明显上来不只是“跑通官方样例”而是“拆开官方样例自己改一遍”这个转变对我这种容易停留在“看别人代码觉得很懂”的人来说非常关键。1.2 第5次任务的核心变化从看懂到改通这一次的任务主线非常清楚做一个完整的语音对话助手。用户对着麦克风说一句话程序识别成文字交给大模型生成回复再把回复合成语音放出来形成一次完整的“听—想—说”闭环。前几次我也跑过单独的语音识别和单独的文字生成但把它们串起来之后才会意识到真正的复杂度在“接口对接”。比如语音识别的输出可能带标点、带语气词大模型不一定理解大模型回复可能很长直接合成语音会让用户等很久录音什么时候结束、环境噪音怎么处理这些都是单模块Demo里不会碰到的问题。所以第5次笔记的价值不在于用了多高深的算法而在于帮你建立“全链路思维”。你把每一个环节都亲手接了一遍后面再做更复杂的功能比如加知识库、加工具调用、加多轮记忆就只是在中间插入插件而已。1.3 为什么选择“三件套”而非一体化方案实现语音对话有很多现成方案比如直接调用某个云厂商的语音助手SDK几分钟就能跑通。但在Easy-Vibe第5次任务里我更推荐用“ASR LLM TTS”三个独立模块自由组合原因有三个第一可替换性。每个模块坏了或者效果不满意可以单独换掉不用推翻整个项目。比如语音识别你觉得识别不准那就换一个模型大模型对话完全不受影响。第二可调试性。三个模块中间有明确的“文字”作为中间产物问题出在哪一环一目了然。如果大模型回得不好但识别文字是对的说明问题在LLM如果识别出来的文字本来就是错的那就去调ASR。这比一体化方案黑盒式的排查体验好太多。第三可理解性。每一步都在做什么你清清楚楚。这对自己学习是友好的你不必把整个系统当成一个魔法盒子。说得生活化一点一体化方案像去吃套餐省事但固定三件套方案像单点菜你可以只换不好吃的那一道。对于学习目的来说单点才能知道每道菜是怎么做的。2. 环境准备与工具选型2.1 硬件与系统环境先说我这次使用的环境普通笔记本16GB内存无独立显卡操作系统是Ubuntu 22.04。这个配置在AI应用Demo里算是比较典型的“穷人配置”没有GPU所有推理都靠CPU完成。很多同学一听“跑AI”就觉得自己电脑不行实际上这里面有个误解大模型训练确实需要高端显卡但“跑推理”远没那么苛刻。只要你选对模型尺寸、合理使用量化16GB内存的机器完全能流畅跑一个7B级别的对话模型只是生成速度比带GPU的机器慢一些而已。如果用的是Windows或macOS也不用担心核心步骤几乎一样。Windows下推荐装Anaconda然后创建一个独立环境macOS则建议先装好Homebrew因为后面可能需要用它安装一些音频依赖库。我这次在Linux下踩了一遍完整的坑下面写的步骤基本都是跨平台通用的。2.2 软件依赖与版本选择我建议先创建一个独立的conda环境避免跟系统里其他Python项目打架。Python版本我选了3.10不是越新越好因为很多音频处理库和PyTorch老版本对3.12、3.13的支持还不算稳定在新版本上编译安装时容易出幺蛾子。conda create -n easyvibe python3.10 -y conda activate easyvibe pip install pyaudio numpy pip install faster-whisperpyaudio是Python访问麦克风的经典库底层依赖PortAudioLinux下如果你用apt安装过portaudio19-dev一般没问题macOS则建议先执行brew install portaudio再pip安装。这块是很多小白被卡住的第一个地方报错信息通常是一大堆编译日志看着吓人其实只是缺了系统级依赖。大模型对话部分我选择了Ollama作为模型运行工具而不是直接用Python的transformers库。原因很实际Ollama对模型文件的管理非常省心下载、切换、删除都是几条命令的事而且它的API接口跟OpenAI格式兼容后续如果想换别的模型或者接入其他应用几乎不用改代码。curl -fsSL https://ollama.com/install.sh | sh ollama pull qwen2.5:7b2.3 模型选型跑得动才是王道语音识别模型我测试了三套方案分别是faster-whisper的base、small以及FunASR里的Paraformer中文模型。简单说下对比模型内存/显存占用中文识别效果速度适用场景faster-whisper base约1GB短句还行长句容易错快入门验证、英文faster-whisper small约2GB中文短句稳定长句可用中等我这次最终选它FunASR/paraformer-zh约1.5GB中文长句更好较快生产级中文场景选small而不是base是因为在实测中base对中文的识别率确实不够看。我说“你好帮我介绍一下今天的天气”它能给我识别成“你好帮我介绍一下今天的天系”这种同音字错误在base上很常见。small虽然慢那么一点点但中文短句的稳定性提升非常明显。对话模型我这里用了qwen2.5:7b7B参数经过量化后实际占用内存大概5GB左右16GB内存的机器不会太紧张。如果你电脑内存只有8GB建议换成qwen2.5:3b体验上会差一些但至少能保证流程跑通。语音合成我准备了两个方案一个偏“验证用”一个偏“后期进阶”。快速验证用微软的edge-tts它本质是调用在线服务好处是不占本地资源中文发音自然配置只需要一行代码后期想完全离线可以换ChatTTS或GPT-SoVITS但这两个模型体积不小在纯CPU环境下合成速度较慢。TTS方案是否联网中文效果延迟说明edge-tts在线自然低适合快速验证ChatTTS本地自然中模型约2GB左右有GPU更好GPT-SoVITS本地可克隆声音高功能强大但配置复杂2.4 音频设备检查先别急着写代码有一种常见的挫败感是代码写好了对着麦克风说了半天结果程序收到的是全零数据。这个问题不是代码逻辑错而是系统默认输入设备压根不是你插的那个麦克风。所以我在写录音代码之前先把系统能识别的音频设备打了出来。PyAudio本身提供了一个简单办法import pyaudio p pyaudio.PyAudio() for i in range(p.get_device_count()): dev p.get_device_info_by_index(i) print(i, dev[name], dev[maxInputChannels]) p.terminate()如果电脑上有多个声音设备比如摄像头自带的麦克风、独立USB麦克风、蓝牙耳机这里会列出好几个。你要记下目标麦克风对应的索引号在录音代码里明确指定input_device_index而不是用默认值0。这个细节我一开始没注意导致测试时一直“录不到声音”后面排查了半天才发现程序一直是从摄像头麦克风采的音。3. 核心链路把语音交互完整串起来3.1 录音采样率和静音检测不能随便写第一步是从麦克风采集音频。这里很多人会顺手用44.1kHz的CD音质采样率但我不建议这么做。语音识别模型基本都是按照16kHz采样率训练的你喂给它44.1kHz的音频它反而需要内部重采样增加计算量且没有任何收益。16kHz听起来似乎“音质差”但它完全覆盖了人类语音的主要频段——人声的关键能量集中在300Hz到3400Hz之间16kHz的采样率对这个范围绰绰有余。我封装了一个带静音检测的录音函数效果是程序一直监听麦克风检测到人开始说话就记录直到说话停顿1.5秒才停止。import pyaudio import numpy as np FORMAT pyaudio.paInt16 CHANNELS 1 RATE 16000 CHUNK 1600 SILENCE_THRESHOLD 700 SILENCE_DURATION 1.5 def record_until_silence(): p pyaudio.PyAudio() stream p.open(formatFORMAT, channelsCHANNELS, rateRATE, inputTrue, frames_per_bufferCHUNK) frames [] silent_chunks 0 print(开始说话...) while True: data stream.read(CHUNK) frames.append(data) audio_data np.frombuffer(data, dtypenp.int16) rms np.sqrt(np.mean(audio_data ** 2)) if rms SILENCE_THRESHOLD: silent_chunks 1 else: silent_chunks 0 if silent_chunks int(SILENCE_DURATION * RATE / CHUNK): break stream.stop_stream() stream.close() p.terminate() return b.join(frames)这里有几个参数值得仔细说说。CHUNK是每次读取的音频块大小我设为1600个采样点。由于采样率是16000Hz1600个采样点就是0.1秒的音频这个粒度不会太碎也不会因为太大而让静音检测反应迟钝。SILENCE_THRESHOLD是判定“静音”的阈值我这里取的是700。怎么理解这个值音频数据用16位整型表示取值范围是-32768到32767。安静环境下麦克风采集到的环境噪音RMS值通常只有几十到两三百而人说话时RMS值能飙到数千甚至上万。在安静的室内把阈值设在700左右比较稳既能过滤掉轻度环境噪音又能保证正常说话不会误判。SILENCE_DURATION是持续静音多久判定“话说完了”我设置为1.5秒。这个值太短会导致说话中间稍微一停顿就截断了太长又会让你等得着急。1.5秒在中文语速下是比较折中的选择。换算下来就是1.5乘以16000再除以1600即连续15个静音块就停止录音。3.2 语音识别faster-whisper的快速实现拿到音频字节流之后下一步就是交给语音识别模型转成文字。我用了faster-whisper它跟OpenAI原版Whisper是同一套模型权重但是推理引擎换成了CTranslate2在CPU上的速度能快好几倍。from faster_whisper import WhisperModel model WhisperModel(small, devicecpu, compute_typeint8) def transcribe(audio_bytes): segments, _ model.transcribe(audio_bytes, languagezh) return .join(seg.text for seg in segments)注意两个细节。第一devicecpu和compute_typeint8是CPU环境下的黄金组合。int8量化会让模型体积缩小、推理速度提升代价是识别精度略有下降但对常规中文识别来说下降幅度不明显完全在可接受范围内。第二model.transcribe返回的segments是一个生成器不能直接打印必须循环取出来拼接成字符串否则你看到的是个对象地址而不是文字。实际测试下来一段4秒左右的语音small模型在CPU上识别耗时大约0.5到1秒这个速度在交互场景里完全够用。如果你感觉识别太慢可以退回base模型如果识别错误多可以升级到medium但那时候内存占用会明显上升16GB内存跑起来会有点吃力。3.3 大模型对话带记忆的上下文处理语音识别出来是一句文字接下来交给大模型生成回复。这里我用Ollama启动了一个qwen2.5:7b的服务通过它的本地HTTP接口请求对话。import requests def chat_with_llm(history, user_input): messages [{ role: system, content: 你是一个友好的语音助手回答尽量简短自然适合被语音播放。 }] messages.extend(history) messages.append({role: user, content: user_input}) resp requests.post( http://localhost:11434/api/chat, json{ model: qwen2.5:7b, messages: messages, stream: False } ) reply resp.json()[message][content] history.append({role: user, content: user_input}) history.append({role: assistant, content: reply}) return reply, history代码里最关键的是维护一个history列表把用户输入和AI回复都追加进去下一次请求时带上这样AI才能记住之前的对话内容。否则你问它“我叫小李”接着问“我叫什么”它根本不知道你在说什么。system prompt也值得琢磨。我明确告诉模型“回答尽量简短自然适合被语音播放”。为什么因为大模型默认倾向于生成完整、详细的回答但做语音交互时一段超过100字的回复会让用户等很久听感也非常累。语音助手的回答应该像人跟人说话一样简短直接。这个约束放在system里非常有效。temperature参数我没有显式设置沿用Ollama默认的0.7左右。这个值既不会让回复过于随机也不会死板到每个问题都给出教科书式答案。3.4 语音合成edge-tts与本地TTS两条路大模型回复是一段文字最后一步是把它变成语音。快速验证阶段我用的是edge-tts它调用的是微软在线语音合成服务代码非常简洁import edge_tts import asyncio async def text_to_speech(text, path): tts edge_tts.Communicate( text, voicezh-CN-XiaoxiaoNeural, rate-10% ) await tts.save(path) asyncio.run(text_to_speech(你好我在听你说。, reply.mp3))voice参数我选了“zh-CN-XiaoxiaoNeural”也就是晓晓这个声音中文自然度比较高。rate设为-10%因为edge-tts的中文默认语速偏快尤其当文本里有数字、英文单词时播出来的节奏会很赶放慢10%之后听感舒服很多。如果要追求完全离线部署可以考虑ChatTTS它的中文语气和停顿更自然但在纯CPU环境下生成一段5秒的语音可能要等好几秒。对我来说验证链路时用edge-tts就够了离线方案可以作为后续优化方向。3.5 主循环把三个环节接起来跑一遍录音、识别、对话、合成这四个环节各自跑通之后最后用一个主循环把它们粘起来。import asyncio import subprocess history [] while True: print(请说话...) audio record_until_silence() # 过滤掉太短的录音小于0.4秒大概率是误触发 if len(audio) 1600 * 4: continue text transcribe(audio) print(你:, text) if text.strip() in (退出, 结束, 再见): break reply, history chat_with_llm(history, text) print(AI:, reply) asyncio.run(text_to_speech(reply, reply.mp3)) subprocess.run([ffplay, -nodisp, -autoexit, reply.mp3])这里的len(audio) 1600 * 4是用来过滤误触发的。因为录音函数一直在监听哪怕有一点响动也可能触发“开始录音”但如果录到的数据太少多半是短暂的环境噪音不应该进入识别流程。0.4秒以下直接丢弃既省时间又避免识别出一堆无意义内容。关于延迟我专门计时测过。一次完整对话的耗时分布大概是语音识别0.5到1秒大模型生成2到5秒语音合成1到2秒再加上录音本身的时间一次交互总延迟在4到8秒之间。CPU机器跑7B模型这个数字属于正常范围不用焦虑。想优化的话优先换3B模型或上GPU效果立竿见影。4. 我踩过的坑与排查实录4.1 录音没声音十个里有八个是设备索引问题第一次测试的时候我对着麦克风喊了半天程序一点反应都没有打印出来的音频RMS值一直是几十。排了半天最后发现问题出在CPU性能上设备太多导致PyAudio默认选了摄像头麦克风而我说话的位置离摄像头又远。解决办法就是前面第2.4节说的先把设备列表打印出来手动指定输入设备索引。还有一个常见坑是系统麦克风权限没给。如果你在macOS或者Windows上跑首次调用麦克风时系统会弹权限请求一旦点了“拒绝”之后再运行程序不会重新弹窗只能自己去系统设置里打开。4.2 ASR识别中文“胡说八道”别硬扛换模型或降噪有一次我说“帮我定一个明天早上八点的闹钟”识别结果变成了“宝贝定一个明天早上八点的闹钟”一个“帮我”变成“宝贝”意思完全跑偏。排查下来一个是base模型本身中文能力弱另一个是环境里风扇噪音大。解决思路分两步。先是换了small模型错误明显减少然后我把电脑风扇调成静音模式麦克风靠近一些识别率又上一层。如果你发现识别文字偶尔多一个字、少一个字但不影响语义就不用太纠结如果错误到影响大模型理解才需要认真调模型或降噪。4.3 LLM响应太慢先从模型和线程数查起7B模型在CPU上跑生成速度大概是每秒5到10个token一句话几十个token就是要好几秒。如果你觉得慢得难以接受有两个方向可以排查。第一确认模型是不是被量化过。我拉取qwen2.5:7b时默认就是量化版本但如果你手动拉取了其他模型可能是未量化的FP16版本内存占用和计算量都会大幅提高。第二可以让Ollama利用更多CPU线程设置环境变量OLLAMA_NUM_THREAD比如export OLLAMA_NUM_THREAD8能榨出一点性能。另外后台如果有其他吃CPU的程序比如浏览器开了一堆标签页记得关掉实测对我的延迟影响非常大。4.4 TTS声音生硬或语速不对调参数比换模型更实际edge-tts默认的晓晓声音已经算自然了但有些情况下你会觉得“读得太赶”或者“像在念课文”。我最开始没加rate参数合成出来的中文听起来像1.5倍速后来统一设置rate-10%好很多。还有一个细节大模型回复如果一长串没有标点TTS合成的停顿会非常奇怪。解决办法是在system prompt里要求模型使用短句和标点配合语音放出来就自然很多。4.5 常见问题速查表现象可能原因排查与解决录音没数据设备索引不对打印设备列表设置input_device_index录音没数据麦克风权限被拒到系统设置里重新授权识别结果乱码采样率不匹配确认RATE16000中文识别错误多模型太小或噪音大换small模型减小环境噪音大模型回复慢CPU推理模型太大换3B模型设置CPU线程数TTS语速太快默认速率偏高设置rate-10%总是误触发静音阈值太低调高SILENCE_THRESHOLD一次对话等太久整个链路串联耗时先缩短录音等待再考虑换GPU5. 一点个人体会Easy-Vibe跟到第5次我自己最明显的变化是不再把AI项目当成一个神秘黑盒了。以前看别人做的语音助手Demo觉得“好厉害这一定很复杂”但亲手把录音、识别、对话、合成一节一节接起来之后你会发现每个环节拆开都有成熟的开源方案难点从来不在某个单独的技术而在怎么让它们顺畅协作。这期任务做完我最大的收获是学会了“用中间产物排查问题”。不管AI项目外面包了多少层语音交互中间必然会有“文字”这个东西。每次测试时我都会把识别结果和AI回复打印出来看一眼就知道当前是哪一环出了问题不用瞎猜。这个习惯看起来简单但能帮你省下大量调试时间。后续我打算做三件事把ASR换成流式识别让程序边说话边出字把TTS换成完全离线的本地方案然后给大模型接上日历和天气工具让这个语音助手不只是聊天还能真正做点事。如果你也在跟Easy-Vibe建议别停在“跑通官方代码”试着改一两个参数、换一个模型你会发现自己对AI应用的理解完全不一样了。