
去年年底我手头多了一块微雪出品的 ESP32-S3 N16R8 开发板当时正好在琢磨一个 AI 陪伴类的项目。市面上现成的陪伴设备要么太封闭要么只做云端对话、本地就是个蓝牙音箱完全没有“设备感”。我想做的是一块小板子既能独立完成语音唤醒、交互反馈又能借助云端大模型的能力实现真正开放的自由对话——于是就有了这套持续演进了大半年的端云架构。这个项目最适合两类人看一类是手里有 ESP32-S3 开发板、想把它做成“能聊天”的实物设备但不知道从哪下手的硬件爱好者另一类是已经在做 AI 硬件原型、但对端云边界、OTA 升级、会话状态这些“上了量才会暴露的问题”还没有完整认知的同学。下面我就从硬件选型、固件架构、云端接入、以及我踩过的那些坑把这套方案从头到尾拆开讲。1. 项目定位与整体设计思路拆解1.1 为什么是 ESP32-S3而不是树莓派或者手机方案做 AI 陪伴设备摆在面前的第一道选择题就是算力平台。树莓派性能强、能本地跑小模型但成本高、功耗大、体积也压不下来做出来的东西更像“开发板拼装”而不是“陪伴设备”。手机方案的开发门槛低但本质上只是套壳用户已经有一个手机了为什么还要做一个依托手机的设备ESP32-S3 在 2023 到 2024 年这个时间节点几乎是端侧 AI 硬件原型的标准答案。理由有三双核 Xtensa LX7 处理器主频 240MHz配合 SIMD 指令集跑音频处理、I2S 采集、屏幕刷新这些任务绰绰有余内置 2.4GHz Wi-Fi 和 BLE 5.0一颗芯片同时搞定网络连接和近场配网不需要外挂模块N16R8 版本带 16MB Flash 和 8MB PSRAM8MB PSRAM 很关键——这决定了你能不能开足够的音频缓冲区以及能不能在端侧跑一个轻量级的唤醒词模型。我的判断是AI 陪伴设备短期内不会在端侧跑大模型真正的主力是“端侧响应 云端智能”。ESP32-S3 恰好是这个分工里性价比最高的端侧载体它不需要有多聪明但必须足够稳、足够省电、足够便宜。1.2 端云架构的边界划分哪些留在端侧哪些交给云端这是整个项目里最重要的架构决策。我见过太多人一上来就把音频流直接推到云端结果网络稍微抖动一下设备就变成“哑巴”。我的划分原则只有一条凡是与用户体验直接相关的实时操作全部留在端侧凡是需要语义理解和知识生成的操作全部交给云端。具体来说端侧负责四件事语音唤醒词检测本地完成响应时间在几百毫秒以内音频采集和 VAD语音活动检测判断用户是否说完了一句话屏幕显示、指示灯、按键反馈等本地交互网络状态监测和断线降级比如提示“网络不佳”而不是直接卡死。云端负责三件事把端侧上传的音频转成文字ASR把文字送入大模型生成回复LLM把回复文本合成语音TTS回传给端侧播放。这个边界的核心价值在于即使云端完全不可用设备本身依然是一个有反应的硬件用户可以立刻感知到问题所在而不是面对一块死屏。从产品体验上看这比“断了网就彻底废掉”的设备高出一个维度。1.3 可持续演进的架构原则“可持续演进”不是一句口号在这个项目里我把它落实成三条具体原则协议先于实现端云通信从一开始就使用 JSON over WebSocket 的通用协议而不是为具体某个大模型定制接口。这样以后换模型、加功能端侧固件不需要大改。配置驱动行为设备的角色设定、音色、语速、唤醒词等都用云端下发的配置来控制而不是写死在固件里。这样不改固件就能调节产品行为。保留端侧扩展位PSRAM 里预留至少 2MB 的余量后续如果要加更复杂的端侧音频处理比如自定义唤醒词、本地指令识别不用换硬件。这套原则在后来的迭代中帮了大忙。最典型的一次是我把对话模型从通用对话换成角色扮演模型端侧固件一行没改只改了云端服务和配置下发整个产品就完成了“升级”。2. 硬件选型与端侧资源盘点2.1 开发板选型为什么最终锁定微雪 ESP32-S3 N16R8市面上 ESP32-S3 的开发板非常多合宙、微雪、官方 DevKitC、各种第三方小板子选择困难症很容易发作。我给自己的筛选标准是三条Flash/PSRAM 容量、扩展接口的丰富度、以及调试排障的便利性。最终选定的微雪 ESP32-S3 N16R8我用的具体型号带了 1.9 英寸 GC9A01 圆形屏幕和 6 轴 IMU核心参数如下参数配置我的用途SoCESP32-S3双核 240MHz主控Flash16MB固件 资源文件 OTA 双分区PSRAM8MB音频缓冲、屏幕缓冲、JSON 解析堆屏幕GC9A01 圆形 240x240 SPI表情与状态显示麦克风板INMP441 或板载模拟麦语音采集扩展接口排针引出全部 GPIO外接功放和扬声器我特别看重 N16R8 这个版本是因为 16MB Flash 在后续做 OTA 升级时太重要了。如果你用的是 4MB Flash 的版本OTA 双分区一划分每个分区只够放一个很小的固件很多功能就得为了空间砍掉。8MB PSRAM 则让内存分配变得奢侈尤其是后期接 GC9A01 屏幕时需要分配的显存和绘制缓冲加起来就能吃掉不少内存。2.2 麦克风方案INMP441 数字麦与板载模拟麦的取舍语音交互是陪伴设备的入口麦克风的选型直接决定识别率。我手头有两块麦克风方案可以对比INMP441I2S 接口的 MEMS 数字麦克风输出直接是 PCM 数据不受模拟走线干扰信噪比高但是需要额外的 L/R 引脚配置焊接也比较麻烦板载模拟麦克风微雪板载方案走 ADC 采集接线简单但是底噪明显偏大在安静房间里做唤醒测试时误唤醒率很高。我的结论是如果你对语音唤醒距离的要求超过 30 厘米直接上 INMP441不要犹豫。模拟麦做原型演示没问题做到成品阶段还是得数字麦。INMP441 接 ESP32-S3 的标准接法我后面会详细说这里先提醒一个关键点INMP441 是单声道 I2S 输出数据格式化后是 32-bit 帧有效数据在高 18 位。这个如果你不知道读出来的音频会全是噪声或者音量小得离谱。2.3 屏幕、扬声器与供电的配套设计GC9A01 这块圆形屏是这个项目里“陪伴感”的重要来源。240x240 的圆形 LCDSPI 接口刷新率能满足 30fps 的动画需求。我把它用来显示两种内容表情动画眼睛、嘴巴的简单几何图形根据设备状态切换待机、聆听、思考、说话调试信息Wi-Fi 信号强度、对话状态码、当前模式方便开发阶段快速排障。扬声器部分我建议用 I2S 功放比如 MAX98357A而不是直接 DAC 输出。MAX98357A 是 I2S 输入的 D 类功放引脚少、音质够用和 ESP32-S3 的 I2S 外设无缝对接。直接用一个三极管驱动的蜂鸣器放语音是灾难性的千万别省这个钱。供电是整个硬件里最容易“带崩”的地方。ESP32-S3 开启 Wi-Fi 的瞬间电流可以冲到 500mA加上屏幕背光和功放峰值电流可能超过 1A。我的建议是如果电池供电选 3.7V 锂电池 至少 500mA 输出的 LDO如 RT9013 或 ME6211如果 USB 供电务必用优质线材劣质 Micro-USB 线的压降会让你反复遇到“重启循环”的灵异问题功放的电源最好从电池端单独引出避免和主控共用一组走线时产生地环路噪声。3. 端侧软件架构与核心模块实现3.1 固件框架选型ESP-IDF 是底线ESP32-S3 的固件开发有 Arduino、MicroPython、ESP-IDF 三条主要路线。我的选择是ESP-IDF并且强烈建议你至少在做一个功能模块时尝试一下 ESP-IDF。原因不复杂Arduino 写起来快但一旦要精细控制 I2S 时钟、调整 PSRAM 的 malloc 策略、做 OTA 的时候Arduino 封装反而变成一层障碍。MicroPython 做原型演示可以跑生产级的音频采集和通信任务GC 停顿和解释器开销会让你头疼。ESP-IDF 的学习曲线是陡的但它的组件体系esp-adf、esp-sr、esp-web-socket-client几乎覆盖了音频设备开发的所有需求而且每个组件都有乐鑫官方维护排障时找资料容易得多。这个项目的固件结构是这样的app_main ├── network_manager // Wi-Fi 连接、断线重连、网络状态上报 ├── ble_config // BLE 配网服务GATT Server ├── audio_pipeline // I2S 采集 - VAD - 环形缓冲 ├── wake_word_detector // ESP-SR 唤醒词检测 ├── display_manager // GC9A01 驱动、表情渲染 ├── cloud_client // WebSocket 客户端、心跳、重连 └── event_dispatcher // 模块间事件总线各模块之间通过事件总线解耦。比如唤醒词模块检测到唤醒发一个EVENT_WAKEUP音频模块收到后开始录音云端模块收到后连接服务器准备上传。这样每个模块都能独立测试后期加新功能也不用在 main 里堆逻辑。3.2 BLE 配网流程给一个没有键盘屏幕的设备配 Wi-FiESP32-S3 没有键盘没有浏览器第一次上电怎么告诉它 Wi-Fi 密码最顺手的方案是 BLE 配网。这里有两个实现路线乐鑫的 SmartConfig / ESP-Touch用手机 App 广播 Wi-Fi 信息简单但不好定制 UI 流程而且对路由器频段有一定要求自建 BLE GATT 服务设备作为 BLE Peripheral手机 App 作为 Central通过自定义的 Service 下发 Wi-Fi SSID 和密码。可控性强、能自定义状态反馈我选了这条。BLE 配网的流程设计上有个小细节值得分享千万不要把 String 类型的 SSID 和密码直接塞进 GATT Characteristic然后让手机一次性写进去。GATT 单次写入的长度有限中文 SSID 或者特殊字符的密码很容易出问题。我的做法是设计一套简单的分包协议服务端 UUID: xxx 特征值 1: wifi_ssid_rx 手机写入最大 32 字节 特征值 2: wifi_pass_rx 手机写入最大 64 字节 特征值 3: ctrl_cmd_rx 手机写入 0x01 表示开始配网 特征值 4: status_tx 设备通知手机IDLE/CONNECTING/CONNECTED/FAILED手机 App 先把 SSID 和密码写入对应特征值再写0x01触发配网。设备收到后开始连接 Wi-Fi每 500ms 上报一次状态直到CONNECTED或FAILED。这个流程用户反馈非常直观比傻等几秒钟不知道发生了什么好太多。还有个关键点配网完成后 BLE 服务要主动关闭否则设备一直处于可被发现状态挺费电的。重新进入配网模式可以通过按键长按触发也可以做成“配置丢失且连续 30 秒没连上 Wi-Fi”时自动进入。3.3 麦克风 I2S 采集从原始 PCM 到有意义的音频帧INMP441 接入 ESP32-S3 的 I2S 外设标准接法是INMP441 VDD - 3.3V INMP441 GND - GND INMP441 SCK - GPIO 4 I2S BCK INMP441 WS - GPIO 5 I2S WS INMP441 SD - GPIO 6 I2S DIN INMP441 L/R - GND 选择左声道还是右声道输出ESP-IDF 里 I2S 驱动用i2s_std_config这个结构体配置关键参数如下i2s_std_config_t std_cfg { .clk_cfg I2S_STD_CLK_DEFAULT_CONFIG(16000), // 采样率 16kHz .slot_cfg I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG( I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO ), .gpio_cfg { .mclk I2S_GPIO_UNUSED, .bclk GPIO_NUM_4, .ws GPIO_NUM_5, .dout I2S_GPIO_UNUSED, .din GPIO_NUM_6, }, };为什么采样率用 16kHz 而不是 44.1kHz因为语音识别场景的带宽上限就是 8kHz16kHz 采样率已经完全够用还能大幅降低上传流量和云端 ASR 的计算成本。数据位宽选 32-bit 是因为 INMP441 的硬件特性但真正有效的数据只在高位读取后要右移 14 位再转成 16-bit PCM。注意把 I2S 读出来的裸数据直接传给云端是不行的。必须先做音量归一化甚至一个简单的静音检测VAD。VAD 我用的方案很简单——计算短时能量超过阈值且持续 250ms 就认为用户开始说话低于阈值持续 800ms 就认为一句话结束。这个逻辑用 16kHz/16bit 的音频帧来算性能开销微乎其微但能把上传的音频裁剪到最短省流量也省延迟。3.4 GC9A01 圆形屏显示表情驱动的关键实现GC9A01 是 SPI 接口的 240x240 圆形屏驱动 IC 是 ST7789 的变种ESP-IDF 里可以直接用 esp_lcd 的esp_lcd_panel_io接口。初始化的时序和寄存器配置网上资料很多我这里重点说显示内容怎么和对话状态联动。我的做法是抽象了一个display_statetypedef enum { DISPLAY_STATE_IDLE, DISPLAY_STATE_LISTENING, DISPLAY_STATE_THINKING, DISPLAY_STATE_SPEAKING, DISPLAY_STATE_ERROR, } display_state_t;不同状态下渲染的内容完全不同IDLE显示一只半闭眼的淡入淡出动画表示设备在待机LISTENING显示一个不断扩大的声波圆环表示正在拾音THINKING显示三个旋转的圆点表示“在思考”SPEAKING显示嘴部张合动画下巴开合幅度和 TTS 的音量包络同步ERROR显示一个 WiFi 图标打叉的图案并闪烁两次。关键实现细节是屏幕刷新不能阻塞主逻辑。ESP-IDF 的 LCD 驱动可以用 DMA 自动搬运帧数据到 SPI 外设所以显示逻辑跑在独立的任务里读取状态机变量然后渲染完全不占用主控的处理时间。帧率我只开到 20fps 左右因为表情动画不需要太高帧率但能省下不少电。记得在 PSRAM 里分配一个lv_color16_t *draw_buf heap_caps_malloc(240*240*2, MALLOC_CAP_SPIRAM)作为屏幕的像素缓冲区。GC9A01 一帧全屏就是约 115KB这内存放在内部 SRAM 会瞬间耗尽放在 PSRAM 则毫无压力。4. 云端服务与 AI 能力接入4.1 云端职责划分网关、会话与模型解耦云端是整个系统的大脑但大脑也不是只有一块。我把云端拆成三个逻辑层接入网关负责和设备保持 WebSocket 长连接做鉴权、心跳、消息路由会话服务维护每个设备的对话历史、角色设定、上下文窗口和具体的模型服务商解耦模型编排对接 ASR、LLM、TTS 三类模型哪一家模型效果好就调用哪家内部做好统一接口封装。这个分层最大的好处是换模型不影响设备端协议改会话逻辑不影响模型调用。这半年来我至少换了三个 LLM 供应商、两个 TTS 引擎设备端固件一次更新都没做过。网关选型我用了 Node.js ws库做原型。原因很朴实WebSocket 生态成熟JSON 处理顺手做原型速度快。生产环境如果并发量大网关这一层可以横向扩展无状态设计后加个 Nginx 负载均衡就能扛。4.2 大模型接入与 Prompt 设计陪伴场景的“人格”从哪来LLM 接入本身不复杂真正难的是如何让模型“稳定地”扮演一个陪伴角色。这个陪伴设备的角色设定是一个叫“小橘”的温暖系小伙伴我跟它说过无论用户聊什么回复都要短、自然、有温度像朋友而不是客服。为此我设计了一套三段式的 Prompt 模板系统提示System 你是“小橘”一个 25 岁的友善伙伴。你的说话风格 1. 句子简短通常不超过 2 句话 2. 不用网络梗不用 emoji 描述表情 3. 语气温暖可以共情但不说教 4. 如果话题超出你的知识范围坦诚说不知道并引导用户聊别的话题。 用户输入User [用户语音转写文本] 对话历史History 保留最近 10 轮对话按时间顺序组织。为什么把历史单独放因为很多模型对上下文的组织格式很敏感System 乱序的 User/Assistant 交替消息很容易让模型进入状态错乱。我干脆把历史拼接到用户消息之前用\n\n分隔这样大多数模型都能稳定理解“这些是之前聊过的现在用户又说了这句”。Prompt 里还有一个容易被忽略的点限制回复长度。很多人以为模型回得越多越好但陪伴设备是语音输出超过 100 个字的回复用户根本听不完体验很差。我在 Prompt 里明确写了“通常不超过 2 句话”同时在代码里设置max_tokens为 150双保险。4.3 情感陪伴场景的会话管理状态与记忆会话管理是这半年里改动最多的模块踩的坑也最多。第一版我把对话历史直接存成数组每轮对话 append 进去满了就丢弃最老的。后来发现一个问题模型对“第 8 轮的某句话”没有记忆用户明天重新开机对话它完全不记得昨天聊了什么。所以“记忆”至少要分两层短期会话上下文当前这一轮连续对话的完整记录存在内存里超过 10 轮就把最早的消息压缩成摘要用 LLM 做 summary长期用户画像用户的关键信息名字、喜欢的音乐、家里有猫、最近在准备考试等存在数据库里每次新会话开始时注入到 System Prompt 里。长期记忆的提取时机很重要。我建议在每轮会话结束后异步调用一次 LLM传入“对话记录 已有画像”让它输出增量更新的结构化 JSON。这个过程的 Prompt 大概是基于这段对话提取关于用户的持久事实喜好、经历、关系等。 只输出新增事实以 JSON 列表返回例如 [{attr: name, value: 小明}]。 如果没有任何新增输出 []。这样长期记忆会随着对话慢慢丰富设备也变得越来越“懂你”。当然这个功能比较费 token我目前的策略是每 3 轮对话执行一次并且限制在 5 个事实以内。4.4 云端的可扩展性设计为多设备并发做准备刚开始做云端时只有一台测试机器我就没考虑并发所有设备的信息都放在进程内存里。后来把设备拿给朋友试用三台设备同时在线上问题就来了一台设备的音频上传卡顿会影响另一台设备的响应因为模型调用是串行的。这次教训让我把云端改造成无状态服务会话状态从进程内存挪到 Redis以device_id为 key过期时间 30 分钟访达的模型调用全部走异步任务队列BullMQ Redis设备发来的请求先入队任务执行完再通过 WebSocket 把结果推回设备网关只做连接管理和消息转发不做业务计算。这样改完之后加设备只需要给网关加并发模型服务的执行节点可以独立扩容。整套体系虽然还是跑在一台机子上但架构上是真正“可持续演进”了。5. 端云协同与数据链路打通5.1 基于 WebSocket 的双向通信心跳、重连与消息格式端云之间的通信我最终选了 WebSocket 而不是 MQTT。原因很简单MQTT 的发布订阅模型更适合“传感器上报”而陪伴设备需要的是一个 Request/Response 式的双工通道——端侧上传音频、云端返回回复WebSocket 天然就支持这种模式而且调试起来也更直接。消息格式统一为 JSON最外层包含type字段核心类型如下方向type说明端-云audio_start开始上传音频流端-云audio_chunk音频二进制块base64 编码端-云audio_end音频发送完毕云-端asr_result识别出的文本云-端llm_chunk大模型逐字生成的回复云-端tts_audioTTS 合成的音频数据base64云-端state_change云端主动推送状态变化如等待唤醒等关键设计点大模型的回复不做“整段处理后一次性返回”而是通过llm_chunk流式推送。这样端侧可以在用户说完话之后几百毫秒就开始听到第一句话而不是等模型挖完整个回复再合成语音延迟体感完全不一样。心跳机制也很重要。ESP32-S3 的 Wi-Fi 在深度休眠后恢复连接需要时间如果云端迟迟没收到设备心跳就会误判设备离线、清除会话状态。我的心跳间隔是 30 秒云端连续 90 秒收不到心跳就断开会话但保留 2 分钟内重连的“会话恢复”能力用户不会因为一瞬间断网就丢失整个对话上下文。5.2 音频上传与 TTS 回传带宽与延迟的平衡音频采集已经在端侧裁剪过了一秒钟 16kHz 采样的 16-bit PCM 是 32KB一次 5 秒的查询音频大约 160KB。直接用 JSON 的 base64 字段传输会让体积膨胀 33%所以我改用 WebSocket 的二进制帧上传音频块元信息继续用 JSON 消息。整个数据链路是这样跑的用户说完话端侧把音频切成 20ms 一帧的 PCM每帧单独通过 WebSocket binary frame 发送帧头带一个递增的 seq 号云端收齐音频后调用 ASR 得到文本云端把文本送入 LLM流式拿到回复回复文本送给 TTS 引擎合成音频后以tts_audio消息返回端侧收到 TTS 音频后边写入 I2S 功放边播放同时驱动屏幕显示“说话中”的表情。第 2 步里帧头带 seq 号很有用万一有丢帧可以及时重传。实测在普通家庭 Wi-Fi 环境下这个链路从用户说完话到设备开口回答延迟大约在 1.5 到 2 秒之间属于可接受的交互范围。TTS 返回的音频格式上我选择 OPUS 编码而非 MP3。OPUS 在 16kbps 下语音可懂度已经很高压缩率比 MP3 好解码库在 ESP32-S3 上跑也没有压力。一开始我为了省事直接传 MP3结果流量大了一倍传输延迟也明显上升。5.3 离线降级与网络异常处理设备不能“装死”无线环境永远是不可靠的。我在开发过程中遇到过无数次 Wi-Fi 信号差、云端超时、DNS 劫持、乃至路由器重启的情况。如果设备在这些异常下直接卡死或静默用户会觉得产品坏了。所以我在端侧实现了一套三级降级策略第一级网络不稳定。WebSocket 连续 3 次心跳超时后设备进入“网络不佳”状态屏幕显示弱信号图标语音提示“我这边信号不太好请靠近路由器试试”但继续尝试重连第二级云端不可用。重连 5 次仍失败设备进入“离线模式”此时唤醒词检测仍然工作但会回复“我暂时没法上网等网络恢复了我们再聊天”第三级完全离线。拔掉电源几小时后再回来设备冷启动后如果 Wi-Fi 和云端都不可用屏幕只显示时间不主动打扰用户。这个三级策略在用户侧的效果非常明显——设备从“莫名其妙没反应”变成“每次都给出合理的反馈”即使用户不懂技术细节也能感觉到这设备是“懂事”的。6. 可持续演进的工程化实践6.1 固件 OTA 升级通道不改硬件就改功能的唯一途径如果你做的是 demo烧录固件用串口没问题如果你做的是陪伴设备每台都要手动插线升级那就不可能持续演进了。ESP32-S3 支持原生 OTA把固件分为ota_0和ota_1两个分区新固件下载到备用分区校验后切换启动。我实现的 OTA 流程是这样的设备端每隔 12 小时向云端请求一次固件版本信息如果云端版本号高于本地版本返回固件下载 URL放到对象存储设备用 HTTPS 下载固件到 OTA 备用分区边下边校验 SHA256下载完成后写入确认标记调用esp_ota_end和esp_ota_set_boot_partition重启进入新固件新固件启动后上报当前版本云端记录。OTA 最容易出问题的点是下载中断。我加了断点续传逻辑每次下载记录偏移量重新连接后从断点继续实测 2MB 的固件在普通宽带下 30 秒内能完成下载成功率超过 99%。不要忘了回滚机制。如果新固件启动后连续 3 次崩溃就要检测到异常并回退到旧分区。ESP-IDF 的esp_ota_mark_app_valid_cancel_rollback和esp_ota_mark_app_invalid这两个 API 就是干这个的一定要用起来。6.2 云端服务的无状态化改造从“演示”到“产品”前面提到过云端一开始是“单进程内存存状态”的写法三台设备同时在线就暴露出串行问题。我把改造分成两个阶段第一阶段把会话状态迁移到 Redis。每条会话的 key 是session:{device_id}value 是对话历史的 JSON 数组过期时间 30 分钟。这样即使处理请求的那台服务实例崩溃另一台实例也能接管用户无感知。第二阶段把模型调用异步化。设备端把音频上传后网关把“ASR 任务”和“LLM 任务”推入 Redis 队列多个 worker 并行消费。TTS 也是异步生成生成完成后直接推回设备。无状态化带来的一个额外好处是可以灰度发布。新版本的服务先部署一台让 10% 的流量走新逻辑观察指标没问题再全量切。设备端的 OTA 也有类似的灰度策略——先升级一台做内测没问题再批量推送。6.3 数据埋点与迭代优化用真实数据改进体验做 AI 陪伴设备最大的困惑不是“功能不够”而是“不知道用户怎么用”。我一开始连基础日志都没有出了问题只能瞎猜。后来我建立了一套极简埋点体系端侧和云端分别记录端侧埋点唤醒成功率、唤醒到开始录音的耗时音频上传的时长和数据量WebSocket 断线次数和重连耗时TTS 首包到达时间从说完话到听到第一个字。云端埋点ASR 每轮识别的置信度和耗时LLM 每次调用的 token 消耗和首 token 延迟TTS 合成的音频长度和返回耗时每轮对话的完整时间轴音频上传完成时间、ASR 完成时间、LLM 首包时间、TTS 完成时间。这些数据是优化体验的“眼睛”。举个例子我通过埋点发现“LLM 首 token 延迟”占了端到端延迟的 40% 以上后来通过换用支持流式输出的模型服务商把这一项从 800ms 降到了 250ms整体体感立刻上了一个台阶。7. 常见问题与排查技巧实录7.1 麦克风 I2S 读出来全是噪声或音量极小这是 INMP441 接入 ESP32-S3 时最常见的坑我自己也耗了两天。现象有两种读出来全是白噪声99% 是 I2S 位宽配置不对。INMP441 输出 24-bit 有效数据但封装在 32-bit slot 里如果你把 slot 配置成 24-bit 或者 16-bit时钟采样错位读出来的自然全是噪声。修正方法就是 32-bit slot mono 模式音量极小确认 L/R 引脚的电平。L/R 拉高是右声道输出拉低是左声道输出。如果你的 slot 配置和 L/R 不匹配采到的不是噪声而是极低的串扰信号。排查 I2S 问题的最好工具是逻辑分析仪抓 SCK、WS、SD 三根线一看波形就明白问题出在哪。7.2 BLE 配网经常失败或者配网成功后设备没有连上 Wi-FiBLE 配网失败的原因里环境干扰、广播参数、以及手机兼容性三者占比最高。我踩过的一个隐蔽坑是设备在“开始配网”信号之后立刻去连 Wi-Fi但这个信号在 BLE 通知发出后如果还有 Pending 的写请求没有处理完系统就会卡在 BLE 协议栈里Wi-Fi 连接延后执行而手机已经认为配网失败。解决办法是把“开始配网”做成延迟 3 秒生效的定时器先把 BLE 协议栈的收尾工作处理干净再启动 Wi-Fi 连接。当然还有常见的问题使用 5GHz Wi-Fi 的朋友连不上因为 ESP32-S3 只支持 2.4GHz这个在配网 UI 里一定要提前提示用户。7.3 屏幕花屏、刷新闪动GC9A01 花屏大多是 SPI 速率过高或者电源纹波问题。SPI 时钟我最初拉到 40MHz花屏严重降到 20MHz 后稳定。如果你的板子走线比较长30MHz 以上基本都有风险。另一个因素是屏幕的背光电源和主控共用一路 LDO屏亮瞬间的大电流拉低了 GPIO 电平也会导致花屏。把背光供电独立出来就能解决。7.4 内存不足跑一段时间后 OOM 重启ESP32-S3 虽然有 8MB PSRAM但如果代码里分配不谨慎还是会被吃干净。最常见的两个原因用 C 标准库的malloc而不是heap_caps_malloc(..., MALLOC_CAP_SPIRAM)大内存块会塞进内部 SRAM只有 512KB瞬间耗尽WebSocket 接收大 JSON 消息时没有限制缓冲区大小云端发来一条超大消息就能让设备 OOM。我的经验是所有超过 4KB 的动态分配全部显式指定MALLOC_CAP_SPIRAMWebSocket 的 buffer 限制为 8KB超出的消息直接丢弃并触发错误重传。7.5 WebSocket 连接几十秒后必断这个问题困扰了我很久。后来发现是 ESP-IDF 的esp-tls组件和服务器之间对 keepalive 的理解不一致。服务器端没设置 keepalive网关默认 60 秒没数据就断开 TCP而我的应用层心跳是 30 秒理论上不会触发。但如果云端做了一层负载均衡比如 Nginx它会独立管理 TCP 连接应用层心跳过了负载均衡但 TCP 层面的空闲超时还是会发生。解决办法就是在云端入口同时配置 TCP keepalive 探活参数保证负载均衡不会因为“空闲”而关闭连接。这个问题在开发机上很难复现一上生产环境必现非常隐蔽。写在最后做这个项目最大的体会是AI 陪伴设备不是“硬件 一个 API”而是一套从端侧实时性到云端智能性的系统工程。ESP32-S3 是一个性价比极高的起点但真正让设备“活起来”的是端云边界怎么划、会话记忆怎么管、异常情况怎么降级、以及整个系统能不能持续不换硬件就升级。我个人在后半段的调试中还有一个强烈感受一定要尽早把埋点和日志体系做起来不要等“差不多能用”了再补。很多看起来很诡异的线上问题比如延迟突变、连接闪断、内存缓慢增长没有数据你根本没法定位。如果你手里也有一块吃灰的 ESP32-S3我建议你从最简版本开始先让设备能唤醒、能录音、能上报音频到云端、能播放云端返回的语音。这条最小通路跑通了再逐步加上屏幕表情、长期记忆、OTA 这些“可持续演进”的部分。一步步来很快你也会有一台属于自己、而且还能越用越懂你的 AI 陪伴设备。