ESP32语音助手:分体式架构让圆屏设备轻松接入大模型 📅 发布时间:2026/9/9 9:58:55 👁 浏览次数: 做桌面语音助手这几年各种方案我都折腾过。最初我总想着把模型塞进设备里觉得这样才显得“智能”结果就是又卡又难用。后来我彻底想通了ESP32这颗圆屏设备根本不负责跑模型它老老实实做一个后台语音客户端就够了。设备端管采集语音、显示反馈、播放声音真正的大模型推理全部放到后台跑本地电脑或者服务器上用Ollama这类工具加载本地模型。这套分体式架构才是当前小硬件接大模型最务实的解法。这篇文章是糖球系列的第三篇专门讲完整体验链路怎么搭圆屏设备端怎么做、后台语音网关怎么接、STT到LLM再到TTS怎么串起来。适合手上有ESP32-S3、想做一个桌面语音终端、又不想被模型推理资源绑死的朋友。跟着这篇文章走哪怕你不懂后端服务也能把整条链路跑通。1. 方案定位为什么圆屏设备不该硬扛模型1.1 圆屏设备的硬件天花板先泼盆冷水。拿ESP32-S3来说双核240MHz、512KB SRAM、8MB PSRAM、16MB Flash这个配置在微控制器里算很能打了但跟大模型的需求一比差距是数量级的。我们看一组直观数据。一个最小的对话模型哪怕只跑0.5B参数权重用4bit量化也要差不多250MB内存真要在端侧推理算力需求、内存带宽、运行时开销全加起来ESP32-S3完全扛不住。更别提你还要在同一个芯片上跑WiFi协议栈、屏幕刷新、音频编解码内存早就被吃光了。我说句实际的在ESP32上强行跑个AI语音识别小模型比如MicroWakeWord做唤醒词这是没问题的因为模型只有几十KB到几百KB。但想跑完整的大语言模型或者跑Whisper这种带Transformer结构的语音识别模型哪怕是最小的tiny版本SRAM也不够塞权重的。烧进Flash再分页加载推理速度慢到你根本不想用。所以这个问题的答案很简单圆屏设备的硬件天花板决定了它不能跑模型也不需要跑模型。它最合适的角色就是把用户的声音干净地采上来把后台生成的语音和文字原样呈现出来做好一个“语音客户端”。1.2 模型端跑在后台设备端做客户端想通这件事之后方案就清晰了把整个系统拆成三层。第一层是设备端也就是圆屏糖球。它负责的事情非常聚焦按键触发或唤醒词触发、麦克风采样、把音频数据通过网络送到后台、接收后台返回的文字和音频、用圆屏显示状态和文字、用扬声器播放语音回复。第二层是后台语音网关。这是一个跑在电脑、树莓派、NAS或者云服务器上的服务程序负责接收设备的音频流调用语音识别服务把识别出的文本交给大模型再把大模型生成的文本交给语音合成服务最后把合成的音频发回设备端。第三层是模型服务层包括STT模型、大语言模型和TTS模型。它可以全部跑在本地比如用faster-whisper做语音识别、Ollama加载Qwen2.5或者LLaMA做对话、Piper TTS做语音合成。也可以混合使用比如本地跑LLM在线用云端TTS。这样的分工带来的好处非常明显设备端固件几乎不用更新就能适配新的模型能力。今天后台换了更强的模型明天设备就能跟着升级对话质量。你不需要去改C代码不需要重新烧录只需要在后台把模型配置换一下。维护逻辑也清晰了——出问题先定位是哪一层采音和播放归设备端识别和生成归后台。不用在嵌入式代码里去调模型参数整个人都轻松了。2. 系统架构与通信链路设计2.1 整体架构设备端-后台端-模型服务我先说这套架构在糖球项目里的具体形态。设备端我用的是一片ESP32-S3开发板外挂1.28寸GC9A01圆屏屏幕分辨率240x240配一颗I2S接口的INMP441 MEMS麦克风再加一块MAX98357A功放模块驱动一个3W小扬声器。整个系统由USB供电藏在圆形的3D打印外壳里看起来就像一个桌面装饰球。后台端我用了Python写一个WebSocket网关服务依赖websockets和requests两个库就够了。这个网关服务启动时加载好语音识别、对话模型和语音合成三个引擎的客户端封装然后等待设备连接。模型服务可以是独立进程也可以是外部HTTP接口。具体到我的项目里语音识别用faster-whisper加载本地模型大语言模型用Ollama的HTTP API语音合成用Piper TTS的本地命令行接口。这三个模型全部跑在同一台安装了Python网关的电脑上局域网内的糖球设备只要知道这台电脑的IP就能完成整个语音交互闭环。这个设计最大的好处是设备的代码量被压到了最小。ESP32那边不需要写任何HTTP请求逻辑不需要处理JSON里的提示词拼接只需要维护一个WebSocket连接把录音数据按块发出去然后播放后台返回的音频块。2.2 通信协议选型WebSocket还是HTTP轮询设备端和后台之间的通信方式我对比过三种HTTP轮询、MQTT和WebSocket。HTTP轮询最直接设备每隔几百毫秒发一次请求有语音数据就POST上去然后等响应。但是语音对话是一个双向持续的过程——设备要持续上传录音后台要主动推送语音回复和文字状态。HTTP轮询做起来很别扭设备要不停问“有新的TTS音频吗”服务器要说“还没有再等等”延迟高不说请求频率高了还很占资源。MQTT是物联网设备常见的通信协议基于发布订阅模式消息路由能力很强。但MQTT处理音频流同样不顺——默认不适合传大量二进制数据需要自己把音频分成很多小块再按顺序发还要处理消息顺序、丢包等问题。做控制指令推送很好用做音视频流不合适。WebSocket是我最终的选择它是一条持久的双向通道。设备端连上来之后两边随时可以发数据不用反复握手。设备端可以连续往服务端推音频二进制帧服务端也能随时往设备端推TTS音频帧和状态文本。延迟低协议简单嵌入式端和Python端都有成熟库支持。具体消息协议我设计成JSON文本帧加二进制音频帧混合。控制信息和状态信息用JSON帧音频数据用二进制帧。设备端上行音频时用二进制帧流程结束时发一个{type:vad_end}的JSON帧告诉后台“我说完了可以开始识别了”。后台下行时先发识别文本和LLM文本的JSON帧再发TTS音频的二进制帧最后发一个{type:tts_end}表示一段播放结束。2.3 语音链路设计采集、编码、传输、播放整条语音链路我按采样率16kHz、16bit、单声道来设计。这个规格对语音识别和TTS都友好也是Whisper模型默认支持的采样率。计算一下数据量16kHz乘以2字节等于每秒32KB也就是256kbps。这个码率在局域网里根本不值一提WiFi轻松扛住。如果要做公网传输建议再加一层Opus压缩能把码率压到24kbps左右不过本地项目里完全没必要白白增加复杂度。录音数据的组织方式上我用的是每200毫秒一个块。为什么是200毫秒因为再小的话每个块只有几KB网络包数量太多CPU开销大再大的话后台识别启动延迟会变高。200毫秒折中下来既能保证后台在用户说完话后立刻处理又不会产生太多小数据包。播放路径有一个坑必须提醒TTS生成的音频要能连续播放不能有间隔。ESP32上用I2S输出音频时如果每一块TTS音频到达后重新初始化I2S一定会产生爆音。正确做法是初始化一次I2S然后把收到的音频数据写入一个播放缓冲区后台持续推送播放端持续往外发。回声问题建议放在后台处理。设备端的扬声器声音会被麦克风重新采进来如果不消除回声后台语音识别会把自己说的话也识别进去形成“自己和自己对话”的循环。比较省事的方案是在后台用音频处理库做回声消除把参考音频和采集音频对齐后做AEC效果比在ESP32上做要稳定得多。3. 设备端核心实现与实操3.1 圆屏UI设计思路圆屏是这台设备的脸面UI做得好不好直接影响“这设备看起来是不是个成熟产品”。我用的是GC9A01圆屏分辨率240x240。驱动方式上我建议直接用LovyanGFX库。它内置了GC9A01的适配绘制圆形、圆角矩形、旋转文字都很方便而且性能比Arduino_GFX好不少刷新大面积色块时延迟更低。用了ESP32-S3的SPI接口主频拉到80MHz实测刷新一个全屏圆环进度动画很流畅。整个UI我分成三层。最底层是状态背景用渐变色区分待机、录音、思考、播放四种状态。比如待机时是深蓝色按住按键说话时变成红色后台识别和生成时显示一个不断旋转的圆弧播放语音时变成绿色脉冲环。第二层是中心内容区显示时钟、星期和心情短语。时钟用RTC芯片或者NTP校时在待机状态下每秒钟刷新一次数字。第三层才是对话内容显示当后台返回STT识别文本时它从底部滚动显示出来LLM的回复文本也会同步显示用户可以同时看文字和听语音。触摸方面GC9A01圆屏一般不带触摸层所以我用的是实体按键加一个旋转编码器。旋转编码器用来调节音量按一下是静音长按是切换对话模式。实体按键做PTT一键对讲触发录音按住说、松开发送这种交互方式在语音对话中比自动VAD稳定得多。3.2 音频采集与播放实现先说硬件接线。INMP441麦克风通过I2S接口连接ESP32-S3使用标准I2S引脚定义SCK接42WS接41SD接40。注意INMP441的L/R引脚要接地表示选择左声道否则收不到声音。MAX98357A功放也是I2S接口但它用的数据和位时钟信号要与麦克风分时复用或者干脆分配两套引脚因为ESP32-S3有多个I2S外设。实际使用中我发现一个常见问题INMP441和MAX98357A是两个I2S设备如果把它们的引脚接到同一组I2S总线上同时读写会互相干扰。工程上最舒服的解法是给麦克风分配GPIO42/41/40给功放分配GPIO5/4/6然后在代码里分别初始化两个i2s_inst_t实例。ESP32-S3的I2S0和I2S1两个外设可以同时工作完全隔离开。录音部分用官方I2S驱动接口配置采样率16000Hz采样位深16bit通道数为单声道使用DMA模式并设置双缓冲。代码如下#include driver/i2s.h #include driver/i2s_common.h #define I2S_MIC_BCK 42 #define I2S_MIC_WS 41 #define I2S_MIC_DIN 40 void mic_init() { i2s_config_t i2s_config {}; i2s_config.mode (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX); i2s_config.sample_rate 16000; i2s_config.bits_per_sample I2S_BITS_PER_SAMPLE_16BIT; i2s_config.channel_format I2S_CHANNEL_FMT_ONLY_LEFT; i2s_config.communication_format I2S_COMM_FORMAT_STAND_I2S; i2s_config.dma_buf_count 8; i2s_config.dma_buf_len 1024; i2s_config.use_apll true; i2s_pin_config_t pin_config {}; pin_config.bck_io_num I2S_MIC_BCK; pin_config.ws_io_num I2S_MIC_WS; pin_config.data_out_num I2S_PIN_NO_CHANGE; pin_config.data_in_num I2S_MIC_DIN; i2s_driver_install(I2S_NUM_0, i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, pin_config); }读麦克风数据时要注意INMP441的原始数据里会有直流偏置直接送给后台识别会噪声偏大。我在设备端做了简单的高通滤波加静音裁剪每200ms读取出来的音频先检查振幅如果连续几个块的能量都很低就直接丢弃不占用网络带宽。播放端类似把MAX98357A初始化成I2S_TX模式设置8kHz或16kHz回调然后写一个环形缓冲区后台每收到一帧TTS音频就写入这个缓冲区I2S中断自动往外DMA搬运。这样播放是连续不间断的。3.3 网络通信状态机的设计设备端的网络状态我用一个简单的状态机来管理每个状态对应一种行为和UI显示。常态是IDLE显示时钟不采集也不发送。按住PTT按键后进入RECORDING开始录音并把音频块通过WebSocket发到后台。松开按键进入WAIT_RESP停止录音发送vad_end帧后台开始识别和生成。一直到收到后台的tts_end帧才回到IDLE屏幕恢复时钟显示同时可能短暂显示一下刚才的对话内容。WiFi断线重连是必须写好的。我用的策略是检测到WiFi断开后先快速重连三次三次都失败就进入低功耗待机屏幕显示“网络断开”每10秒尝试一次重连。WebSocket连接空闲时每隔30秒发一个ping帧后台收到后回pong连续三次没有pong就主动断开重连。WebSocket库推荐用ArduinoWebsockets它对ESP32的支持很成熟缓冲区可以手动调大。默认的几KB缓冲区对音频帧不够我设置成32KB保证一个200ms的音频块能整帧发出去。#include WiFi.h #include ArduinoWebsockets.h using namespace websockets; WebsocketsClient ws; void connect_ws() { ws.connect(ws://192.168.1.100:8765); ws.onMessage([](WebsocketsMessage msg) { if (msg.isText()) { handle_text_frame(msg.data()); } else { handle_binary_frame(msg.data()); } }); }3.4 关键交互逻辑与代码路径整个对话流程的代码逻辑可以提炼成这么几步第一步初始化外设屏幕显示logoWiFi连接WebSocket连接然后进入待机循环。第二步检测按键事件如果按下PTT屏幕转为录音状态开始麦克风采集循环。第三步采集循环里每次读200ms音频检查振幅高就通过WebSocket发送二进制帧振幅太低就发静音标记。第四步抬起按键时发送vad_end帧。后台返回的帧在onMessage回调里处理。收到stt_text帧就把文字存起来显示在屏幕顶部收到llm_text帧就滚动显示对话内容收到tts_audio二进制帧就把音频写入播放缓冲区收到tts_end帧就播放最后一个缓冲的数据然后把状态机切回IDLE。这里有一个小细节为了省电和减少噪音扬声器和播放缓冲区在IDLE状态下应该完全关闭只有收到第一帧tts_audio时才调用I2S播放初始化然后等tts_end帧后延迟200毫秒再关闭。这个延迟不能省否则最后一句会被截断。4. 后台语音链路与模型对接4.1 STT语音识别引擎选型后台的语音识别我用的是faster-whisper它比原版whisper快得多而且能直接加载量化模型。模型我选的是smallint8量化版本在CPU上识别16kHz录音一句五六秒的话大概需要两秒左右延迟完全可接受。如果你电脑配置好N卡有至少4GB显存可以换faster-whisper的base或small模型跑在CUDA上速度能到几百毫秒级。另外也可以尝试FunASR的Paraformer模型它在中文识别上的准确率超过同尺寸Whisper而且流式推理延迟更低。STT服务我封装成一个类核心方法接收PCM音频字节流返回识别文本。有一点必须注意faster-whisper在加载时会占用大量内存如果后台同时还要跑大语言模型建议用vad_filterTrue过滤静音段避免把大段空白也送去识别既降延迟又省内存。from faster_whisper import WhisperModel class STTEngine: def __init__(self): self.model WhisperModel(small, devicecpu, compute_typeint8) def transcribe(self, pcm_bytes: bytes) - str: import io import wave buf io.BytesIO() with wave.open(buf, wb) as wf: wf.setnchannels(1) wf.setsampwidth(2) wf.setframerate(16000) wf.writeframes(pcm_bytes) segments, _ self.model.transcribe( buf.getvalue(), languagezh, vad_filterTrue, beam_size5 ) return .join(seg.text for seg in segments).strip()4.2 大语言模型接入方式大语言模型部分如果你手头有能跑模型的电脑我最推荐的还是Ollama。它把模型管理、量化、上下文窗口、并发请求全封装好了后台程序只需要用HTTP调用非常省心。以我目前的配置为例Ollama里加载qwen2.5:7b量化等级默认就是Q4_K_M内存占用大概4.5GB。在i5-12400 16GB内存的机器上生成速度大约是每秒15到20个token对于一个语音助手来说完全够用因为TTS生成和播放本来也有时间。Ollama的接口非常简洁后台程序只需要POST/api/chat接口即可import requests import json def llm_chat(messages): r requests.post(http://localhost:11434/api/chat, json{ model: qwen2.5:7b, messages: messages, stream: True }, streamTrue) for line in r.iter_lines(): if not line: continue data json.loads(line.decode(utf-8)) if not data.get(done): yield data[message][content]这个接口支持流式输出后台可以把LLM生成的文本边生成边发给设备端屏幕上能看到文字一个一个字蹦出来体验比等全部生成完再回传好太多了。关于系统提示词我建议写清楚“你是一个桌面语音助手回答要简洁控制在50个字以内”。加了这句话之后模型生成的回复明显更适合TTS阅读也不会在对话里输出Markdown符号。如果你需要联网搜索能力可以把OpenAI兼容的搜索工具接入Ollama但这就超出本文范围了。4.3 TTS语音合成方案TTS这一环我对比过Piper、edge-tts和国内几家云厂商。edge-tts合成的自然度最高免费但要走微软的接口依赖网络不适合纯离线场景。云厂商合成质量更稳定但要申请密钥还要联网。我的场景选择了Piper TTS它是完全离线的用VITS模型合成英语和中文效果都还行。中文需要下载中文语音包比如zh_CN-huayan-medium整个模型只有60MB左右CPU上合成一段五秒语音大约需要1秒速度够用。Piper的使用方式最方便是命令行调用把LLM生成的文本通过stdin喂进去它输出16kHz的WAV裸PCM数据。后台程序再把这个PCM数据分包通过WebSocket发给设备端。为了减少首字延迟我建议对文本做分句处理遇到句号或者问号就切成一句立刻合成这一句并发出去不需要等整段生成完。subprocess.run( [piper, --model, zh_CN-huayan-medium.onnx, --output_raw], inputtext.encode(utf-8), capture_outputTrue, checkTrue )这里注意Piper默认输出22050Hz的PCM而设备端播放器是16kHz的不统一会有变调问题。要么在合成时加--length-scale参数并自行降采样要么就用sox做一次重采样到16kHz。我这边直接在后台合成后用AudioSegment.from_raw(...).set_frame_rate(16000)重采样一次然后转成base64发给设备端。4.4 后台网关服务的完整串联把上面几个引擎串起来的核心是一个基于websockets库的异步网关服务。它接收设备连接维护一个设备会话然后按顺序调用STT、LLM、TTS三个引擎。import asyncio import websockets import json from stt import STTEngine from llm import llm_chat from tts import tts_synthesize from audio_utils import pcm_to_base64, downsample_to_16k async def handle_client(ws): audio_buf bytearray() async for msg in ws: if type(msg) is bytes: audio_buf.extend(msg) else: data json.loads(msg) if data[type] vad_end: text stt.transcribe(bytes(audio_buf)) await ws.send(json.dumps({type: stt_text, text: text}, ensure_asciiFalse)) messages [{role: user, content: text}] reply_text for chunk in llm_chat(messages): reply_text chunk await ws.send(json.dumps({type: llm_text, text: chunk}, ensure_asciiFalse)) for sentence in split_sentences(reply_text): pcm downsample_to_16k(tts_synthesize(sentence)) await ws.send(pcm_to_base64(pcm)) await ws.send(json.dumps({type: tts_end})) start_server websockets.serve(handle_client, 0.0.0.0, 8765) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()这里面有几个可以打磨的细节。第一llm_chat是流式的所以llm_text帧是一小段一小段推给设备的设备端屏幕会有打字机效果。第二split_sentences按标点切分后要保留标点本身避免合成时语气生硬。第三TTS合成是一个耗时的CPU操作如果在异步循环里直接调用会阻塞整个服务建议把TTS放到asyncio.to_thread或者线程池里执行我这里为了演示简单没写实际项目要处理。另外STT的识别结果往往带上标点可以直接显示。LLM的回复加了系统提示词之后就不会再出现“好的我来回答”这种冗长前缀TTS读起来也更自然。5. 常见问题与排查实录5.1 编译烧录时的环境问题设备端开发环境我用的是VS Code加ESP-IDF插件版本6.x。如果分享几个容易踩的点。第一如果你参考网上的教程环境变量里可能残留了旧版本IDF路径编译时会出现找不到ninja或者ninja.exe直接退出、退出代码非0的情况。一般是因为VS Code没有正确识别IDF的安装路径可以在设置里检查idf.espIdfPath是否正确。如果实在不行直接重启VS Code有时候就是环境变量缓存的问题。第二用ESP-IDF 5.3.2时部分老项目的I2S驱动API已经变了。老的i2s_config_t结构体和i2s_driver_install函数仍然能用但ESP-IDF官方已经推荐新的i2s_std驱动API。项目如果从老版本升级上来一定要检查API兼容性否则各种编译报错。第三内存不足的问题是常态。ESP32-S3虽然有8MB PSRAM但PSRAM带宽有限不适合放需要频繁读写的音频缓冲区。建议音频缓冲区和DMA buffer放在内部内存只把显示帧缓冲区和一些大数组放到PSRAM。如果编译后链接失败提示“region RAM overflowed”就去检查是不是把大数组声明成了全局变量而没有用EXT_RAM_ATTR。5.2 音频质量问题的实测记录音频是语音交互体验的核心录音质量差后面识别全都白搭。我实测中遇到最典型的问题有三个。第一个是沙沙声和底噪。排查下来80%的情况是电源问题——INMP441直接用开发板的3.3V供电而WiFi天线在发射时会把电源拉偏导致ADC采样不准。解决方法是给麦克风单独加一颗LDO和钽电容或者把麦克风供电和功放供电分开。实测用了独立LDO后底噪大概降低了10dB。第二个是音量忽大忽小。INMP441是MEMS麦克风增益是固定的但说话距离不定所以音频动态范围很大。建议在后台STT之前加载一个自动增益控制模块把音频先归一化到-20dBFS左右再送识别。我用的方案是audioop.ratecv和audioop.mul组合简单粗暴但有效。第三个是回声问题。刚开始设备端扬声器播放TTS时麦克风会把声音采回来后台就会把TTS内容再识别一遍导致设备不断重复。解决方法是后台做了回声消除设备端把扬声器正在播放的PCM数据和麦克风采集的PCM数据一起上传后台用AEC库做参考信号对消。这样系统就再也不会自说自话了。5.3 WebSocket连接与音频延迟排查设备端和后台通信偶尔会有断连和延迟问题我记录一下排查思路。连接不稳定时先看WebSocket心跳机制是否正常工作。如果设备长时间不发任何音频数据后台会因为TCP超时把连接断开。所以即使待机时也要每隔30秒发一个ping帧。我在ESP32端用定时器发ping后台收到后回复pong这样连接能保持几天不断。延迟大的原因通常不是网络而是处理链路。最典型的坑是STT识别引擎的模型加载没有做预热第一次识别要等好几秒。解决办法是后台启动时先跑一次空转识别把模型加载进内存。之后每次识别的延迟就稳定了。还有一个容易被忽略的点TTS合成是CPU密集型操作如果后台机器同时在跑Whisper识别和LLM推理CPU会被打满TTS延迟会突然飙到好几秒。解决方法是把STT、LLM、TTS三个引擎分配到不同的进程必要时限制它们的CPU核数。实测我这边i5-12400跑Qwen2.5-7B和faster-whisper比较吃力后来把LLM换成Qwen2.5-3B体验反而更好因为延迟降低了。5.4 整轮交互延迟实测我把一套完整对话的延迟测量下来给大家一个参考基准。测试环境ESP32-S3设备通过2.4GHz WiFi连接路由器后台电脑是有线连接路由器的i5-12400主机模型加载本地的qwen2.5-3b和faster-whisper small。阶段耗时音频上传到后台结束约0.2至0.4秒STT语音识别5秒音频约1.0至2.0秒LLM首token生成约0.3至0.8秒LLM完整回复约30字约1.5至3.0秒TTS首句合成约0.5至1.0秒TTS首句传输并开始播放约0.2至0.5秒整段回复播放完成约5.0至8.0秒从松开按键到听到第一句回复大概在2到4秒之间这个延迟对桌面语音助手来说是可以接受的。如果想要更快可以优化STT模型为base或并行两项工作但不要再贪心把模型塞进设备端那是倒退。6. 后续还能怎么扩展6.1 离线唤醒词这套架构里设备端只是一个哑客户端所以扩展离线唤醒其实很简单。在设备端运行一个轻量级唤醒词模型比如ESP-SR或者MicroWakeWord当检测到唤醒词“糖球糖球”时才建立WebSocket连接并进入工作状态。这个方案的本质是让设备端始终处于低功耗待机唤醒后临时连接后台。好处是省电也避免一直占用后台连接资源。实测ESP32-S3做唤醒词监听时功耗可以压到30mA以内监听灵敏度也够用。6.2 多台糖球设备联动如果家里有多台糖球设备后台网关需要支持多设备会话隔离。最简单的方式是每台设备连上时分配一个全局ID后台用字典保存会话语音识别和LLM的提示词里带上设备ID这样在客厅喊一句“糖球把卧室的灯打开”后台就知道是客厅设备发起的请求。进一步可以做设备和扬声器的多房间联动。比如在卧室喊糖球回复音频既可以在卧室的糖球上播放也可以选择同时推送到客厅的设备这就需要后台维护一个设备组关系。这种玩法是在纯设备端方案里做不到的只有客户端加后台架构才能这么灵活。6.3 后台模型随时可换最后说一个我觉得这个架构最值的地方换模型成本极低。今天后台连接的是本地Qwen明天想试试别的模型只需要改后台代码里的模型名设备端什么都不用动。想做带视觉的糖球加个摄像头把图片Base64通过WebSocket传到后台让后台调用多模态模型就行设备端依然只是音频和显示客户端。这套思路慢慢演化下去糖球可以成为一个家庭语音终端本身不携带智能但通过后台接入各种能力。保持设备端薄、后台厚的设计原则项目迭代起来会非常舒服。