ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ESP32-S3端云协同实战:从零搭建AI语音陪伴设备

2026/9/9 6:44:08 拓冰建站 浏览量
ESP32-S3端云协同实战:从零搭建AI语音陪伴设备 最开始我只是想做一个能摆在床头、可以聊天的“电子宠物”。当从抽屉里翻出那块吃灰很久的 ESP32-S3-DevKitC-1 开发板时我并没有想到几个月后它会变成一台支持离线唤醒、在线对话、还能远程升级的 AI 陪伴设备。整个过程最大的收获不是这块板子有多能打而是我一步步验证了一条“端侧做端侧该做的事云侧做云侧该动脑的事”的端云架构路子并且这条架构能持续演进不被某一家模型厂商或某一种硬件形态绑死。这篇内容想把完整的搭建过程拆开讲清楚为什么选 ESP32-S3音频链路怎么通唤醒词怎么落地云端大模型对话怎么编排延迟怎么压进两秒以及最重要的是——怎么设计一套能“持续演进”的架构让设备联网后还能不断变聪明。适合正在做 AIoT 原型、想做语音交互硬件、或者单纯好奇“一块开发板怎么接大模型”的朋友参考。1. 项目整体设计思路不是把开发板接到大模型就完事1.1 这个项目要解决的真实问题市面上很多“AI 硬件”演示本质上就是把麦克风录一段音送到云端识别成文字再丢给大模型返回一段话最后用 TTS 念出来。逻辑上没错但真要落地成日常能用的设备问题会冒出一大堆唤醒怎么办设备不能一直录音上传成本和隐私都受不了得有一个本地的“小耳朵”始终监听。打断怎么办设备正在说话时用户开口了要不要立刻停断网怎么办家里 Wi-Fi 抖动、云端服务超时设备是变砖还是退回本地能力延迟怎么控从“用户说完”到“设备开口”超过两秒就会觉得“这设备好蠢”。模型升级怎么办今天接入的模型是 A明天想换成 B不能重新烧固件。唤醒词不准怎么办灵敏度调高了误唤醒频发调低了叫不醒。所以这套方案的核心不是某个单一功能而是把上述问题统一到一个分层架构里端侧负责实时性要求高的部分云侧负责智力密集的部分两端通过一条设计良好的链路协作。1.2 ESP32-S3 芯片选型的四个理由先说结论如果做“语音交互 简单控制”的陪伴设备原型ESP32-S3 是现阶段性价比很高的选择。第一算力足够且专门为语音场景做了优化。ESP32-S3 是双核 Xtensa LX7最高 240MHz带向量扩展指令跑 ESP-IDF 官方的 ESP-SR 语音识别框架时唤醒词和语音活动检测VAD可以在本地实时完成。实测下来双核跑“唤醒监听 Wi-Fi 协议栈 音频采集”还有不少余量。第二内存和存储配置灵活。芯片自带 512KB SRAM很多模组还外挂了 8MB 或 16MB 的 PSRAM。做音频缓存、协议栈缓冲、TTS 播放缓冲都非常吃内存没有 PSRAM 会非常局促。我用的开发板是 8MB PSRAM 的版本整个开发过程基本没被内存卡过脖子。第三外设接口齐全。I2S 直接接数字麦克风和数字功放I2C 接传感器UART 接屏幕或舵机GPIO 控制灯和电机。这意味着一块板子既能做“陪伴聊天”也能顺手把“开灯”“摇尾巴”“显示表情”这些动作一起做了。第四生态成熟。ESP-IDF 本身就是做物联网产品级的框架OTA 升级、Wi-Fi 断线重连、低功耗管理都有现成组件。相比在裸机 MCU 上从零搞网络协议栈省下来的时间可以全花在业务逻辑上。有人会问为什么不用树莓派或 RK3588 这类更强的板子答案很简单成本和功耗。树莓派跑本地大模型确实可以但一块树莓派加散热和电源成本是 ESP32-S3 方案的十倍以上待机功耗也高一个数量级。陪伴设备是长时间待机的产品不是训练服务器。1.3 整体端云链路预览这条链路可以概括为六个环节麦克风采集音频16kHz/16bit/单声道。端侧 ESP-SR 做唤醒词检测和语音活动检测判断用户是否在说话。设备通过 Wi-Fi 把音频流推送到云端接入层WebSocket 长连接。云端依次调用 ASR 识别服务、大模型对话服务、TTS 语音合成服务。合成好的音频数据回传到设备端。设备端通过 I2S 功放播放音频同时保持监听随时准备被打断。这里面最关键的设计原则是音频是唯一的交互媒介但智能逻辑全部在云端端侧只保留“听的耳朵”“说话的嘴”和“动手的四肢”。这样后续无论换更强的模型还是加入新的技能都不需要动硬件。2. 端云分工哪些逻辑放端侧哪些放云端2.1 为什么不能全端侧也不能全云端先算一笔账本地跑一个 7B 参数的量化大模型模型文件普遍在 3GB 以上推理一次需要至少 4GB 内存。ESP32-S3 的 PSRAM 一共才 8MB差了三个数量级所以“在 ESP32-S3 上直接跑大模型”这条路在物理上就不通。但反过来如果把所有逻辑都放云端也不行。设备断电断网就完全变成一块砖头用户说“打开灯”这种固定指令也要等云端绕一圈延迟高且不稳定还有人会担忧隐私——房间里所有对话都实时上传体验上就很别扭。所以最终采用“端云协同”的折中方案端侧负责毫秒级响应的能力唤醒词检测、语音活动检测、简单指令识别、回声消除、播报控制。云侧负责高智力密度的能力自由对话理解、长短期记忆、情感陪伴、语音合成、各类技能编排。两边重叠的部分比如简单指令也可以云端再兜底识别一次作为容错冗余。2.2 端侧具体做什么唤醒、降噪、本地指令我把端侧的能力收敛成三个模块第一个是唤醒。使用 ESP-SR 的 WakeNet支持自定义唤醒词。实测中文三音节和四音节唤醒词效果最好比如“你好小灵”四个字既不容易误唤醒叫起来也顺口。如果唤醒词太短比如“小灵”两个字模型很难区分日常对话中的相似音节误唤醒率会明显上升。第二个是语音活动检测。这解决的是“用户说完没有”的问题。设备采集到声音后需要判断人声开始和结束的边界把有效语音段截出来再上传。ESP-SR 自带的 VAD 模型可以设置静音阈值太灵敏会把呼吸声也当成语音太迟钝又会切掉句尾。我最终把阈值调到了 0.15 附近并且加了一段 300ms 的尾音缓冲确保“嗯”“啊”这类语气词不会导致句子被提前切断。第三个是本地指令表。我内置了十几个高频固定指令比如“打开台灯”“关闭台灯”“讲个冷笑话”“现在几点”。识别逻辑很简单端侧跑一个轻量关键词匹配匹配上就走本地执行路径不需要联网。这一层让设备在弱网环境下也保留基本可用性体验提升非常明显。2.3 云端具体做什么ASR、大模型对话、TTS云端这条链路由三个服务组成我用一个对话网关服务串起来ASR 服务负责把音频变成文字。我在接入时选择了支持 WebSocket 流式识别的方案一边收音频一边出中间识别结果。这样用户话音刚落识别文本基本已经可用省掉了“录音完整上传再等结果”的往返时间。大模型对话服务负责语义理解和回复生成。这里没有直接用裸的模型 API而是在外面包了一层对话编排层把系统提示词、多轮历史、用户画像、内容安全过滤都做在这里。对比直接调 API这种方式的优势是后续可以随时换模型供应商对话逻辑对设备端完全透明。TTS 服务负责把文字变成语音。为了让设备尽快“开口”我选择支持流式返回的 TTS先合成第一句话的开头就立刻下发给设备端播放而不是等整段音频合成完。实测首包音频 600 毫秒左右就能到设备体感上比非流式方案快了近一倍。2.4 让系统可持续演进的三个关键设计“可持续演进”不是一句口号而是架构里实实在在的约束条件。我总结下来有三个关键设计必须从第一天就做第一端侧和云侧之间只走“音频帧 文本消息 控制指令”三类协议禁止端侧解析任何业务数据。这样云端改业务逻辑、换模型、调整人设设备端固件完全不用动。第二云端必须做模型网关层。设备端和对话服务都不直接依赖具体模型的 SDK而是统一走一套兼容接口。换模型时只需要改网关配置客户端协议零改动。这避免了“换一个模型就要重新发一版固件”的灾难。第三OTA 通道和日志链路要优先搭建。很多硬件项目是把功能做完才补 OTA结果发现升级分区没留、证书没烧、日志没法远程拉取只能拆机刷固件。我是先把升级通道打通再开始写业务逻辑后面的迭代效率完全不一样。3. 端侧核心实现从开发板到能说话的硬件3.1 硬件连接与音频链路搭建我用的硬件清单如下组件型号作用主控开发板ESP32-S3-DevKitC-18MB PSRAM主控与 Wi-Fi 联网数字麦克风INMP441I2S 接口采集用户语音I2S 功放MAX98357A驱动喇叭播放 TTS 音频喇叭3W/4Ω 小喇叭声音输出电源5V/2A USB 供电开发阶段供电INMP441 是 I2S 接口的 MEMS 麦克风接法非常固定VDD 接 3.3VGND 接地SCK 接 I2S 时钟WS 接字选择信号SD 接数据输入。MAX98357A 那边同样走 I2SBCLK、LRC、DIN 分别接对应引脚GAIN 引脚悬空即可获得默认 9dB 增益。这里有几个容易踩的坑INMP441 的 L/R 引脚决定它输出在左声道还是右声道。我把它接高电平右声道代码里读数据时就要听右声道左右搞反会导致噪声数据一片混乱。MAX98357A 的供电电流不能忽视峰值时可能到 1A 以上。如果和开发板共用同一个稳压源低电量时会出现喇叭破音甚至开发板重启。建议功放供电单独走一路至少保证电源能提供 2A 电流。I2S 时钟引脚不要和 Wi-Fi 天线走线靠太近否则音频数据会出现偶发丢帧表现就是播报时“滋啦”一声杂音。3.2 ESP-SR 唤醒词与 VAD 配置实操ESP-SR 不是开箱即用的需要先在 ESP-IDF 里把组件拉下来。如果是在现有工程里加可以用idf.py add-dependency espressif/esp-sr的方式引入。关键配置都在menuconfig里唤醒词模型Component config → ESP-SR → Wake word engine → Wake word model选择对应的中文唤醒词模型文件。默认带几个预置模型如果都不满意可以用 ESP-SR 的训练服务训练自定义唤醒词训练出来的是一个.bin文件放到工程里并配置路径即可。VAD 模式VAD model → VAD mode可选 0 到 3 四档。0 最灵敏3 最迟钝。我这里选 2配合代码里的尾音缓冲实测在正常家居噪声下效果最好。音频输入格式Audio front-end里确认是 16kHz、16bit、单声道。如果麦克风通道配置不对后面唤醒和识别都会出问题。代码层面的逻辑可以简化为初始化 I2S 麦克风持续读取音频帧。每帧音频同时喂给 WakeNet 和 VAD。一旦 WakeNet 检测到唤醒词设置状态为 “listening”开始积累音频。VAD 检测到人声结束后把缓存的有效音频取出来打包上传云端。在等待云端返回期间继续跑 WakeNet一旦再次检测到唤醒词立即打断当前播放并开始新一轮录音。3.3 音频数据打包与上传协议音频上传协议我设计得很简单但很实用。设备端和云端建立 WebSocket 长连接后所有上行音频都是二进制帧。帧结构如下字节偏移字段说明0-3Magic固定0xA1E0F0用于快速校验4Version协议版本号5Type1音频帧2文本指令3控制响应6-9Seq帧序号用于乱序检测10-13Timestamp采集时间戳用于日志排查14-15Payload Length负载长度16PayloadOpus 编码的音频数据音频编码我选了 Opus20ms 一帧码率控制在 24kbps 左右。原始 PCM 16kHz/16bit/单声道是 256kbpsOpus 压缩后不到原来的十分之一。这不仅省流量更关键的是让弱网环境下的上行更稳定。开发调试时也可以用原始 PCM 格式但原型验证之后建议尽早切到 Opus。这里要提醒一句ESP-IDF 自带 Opus 编解码库但需要自己在idf.py menuconfig里把esp_opus组件打开。否则在代码里调用opus_encode时会链接不到函数。3.4 端侧状态机与本地回退策略一个语音交互设备的端侧状态我用一个简单的状态机来管理状态触发条件动作IDLE上电初始化完成麦克风监听唤醒词其他外设休眠LISTENING唤醒词命中开始缓存音频等待 VAD 判定人声结束UPLOADING人声结束压缩音频帧并上传云端等待响应PLAYING收到云端音频播放 TTS 音频同时继续监听唤醒词BUSY正在执行本地指令执行完自动回到 IDLE这个状态机最大的作用是防止死锁。比如设备正在 UPLOADING 时网络超时要能自动回到 IDLE 并提示“网络好像不太好请稍后再试”。如果没有状态机制约很容易出现“一直停留在录音中”这种诡异 Bug。本地回退策略也很重要。我在设备里内置了一个 JSON 格式的本地指令表包含开关灯、播放白噪音、报时间这类不依赖网络的技能。当 Wi-Fi 断开或云端连续三次超时时设备自动进入“本地模式”本地指令全部可用自由对话提示“联网后我才能陪你聊天”。这个设计看起来不起眼但决定了设备在断网时是“小废物”还是“基本可用”。4. 云端服务层对话网关的实现细节4.1 ASR 接入与音频切片策略ASR 服务我选用的是支持流式识别的云端方案。WebSocket 上行音频后服务端会持续返回“中间结果”和“最终结果”。实际开发时有一个经验不要等整个句子说完再处理。我的做法是设备端每隔 20ms 发送一个 Opus 音频帧。云端 ASR 客户端实时把音频帧转发给识别服务。ASR 服务返回的中间结果先缓存在上下文里。当收到“最终结果”标记时把完整句子拼好送入下一步对话编排。音频切片的时间不能太长。 最开始我试过 200ms 一个包结果 ASR 服务端频繁报“音频帧间隔超时”识别率也下降。后来改为严格按照 20ms 一包正好一个 Opus 帧问题立刻消失。这个细节一定要在协议设计时考虑进去端侧的帧周期要和云端 ASR 服务的期望对齐。4.2 大模型对话编排人设、记忆与内容安全大模型对话服务是整个设备“情商”的核心。我没有把它做成简单的“请求-响应”接口而是做成了一个带上下文的对话引擎。人设System Prompt通过系统提示词设定 AI 陪伴者的性格、说话方式、知识边界。比如我设置的是“温柔可靠的朋友说话简洁自然偶尔幽默”。这套人设直接影响回复质感值得反复打磨。多轮记忆管理把用户和设备的历史对话存在云端内存里同时做长度裁剪。否则对话轮数一多请求上下文会膨胀到超出模型窗口长度。我的策略是只保留最近 10 轮对话更早的内容做摘要后拼接在系统提示词里。技能路由对话引擎收到用户文本后先过一个轻量的意图分类器。如果是“打开台灯”这类明确指令直接走设备控制服务下发指令给端侧不占用大模型推理只有自由聊天、开放问答才走大模型。这样既省钱又降低延迟。内容安全过滤在送入大模型之前和拿到回复之后分别做一轮内容安全筛查。这是 AI 应用上线的硬性要求不能省略。具体做法是维护一个敏感词表再用一个轻量文本分类器做二次过滤命中风险就返回预设的安全话术。对话编排的伪代码如下def process_user_message(user_text, session_id, device_id): session memory.get_session(session_id) intent intent_classifier.classify(user_text) if intent.action device_control: control_payload skill_manager.execute(intent) return {type: control, payload: control_payload} messages build_messages(session.system_prompt, session.history, user_text) reply model_gateway.chat(messages, temperature0.7) if safety_checker.is_unsafe(reply): reply SAFE_FALLBACK_REPLY session.add_turn(user_text, reply) session.trim_history(max_turns10) return {type: chat, reply: reply}4.3 TTS 语音合成与流式下发TTS 我最终选了流式合成的方案。流程是对话网关拿到大模型回复文本后立刻调用 TTS 服务。TTS 服务边合成边返回音频分片。网关每收到一个分片就通过 WebSocket 下发给设备端。设备端收到第一个分片就开始播放后续分片按顺序排队。这个方案把“等待完整音频”的时间省掉了。实测从大模型生成完文本到设备端发出第一个字流式方案能比非流式快 400 到 800 毫秒。对用户感知来说这就是“反应快”和“反应慢”的分水岭。TTS 服务的语音选型也很重要。陪伴设备不宜用新闻联播式的字正腔圆音色温暖、语速偏慢、带一点感情起伏的更适合。我最后选定了一款支持 SSML 标记的 TTS用break time200ms/来控制句间停顿用prosody rate0.95调整语速生成效果自然不少。4.4 延迟预算拆解两秒响应的目标怎么实现整条链路里每个环节都有延迟必须逐项拆解、做预算。环节预算毫秒优化手段麦克风采集 唤醒检测200-400本地 WakeNet无网络消耗语音活动判定100-200VAD 静音阈值 尾音缓冲音频上行传输100-300Opus 压缩 WebSocket 长连接云端 ASR 识别300-600流式识别边传边识别大模型首 token 生成300-800模型网关限流控制 并行预请求TTS 首包合成与下发300-600流式 TTS 边合成边下发设备端播放0-100分片播放器首包即播整条链路预算加起来约 1.7 到 3.0 秒。实测在家庭 Wi-Fi 环境、云端服务正常时整体响应约 2 秒网络抖动时会到 2.5 秒。这个体感对于陪伴设备来说可以接受但不是终点。后续计划通过“端侧预测用户说完时机提前预请求大模型”的方式进一步压缩到 1.5 秒以内。优化延迟时一定要按阶段打点记录每个环节的时间戳。一句话的完整日志串起来哪个环节超时一目了然。什么都不记录直接拍脑袋优化大概率会浪费大量时间在错误的地方。5. 可持续演进OTA、模型网关与形态扩展5.1 固件 OTA 升级的坑与方案设备联网后的第一件大事就是能远程升级固件。ESP-IDF 的 OTA 机制比较成熟但有几个坑必须先知道。需要在分区表里留出双 OTA 分区。即ota_0和ota_1各占约 1.5MB加上一个factory分区做最终兜底。升级时固件写入非当前运行的 OTA 分区写入完成后校验 SHA-256通过后切换启动分区并重启。万一新固件启动失败连续三次崩溃bootloader 会自动回滚到旧分区。升级文件的传输不能直接裸传大文件。一个小固件也有 1MB 左右直接走不稳定的 HTTP 很容易传到一半断掉。我加了断点续传逻辑设备端记录已接收偏移量重连后从偏移处继续下载。实测在 50% 丢包率的弱网环境下也能最终升级成功。最容易被忽略的是升级触发时机。 设备正在播放音频、正在对话时绝对不能重启。我在云端下发升级指令前会先查询设备状态只有 IDLE 状态才允许升级。这个看似简单的约束避免了一大堆线下返修问题。5.2 云端模型可插拔模型网关层模型网关是整个云端架构里最重要的抽象层。设备端和对话编排层都不关心底层用的是哪个大模型只调用模型网关的统一接口class ModelGateway: def __init__(self): self.provider os.getenv(LLM_PROVIDER, openai_compatible) self.base_url os.getenv(LLM_BASE_URL) self.api_key os.getenv(LLM_API_KEY) self.model_name os.getenv(LLM_MODEL_NAME) def chat(self, messages, temperature0.7): if self.provider openai_compatible: # 统一走 OpenAI 兼容协议 ...接新模型时只需要更新环境变量LLM_BASE_URL指向新模型的端点LLM_MODEL_NAME改成新模型名称。如果新模型不支持 OpenAI 兼容协议再为网关增加一个 provider 分支即可。网关层还要统一处理超时、限流、重试。 大模型服务偶尔会抽风网关会自动重试一次并在第二次失败时返回降级话术。不能让模型服务的故障直接暴露成设备的“哑巴”。5.3 从陪伴设备扩展到其他形态这套端云架构最大的价值是可复用。我后来把同一套架构快速迁移到了两个新硬件上一个是带表情屏幕的桌面机器人一个是支持按键交互的儿童故事机。改动量比预期小很多。原因在于架构上做了清晰的边界端侧只负责“音频采集、播放、物理控制、状态上报”云端只负责“感知、理解、决策、生成”。任何新的硬件形态只要具备麦克风、喇叭、网络这三样东西就可以接入同一个云端大脑。如果要扩展视觉能力比如加一个摄像头做表情识别也只需要在端侧增加一个“图像上传”通道云侧增加一个“视觉理解”服务。主干架构完全不需要推翻。这也是我认为这套方案能“持续演进”的根本原因它不是为某一个产品定制的而是为“设备接入智能”这件事设计的基础设施。6. 常见问题与排查技巧实录6.1 唤醒词识别率低、误唤醒频繁这是我调试过程中最闹心的一环。先说排查思路先确认麦克风通道正确。我用 INMP441 时发生过左右声道接反识别率惨不忍睹但很多人不会想到是声道问题。再看环境噪声。靠近风扇、空调出风口时误唤醒会明显增加。VAD 的阈值可以适当调高让设备只在“相对安静”的环境下才响应唤醒。唤醒词本身也很关键。两个字的唤醒词几乎必然误唤醒四个字的明显好很多。在实际场景里连续测试一周记录误唤醒次数再决定是否换模型。有些唤醒词模型自带相似词需要尽量避开生活高频词。6.2 对话延迟时高时低延迟波动最大的瓶颈通常不在设备端而在云端服务的冷启动和网络链路。排查方法是给每个环节加时间戳日志。设备端打印音频发送时间云端网关打印收到时间、ASR 返回时间、大模型首 token 时间、TTS 首包时间。把一条完整请求的日志拼接起来立刻能定位是哪个环节慢。我踩过的一个坑是云端服务在多设备同时请求时出现排队。网关旁路了数据库查询和日志上报导致阻塞后来把日志改成异步上报延迟就稳定下来了。另一个因素是 DNS 解析和 TLS 握手。长连接能避免每次请求都重新握手所以 WebSocket 比 HTTP 调用在延迟表现上有明显优势。6.3 设备播报时出现杂音和破音这个问题通常和电源、功放增益、音频格式有关。先检查电源是否用了额定电流不足的充电头功放和主控共地是否良好这些都可能导致电流噪声。再调低功放增益MAX98357A 的 GAIN 引脚可以选择 3dB、6dB、9dB、12dB 等不同档位。如果喇叭额定功率只有 1W强行上 12dB 增益必然破音。最后检查音频格式设备端初始化 I2S 时要匹配 TTS 返回的采样率比如 16kHz 的音频用 48kHz 播放声音就会“变速”甚至出现嘶嘶声。6.4 调试设备时的高效技巧我强烈建议给设备加一个“调试按键”和“调试模式”。长按按键后设备会把接下来 30 秒的原始音频保存到 SD 卡或上传到云端日志服务。这样用户报障“唤醒不了”时能直接拉到当时的音频文件人工回听比猜原因高效得多。云端日志要带设备 ID 和请求 ID。设备端每个请求都生成一个递增序号云端每一步处理都带着这个序号打日志。排查问题时用设备 ID 和请求 ID 一把梭出来整条链路的情况全在眼前。没有这套日志体系做语音交互设备的排障效率会低到让人崩溃。写在最后的实践经验这套“从一块 ESP32-S3 开发板到 AI 陪伴设备”的端云架构我前后跑了几个月最大的体会是端侧和云侧的边界一定要定义得早、定义得稳。设备端一旦被业务逻辑绑架后续每一次模型升级、技能扩展都会变得寸步难行。反过来只要边界清晰端侧哪怕只有最简单的音频采集和播放能力云端也可以不断给它注入新的智能。最后再分享一个小技巧从一开始就在代码里加上时间戳打点连调试串口打印都不需要太复杂只要能让你事后拼出“麦克风采集、上行、ASR、大模型、TTS、播放”的完整时间线你的优化效率就会比别人高一个数量级。等到做延迟优化和问题定位时你会由衷感谢当初那个愿意多写几行日志的自己。