我身边常有人拿 ESP32-S3 问我,说想做 AI 陪伴设备,但不知道从哪下手。最典型的两条歪路:一是打算在板子上硬塞一个能聊的大模型,结果固件刚起来就 OOM,连 WiFi 握手都过不去;二是彻底放弃端侧逻辑,把开发板当成一个“物理按键的网页版聊天输入框”,一断网就全部瘫痪。我自己做这块设备时,把重点放在“端云架构”的合理切分上:ESP32-S3 在端侧负责音频、视觉、显示这类硬件交互,云上跑大模型和 AI Agent 层,负责复杂对话和长期记忆。这篇文章会从硬件选型、端侧固件结构、云侧服务、通信协议到 OTA 演进,完整拉一遍我们的设计过程。适合手里有 ESP32-S3 开发板、想做一个真正稳定可迭代的 AI 硬件项目的开发者参考。
1. 为什么锚定 ESP32-S3:先看清这块芯片的真实边界
1.1 与手机、树莓派和纯云方案对比,它赢在哪里
做 AI 硬件设备,很多人会纠结到底用 ESP32-S3、树莓派还是直接调云 API。我的结论是:ESP32-S3 不是万能的,但在“陪伴设备”这个场景下,它是性价比和工程复杂度的平衡点。
手机方案看似现成,但问题是它不是一个“独立设备”。你得处理 App 常驻后台、屏幕熄不熄、系统杀进程、蓝牙断连这些事,做出来更像一个手机配件,而不是陪伴机器人。树莓派性能确实强,但功耗高、启动慢、结构大,更麻烦的是掉电容易坏 SD 卡。纯云端方案则把设备的命运全押在网络和服务器上,离线时连本地语音唤醒都做不了,交互体验非常断裂。
ESP32-S3 是双核 Xtensa LX7,最高 240MHz,带 512KB SRAM 和可选的 8MB PSRAM,最关键是内置向量指令,能跑轻量的神经网络推理。这意味着它可以做到:随时待机、毫秒级唤醒、离线工作部分功能、体积小、功耗低。做一个桌面陪伴设备,这个芯片是非常合理的选择。
1.2 我最终确定的硬件清单与引脚规划
说句实话,ESP32-S3 的型号非常多,选错了后续会很痛苦。我的建议是直接买带 8MB PSRAM 的版本,因为摄像头帧缓存、音频缓冲、TTS 解码都极度依赖 PSRAM。没有 PSRAM 的版本,做不了带摄像头的 AI 视觉项目。
以下是这个项目的硬件清单:
| 模块 | 型号 | 接口 | 作用 |
|---|---|---|---|
| 主控 | ESP32-S3-WROOM-1 N16R8 | - | 4MB flash + 8MB PSRAM |
| 麦克风 | INMP441 | I2S | 采集用户语音 |
| 功放+喇叭 | MAX98357A + 3W 小喇叭 | I2S | 播放 TTS 语音 |
| 摄像头 | OV2640 | DVP 并行接口 | 拍照与图像上行 |
| 显示屏 | ST7789 1.3 寸 IPS | SPI | 显示表情与状态 |
| 电池 | 3.7V 18650/聚合物电池 | 充电管理 IC | 移动使用 |
| 电平转换 | 3.3V-5V 模块 | - | 兼容部分外设 |
引脚分配上,我踩过不少坑,最常见的坑就是 GPIO 冲突。比如 GPIO0 是 boot 引脚,接了按键之后,某些外设初始化会干扰启动。我最终稳定使用的引脚分配是:
- I2S 麦克风:SCK=GPIO4,WS=GPIO5,SD=GPIO6
- I2S 功放:BCLK=GPIO15,LRC=GPIO16,DIN=GPIO17
- ST7789 屏幕:SCLK=GPIO21,MOSI=GPIO22,CS=GPIO10,DC=GPIO11,RST=GPIO12,BL=GPIO13
- OV2640 摄像头:使用标准的 DVP 引脚组,PCLK=GPIO18,XCLK=GPIO19,VSYNC=GPIO38,HREF=GPIO39,SDA=GPIO40,SCL=GPIO41
- 电池电压检测:GPIO35,接电阻分压
1.3 功耗粗账:陪伴设备的续航底线
这块相对容易被忽略,但我建议别等做完了才后悔。设备端功耗是架构层面的约束——如果上云唤醒词检测功耗太高,整个“随时待命”的设计就会失效。
实测下来,这个设备在不同的运行状态下的功耗大概是:
| 状态 | 电流 | 对应场景 |
|---|---|---|
| Deep Sleep | ~150uA | 无活动休眠 |
| 语音唤醒待机 | ~30mA | WakeNet 监听中 |
| 屏幕亮+WiFi 连接 | ~90mA | 显示界面、云端在线 |
| 持续录音上传 | ~160mA | 对话中 |
| 拍照+上传 | ~220mA | 视觉交互 |
配 2000mAh 电池,待机能撑 3~5 天,持续对话大约 4~6 小时。如果你做的是桌面设备,这个续航足够日常使用了。这里的心得是:优先用 ESP-IDF 的 Power Management 框架,它能在降频和保留实时响应之间自动切换。
2. 端云分工的最先一刀:哪些留在端侧,哪些必须上云
2.1 陪伴场景对延迟、隐私与成本的三角约束
端云架构不是“能往云端丢就往云端丢”,也不是“尽量在本地算完”。真正要看你设备的场景需求。AI 陪伴设备有三条绕不过的约束。
第一是延迟。对话是强实时交互,如果每次唤醒后要 3 秒才能听到回声,用户马上会觉得这是个残次品。本地方可承担高实时性的部分,把网络延时的不可控因素挡在关键路径之外。
第二是隐私。陪伴设备会放在卧室、书桌上,用户可能随时说什么敏感内容。如果所有声音都无脑传云端,不仅用户体验差,隐私合规也是个雷。最好的做法是端侧先做 VAD 和本地唤醒,只有用户明确开始对话时,语音才上行。
第三是成本。大模型 API 调用不是免费的,而陪伴场景是高频交互。如果每次设备随意说的一句话都要跑一轮大模型,你的账单会被快速打爆。端侧用规则引擎过滤掉无意义的杂音,云端只处理真正有意向的请求。
2.2 最终的职责边界表
我把系统划分为三层边界,每一层都有明确职责:
| 层级 | 职责 | 典型实现 |
|---|---|---|
| 端侧即时响应层 | 唤醒词、LED/表情、按键、本地命令 | WakeNet、LVGL |
| 端侧感知层 | VAD、音频采集、拍照、姿态传感器 | I2S、DVP、IMU |
| 云侧智能层 | LLM 对话、工具调用、长期记忆、个性化 | FastAPI + AI Agent |
| 云侧服务层 | TTS 生成、推送通知、任务调度 | 流式 TTS、MQTT |
设备端永远不直接拼长文本大模型推理,而是把“感知到的上下文”整理成简洁的事件,发给云端。云端返回的也不是一长串文本,而是结构化指令:要说什么、要不要拍照、要不要执行某个工具。
2.3 一次完整交互的时序链路
如果读者想理解这套架构,我建议先记住一条完整的时序链路。下面是我项目中一次普通对话的流程:
- 用户说“小伴小伴”。
- ESP32-S3 上的 WakeNet 识别到唤醒词,拉高系统主频,开始 VAD。
- 用户继续说话,VAD 检测到起始点,ESP32-S3 开始采集 16kHz/16bit 单声道 PCM 音频。
- 检测到语音结束点,设备把这段音频数据编码后通过 MQTT/WebSocket 上传到云端。
- 云端服务调用语音识别,得到文本。
- AI Agent 层把这个文本和当前上下文交给大模型,大模型决定是调用工具还是直接回复。
- 如果回复是“我可以看看桌面”,Agent 层下发一个“take_photo”指令。
- ESP32-S3 执行拍照,将 JPEG 上传,云上视觉模型理解图像内容,再生成最终回复。
- 云端 TTS 生成语音,通过 WebSocket 返回音频流,ESP32-S3 播放。
这整个过程里,设备端是“感知 + 执行”的角色,云端是“理解 + 决策”的角色。边界清楚之后,任何一层要升级,都不需要推翻另外一层。
3. 设备端软件:让 ESP32-S3 同时扛住音频、图像与联网
3.1 用 ESP-IDF 还是 Arduino:我的选择与理由
ESP32-S3 可以用 Arduino 框架快速上手,也可以用乐鑫官方的 ESP-IDF 做深度开发。就 AI 陪伴设备这种多外设、多任务、需要精细内存控制的场景,我更推荐 ESP-IDF。
原因有三:一是 ESP-IDF 对音频、摄像头、WiFi 这三大件有更完整的原生驱动;二是它的内存管理能力更强,可以用heap_caps_malloc指定把大块 buffer 放在 PSRAM,避免 SRAM 不足;三是 OTA、NVS、电源管理等关键能力在 ESP-IDF 中是第一公民,Arduino 总是隔一层。
当然,如果你只是做原型验证,Arduino 也可以。但我的项目已经跑到量产迭代阶段,所以整个端侧固件都是基于 ESP-IDF v5.x 写的。
3.2 音频子系统的实现细节
音频是陪伴设备最核心的交互通道。ESP32-S3 自带两个 I2S 控制器,可以同时跑音频输入和输出。我在输入侧使用 INMP441 MEMS 麦克风,输出侧使用 MAX98357A D 类功放。
输入流的典型配置是:
i2s_config_t i2s_in_config = { .mode = I2S_MODE_MASTER | I2S_MODE_RX, .sample_rate = 16000, .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 = 1024, .use_apll = false, };这里的关键参数是 DMA buffer。dma_buf_len = 1024、dma_buf_count = 8意味着总共 8 个 1024 帧的缓冲区。因为 16kHz 采样率下,每个 buffer 约 64ms 音频,8 个 buffer 可以提供约 512ms 的 FIFO 深度,足以吸收 Wi-Fi 峰值延迟。如果 buffer 设置太小,会发生 DMA overrun,音频会断断续续;如果过大,端到端延迟又会增加。
唤醒词检测我用的是乐鑫的 ESP-SR WakeNet。它可以在 ESP32-S3 上跑,不需要很大的 RAM。唤醒之后才进入轻量级 VAD 循环,只有检测到人说话,才开始真正上传录音。
这里有个容易忽略的细节:不要直接把整段麦克风 PCM 数据推上云,带宽和云成本都扛不住。16kHz/16bit 单声道是 32KB/s,如果用户说 10 秒的话,就是 320KB 数据。要在端侧做 VAD 切割,只上传语音有效区间,并且在靠近尾部加 300ms padding,避免尾部字被切断。
3.3 摄像头拍照与图像压缩上报
设备的视觉能力我用 OV2640 实现。OV2640 是 200 万像素的 DVP 摄像头,在 ESP32-S3 上要用esp32-camera驱动库。我在架构里特意把它设计成“按需拍照”,而不是持续视频流。一来陪伴场景不需要实时视频,二来持续视频流会占满上传带宽和云端的视觉处理成本。
拍照触发有两种方式:用户直接说“看看我这边”,或者 AI Agent 判断当前场景需要图像信息。调用拍照时,我先把帧缓冲区指向 PSRAM:
camera_config_t config = { .pin_pwdn = -1, .pin_reset = -1, .pin_xclk = 19, .pin_sccb_sda = 40, .pin_sccb_scl = 41, .pin_d7 = 48, // ... 省略其他引脚 .frame_size = FRAMESIZE_UXGA, .jpeg_quality = 12, .fb_count = 2, .grab_mode = CAMERA_GRAB_WHEN_EMPTY, .fb_location = CAMERA_FB_IN_PSRAM, };这里必须注意fb_location设为 PSRAM。UXGA 分辨率的 RGB565 帧 buffer 要占用 3MB 以上,如果放到内部 SRAM 会直接崩溃。实际使用时我几乎不保存原始 RGB,而是直接把 JPEG 数据打包传输到云端,这样单帧只有 100KB 左右,上传很快。
3.4 任务划分与事件循环设计
ESP32-S3 是双核架构,我把它跑成“一个主控核 + 一个通信核”的分工模式:
- 核0:跑 WiFi/MQTT/HTTP 网络栈。这个核上的任务允许阻塞,因为网络操作天然有等待。
- 核1:跑音频、摄像头、显示,以及主事件循环。这些任务必须保持低延迟,不能因为网络抖动而卡顿。
任务间通信不要用复杂的锁,尽量用队列。比如音频采样任务把 PCM 数据块扔进队列,上传任务从队列里取数据。队列深度设为 16,如果队列满就丢最旧的数据块,而不是直接阻塞录音,这样能保证语音采集的连续性。
屏幕上我显示的是一个简单的卡通表情,用 ST7789 驱动。表情变化通过主事件循环接收云端指令来触发。注意 SPI 总线和 I2S 在同时工作时,要避免同频干扰,最好把屏幕刷新和音频数据搬移放在不同核上。
4. 云端服务:把“陪伴”组织成有状态的 AI Agent
4.1 网关层:设备接入与会话管理
云端最外层是一个网关服务,负责设备接入认证、WebSocket/MQTT 连接维护、会话管理。我这里用的是 Python FastAPI 写的一个轻量网关,设备通过 MQTT 上报事件,通过 WebSocket 接收流式 TTS 音频。
每个设备有一个唯一 ID,首次配网时从云端换取 token。后续所有请求头都带这个 token,服务端校验后才允许建立长连接。设备上线后,服务端会为它创建一个 session 对象,session 里保存当前对话的上下文指针、用户偏好、最近操作历史。
设备上行消息格式我设计得很精简:
{ "device_id": "desk_01", "type": "audio_upload", "session_id": "s_8f3a", "duration_ms": 3200, "audio": "<base64>" }这里 audio 字段用的是 base64 编码的 PCM 或 Opus 数据。虽然 base64 会增加约 33% 的传输量,但好处是 JSON 调试非常方便,一个工具链就能通吃整个链路。如果你对功耗和流量极其敏感,也可以改成二进制分帧协议,不过初期调试成本会高很多。
4.2 大模型服务的接入与参数调校
网关收到语音后,先调用 ASR(语音识别)把音频转文本,然后把文本交给大模型服务。我的云端项目里,大模型部分兼容 OpenAI 格式的 API,所以可以灵活切换不同供应商,哪个服务质量好、价格合适就换哪个。
对话能力的核心提示词设计很关键。陪伴设备的大模型 system prompt 要短,角色要明确,还要规定输出格式。我们采用“行为指令 + 工具调用”的提示词结构:
你是一个桌面 AI 陪伴助手,名字叫“小伴”。 你的职责: - 用简短、温暖、自然的语气和用户聊天 - 如果用户请求涉及当前环境,调用 visual_understand 工具理解图像 - 如果用户请求涉及时间和任务,调用 reminder 工具 - 不要长篇大论,回复不超过 50 字 - 如果意识到用户情绪低落,适当给出共情回复大模型的temperature我设在 0.7,让它保留一点随机性和亲切感。max_tokens设成 256,防止模型偶尔抽风生成一大段废话,导致 TTS 都处理不过来。
4.3 工具调用与结构化输出
我强烈建议在陪伴设备这类场景中,不要直接让大模型输出自然语言给 TTS,而是先输出结构化指令,再由服务端转成自然语言。原因是大模型直接生成的自然语言经常包含无法读出来的符号、重复词、语气词,TTS 会很僵硬。
我的做法是用 function calling 机制。大模型先决定调用哪个工具,比如photo、reminder、weather,工具执行完再把结果注入到下一轮 prompt。云上工具集的实现示例:
def execute_tool(name, args): if name == "take_photo": # 下发拍照指令到设备,等待 JPEG 返回 photo = device.capture_once() caption = vision_model.describe(photo) return {"photo": photo, "caption": caption} elif name == "create_reminder": return reminder_service.create(args["text"], args["time"])工具调用的响应再喂回大模型,生成最终用户可见的回复文本。这个过程虽然多了一次模型往返,但换来的是输出质量的巨大提升。
4.4 记忆与个性化:陪伴感的来源
如果设备每次对话都是“失忆”的,那根本谈不上陪伴。我做了两层记忆:短期会话记忆和长期用户画像。
短期会话记忆保存在 Redis 里,存最近 20 轮对话摘要,每轮对话后由一个小模型把关键信息压缩成一句话。长期用户画像则存用户的偏好、作息、重要事件。比如用户说过“我周三早上有晨会”,这个信息会被 extract 出来,进入用户 profile。下次用户问“明天早上我有什么安排”,模型就可以结合提醒事项直接回答。
记忆的数据模型大概是:
{ "user_id": "u_123", "preferences": {"lights": "nonono", "voice": "gentle"}, "facts": [ {"content": "用户是前端工程师", "ts": 1699999999}, {"content": "用户有一只猫叫豆豆", "ts": 1700000000} ], "last_topic": "工作压力" }5. 端云通信协议与容错:设备联网不只是连上 Wi-Fi
5.1 MQTT、HTTP 与 WebSocket 在链路中的分工
通信层是整个端云架构里最容易“看起来全通,实际坑一堆”的部分。我在项目中用三种协议,各管一段:
| 协议 | 用途 | 理由 |
|---|---|---|
| MQTT | 设备事件上报、状态心跳、云端下发指令 | 轻量,支持断线重连,QoS 可靠 |
| WebSocket | 流式 TTS 音频回传 | 低延迟,双向流式,适合音频 |
| HTTPS | 固件 OTA、配置拉取、一次性图片上传 | 简单可靠,有大文件传输能力 |
MQTT 只承担小于 1KB 的控制消息,比如“设备上线”“唤醒词触发”“拍照指令”。音频流和图像流不走 MQTT,否则 QoS 重传会把整个链路堵死。
5.2 消息协议设计:让设备端不用理解自然语言
设备端收到的云端指令必须足够结构化。我定义了一个action枚举,设备端固件只需要对这个枚举做分发,不需要理解自然语言:
{ "action": "speak", "params": { "text": "好的,我来看一下你的桌面。", "tts_url": "https://...", "duration_ms": 2300 } }常见的 action 有speak、take_photo、set_led、set_screen、start_upload、do_ota。设备端就是一个轻量的 action dispatcher,收到什么执行什么。这样做的好处是云端升级对话逻辑时,设备固件完全不用变,甚至可以在不重新烧录的情况下,新增一种云端临时指令。
5.3 断网、弱网与重连:一套降级策略
设备永远在线是一个美好的愿望,真实环境里 5G/Wi-Fi 总会抖。我设计了三档降级策略:
- 完全在线:全功能使用。
- 网络闪烁:语音能唤醒,本地显示“网络不稳定”,云端请求正常发出,但超时时间降为 3 秒;如果超时,设备本地播放“网络不太好,我等下再回答你”。
- 完全离线:设备进入本地回退模式,只能播放一些预置音频、显示时钟、充当普通摆件。绝不尝试反复重试导致死机。
重连逻辑有一个容易被忽视的细节:Wi-Fi 断线后,不要立即重连。ESP32-S3 底层自带重连机制,如果应用层再叠加一层重连,会出现两个线程同时抢连接的情况。我的做法是应用层只监听WIFI_EVENT_STA_DISCONNECTED,一旦触发就停止所有上行任务,清理 socket,5 秒后手动触发一次esp_wifi_connect()。重连成功后,再按顺序恢复 MQTT、WebSocket。
5.4 安全与合规的做法
设备与云端之间我用 TLS 加密。因为 ESP32-S3 的硬件加速支持 AES 和 SHA,TLS 握手并不算太慢,实测大概 1~2 秒,完全能接受。设备端存储 token 用 NVS 分区加密,不能明文存放在 flash 里。
用户语音数据上传时,网关层只保留匿名化的 session id,不直接关联真实手机号。用户注销时,云端要能一键删除该用户的所有语音文件和记忆 profile。这些能力要在架构设计时留好接口,不要等上线后被要求整改时才补。
6. 可持续演进:OTA、配置与接口版本的工程化
6.1 ESP32-S3 的双 OTA 分区与回滚机制
“可持续演进”是我这个项目给自己定的硬指标:设备必须能远程升级固件,并且升级失败后能自动回滚。ESP32-S3 支持 OTA,但前提是分区表要专门设计。
我的分区表(partitions.csv)定义:
# Name, Type, SubType, Offset, Size nvs, data, nvs, 0x9000, 0x5000 otadata, data, ota, 0xe000, 0x2000 app0, app, ota_0, 0x10000, 0x200000 app1, app, ota_1, 0x210000, 0x200000 storage, data, fat, 0x410000, 0x1f0000这里关键是app0和app1两个 OTA 分区各占 2MB。当前运行的固件在 app0,新固件下载到 app1,校验通过后重启,bootloader 切换到 app1。如果新固件启动后 30 秒内没有上报“启动成功”,系统会触发 watchdog 回滚到上一个版本。这等于给设备装了一个安全降落伞,我可以放心大胆地推送新功能。
6.2 云端 API 的版本演进策略
固件能升,云端接口也得能平滑演进。我踩过最大的坑是:某个接口返回的 JSON 加了新字段,结果老固件不识别,直接解析报错。从那之后,我所有的上下行 JSON 都带schema_version字段。设备端解析器碰到不认识的版本,会忽略未知字段,只解析自己认识的字段。云端加字段永远算 minor change,不允许破坏老字段语义。
设备状态上报也一样。device_state结构体初期只有battery、wifi_rssi,后来加了temperature、uptime。只要新字段是可选的,老固件就不会出问题。如果实在要做 breaking change,我宁愿新增一个 action,而不是改旧 action 的语义。
6.3 数据回流与模型迭代闭环
一个端云系统的价值在于能不断变聪明。设备每次交互的匿名化数据会回流到云端数据湖,包括:唤醒成功率、VAD 误触发率、云端 ASR 置信度、用户明确表达的“满意/不满意”情绪、工具调用失败次数。
这些数据我每两周产出一份指标报表,用来指导三件事:
- 调整唤醒词灵敏度:如果 VAD 误触发太多,就提高置信度阈值。
- 优化提示词:如果用户频繁说“不是你问的这个”,说明大模型的追问方式有问题。
- 新增功能:如果用户在特定时间段密集问某一类问题,就考虑做成快捷指令。
整个闭环让设备不只是“能用”,而是“越用越准”。这也是我说的“可持续演进”的真正含义。
7. 最后说几个我们踩过的坑
7.1 音频失真的根源竟然在电源
最开始调试音频时,只要音量开大,喇叭里就有“滋滋”的底噪。用示波器一看,I2S 数据线上的信号在喇叭峰值时产生毛刺。根因是 MAX98357A 瞬时电流拉低了 3.3V 电源轨,导致 I2S 逻辑电平抖动。解决方案很简单:功放电源单独走 5V 供电,并且在电源输入端加 470uF 电容。这个问题不遇到真想不到,所以建议大家在画板或接线时,功放供电从一开始就单独一路。
7.2 摄像头 OOM 崩溃:不是代码问题,是 buffer 位置问题
运行一个小时左右,设备偶尔会重启。查 log 发现是esp_camera_fb_get()返回 NULL。排查半天,发现我把 frame buffer 分配在了内部 RAM,虽然设置了fb_count=2,但 UXGA 的 JPEG 帧在复杂场景下可能瞬时膨胀,直接把内部 SRAM 挤爆。把fb_location改成 PSRAM 后,问题彻底消失。另外一个隐藏问题:grab_mode要选CAMERA_GRAB_WHEN_EMPTY,如果选CAMERA_GRAB_LATEST,在高分辨率下 DMA 会持续占用带宽,导致 WiFi 吞吐量下降。
7.3 WiFi 中断和音频 DMA 抢 CPU
在双核上,WiFi 协议栈的中断频率非常高,尤其是传输大数据包时。如果音频 DMA 中断也跑在同一核,会出现音频卡顿。解决方法是把 WiFi 固定在核0,音频搬移循环固定在核1,并通过IRAM_ATTR标记中断回调。ESP32-S3 两个核是独立的中断控制器,合理分配核间任务后,整体稳定度提升了一个量级。
7.4 云端超时导致的假死现象
设备初期经常出现“喊它没反应”的假死状态,但 RSSI 显示 WiFi 正常。查 log 发现设备在等待云端返回 TTS 音频时,线程被recv()阻塞,而这期间唤醒词回调无法执行。后来我加了一个 8 秒的看门狗定时器,如果 WebSocket 超过 8 秒没有收到任何数据,就强制断开重连。核心教训是:任何阻塞调用都不能是永久阻塞的,硬性超时是设备稳定性的红线。
7.5 MQTT QoS 的误用
说实话,项目初期我把所有下行指令都设成 QoS 2,觉得这样最可靠。结果是网络一抖动,消息重传堆积,设备端队列直接堵死。后来我把 MQTT 分成两个 topic:cmd/control用 QoS 1,适合拍照、播放这类要可靠送达的指令;event/report用 QoS 0,因为状态上报可以丢弃,下一次上报会覆盖。这样既保证了关键指令的可靠性,又避免了队列拥塞。
这套端云架构从原型到现在,已经稳定跑了几个月。我个人最大的体会是:做 AI 硬件,真正难的不是把某个模型跑在某块板子上,而是把“设备端感知-云侧决策-设备端执行”这条链路做成一个可以持续升级的闭环。ESP32-S3 不是最强的算力平台,但它让我用很低的功耗和成本,把云侧那些大模型的智能带进了真实桌面。如果你也在做类似项目,建议从最小的音频交互闭环开始,先把唤醒、录音、上传、回复这条路跑通,再往上面加视觉和记忆。每一步都保持端云边界清晰,未来演进就不会被一段写死的逻辑卡住。