资讯详情

资讯详情

ESP32-S3+大模型:从零构建端云协同AI语音助手

我手里这个项目用一句话概括就是用一块支持Wi-Fi的ESP32-S3开发板配合麦克风阵列扩展板和喇叭做成一个能“对话”的桌面语音助手语音识别和AI对话都在电脑端或云端的大语言模型上完成板子只负责采集声音、播报回答。这听起来像是一个很“硬核”的嵌入式项目但说实话它比你想象中简单得多因为它巧妙避开了在单片机本地跑神经网络这个最大的坑把重活都交给了Python和大模型API。我前后折腾了大概两周踩了不少坑也总结出了一套从零到能用的完整方案。这篇文章就沿着我当时的思路把硬件选型、代码框架、大模型接入到最后的调试排错一整个链路写清楚如果你也想做一个类似的AI语音助手可以直接照着走。1. 项目整体设计与思路拆解1.1 为什么选ESP32-S3而不是树莓派或者普通ESP32先聊选型。很多人拿到这个项目的第一反应是既然还要接Python和大模型那我直接用树莓派不就行了树莓派当然能做但体积、功耗、启动速度、价格都不是一个量级。桌面级语音助手要的是“随时唤醒、即开即用”树莓派那套完整Linux系统启动就要十几秒而且长时间待机发热和成本都是问题。ESP32-S3则是一颗专门为AIoT场景设计的双核MCU主频240MHz带向量指令扩展性价比非常高一两百块的开发板就能过得很好。再对比老款ESP32。经典ESP32双核240MHz也不是不能用但内存只有320KB左右的SRAM想要做稍微像样点的音频缓冲、FFT处理或者本地唤醒词检测余量就非常紧张。ESP32-S3的SRAM达到512KB还带16MB的Flash支持PSRAM外部扩展处理PCM音频数据流更从容而且它对I2S外设的支持更完善。最关键的是ESP32-S3自带向量指令跑一些轻量神经网络比如关键词唤醒性能翻倍。既然标题叫“从零构建”那就选上限更高、资料更多、后期还能继续玩本地AI的S3。1.2 系统架构板子负责耳朵和嘴电脑负责脑子整个系统的分工我是按下面这张“隐形架构图”来设计的ESP32-S3端负责通过I2S接口从麦克风采集16kHz/16bit的PCM音频流通过Wi-Fi把音频上传到电脑端软件接收电脑端返回的文本通过I2S/DAC输出到喇叭播放TTS语音结果本地可以预留一个唤醒词检测比如“你好小智”避免全程录音上传带来的隐私和流量浪费。电脑端Python架设一个简单的WebSocket或HTTP服务接收ESP32发来的音频数据调用语音识别ASR引擎把音频转成文本将文本交给大语言模型LLMAPI获取回答再把回答文本交给语音合成TTS引擎生成音频返回给ESP32播放。大语言模型API这里的选型比较灵活可以是云端的GLM、通义千问、DeepSeek、Kimi等任何兼容OpenAI接口格式的服务也可以用本地部署的Qwen、ChatGLM等模型。项目里我用的云端API因为免去显卡和部署的烦恼。这个分工的最大好处是ESP32-S3的固件量可以做得非常精简稳定性高Python端拥有整个生态可以随意调库、换模型不需要在单片机上抠内存。换句话说这是一条典型的“端云协同”路线和市面在售的智能音箱原理是一模一样的只不过我们把云端大脑换成了自由度更高的开源模型。1.3 为什么把ASR/TTS/LLM全部独立成模块一开始我图省事想把整个链路塞进一个大Python脚本里。后来发现每次换TTS引擎或者想调试某个环节都要动整个链路调试日志混乱不堪。于是我花了一点时间把程序拆成了六个模块主服务Main Server、音频接收Audio Receiver、语音识别ASR Engine、大模型对话LLM Engine、语音合成TTS Engine、设备管理Device Manager。每个模块之间通过简单的消息队列或回调函数通信互不依赖具体实现。比如今天我用的是讯飞ASR明天想换成Whisper只需要把ASR Engine内部代码替换一下对外接口不变即可。这种“插件化”的思路不仅让整个项目好维护也方便社区协作——别人想贡献一个更好的TTS实现直接写个新类替换就行。2. 硬件准备与开发环境搭建2.1 开发板、麦克风、喇叭怎么选我先列一下我的配置给大家参考硬件型号/规格说明与避坑建议主控开发板ESP32-S3-DevKitC-18MB Flash8MB PSRAM建议带PSRAM后续可玩图像/更多内存场景麦克风INMP441I2S MEMS麦克风便宜好用I2S数字输出无需运放功放喇叭MAX98357A I2S功放 3W/8Ω小喇叭MAX98357A同样走I2S接线超简单电源5V/2A USB供电喇叭音量开大后电流波动劣质充电头会导致重启其他杜邦线若干、面包板实验阶段别急着焊接改线会崩溃这里有个非常关键的点麦克风和功放都使用I2S接口但I2S在同一时刻是单向通信的——麦克风占用接收通道RX功放占用发送通道TX在ESP32-S3上两者可以共用一个I2S外设的不同引脚但需要开启全双工模式。很多人第一次做的时候发现只有录音或只有播放多半就是把这层关系搞混了。2.2 麦克风与喇叭的接线速查表INMP441和MAX98357A都是标准I2S从设备所以接线思路一致各自的SCK位时钟、WS左右声道选择、SD/SDOUT数据接到ESP32-S3的对应引脚即可。我用了如下引脚分配通过Arduino IDE或ESP-IDF代码都可修改ESP32-S3引脚连接设备/功能说明GPIO 4麦克风SCK / 功放SCK共用I2S位时钟BCLKGPIO 5麦克风WS / 功放WS共用左右声道时钟LRCKGPIO 6麦克风SD数据输出麦克风数据进ESP32GPIO 7功放DIN数据输入ESP32数据出到功放GPIO 15功放SD_MODE拉高使能功放也可接PWM控制音量3.3V/GND各设备电源注意INMP441的VDD和GND不能反接我实际使用中I2S的BCLK频率设为2.4MHz采样率16000Hz16bit单声道效果很稳定。注意INMP441有个L/R引脚决定它输出到左还是右声道如果接GND则输出左声道接VDD则输出右声道。因为我只用一个麦克风所以把L/R直接接GND然后代码里读取的是I2S的left通道数据。2.3 烧录ESP32-S3固件Arduino还是ESP-IDF给ESP32-S3写固件主流有两条路Arduino框架或ESP-IDF框架。我的建议是如果你只做这个语音助手项目直接用Arduino如果你想长期做复杂音频产品学ESP-IDF。标题说“从零构建”所以我的重点放在快速落地Arduino生态里库多、上手快很多I2S音频例子改改就能用。环境配置方面在Arduino IDE里添加ESP32开发板支持包链接然后安装esp32 by Espressif Systems开发板包。值得注意的是Arduino IDE开发板管理器现在能直接搜到esp32不要下载来路不明的第三方包否则后面调试GPIO和Wi-Fi问题会让你崩溃。装好之后选板型ESP32S3 Dev ModuleFlash大小和PSRAM都要按你板子的真实配置来比如我的是16MB Flash / 8MB PSRAM。烧录前按住板上的BOOT键多数开发板这样才能进入下载模式。串口波特率选921600烧录速度会快很多。2.4 电脑端Python环境准备电脑端我用的Python 3.103.11/3.12也完全兼容但某些音频库对3.12的wheel支持还不全稳妥起见用3.10。强烈建议用conda或venv建独立虚拟环境避免污染系统Python。项目依赖的库其实不多核心是pip install websockets pyaudio requests openai如果你要在本地用FunASR或Vosk做离线ASR再额外装pip install vosk funasr modelscopePyAudio在Windows上偶尔会因为没有预编译的wheel安装失败简单粗暴的解决方案是下载PyAudio‑0.2.14‑cp310‑cp310‑win_amd64.whl这一类的二进制文件然后本地pip install。macOS上则更流畅直接brew install portaudio再pip install pyaudio。3. 核心代码实现从录音到对话到播报3.1 ESP32-S3侧I2S录音与播放先上ESP32端最简录音示例Arduino框架。这段代码初始化了I2S全双工模式一个输入麦克风、一个输出功放#include driver/i2s.h #define I2S_BCLK 4 #define I2S_WS 5 #define I2S_SD 6 #define I2S_DIN 7 // 功放数据输入 #define SAMPLE_RATE 16000 #define BUFFER_SIZE 512 void setup() { i2s_config_t i2s_config { .mode (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX), .sample_rate SAMPLE_RATE, .bits_per_sample I2S_BITS_PER_SAMPLE_16BIT, .channel_format I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags ESP_INTR_FLAG_LEVEL1, .dma_buf_count 8, .dma_buf_len BUFFER_SIZE, .use_apll false, .tx_desc_auto_clear true, .fixed_mclk 0 }; i2s_pin_config_t pin_config { .bck_io_num I2S_BCLK, .ws_io_num I2S_WS, .data_out_num I2S_DIN, .data_in_num I2S_SD }; i2s_driver_install(ESP32_I2S_NUM0, i2s_config, 0, NULL); i2s_set_pin(ESP32_I2S_NUM0, pin_config); }录到数据后你需要加上Wi-Fi传输逻辑。我的做法ESP32连上路由器通过WebSocket连接到电脑端服务每采集到一段2048字节数据就立刻发送。音频数据是16bit PCM按小端序打包后直接传二进制帧。这一步很考验网络稳定性我会在调试篇详细讲。播放端则反过来电脑端把TTS生成好的PCM流拆包推过来ESP32收到后调用i2s_write写到功放。这里有个经验播放前先清空DMA缓冲不然会夹杂开机或上一次播放的残留数据。3.2 Python服务端WebSocket接收音频流Python端我用websockets库起一个异步服务监听8765端口。每个ESP32设备连上来后分配一个会话ID后续所有音频帧和文本都挂在这个会话下管理。import asyncio import websockets SESSIONS {} async def handle_client(websocket): device_id await websocket.recv() SESSIONS[device_id] {ws: websocket, audio_buffer: b} print(f[设备上线] {device_id}) try: async for message in websocket: process_audio_chunk(device_id, message) except websockets.exceptions.ConnectionClosed: print(f[设备离线] {device_id}) SESSIONS.pop(device_id, None) async def main(): async with websockets.serve(handle_client, 0.0.0.0, 8765): await asyncio.Future() asyncio.run(main())process_audio_chunk里做的事情很简单收到的音频帧先缓存缓存长度超过一定阈值后把整块交给ASR引擎识别。为什么要攒够一帧再识别因为Vosk这类流式ASR是按固定帧长处理的你一次扔一个不完整的块识别错误率会明显升高。我的阈值设为3200字节约100ms的16kHz音频。3.3 语音识别ASR云端API还是本地VoskASR是整个链路里最容易出效果的环节也是翻车最狠的环节。我建议根据你的网络和机器情况二选一在线ASR方案比如讯飞、阿里云、腾讯云识别率高、支持多语言但私密性和免费额度都有限。适合先跑通全流程。本地Vosk方案完全不依赖外网模型也不大几十MB中文识别效果在安静环境下还不错。适合离线场景。我最终的方案是本地Vosk为主 云端ASR备用。Vosk的接入代码非常简单加载模型后用KaldiRecognizer边接收音频边输出结果from vosk import Model, KaldiRecognizer import json model Model(vosk-model-small-cn-0.22) rec KaldiRecognizer(model, 16000) def asr_recognize(pcm_bytes): if rec.AcceptWaveform(pcm_bytes): result_json rec.Result() text json.loads(result_json).get(text, ) return text return 注意它的AcceptWaveform不是一次把所有音频塞进去就出结果而是持续投喂PCM数据等到说话停顿或结束才返回整句。我在系统里做了一个音量检测当检测到用户沉默超过800ms时就调用rec.FinalResult()强制取出最终结果避免对话等待时间太长。3.4 大语言模型接入兼容OpenAI接口的通用方案这一部分是整个项目的灵魂。考虑到目前可选的模型API很多而且经常有免费或低价额度我推荐的做法是使用OpenAI SDK但把base_url换成你想要调用的大模型服务的地址。这样可以随时切换后端模型不用改业务代码。例如调用一个兼容OpenAI协议的在线免费/低价模型from openai import OpenAI client OpenAI( base_urlhttps://你的模型服务地址/v1, api_key你的API_KEY或占位符 ) def ask_llm(system_prompt, user_message): response client.chat.completions.create( modeldeepseek-chat, # 或 qwen-turbo / glm-4-plus / 本地模型名 messages[ {role: system, content: system_prompt}, {role: user, content: user_message} ], temperature0.7, max_tokens1024 ) return response.choices[0].message.content说到“还有没有免费的大模型API”我的实践体会是真正的长期免费商用几乎没有但各家新用户/开发者试用额度很充足。比如有些平台会送几百万tokens对小语料个人助手来说够玩一两个月。与其纠结找一个永久的免费源不如写一个可配置的适配层把API的key和base_url放在一个config.ini里随时切换。尤其别因为某个渠道免费就去薅毕竟这种渠道稳定性没保障今天能用明天挂掉的概率很大。另外如果你有一定预算或手头有显卡本地部署大模型也是非常推荐的进阶路线。用ollama跑一个7B-14B的量化模型整体体验完全碾压在线小模型而且延迟更低、隐私更好、也没有内容审核限制。本地部署的命令非常简单ollama pull qwen2.5:7b ollama serve然后Python端直接把base_url改成http://localhost:11434/v1即可。3.5 语音合成TTS让助手开口说话TTS方案的选择直接影响助手的“人味”。我测试过几种Edge TTS微软出品免费有多种中文音色音质自然度很高。它对标的是在线TTS服务。缺点是需要联网且接口偶尔会抖动。pyttsx3本地计算完全离线但音色机械适合快速验证流水线。PaddleSpeech / GPT-SoVITS本地高质量TTS但部署相对繁琐适合进阶。日常使用我强烈推荐edge-tts一行命令就能生成MP3而且支持调节语速、音量、音调import edge_tts async def text_to_speech(text, output_path): tts edge_tts.Communicate(text, voicezh-CN-XiaoxiaoNeural, rate10%) await tts.save(output_path)生成MP3之后怎么给ESP32播放是个小坑。ESP32的I2S功放不支持MP3硬解所以你必须先把MP3转成PCM单声道16bit WAV再拆成小块推送过去。这个转换用Python的pydub或ffmpeg一行命令搞定。我自己是写了一个工具函数from pydub import AudioSegment def mp3_to_pcm16_mono(mp3_path): audio AudioSegment.from_mp3(mp3_path) audio audio.set_frame_rate(16000).set_channels(1).set_sample_width(2) return audio.raw_data拿到PCM裸数据之后按照上面说的WebSocket协议分包发送给ESP32就行。播放完成的同步很重要我是在ESP32收到最后一个数据包后主动回发一个END_PLAY标记Python端收到标记才认为语音播放完毕再进入下一轮唤醒监听。3.6 全流程串联状态机让系统有序运转把ASR、LLM、TTS串起来后最容易遇到的问题就是“数据流打架”。比如用户上一句话还没回复完下一句又开始识别了。我听过大佬的建议是用状态机管理整个流程。我定义了几个简单状态状态含义进入条件退出条件IDLE空闲等待语音输入系统启动收到唤醒词/按键触发LISTENING录音中唤醒/按键触发检测到静音超时或音频超长PROCESSINGASRLLM处理中音频采集结束得到回答文本SPEAKINGTTS播放中文本生成完成播放结束回到IDLE每个状态内只响应自己状态允许的事件。比如在PROCESSING阶段收到新的音频帧直接丢弃在SPEAKING阶段则只允许按键打断不支持语音打断做语音打断要另加VAD线程比较复杂初版不做。4. 调试心得与常见问题排查4.1 串口输出乱码或烧录失败ESP32-S3的串口乱码首先怀疑波特率不匹配。Arduino IDE默认串口监视器是115200但如果你的固件里Serial.begin(9600)那当然满屏乱码。还有个经典问题是开发板接线时USB转串口芯片常见的有CP2102和CH343驱动没装好在设备管理器里能看到未知设备或者能识别但打开串口报错。Windows系统装好CP210x驱动后基本能解决。烧录失败更常见的是“芯片处于保护状态”或“连接超时”。我的经验是按住开发板上的BOOT键不放然后点烧录看到“Connecting...”提示后立即松开BOOT键成功率会高很多。另外杜邦线过长也会导致下载不稳定USB线尽量用质量好的数据线而不是充电线。4.2 I2S只有录音没有播放或全是电流声I2S的播放问题我排查了很久。只有数据没有声音先量一下功放MAX98357A的SD_MODE引脚电压如果为低芯片就在关断状态。这个引脚不能悬空必须接高电平或PWM。电流声大则有三个常见来源电源噪声喇叭和开发板共用USB供电当音量开大时电源纹波会直接耦合进音频。改用独立5V电源或在功放电源脚加100uF电解电容0.1uF瓷片电容。WS和BCLK接反I2S三根线里任何两根互换出来的都是刺耳噪音或完全无声。GND回路电压差麦克风、开发板、功放各自的GND都用杜邦线连接时如果线阻不一致会产生地环路噪声。尽量采用星型接地模式所有GND从开发板电源引脚单点引出。4.3 大模型API超时或频繁断连在线API时不时超时太正常了。ESP32里用WiFiClient直连TCP会经常超时Python端用WebSocket反而好一点。我的建议是Python端把LLM调用放进独立线程池这样某个请求卡住时不影响ASR和TTS主循环继续工作。如果彻底不想被在线API绑死就尽早换成本地大模型。另外大模型返回格式偶尔会把回答包在Markdown代码块里TTS会把反引号、星号这些符号读出来很难听。我在Python端做了一个清理函数把Markdown语法符号和多余换行全部替换成句号或自然停顿后再送给TTSimport re def clean_markdown_for_tts(text): text re.sub(r[#*_~\[\]()], , text) text re.sub(r\n, , text) text re.sub(r\s, , text).strip() return text4.4 唤醒词与误唤醒比想象中麻烦很多教程里唤醒词是简单粗暴的“一直录音本地检测到关键词再上传”。但一直录音占带宽、耗电而且麦克风把空调声、键盘声全采进去误唤醒率很高。我采用的策略是先用ESP32-S3本地跑一个轻量的关键词识别只有识别到唤醒词才开启网络传输。ESP32-S3上可以跑TinyML的唤醒词模型比如使用ESP-DL框架做关键词分类。不过这个对新手有点门槛。更简单的替代方案是ESP32本地只做能量检测过零率检测当声音能量大于阈值并持续超过200ms才上传给电脑端做云端ASR。这样既减少了网络数据量又能挡住大部分静音环境下的误触发。如果你想实现“你好小智”这类真·唤醒词可以直接用ESP-SR官方语音识别库它自带中文唤醒词模型跑在ESP32-S3上非常流畅。不过该库在Arduino框架下的集成文档较少需要一些耐心。4.5 延迟优化从Wi-Fi到TTS播放的链路压缩语音助手的体验核心就一个字“快”。我实测全链路的时延分布大致是音频采集上传约200-400msASR约100-300msLLM推理约500-1500ms取决于模型和排队情况TTS合成约300-800ms播放约500ms。整体大概2-4秒。如果想让体验更好可以考虑这几个优化点语音识别改成流式ESP32边录音边发ASR边识别边产出中间结果用户说完之前就能把前半句提交给LLM。LLM流式输出大模型边生成边把文本片段送到TTS语音合成也流式生产这样用户听到第一个字的延迟能大幅缩短。TTS音频预缓存针对一些高频固定回复比如“我在呢”“请讲”提前生成好语音缓存播放时直接读内存省掉一次合成时间。这几点我在第二版迭代时逐步加了进去整体响应时间缩短到1.5秒左右已经接近市售智能音箱的体验。5. 进阶玩法与后续扩展思路5.1 从“回话机器”升级为“AI Agent”目前做出来的语音助手本质上还是一个“问答机器”——用户问什么它答什么。要让它的实用性上一个台阶就要引入AI Agent的概念。最简单的方式是给Python端加一个工具调用Function Calling层让大模型在回答前可以调用你预定义好的几个函数比如查询天气大模型输出{function: weather, args: {city: 上海}}Python端自动请求天气接口把结果再送回模型生成最终回答。控制智能家居同样让模型输出控制指令Python端通过MQTT或HTTP控制家里的灯光、插座。设置倒计时模型解析出时间参数后Python端调用系统定时器到点后触发播报。这个扩展不复杂核心是把大模型返回的内容解析成结构化的工具调用再走一遍生成流程。一旦跑通你的语音助手就不是只能回答问题而是能做事情的助手了。5.2 加入视觉能力与屏幕交互考虑到ESP32-S3的PSRAM和算力还可以接一个OV2640摄像头做成“AI语音视觉”的双模态助手。比如你对着它说“帮我看看桌上有什么”ESP32拍一张照片传到Python端交给视觉大模型分析再把结果语音播报出来。如果你不想接摄像头也可以加一块小屏幕如ST7789把大模型的回答滚动显示在屏幕上和语音播报互补——有些信息听不清时看屏幕反而更直观。这部分代码不算复杂但要额外处理屏幕的刷新逻辑不要让屏幕绘制阻塞了音频主循环。5.3 隐私保护的离线全栈方案我开头说了“板子负责耳朵和嘴电脑负责脑子”如果想进一步做隐私保护可以把这个架构改成完全离线。具体来说ESP32-S3和电脑之间通过本地局域网或USB直连ASR用Vosk本地模型LLM用ollama或llama.cpp本地推理TTS用PaddleSpeech或Piper。这样整个链路除了你自己没有任何第三方服务器参与不仅隐私绝对安全响应速度也快尤其适合在公司或敏感环境里使用。代价是本地模型的质量和配置门槛比在线API高一些。我的建议是先把在线方案跑通体验到完整流程再慢慢切换到离线方案。一口吃不成胖子。写在最后一点个人体会这个项目带给我最大的收获不是“语音助手”本身而是让我真正理解了端云协同的工程化思路——硬件端做好采集和播放云端做好计算和生成两边用一条稳定的数据通道解耦。这个思路用在智能家居、可穿戴设备、工业语音交互上都是一脉相通的。ESP32-S3Python大模型这套组合可能是目前玩AI硬件最舒服的组合之一它把硬件的门槛降到了极低把AI的能力发挥到了极致。如果你按这篇文章试着做一次我敢说你踩的大部分坑都能在最后一章找到答案。如果真的遇到我文章里没提到的问题也欢迎在评论区留言讨论。做硬件就是这样最快乐的不是代码跑通的那一刻而是自己动手把一个个问题从“玄学”变成“科学”的过程。祝你们都能做出一个愿意天天陪自己说话的小助手。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →