1. 项目背景与整体设计思路
拿一块 ESP32-S3 开发板,能做出什么别人愿意天天用、甚至愿意带在身边的东西?我最初的答案也是那几个:连个传感器上报温湿度、做个网络时钟、驱动小屏幕跑跑动画。直到我把大模型接进来,才发现这块板子真正的潜力在于——它能成为一个有“人格”的实体设备,而不只是数据采集终端。
这个项目最终做成了一台 AI 陪伴设备:能语音唤醒、能对话、能记住你是谁、能主动播报天气和提醒事项,最关键的是,它可以在不出家门的前提下,通过云端大模型获得持续的“智力升级”。我给它起名叫“小伴”,本质就是一套端云协作架构:ESP32-S3 负责所有与人的物理交互,云端负责语义理解、长期记忆和多轮对话,两者通过一套轻量协议衔接。这篇文章把整套架构、关键代码逻辑、实测数据和踩过的坑全部摊开讲,给同样想把手头开发板变成 AI 设备的人一条可以照抄的路径。
1.1 为什么是“端云架构”而不是“纯端侧”或“纯云端”
纯端侧跑大模型,以 ESP32-S3 的算力完全不现实。S3 虽然有双核 240MHz 和向量指令加速,但它的定位是边缘 MCU,不是 NPU 推理卡,跑个唤醒词、做做音频降噪、驱动外设还行,让它直接跑对话模型,等待时间和内存都会直接击穿。纯云端方案也有问题:你不可能让一个设备把所有音频都传上去,延迟、流量、隐私都扛不住,而且一旦断网设备就是一块砖。
所以最终的架构分工很明确:端侧负责“感知 + 表达”,云侧负责“认知 + 记忆”。设备端做语音唤醒、录音、按键交互、指示灯反馈、音频播放,以及简单的本地命令匹配;云端做语音识别、大模型对话、语音合成、历史记忆存储和技能调用。两边通过 MQTT 或 WebSocket 保持长连接,消息格式统一走 JSON,这样以后换任何端侧硬件,甚至换任何云服务商,都不需要动架构主干。
这个拆分还有一个隐蔽的好处:可演进性。端侧固件更新慢,云端服务迭代快。我把所有“会变的东西”——人设 Prompt、技能逻辑、模型版本、记忆策略——全部放在云端。硬件端只保持一个稳定接口,固件更新主要解决 bug 和交互体验,真正的“智力增长”完全由云端持续交付。这就是标题里“可持续演进”四个字的真正含义。
1.2 这套架构适合谁参考
如果你是嵌入式开发者,这篇文章能帮你理解 MCU 如何与云端大模型协作,尤其是音频流、协议设计、OTA 这类容易被忽视的细节。如果你是做 AI 应用开发的,你会看到大模型能力落进物理设备时,真正麻烦的往往不是模型本身,而是端侧的约束:内存、功耗、网络波动、实时性。如果你只是想复刻一个类似的陪伴设备,按照文章里的硬件选型和云端服务搭建步骤,完全可以在几天内跑通第一个版本。
我不会假设你已经有很深的嵌入式基础,但至少要知道 ESP32 是什么、能烧录固件。涉及到的关键代码和配置我会完整贴出来,尽量做到拿来即用。
2. 硬件选型与开发板核心资源盘点
2.1 为什么最终锁定 ESP32-S3
市面上能做端云交互的开发板很多,ESP32、ESP32-C3、ESP32-S3,再往上还有各种 Linux 级开发板。我最后选了 S3,不是因为它最便宜,而是因为它刚好站在“够用”和“低功耗”的交叉点上。
S3 最核心的几个资源块我列一下:
- 双核 Xtensa LX7,主频最高 240MHz,带 SIMD 向量指令,对音频处理有明显加速。
- 最多 16MB 的 Octal PSRAM。这个非常关键,后面会讲,跑音频缓冲和未来可能的端侧小模型全靠它。
- 内置 USB OTG 控制器,可以直连 USB 摄像头,我后面做视觉辅助功能时就靠这个口。
- 丰富的外设接口:I2S、I2C、SPI、UART、ADC、DAC、LEDC,接麦克风、功放、屏幕、灯带都方便。
- 原生 Wi-Fi 和 BLE,支持 802.11 b/g/n,做端云长连接的基础。
- 支持安全启动和 Flash 加密,以及 OTA 固件升级,这是设备可持续演进的前提。
对比之下,ESP32 原版虽然也有双核,但只有 520KB SRAM,跑现代音频算法略显紧张,而且 USB 外设、向量指令这些都没有。ESP32-C3 是单核 RISC-V,适合做轻量节点,但做语音交互主控太吃力。Linux 级开发板(比如市面常见的 T113、RK3506 这类小板子)性能强很多,但功耗和成本也上去一个量级,对陪伴设备这种需要长时间待机的场景并不友好。所以从整机成本和开发效率看,S3 是甜点选择。
2.2 外设搭配与音频方案
音频链路是整个设备最需要打磨的部分。我用的麦克风是 INMP441,I2S 数字输出,信噪比高,接线简单。功放选的是 MAX98357A,同样是 I2S 接口,直接驱动 3W 小喇叭。这两个芯片是 ESP32 生态里最主流的搭配,驱动代码成熟,几乎不需要调试底层的 I2S 时序。
供电方面要注意,S3 开发板本身有 USB 供电,但如果在带 WiFi 和音频同时工作的情况下,瞬时电流可能到 500mA 以上,普通的电脑 USB 口可能供电不稳导致重启。我用的是 5V/2A 的独立电源适配器,并在电源入口并联了一个 470uF 电解电容和 100nF 瓷片电容做去耦。这个细节看起来小,实际能解决大半无故重启的玄学问题。
如果要做视觉功能,可以接 OV2640 摄像头走 SPI/摄像头专用接口,或者直接用 USB 摄像头通过 S3 的 USB OTG 口读取。我实测过 USB 摄像头,S3 可以枚举 USB 设备并抓取 MJPEG 帧,但要注意帧率和分辨率必须降下来,否则内存会爆。
2.3 开发板选型时的避坑经验
- 优先选带 PSRAM 的版本。同样是 S3,有没有 PSRAM 完全是两个体验。跑语音缓冲、解码音频、处理图像,没有 PSRAM 会频繁 Out of Memory。
- 尽量买引出完整排针的板子,别买那种集成式迷你板。开发阶段要频繁飞线接麦克风和功放,引脚引出不全就是灾难。
- 留意 Flash 大小,建议选 8MB 以上。固件、字库、音频资源、OTA 双分区都需要空间,4MB 会非常局促。
3. 设备端软件设计与交互链路实现
3.1 整体软件框架:基于 ESP-IDF 的 FreeRTOS 体系
设备端我直接用乐鑫官方的 ESP-IDF 开发,没有用 Arduino。虽然 Arduino 上手快,但做这种涉及长连接、音频流、OTA 的复杂项目,IDF 的可控性高得多:任务栈大小自己定,内存分配看得见,底层驱动可以直接改。FreeRTOS 天然支持多任务,我把整个设备分成几个独立任务:音频采集任务、按键/灯效任务、MQTT 网络任务、音频播放任务,外加一个主状态机任务做逻辑调度。
每个任务独立运行,通过队列和事件组通信。我举个例子,当麦克风采集到唤醒词后,音频采集任务会往主状态机发一个WakeUp事件,主状态机再命令网络任务开启音频上传。这样任何一个任务卡住,都不会连累整个系统,排查问题也更方便。
代码骨架大致如下:
// 任务划分示例 void audio_capture_task(void *arg) { // 初始化 I2S,持续读取麦克风数据 // 交给唤醒词引擎处理,命中后发送事件 } void network_task(void *arg) { // 维护 MQTT/WebSocket 连接 // 监听下行消息,解析后转发给播放任务 } void main_state_task(void *arg) { // 事件循环,处理状态流转:IDLE -> LISTEN -> THINKING -> SPEAKING }3.2 唤醒词与本地交互反馈
设备不能每时每刻都在录音上传,否则功耗和流量都受不了。我用了乐鑫官方的 ESP-SR 语音识别库,在本地跑唤醒词模型。默认支持“Hi,乐鑫”这样的词,也可以通过训练工具自定义。我把它改成了“你好,小伴”。
唤醒之后设备会立刻播放一个短促提示音,同时点亮 LED 灯环,表示“我在听”。这个反馈必须快,最好在 100ms 以内,否则用户会感觉设备迟钝。这里我踩过一个典型坑:刚开始把提示音播放放在 MQTT 任务里,结果网络抖动时提示音也跟着卡顿。后来把音效播放独立成一个高优先级任务,预先加载到内存,播放延迟降到 20ms 左右。
按键是第二交互入口。我保留了一个物理按键,短按开启对讲式问答,长按进入配网模式。为什么保留按键?因为语音在某些环境下不可靠(比如旁边有人说话时),按键可以兜底,而且用户天然会去找实体交互,有一种安全感。
3.3 音频上传与 VAD 检测
唤醒后设备开始往云端上传音频流。但如果把整段“听到的”全传上去,容易把环境噪音和无关对话也送进大模型,既浪费 tokens 又影响回答质量。所以在端侧做了一次“端点检测”(VAD),只截取从唤醒后到用户停止说话之间的一段有效语音。
我用的 VAD 方案是基于能量和过零率的传统算法,没有上模型。因为 ESP-SR 本身已经带了简单的 VAD 能力,而且传统算法零延迟、占用极低。截取到音频后做格式统一:16kHz、16bit、单声道 PCM,然后通过 WebSocket 分帧发送。每帧 320 字节(20ms 音频)加上帧序号和时间戳,云端按序号组装成完整音频。
这个协议设计我用了 protobuf 的思想,但没有真的上 protobuf,而是精简成二进制头加 PCM 载荷的方式,减少解析开销。上行消息的结构大概长这样:
{ "type": "audio_event", "device_id": "s3_dev_001", "session_id": "uuid", "event": "speech_start", "timestamp": 1712345678901 }speech_start表示端点检测到有效语音开始,之后持续发音频帧,最后发speech_end。云端收到speech_end后才会触发后续的 ASR 和 LLM 流程。
3.4 下行播放与状态指示
云端处理完对话后,返回的是完整的 TTS 音频,我统一转成 MP3 格式再下发。为什么不用原始 PCM?因为 PCM 数据量太大,同样一句话 MP3 可能只有几十 KB,PCM 要几百 KB,对网络带宽和 Flash 缓存都是负担。S3 内置的音频解码器可以直接解码 MP3,不需要额外芯片。
播放任务从网络任务接收到音频数据后,先写入一个环形缓冲区,解码线程边读边解码边通过 I2S 输出到功放。这样即使网络出现短暂抖动,音频也能连续播放不中断。播放过程中设备进入SPEAKING状态,灯环显示呼吸效果,同时屏蔽新的唤醒词,防止设备自己说话把自己唤醒。
所有状态切换都用事件组驱动。状态机里还加了超时保护:如果用户唤醒后 5 秒没有说话,自动回到待机;如果云端 10 秒没返回结果,播放一段“网络好像有点慢,请再试一次”,然后复位状态。
4. 云端服务搭建与大模型集成
4.1 云端模块划分
云端是整个设备“智力”的核心。我把它拆成五个独立服务:网关服务、语音识别服务、对话引擎、记忆服务、语音合成服务。每个服务独立部署,通过内部 API 通信,这样任何一个服务升级都不影响整体。
网关服务是设备端唯一连接的入口,负责维持 WebSocket 长连接、鉴权、设备状态管理、消息路由。设备的所有上行消息先到网关,网关再根据消息类型分发给下游服务。对话引擎调用大模型,并把大模型返回的文本流交给 TTS 服务合成音频,最后网关把音频分帧推回设备。
这个拆分不只是为了代码清晰,更重要的是性能隔离。语音识别慢了不会拖垮对话引擎,大模型 API 抖动也不会影响已经建立的设备连接。我甚至可以把不同服务放在不同地域的服务器上,按实际延迟调优。
4.2 使用 Spring AI 统一大模型接入
对话引擎我基于 Spring AI 框架构建。为什么选它?因为 Spring AI 提供了一套统一的接口抽象,底层可以无缝切换不同的模型厂商。项目初期我用的是通用的在线大模型 API,后来想测试另一家模型的效果,只改了一行配置,业务代码完全没动。这种“模型可插拔”的能力对长期演进非常重要,因为你不知道半年后哪家模型更好更便宜。
对话引擎里的核心是“角色人设 + 功能调用”的编排。我给设备定义了一个系统 Prompt,设定它叫“小伴”,语气亲切但不幼稚,知道自己是用户的 AI 伴侣,可以聊生活、查资料、做提醒。同时注册了一组 Agent 工具函数,包括:查询天气、设定闹钟、查百科、控制智能家居、阅读当天新闻。大模型在对话中判断是否需要调用工具,需要的话就返回一个结构化指令,Spring AI 的@Tool机制帮我自动做参数解析和函数调用,非常省事。
4.3 长期记忆与会话管理
陪伴设备如果每次对话都是“失忆”的,用户不会有黏性。所以记忆服务是必不可少的。我用 Redis 做短期会话缓存,存最近二十轮对话的内容;用 MySQL 做长期记忆库,存用户的核心信息,比如生日、喜好、常用称呼、最近关心的事。
每次对话开始时,对话引擎会先从记忆服务拉取用户画像和最近聊天摘要,注入到系统 Prompt 中。这样用户说“我之前跟你说过我喜欢钓鱼”,设备能接得住。这里有个实现细节:不能把全部历史文本都塞进 Prompt,否则上下文太长,延迟和费用都不可控。我用关键词抽取加摘要压缩的方式,只注入最相关的那部分记忆。
4.4 语音合成与音频回传优化
TTS 服务我选的是带流式输出的语音合成 API,边合成边返回音频分片。网关收到后不等完整文件,直接把分片推回设备。这样用户听到第一个字的延迟可以缩短 30% 以上。实测下来,完整合成再传输的端到端首包延迟约 1.8 秒,流式方案能压到 1.1 秒左右,体感差异很明显。
音频格式方面,云端统一输出 16kHz 单声道 MP3,码率 32kbps。这个参数在音质和流量间取了一个平衡。陪伴设备主要是语音对话,不需要高保真音乐,32kbps 听起来足够清晰,一小时连续对话流量摊销大约 14MB,在 Wi-Fi 场景下完全无压力。
5. 端云联调、延迟拆解与实测数据
5.1 一条完整对话请求的旅程
我拿一条真实的交互链路来拆解:“你好小伴,明天天气怎么样?”
- 设备端:S3 麦克风持续监听,唤醒词引擎在本地识别到“你好小伴”,耗时约 180ms。
- 设备端:触发 VAD,录制“明天天气怎么样”这一段,约 1.2 秒。
- 设备端 → 云端:通过 WebSocket 上传音频,16kHz PCM,大小约 38KB,走 Wi-Fi 耗时约 200ms。
- 云端:语音识别服务把 PCM 转成文本,耗时约 500ms。
- 云端:对话引擎带着用户记忆和工具列表调用大模型,模型判断需要调用天气工具,先返回工具调用指令,再结合天气结果生成最终回答,耗时约 1.5 秒。
- 云端:TTS 服务合成回答音频,首包流式返回,耗时约 800ms。
- 云端 → 设备端:音频分片开始到达,设备边收边播。首包到达时间距离设备请求发出约 3.4 秒。
- 设备端:播放完整回答约 3.5 秒,同时灯环显示状态。
从用户说完话到听到回答的第一个字,实测约 3.4 秒。这个数字在智能音箱里算中等偏上,考虑到用的是通用 MCU 加在线 API,已经是可以接受的水平。如果想要更快,可以把 ASR 和 LLM 做并行管线,用户还没说完话就提前开始识别处理。
5.2 各环节耗时实测表
我整理了一组 50 次对话实测的平均数据,供你参考:
| 环节 | 平均耗时(ms) | 备注 |
|---|---|---|
| 唤醒词识别 | 180 | 本地 ESP-SR,稳定 |
| 语音采集与 VAD | 1200 | 根据说话时长浮动 |
| 音频上传 | 210 | 受 Wi-Fi 信号影响 |
| 云端 ASR | 520 | 通用短语音接口 |
| 大模型生成 | 1500 | 带工具调用时稍长 |
| TTS 首包返回 | 780 | 流式合成 |
| 设备端解码播放缓冲 | 120 | 环形缓冲填充 |
| 总端到端首包时长 | 3400 | 说话结束到首字出声 |
5.3 优化延迟的几个真实手段
- 提前做语音活动检测,不要等用户完全停止说话才开始上传,允许“半句上传、边传边补”。
- 上行音频做 Opus 压缩再传,16kHz 单声道 Opus 码率可以用到 24kbps,体积比 PCM 缩小近六倍,上传时间省一半以上。
- 大模型响应用流式输出,文本每次生成一小段就交给 TTS 合成,不用等完整句子。实测这句话回复的场景,首包延迟能再降 300ms 左右。
- 设备端做 DNS 缓存和连接复用,避免每次对话都重新握手。WebSocket 长连接保活,心跳间隔 30 秒。
6. 可持续演进:OTA、配置热更新与技能扩展
6.1 固件 OTA 升级链路
端侧固件不能永远不变,我上线后至少修了十几个交互细节。OTA 用的是 ESP-IDF 自带的esp_https_ota组件,固件编译后上传到私有服务器,设备每次启动时检查版本号,发现新版本就下载并切换到备用分区,重启后自动激活。
这里有一个关键经验:固件必须开启双分区方案,也就是 ota_0 和 ota_1 两个应用分区。一旦新固件启动失败,Bootloader 会自动回滚到上一个正常版本,保证设备不会变砖。我有一次在固件里加了一个内存泄漏比较严重的音频解码库,持续运行两小时就会崩溃,如果没有分区回滚机制,那批设备就全瘫了。
OTA 更新的时机也要控制。我设置在每天凌晨三点,设备大概率处于空闲状态时执行,避免打断用户对话。
6.2 配置热更新:不改固件改行为
设备很多行为参数是可以云端下发的。我建了一个简单的配置管理系统,把唤醒响应音、音量大小、灯光颜色、待机超时时长、最大音量限制这些都做成动态配置。云端修改配置后,通过 MQTT 下行消息推给设备,设备收到后写入 NVS 存储,实时生效。
这个设计大大减少了固件更新频率。比如用户反馈“设备声音太小”,我直接在后台把默认音量从 60% 调到 75%,十秒钟推完,完全不用发版。配置项还加了版本号,设备启动时如果本地配置版本低于云端,会自动拉取最新配置。
6.3 云端技能扩展与模型升级
设备的“技能”是在云端以插件形式管理的。每增加一个新技能,只需要在对话引擎里注册对应的工具函数和描述信息,大模型自动学会在合适的场景调用它。例如我后来加了一个“讲睡前故事”技能:工具函数会根据故事主题和时长要求生成故事文本,TTS 用更柔和的音色朗读。这个过程没有动任何设备端代码,全部在云端编排。
Prompt 版本管理我用了简单的 Git 仓库加平滑发布。每个人设 Prompt 都打上版本号,新版本先在少量设备上灰度,观察对话满意度和报错率,确认没问题再全量发布。一旦出现大面积异常回答,立刻回滚到上一个版本。
6.4 成本控制与数据回流
大模型 API 按 token 计费,所以成本控制是必须考虑的。我做了几层优化:
- 相同问题的答案做了缓存,命中缓存完全不调用模型。
- 容易产生固定回答的场景(如“你是谁”)走本地话术库。
- 长对话超过一定轮数后自动做摘要压缩,保留核心信息,丢弃冗长历史。
- 多个模型按任务分级:简单任务用轻量模型,复杂推理用强模型。
使用数据会上报到云端做分析。我关注几个指标:每天唤醒次数、平均对话轮数、单次对话 token 消耗、用户主动说“谢谢”或“再见”的频率。这些指标直接反馈陪伴体验好不好,并且能发现用户最常问的问题,从而补充到技能库或话术库。
7. 常见问题与排查技巧实录
7.1 音频异常类问题
问题一:麦克风没有声音或声音时断时续。
先查 I2S 引脚配置是否和实际接线一致,再用示波器或逻辑分析仪检查 BCK、WS、DATA 三根线的时序。INMP441 的 L/R 引脚接地时输出在左声道,接 VDD 时在右声道,这个接错会导致录到的音频电平极低或只有单边数据。代码里还容易犯的一个错误是 I2S 采样率配置与实际主时钟不匹配,导致采出来的声音变调。
问题二:TTS 播出来的声音有杂音或破音。
检查功放供电是否足够,MAX98357A 在 3W 输出时需要的电流不低,供电不足会削波。另外确认 I2S 输出的位宽和采样率与功放芯片配置一致,我遇到过 ESP-IDF 默认输出 32bit 位宽,而功放配置成 16bit 的情况,结果声音全是噪音。
7.2 网络连接与稳定性问题
问题三:设备经常掉线,重连又很慢。
Wi-Fi 信号弱是主因,但代码层面也有优化空间。我开启了 Wi-Fi 的WIFI_PS_MIN_MODEM省电模式,这个模式在待机时非常省电,但代价是每次收发数据都需要重新唤醒射频,延迟会增加几毫秒。如果设备离路由器近,可以改成无省电模式,连接明显更稳定。还建议把 MQTT 的 keepalive 时间设置短一点,默认 60 秒,如果网络环境差,改成 30 秒能让服务端更快发现死连接。
7.3 内存与稳定性问题
问题四:长时间运行后设备变慢或死机。
极大概率是内存泄漏。ESP32-S3 的 PSRAM 虽然大,但堆内存碎片和泄漏是缓慢杀手。我用 ESP-IDF 自带的堆调试工具配合heap_caps_get_free_size()定时打印剩余内存,逐步定位到问题代码。另外注意,不要在中断回调里做内存分配或耗时操作,所有音频数据处理都放到任务上下文执行。此外,Wi-Fi 和音频播放同时进行时,DMA 缓冲区配置不合理也会导致系统崩溃,我给 I2S 的 DMA buffer 长度设成 480 字节、数量 8 个后稳定很多。
7.4 对话体验类问题
问题五:大模型回答的内容和角色设定不一致。
这就是 Prompt 工程问题。我后来把所有角色规则写成独立系统 Prompt,并定期补充用户反馈的失败案例,让模型“不要这样做”。比如用户反馈“小伴太啰嗦了”,我就在 Prompt 里加了“回答要简短口语化,一般不超过 50 个字”。这类调整全部在云端完成,设备端无感,迭代速度非常快。
问题六:用户说话时设备经常响应慢半拍。
可能是 VAD 的结束判定太保守。我调低了静音判定阈值,让设备更早判断用户说完了,省下约 300ms。但阈值调太低又会出现“话还没说完就被打断”的情况。这里没有银弹,只能靠实际场景反复调参,我做了三个档位让用户可以在手机端调节,算是一种务实解法。
写在最后的一个小分享
做这个项目最深的体会是:设备端代码反而越精简越好,真正让人“上瘾”的能力全部沉淀在云端。我自己后续扩展的方向有几个:一是把 USB 摄像头画面接入视觉大模型,让小伴能“看到”家里情况;二是给设备增加本地小模型做离线兜底,断网时能继续聊一些简单话题;三是把云端这套架构整理成可配置的平台,让完全不懂嵌入式的朋友也能定义自己的设备角色。如果这篇文章能让你少踩几个坑,哪怕一个,我就觉得写得很值了。