ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP32-S3端云混合架构打造AI陪伴设备实战指南

ESP32-S3端云混合架构打造AI陪伴设备实战指南 说实话拿到这块 ESP32-S3 开发板之前我完全没想到它能变成面前这台会眨眼、会接话、断电还能继续呼吸发光的桌面小设备。前前后后折腾了快两个月中间推翻过一版纯端侧方案也试过把所有逻辑全塞云端最后定下来的是一套典型的端云混合架构唤醒词、状态机、本地表情反馈放在板子上语音识别、对话生成、语音合成交给云端中间靠一条可靠的长连接串起来。这篇文章把我在这套架构里的每个关键决策、代码思路、延迟数据和踩过的坑都整理出来给准备用 ESP32-S3 做 AI 硬件、尤其是做陪伴类产品的朋友一个可以直接参考的落地样本。1. 项目起点与架构选型思路1.1 为什么选 ESP32-S3 而不是树莓派或 Linux 盒子项目立项时第一个问题不是“跑什么模型”而是“用什么硬件”。我们内部讨论过树莓派 Zero 2W、RK3308 这类 Linux 小板子也讨论过纯 MCU 方案。最终定在 ESP32-S3 N16R816MB Flash 8MB PSRAM核心原因有三个。第一是成本和功耗。一块开发板三四十元整机 BOM 控制在百元以内而且待机时整板电流可以压到几十毫安级别。树莓派哪怕只是挂着功耗也是瓦级起步这就决定了它在桌面陪伴场景里很难做到“一直在线”的状态。第二是外设匹配度。ESP32-S3 自带 I2S、SPI、I2C 这些接口麦克风、功放、圆形彩屏都是直接对接不需要额外转换芯片。它还有向量指令扩展虽然跑不了大模型但跑唤醒词、VAD语音活动检测、AEC回声消除这些音频前端算法绰绰有余。第三是量产友好度。ESP32-S3 的 WiFi、BLE、OTA、低功耗管理都集成在官方 SDK 里供应链也稳。创业项目做到中后期一定会考虑量产和固件远程升级这颗芯片能把这条路提前铺好。如果你只是想快速验证 AI 对话体验树莓派当然更快。但如果你要做的是“长期摆在桌上不关机、低成本、可量产”的陪伴设备ESP32-S3 是更合理的起点。1.2 端云边界怎么划确定硬件后下一个问题才是关键哪些事情必须在端侧做哪些可以交给云端。我的划分原则很简单——凡是需要“即时响应”或“持续感知”的放端侧凡是需要“大模型能力”的放云端。端侧必须做的三件事唤醒词检测。设备不能一直把音频传上云端本地检测到唤醒词后才开始录音上传这也是隐私上更稳妥的做法。对话状态机与打断逻辑。用户说话时要不要打断当前播放播放时麦克风采样怎么处理这些逻辑必须在端侧毫秒级完成不能等云端反馈。基础的反馈反馈。屏幕表情、LED 呼吸、提示音这些本地反馈要跟状态切换联动做到零延迟。云端负责的三件事ASR 语音识别把音频变成文本LLM 对话生成包括角色人设、上下文管理TTS 语音合成把回复文本变成音频流回传。为什么不把 LLM 也放端侧现实很直接ESP32-S3 有 8MB PSRAM但一个像样的量化小模型动辄几十 MB而且端侧跑一次推理的耗电和耗时都不划算。端云混合不是妥协而是在当前硬件成本约束下的最优解。2. 硬件搭建与音频链路2.1 器件选型清单这套项目的硬件清单最终是这样的模块型号关键参数作用主控ESP32-S3 N16R816MB Flash、8MB PSRAM端侧逻辑、WiFi/BLE麦克风INMP441I2S 数字输出24bit拾音功放MAX98357AI2S 输入3W 输出驱动喇叭喇叭3W 8Ω 小口径直径约 40mm语音播放屏幕GC9A01240x240 圆形 LCDSPI表情与状态显示电池3.7V 锂电池 1000mAh带充放电保护供电这套组合不是随手指的。INMP441 是 I2S 数字麦克风信号直接以数字形式进入芯片不需要模拟放大和 ADC 采样抗干扰能力强很多。MAX98357A 也是 I2S 接口主控直接输出数字音频给它就行省掉了 DAC 和模拟功放电路。麦克风、功放都是 I2S意味着整条音频链路是纯数字的这在稳定性上优势很明显。2.2 I2S 麦克风采集链路ESP32-S3 的 I2S 外设配置是做语音交互的第一步。INMP441 需要三个信号BCLK位时钟、LRCLK左右声道时钟、DIN数据。在 ESP-IDF 里的配置逻辑大致如下我用 Arduino ESP32 框架比较多所以以 Arduino 环境举例#include driver/i2s.h #define I2S_WS 41 #define I2S_SCK 42 #define I2S_SD 2 void setup() { i2s_config_t i2s_config { .mode (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate 16000, .bits_per_sample I2S_BITS_PER_SAMPLE_32BIT, .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 1024, .use_apll false, .tx_desc_auto_clear false, .fixed_mclk 0 }; i2s_pin_config_t pin_config { .bck_io_num I2S_SCK, .ws_io_num I2S_WS, .data_out_num I2S_PIN_NO_CHANGE, .data_in_num I2S_SD }; i2s_driver_install(I2S_NUM_0, i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, pin_config); }有几点实际经验。INMP441 的 L/R 引脚决定数据走左声道还是右声道接 GND 表示左声道接 VDD 表示右声道两端不能悬空。我一开始把 L/R 悬空录音数据完全错乱。采样率统一用 16kHz、32bit 接收因为 INMP441 输出 24bitI2S 会放在 32bit 帧里后面转成 16bit PCM 再上传识别效果最稳定。DMA buffer 不要太小否则 WiFi 和 I2S 并发时容易丢数据我最终用的 8 个 buffer、每个 1024 帧。2.3 圆形屏与表情反馈GC9A01 是一块 240x240 的圆形 LCDSPI 接口常见的库是 TFT_eSPI 或 LovyanGFX。如果你只做表情展示TFT_eSPI 就够用。在User_Setup.h里需要把屏幕驱动改成 GC9A01并配置正确的 SPI 引脚。表情系统的设计比想象中重要。陪伴设备的第一印象不是语音而是视觉反馈。用户对着一张黑屏说话是很怪的。我实现了一套最简单的状态对应表情映射待机眼睛半闭缓慢闪烁唤醒眼睛睁开瞳孔轻微移动录音中眼睛放大底部出现波形动画播放回复眼睛眯起嘴角微微上扬断网或出错眼睛变成“X_X”。表情切换用 30ms 间隔逐帧刷新整个动画在 ESP32-S3 上非常流畅CPU 占用很低。别忘了屏幕背光引脚单独接到 GPIO待机时可以 PWM 调低背光这能省不少电。3. 端侧代码与配网实现3.1 BLE 配网流程一台没有屏幕的设备配网是最容易劝退用户的环节。我最后选择了 BLE 配网思路是设备首次上电进入配网模式蓝牙广播“AI-PAL-XXXX”的服务手机小程序或 App 连接后通过 BLE 写入 WiFi 的 SSID 和密码设备收到后连接路由器连接结果再通过 BLE 回传给手机。核心流程可以拆成这几步初始化 NVS 和 WiFi读取上次保存的 WiFi 配置如果没有有效配置进入配网模式初始化 BLE GATT Server定义 Service UUID 和 Characteristic UUID用于接收配网数据手机端写入格式约定为SSID|PASSWORD用|做分隔符设备收到后断开 BLE调用WiFi.begin(ssid.c_str(), password.c_str())连接成功或失败后重新开启 BLE 把状态写回手机成功后清除 BLE 广播。配网数据用竖线分隔而不是 JSON主要是降低解析复杂度和出错率。写入和返回状态用两个不同 Characteristic避免读写混淆。这里有个小坑ESP32-S3 默认 WiFi 和 BLE 是共存的但如果在配网完成后没有释放 BLE 资源后面 WiFi 的吞吐量会受到影响。配网成功后执行btStop()释放蓝牙栈内存实测 WebSocket 传输稳定性会更好。3.2 唤醒词、VAD 与录音上传设备日常处于待机状态ESP32-S3 里跑着一个常开的唤醒词检测。我用的乐鑫官方 ESP-SR 里的 WakeNet中文唤醒词“你好小智”模型放在 Flash 里运行时占用约 1MB 左右的 PSRAM。唤醒词检测到了一定要加去抖逻辑连续确认两次激活才真正进入录音状态这样可以避免误唤醒。唤醒成功后进入录音上传阶段。这段逻辑是整个端侧代码里最值得打磨的地方。录音采用“VAD 静音超时”策略唤醒后持续采集 16kHz/16bit 单声道 PCM 数据同时做能量计算。当检测到连续 1.5 秒的静音静音阈值可以通过环境噪声自动校准认为用户这句话说完了把整段音频上传云端。代码结构大致是这样// 伪代码完整逻辑看工程 while (recording) { i2s_read(I2S_NUM_0, pcm_buf, buf_len, bytes_read, portMAX_DELAY); // 计算RMS能量 float rms compute_rms(pcm_buf, bytes_read); if (rms noise_floor * 2.5f) { silent_ms 16; } else { silent_ms 0; } // 写入PSRAM 音频队列 audio_queue.push(pcm_buf, bytes_read); if (silent_ms 1500) { recording false; } }上传格式我先用了原始 PCM好处是简单可靠云端 ASR 直接可用。但 5 秒的语音大约 160KBWiFi 上传虽然能接受局域网或弱网下还是偏大。可以考虑用 Opus 压缩后传到云端云端再解码喂给 ASR能省下大约 10 倍流量。这块我放在演进计划里第一版先用 PCM 把链路跑通。3.3 对话状态机端侧逻辑如果不做状态管理代码很快就会变成一团乱麻。我设计的状态机是这套架构里最核心的部分一共五个状态状态含义进入条件离开条件IDLE待机初始化完成检测到唤醒词LISTENING录音中唤醒成功VAD 静音超时PROCESSING等待云端音频上传完成收到 TTS 首包PLAYING播放回复收到音频分片播放完成/被打断ERROR异常网络错误或超时3 秒后自动回 IDLE打断逻辑很关键。用户对陪伴设备最不能忍的一点是“我都插话了它还在自顾自地播”。我在 PLAYING 状态下让麦克风保持采样但不是做唤醒词识别只做能量检测。如果用户说话的音量明显超过环境底噪就立即停止播放回到 LISTENING 进入下一轮录音。实测这个方案比“用唤醒词打断”更自然因为用户在对话中不会每次都说唤醒词。状态迁移一定要全走中心化的事件分发不要在业务代码里到处改状态。我用了一个简单的enum和set_state()函数所有状态切换都必须从它过。这样后面接屏幕动画、接日志、接云端埋点都非常方便。4. 云端服务编排与端云协议4.1 端云协议设计端云之间的协议我坚持自己定义一套极简消息格式而不是直接对接某个厂商 SDK。原因很简单厂商 SDK 跟硬件耦合太深换一家服务商就要改一堆端侧代码。自定义协议之后ASR、LLM、TTS 全都可以替换。我用的 WebSocket 作为上行通道消息是 JSON 文本帧加二进制帧混合{type:session_start,device_id:s3-1234,timestamp:1719745000} {type:audio_end,duration_ms:3200}音频数据单独用二进制帧发送减少 Base64 编码开销。云端收到的处理顺序是session_start - audio_binary(若干帧) - audio_end处理完成后云端通过同一条连接推回{type:tts_start,text:我在呢你说。,session_id:xxx} {type:tts_chunk} 二进制音频 {type:tts_end,reason:completed}选择 WebSocket 而不是 HTTP 长轮询主要是为了 TTS 流式回传和未来的主动推送能力。设备端心跳每 30 秒发一次云端超过 90 秒没收到心跳就主动断开避免僵尸连接。断线重连使用指数退避策略第一次等待 1 秒之后 2 秒、4 秒、8 秒最大 60 秒。4.2 ASR 与 LLM 角色编排云端我用了一个轻量的 Node.js WebSocket 网关设备连接进来后维护一张 Session 表。一次完整对话的编排逻辑如下收到audio_end后把整段 PCM 音频交给 ASR 服务拿到用户文本将用户文本拼接到上下文里请求 LLM 生成回复LLM 返回文本后调用 TTS 服务合成语音边合成边推送给设备不等待完整音频生成完。陪伴类产品的人设稳定是最重要的。我在 system prompt 里固定了角色定义你是小烛一个摆在桌面上的AI陪伴设备。 你说话温柔简洁口语化绝对不用书面语。 每次回答控制在50字以内最多不超过两句话。 如果用户说的话题你不了解就坦白说不知道并转到一个轻松的话题。 不要评价国家、体制、宗教不讨论敏感事件。这个 prompt 经过多轮调整。最开始没限制回复字数LLM 动不动写一段小作文TTS 要播 40 秒体验极差。加了 50 字限制后平均回复时长降到 5 秒以内用户感觉像是“自然在聊天”而不是“在听朗读”。上下文管理采用滑动窗口保留最近 8 轮对话并在每轮结束后让 LLM 生成一句话摘要作为长期记忆存储在 Redis 里。第二天用户再跟设备打招呼设备还能记得“昨晚聊过想学做饭”这件事。这算是一个不需要向量数据库的轻量记忆方案对小流量项目足够用。4.3 TTS 流式回传TTS 的服务商通常都支持流式合成关键是要把“流式”真正用到端到端链路上。我最初的做法是等 TTS 把整段音频合成完再推给设备导致用户说完话到听到回复的间隔长达 3 到 4 秒。优化后改为边合成边推第一段音频也就是 TTS 的 first package在 400-700ms 内就能到端侧整体体感立刻就不一样了。端侧收到tts_chunk后需要边收边播。ESP32-S3 播放 MP3 需要解码器我用的是 libhelix-mp316-bit PCM 直接支持。如果 TTS 服务返回的是 Opus 编码就要先在端侧做解码再交给 I2S。我第一版用的是 16kbps Opus体感延迟低但解码库移植麻烦后来为了稳定直接改用 24kHz MP3解码占用 CPU 大约 20%播放流畅度完全可接受。还要注意播放缓冲区的管理。网络抖动会导致音频断流我在端侧维护了一个 200ms 的 jitter buffer。如果 buffer 空了播放器先输出一段极短的静音而不是直接卡住。这个细节对听感影响非常大表现是“偶尔微顿一下”而不是“一顿一顿完全没法听”。4.4 断网兜底与 OTA 演进纯云端的方案最怕断网。设备如果完全没反馈用户第一反应是“这设备是不是坏了”。我在端侧做了一套降级策略断网并检测到唤醒时屏幕显示“X_X”表情播放一段本地 wav 提示音“网络好像不太好稍后再试”WiFi 断线超过 30 秒自动扫描重连重连成功后恢复待机表情设备本地缓存了 3 条固定应答语断网状态下如果用户触碰设备会随机播出一条至少让用户知道设备是活的。固件升级走 OTA。ESP32-S3 支持双分区 OTAA/B 分区设计可以做到“升级失败自动回滚”这对无人值守的桌面设备来说是刚需。OTA 触发我放在云端管理接口上设备每天连接时上报当前固件版本云端发现新版本后下发升级指令设备下载固件并切换到新分区运行。这套内容后面可以往两个方向演进一是端侧接入异常声音识别比如哭声、敲击声在唤醒词之前就做出响应二是多设备联动让同一个账号下的多个 ESP32-S3 分享同一套对话记忆。这两块我在后续版本里会逐步落地。5. 实测数据、常见问题与演进心得5.1 实测效果与延迟拆解把整套链路跑通之后我对系统做了三天的持续观察最终记录下的核心数据如下指标实测值说明唤醒响应150-250ms从说话到屏幕亮起录音结束到 ASR 结果600-900ms取决于网络与 ASR 服务LLM 首 token300-800ms使用流式接口TTS 首包400-700ms边合成边推端到端延迟1.8-2.5s用户说完到听到回复待机电流75-95mA屏幕熄灭、唤醒词开启播放电流150-200mA音量中等1000mAh 电池续航6-8 小时中度使用安静环境下唤醒率在 95% 以上嘈杂环境里大概 85% 到 90%。我对噪声场景的处理方法是在静音时动态记录环境底噪把唤醒阈值提高到底噪的 2 倍以上。这个逻辑有一个前提底噪不能在短期内剧烈变化否则会有一次误唤醒或漏唤醒。端到端延迟 2 秒左右是一个比较微妙的量级。用户能明显感知到“这是一个 AI 设备在思考”但也正因为这个延迟对话反而显得不那么机械。如果再往下压就要从 ASR 的流式半字识别和 TTS 更激进的流式切片入手工程量会大不少对这个场景不是优先项。5.2 常见问题速查我把我遇到过的、以及网友群友遇到过的典型问题整理成了一张速查表给后面复刻这套方案的人当参考现象原因解决方案录音是乱码/沙沙声INMP441 L/R 引脚悬空L/R 接 GND 或 VDD确定声道WiFi 配网后连不上路由器密码包含 字符播放 TTS 时有“嘶嘶”底噪功放电源与数字电源共地干扰功放供电串磁珠加 100uF 电容WebSocket 长时间运行后断流NAT 超时/无心跳清理加 30 秒心跳断线指数退避重连屏幕表情卡顿刷新循环用了阻塞延时表情渲染放到独立任务用 Tick 管理麦克风采集与播放冲突I2S 端口或 DMA 分配重复麦克风用 I2S_NUM_0功放用 I2S_NUM_1设备唤醒后没有声音系统音量被设置为 0状态机里初始化统一走set_volume()还有一个比较隐蔽的坑ESP32-S3 开启 PSRAM 后WiFi 驱动的默认 buffer 大小可能需要重新调整。如果你的设备在配网成功后第一次 WebSocket 连接总是失败多半是 WiFi 的 RX buffer 太小导致丢包严重。用esp_wifi_set_ps(WIFI_PS_NONE)关闭省电模式只是最初步的动作更彻底的方案是在初始化里显式调大 WiFi buffer或者把 WebSocket 客户端的内存分配到 PSRAM。5.3 折腾下来的一些体会整套架构做完我最大的感受是硬件 AI 产品真正难的地方不在大模型而在“藏住架构”。用户不会关心你是端侧唤醒还是云端识别他们在意的只是说话之后设备有没有反应、反应够不够快、声音像不像真人、连续聊几天后还记不记得自己。这个项目给我的另一条经验是端云协议一定要在第一天就设计得抽象一点。最初我把 ASR、LLM、TTS 的厂商 SDK 直接揉进了网关路由后来想换其中一家服务商光是改消息格式和错误码就花了两天。如果重写一版我会让网关只面向“音频进、音频出”这件事把每个厂商 SDK 封装成独立的 adapter切换服务商时只改配置不动业务逻辑。最后说一个很多人忽略的小事情陪伴类设备的提示音设计。我一开始觉得提示音不重要随便播了个系统 beep。后来发现每次唤醒时的“叮”一声如果太尖锐用户会觉得设备在“训人”。建议用正余弦波合成一个 1000Hz 降到 600Hz 的柔和短音时长远一点、音高缓降听感会温柔非常多几乎零成本地提升了整台设备的高级感。如果你也想拿 ESP32-S3 做一个类似的 AI 陪伴设备我建议按这个顺序走先把本地状态机跑通屏幕表情 提示音再接云端 TTS 播报固定句子最后才接 ASR 和 LLM。一上来就想全链路打通排错时你会分不清问题出在回声消除、网络抖动还是提示词上。一步一步来这块板子能给你的惊喜比我一开始预想的多得多。
返回列表