ESP32-S3微控制器上实现MicroPython大模型对话机器人:从云端API到边缘AI的实践
1. 项目缘起为什么要在微控制器上跑大模型对话最近几年大语言模型LLM的风潮席卷了整个科技圈从云端API到本地部署玩法层出不穷。但不知道你有没有想过如果把一个能对话的AI大脑塞进一块只有指甲盖大小、成本几十块钱的微控制器里会是什么体验这听起来有点像“让自行车跑出F1的速度”充满了技术上的矛盾与挑战。主流的大模型动辄需要几个GB甚至上百GB的显存而一块典型的ESP32开发板其可用RAM通常只有几百KB。这中间的差距是几个数量级的鸿沟。然而正是这种“不可能”的尝试才最能激发极客的探索欲。我这次折腾的“K10大模型对话机器人”核心目标就是打破这种思维定式。它不是一个追求完美对话体验的商用产品而是一个技术验证和创意实现的载体。我想看看在资源极度受限的嵌入式环境中我们究竟能实现多大程度的“智能”交互。这里的“K10”并非指某个特定的开源模型而更像是一个项目代号代表着在“K”B级别内存10KB量级约束下实现一个可对话的“10”分有趣的AI应用。选择MicroPython作为实现语言是另一个关键决策。相比传统的C/C嵌入式开发MicroPython的高级特性和交互式解释器极大地降低了开发门槛。你可以像在PC上写Python脚本一样快速进行原型验证、功能迭代和问题调试。这对于需要频繁调整提示词Prompt、测试模型响应逻辑的AI应用来说效率提升是巨大的。虽然牺牲了一部分极限性能但换来了无与伦比的开发灵活性和乐趣。所以这个项目的价值不在于复现ChatGPT而在于探索一条路径如何利用现有的、轻量化的技术组件如超小型语言模型、高效的文本处理库在MicroPython的舞台上搭建一个能听、能说、能简单思考的嵌入式AI节点。它可以是一个会讲故事的智能玩具一个能回答问题的桌面摆件或者一个通过简单自然语言控制智能家居的终端。接下来我就把自己从硬件选型、模型瘦身、到代码实现和效果优化的完整过程以及踩过的那些坑毫无保留地分享给你。2. 硬件与软件栈选型在螺蛳壳里做道场要在微控制器上跑AI第一步就是精打细算地选择硬件和软件每一分资源都要花在刀刃上。2.1 核心硬件ESP32-S3的性价比之选经过一番对比我选择了乐鑫的ESP32-S3芯片作为核心。理由很充分性能与内存的平衡ESP32-S3通常配备512KB的片上SRAM部分型号甚至可以通过PSRAM扩展到8MB。对于我们的项目512KB是底线如果能找到带4MB或8MB PSRAM的型号如ESP32-S3-DevKitC-1-N8R8那操作空间就大得多。它双核240MHz的主频处理文本和简单逻辑绰绰有余。丰富的接口它自带Wi-Fi和蓝牙这意味着我们的机器人可以轻松联网获取信息比如查询天气、调用在线API进行更深度的推理或者通过蓝牙与手机App交互扩展了应用场景。完善的MicroPython支持乐鑫官方对MicroPython的支持非常积极固件稳定外设驱动库丰富社区资源也多遇到问题容易找到解决方案。除了主控你还需要一些基础模块语音输入为了方便我直接用了集成好的MAX9814麦克风放大模块。它自带增益调节和自动增益控制AGC能提供比较干净的音频信号直接连接到ESP32的ADC引脚即可。语音输出为了播放机器人的回答我选用了一款简单的PAM8403 Class D音频放大模块连接一个8欧姆的小喇叭。ESP32通过I2S接口输出音频数字信号给PAM8403音质足够清晰。交互界面为了显示状态和对话内容我加了一块1.3英寸的OLED屏幕SSD1306驱动通过I2C与ESP32通信。它能显示文字和简单图形成本低功耗也小。其他按键用于唤醒/复位、LED指示灯显示工作状态、以及必要的电阻电容和杜邦线。注意电源管理很重要。ESP32-S3在峰值运行时耗电不小尤其是驱动喇叭时。建议使用能提供至少5V/2A稳定输出的电源模块或充电宝供电避免因电压跌落导致系统重启。2.2 软件基石MicroPython固件与核心库硬件搭好接下来是软件环境。刷写MicroPython固件 首先去MicroPython官网下载针对ESP32-S3的最新稳定版固件.bin文件。然后使用乐鑫官方的esptool.py工具进行刷写。连接开发板到电脑找到串口号执行如下命令请替换COMx和firmware.bin为你的实际端口和文件名esptool.py --chip esp32s3 --port COMx --baud 921600 write_flash -z 0x0 firmware.bin刷写成功后通过串口工具如PuTTY、Thonny连接你应该能看到MicroPython的交互式解释器REPL提示符。安装必备的库 MicroPython的标准库功能有限我们需要用upipMicroPython的包管理器或手动上传一些核心库文件.mpy或.py。urequests/requests用于HTTP网络请求从云端API获取AI响应。ujson高效地解析JSON数据这是与大多数AI API交互的数据格式。micropython-esp32-i2s用于驱动I2S音频输出这是播放合成语音的关键。ssd1306.py驱动OLED屏幕的库。machine和time这些是内置库用于控制GPIO、定时器和延时。如果你的ESP32-S3连接了PSRAM务必确保刷写的固件包含了PSRAM支持并在代码中正确初始化这样才能利用那额外的几MB内存来缓存模型参数或音频数据。2.3 “大模型”的轻量化实现策略在资源受限的环境下直接运行像LLaMA、ChatGLM这样的模型是天方夜谭。我们的策略是“云端协同”和“极致轻量”。方案A云端API调用推荐给初学者这是实现起来最快、效果最好的方式。ESP32只负责采集语音、转换成文本然后通过Wi-Fi将文本Prompt发送到云端的大模型API如OpenAI的GPT-3.5/4国内的可选百度文心、讯飞星火、智谱AI等拿到返回的文本后再在本地或通过云端TTS合成语音。优点对话质量高功能强大开发简单。缺点依赖网络有API调用成本响应速度受网络影响。关键技术点如何设计一个高效的HTTP客户端处理网络重连、超时和JSON解析。你需要精心设计Prompt让模型返回简短、准确的回答避免冗长的散文。方案B本地微型模型推理硬核挑战这是真正的“边缘AI”。我们需要寻找能在微控制器上运行的超轻量级模型。这里有几个方向TinyStories或微软的Phi-2小型化变体这些是专门为资源受限环境设计的文本生成模型参数量可能在千万10M级别。通过工具如ONNX Runtime Micro 或 TensorFlow Lite for Microcontrollers可以尝试将其转换为可在ESP32上运行的格式。关键词匹配与模板回答这算不上真正的模型但是一种实用的“伪智能”。预先定义一系列关键词和对应的回答模板。当用户输入文本后系统查找匹配的关键词并填充到模板中生成回答。可以结合简单的句法分析如micropython-nltk的极简版来提升一点智能感。检索增强RAG的极简版在Flash中存储一个小型的知识库QA对。当用户提问时使用TF-IDF或更简单的词频匹配算法在知识库中查找最相关的问题返回对应的答案。对于MicroPython而言方案B中的第1点本地微型模型实施难度极高主要受限于计算力和内存。第2和第3点是更可行的路径它们能实现特定领域内的、可控的对话。考虑到项目的演示性和完整性下文我将以方案A云端API为主线进行阐述因为它能最快地让你看到一个效果惊艳的对话机器人。同时我也会在关键部分探讨如果转向方案B需要做哪些改造和优化。3. 核心模块实现从声音到智慧的链条一个完整的对话机器人工作流程是环环相扣的声音录入 - 语音识别 - 智能处理 - 语音合成 - 声音播放。我们分步拆解。3.1 语音录入与识别让设备“听见”ESP32通过ADC读取麦克风模块的模拟信号。但直接处理原始音频进行识别是不现实的我们需要借助云端语音识别ASR服务。import machine import time import urequests import ujson # 配置ADC引脚例如GPIO1 adc machine.ADC(machine.Pin(1)) adc.atten(machine.ADC.ATTN_11DB) # 设置衰减以适应3.3V量程 adc.width(machine.ADC.WIDTH_12BIT) # 12位精度 def record_audio(duration_ms3000, sample_rate8000): 录制一段音频。 由于内存限制我们无法长时间录制。这里录制为PCM原始数据。 实际应用中需要边录边通过HTTP流式上传或编码为WAV等格式。 samples [] sample_count int(duration_ms * sample_rate / 1000) for _ in range(sample_count): samples.append(adc.read()) time.sleep_us(int(1000000 / sample_rate)) return bytes(samples) # 注意这里需要将整数列表转换为字节流实际更复杂 def speech_to_text(audio_data): 调用云端语音识别API。 这里以百度语音识别API为例你需要替换为自己的API Key和Secret。 # 1. 获取Access Token (需要实现) token get_baidu_token() # 2. 准备请求头和数据 url https://vop.baidu.com/server_api headers {Content-Type: application/json} # 百度API需要base64编码的音频数据这里audio_data需要是完整的wav格式数据 # 为简化我们假设audio_data已经是符合要求的base64字符串 payload { format: wav, rate: 16000, channel: 1, cuid: esp32_test, token: token, speech: audio_data, # 这里应是base64字符串 len: len(audio_data) } try: resp urequests.post(url, headersheaders, dataujson.dumps(payload)) result resp.json() resp.close() if result[err_no] 0: return result[result][0] else: print(识别错误:, result[err_msg]) return None except Exception as e: print(请求失败:, e) return None # 实际使用中录音和识别需要更复杂的处理例如使用I2S接口获取数字音频并编码为API要求的格式。实操心得内存限制在ESP32上一次性录制长音频如10秒的PCM数据会占用大量内存10s * 16kHz * 2字节 ≈ 320KB。这很可能导致内存不足MemoryError。因此流式上传是必须的。你需要寻找支持分块或流式传输的语音识别API或者在本地先进行压缩编码如ADPCM。网络稳定性Wi-Fi连接可能不稳定。代码中必须有健全的重试机制和超时处理。识别失败时可以给用户一个视觉或声音提示比如让OLED屏幕显示一个问号或者播放一段“抱歉我没听清”的提示音。唤醒词为了省电和隐私通常需要有一个本地唤醒词检测如“小K小K”。这可以在ESP32上通过一个非常轻量级的模型如TensorFlow Lite Micro训练的Keyword Spotting模型来实现但会增加复杂度。初期可以先用一个物理按键来触发录音。3.2 智能对话核心与“大脑”交互这是项目的灵魂所在。我们通过HTTP POST请求将识别到的文本发送给大模型API。class AIClient: def __init__(self, api_key, base_urlhttps://api.openai.com/v1): self.api_key api_key self.base_url base_url self.headers { Authorization: fBearer {api_key}, Content-Type: application/json } # 简单的对话历史管理由于内存限制只保留最近几轮 self.conversation_history [] self.max_history_turns 3 def _build_messages(self, user_input): 构建符合OpenAI API格式的消息列表。 # 添加历史记录 messages [] for role, content in self.conversation_history[-self.max_history_turns*2:]: # 保留最近N轮 messages.append({role: role, content: content}) # 添加当前用户输入 messages.append({role: user, content: user_input}) return messages def get_response(self, user_input, modelgpt-3.5-turbo): 调用Chat Completion API获取AI回复。 url f{self.base_url}/chat/completions messages self._build_messages(user_input) data { model: model, messages: messages, max_tokens: 150, # 限制回复长度节省token和内存 temperature: 0.7, } try: resp urequests.post(url, headersself.headers, dataujson.dumps(data), timeout10) if resp.status_code 200: result resp.json() ai_reply result[choices][0][message][content].strip() # 更新历史记录 self.conversation_history.append((user, user_input)) self.conversation_history.append((assistant, ai_reply)) # 清理过旧的历史防止内存泄漏 if len(self.conversation_history) self.max_history_turns * 2 2: self.conversation_history self.conversation_history[-(self.max_history_turns * 2 2):] resp.close() return ai_reply else: print(fAPI请求失败: {resp.status_code}) resp.close() return 网络好像有点问题请稍后再试。 except Exception as e: print(f请求异常: {e}) return 哎呀我好像断线了。 def clear_history(self): 清空对话历史。 self.conversation_history [] # 初始化 ai_client AIClient(api_keyyour_openai_api_key_here)关键技巧与避坑指南Prompt工程是灵魂在资源有限的情况下一个精心设计的系统提示词System Prompt能极大提升回复质量并控制风格。例如你可以这样设置你是一个运行在嵌入式设备上的智能助手名叫K10。请用简短、清晰、口语化的句子回答用户的问题每次回答尽量不超过50个字。如果问题涉及需要联网查询的实时信息请直接告知“我需要联网查询但目前无法做到”。 这能有效避免API返回长篇大论节省传输数据量和本地处理时间。管理对话历史如上代码所示维护一个简短的对话历史能让AI拥有上下文记忆体验更连贯。但务必严格限制历史轮数如2-3轮并用list的切片操作及时清理这是防止内存溢出的重要手段。错误处理与降级网络请求可能失败API可能超时。必须要有降级策略。例如当检测到网络不可用或API调用失败时可以切换到一个本地的、基于关键词匹配的简单对话模式或者播放一段预录制的道歉语音。成本控制使用云端API会产生费用。务必在代码中设置max_tokens参数限制每次请求和回复的长度。对于ESP32项目回复长度在100个token以内通常足够。同时可以考虑使用更经济的模型如gpt-3.5-turbo甚至是一些提供免费额度的国内API。3.3 文本转语音与播放让机器人“开口说话”拿到AI返回的文本后我们需要将其转换为语音。同样有两种主流方案方案一云端TTS API。优点是音质好自然度高。我们可以继续使用像百度、阿里云、微软Azure或Google的TTS服务。调用方式与语音识别类似将文本POST过去接收返回的音频文件通常是MP3或PCM然后在ESP32上解码播放。挑战在于音频文件可能较大需要流式下载和播放对内存和网络稳定性要求高。MP3解码在ESP32上比较吃力最好选择PCM或WAV格式或者使用专门的解码芯片。方案二本地TTS合成。这是更“嵌入式”的方案。我们可以集成一个轻量级的TTS引擎到MicroPython中。eSpeak NG这是一个开源的语音合成软件有C语言库。理论上可以移植到ESP32但工作量巨大且声音比较机械。拼接合成预先录制好所有音素或常用词组的音频片段存储在SPI Flash中。根据文本动态检索并拼接这些片段进行播放。这种方法声音不连贯但内存占用极低适合词汇量有限的特定场景如报时、报温度。由于本地TTS实现复杂我建议项目初期采用云端TTS方案。下面是一个简化的播放流程示例假设我们已经从云端下载了一个PCM格式的音频数据流import i2s from machine import Pin def play_audio_from_stream(audio_stream, sample_rate16000, bps16, channel1): 通过I2S接口播放音频流。 audio_stream: 一个可迭代对象每次yield一段音频数据(bytes)。 # 配置I2S输出引脚 (BCLK, LRC, DIN) i2s_out i2s.I2SOut(bit_clockPin(16), word_selectPin(17), dataPin(18), modei2s.MODE_PDM) # 配置I2S参数 audio_config (sample_rate, bps, channel) i2s_out.configure(audio_config) # 开始播放 i2s_out.start() for chunk in audio_stream: i2s_out.write(chunk) # 在这里可以加入检查比如是否有停止播放的指令 i2s_out.stop() i2s_out.deinit() # 假设有一个函数从网络获取音频流 def fetch_tts_audio_stream(text): 调用TTS API并返回一个音频数据块的生成器。 # 这里省略具体的HTTP流式请求代码 # 模拟返回一些数据块 yield b\x00\x00\x01\x01... # 第一块PCM数据 yield b\x02\x02\x03\x03... # 第二块PCM数据 # 使用示例 # text_to_speak ai_reply # stream fetch_tts_audio_stream(text_to_speak) # play_audio_from_stream(stream)播放环节的坑缓冲区与实时性网络下载速度可能跟不上播放速度导致声音卡顿。你需要设计一个双缓冲区或环形缓冲区。一个线程/任务负责下载音频数据并填充缓冲区另一个任务负责从缓冲区读取数据并送往I2S接口播放。在MicroPython中可以使用_thread模块实现简单的多线程但要注意线程安全。音频格式与解码确保TTS API返回的音频格式采样率、位深、声道数与你的I2S配置完全一致。如果返回的是MP3你需要在ESP32上实现软解码非常消耗CPU和内存或使用硬件解码芯片如VS1053B。最省事的办法是请求API返回原始PCM或WAV格式。功耗与散热I2S驱动喇叭播放时尤其是音量较大时耗电会增加。长时间运行需要注意开发板的温升。4. 系统集成与优化让一切丝滑运行将各个模块组合起来形成一个稳定、可用的系统是项目从“Demo”到“产品”的关键一步。4.1 状态机设计管理机器人生命周期一个清晰的程序状态机能让逻辑井然有序避免代码变成一团乱麻。我们的机器人至少有以下几种状态休眠态低功耗模式等待唤醒按键或唤醒词。监听态唤醒后亮起指示灯屏幕显示“正在聆听...”开始录音。思考态录音结束屏幕显示“思考中...”依次执行语音识别、AI对话、TTS合成。说话态播放合成语音屏幕可以显示文字或动画。错误态任何环节出错网络断开、识别失败、API错误进入此状态显示错误图标一段时间后自动回到休眠态。class RobotState: SLEEPING 0 LISTENING 1 THINKING 2 SPEAKING 3 ERROR 4 class K10Robot: def __init__(self): self.state RobotState.SLEEPING self.oled ... # 初始化屏幕 self.led ... # 初始化LED self.button ... # 初始化按键 self.ai_client AIClient(...) # 其他硬件初始化 def run(self): while True: if self.state RobotState.SLEEPING: self._sleep_loop() elif self.state RobotState.LISTENING: self._listen_loop() elif self.state RobotState.THINKING: self._think_loop() elif self.state RobotState.SPEAKING: self._speak_loop() elif self.state RobotState.ERROR: self._error_loop() time.sleep(0.05) # 短暂延时防止忙等待 def _sleep_loop(self): 休眠循环检测唤醒事件 self.oled.poweroff() # 关闭屏幕省电 if self.button.value() 0: # 按键按下 self.state RobotState.LISTENING self.oled.poweron() self.oled.fill(0) self.oled.text(Hi!, 50, 20) self.oled.show() def _listen_loop(self): 监听循环录音 audio_data record_audio(5000) # 录音5秒 if audio_data: self.state RobotState.THINKING self.oled.fill(0) self.oled.text(Thinking..., 20, 30) self.oled.show() # 将音频数据传递给思考环节 self._process_audio(audio_data) else: self.state RobotState.ERROR def _think_loop(self): 思考循环识别、AI对话、TTS # 此函数应在录音完成后由_listen_loop调用或作为一个独立任务 # 这里包含ASR、AI、TTS的完整链式调用 text speech_to_text(self.last_audio_data) if text: reply self.ai_client.get_response(text) if reply: # 触发TTS合成和播放 self.tts_audio_stream fetch_tts_audio_stream(reply) self.state RobotState.SPEAKING self.oled.fill(0) self.oled.text(reply[:16], 0, 0) # 在屏幕上显示回复的前几个字 self.oled.text(reply[16:32], 0, 16) self.oled.show() return # 任何一步失败进入错误状态 self.state RobotState.ERROR def _speak_loop(self): 说话循环播放音频 try: play_audio_from_stream(self.tts_audio_stream) # 播放完毕回到休眠态 self.state RobotState.SLEEPING except Exception as e: print(播放失败:, e) self.state RobotState.ERROR def _error_loop(self): 错误处理循环 self.oled.fill(0) self.oled.text(Error!, 40, 20) self.oled.show() time.sleep(3) self.state RobotState.SLEEPING4.2 内存与性能优化在极限边缘游走在ESP32上内存是永恒的敌人。以下是一些实战中总结的优化技巧使用预分配缓冲区避免在循环中频繁创建和销毁大的bytes或list对象。为音频录制、网络数据接收预先分配固定大小的bytearray缓冲区并复用它们。及时释放资源urequests的响应对象response在使用后务必调用response.close()来释放底层socket资源。文件操作、网络连接同理。碎片化收集MicroPython有垃圾回收GC但频繁分配大对象会产生内存碎片。在关键操作如处理一次完整对话前后可以手动调用gc.collect()来强制进行垃圾回收有时能缓解内存不足的问题。流式处理这是最重要的原则。无论是录音、网络下载还是播放都不要试图将完整的数据保存在内存中。设计成“流水线”模式录一点传一点收一点播一点。使用生成器yield是实现流式处理的优雅方式。冻结字节码将那些不常变化的库文件如ssd1306.py编译成.mpy字节码文件或者直接冻结frozen到固件中。这能节省宝贵的RAM因为字节码直接从Flash读取执行而不是加载到RAM中。4.3 网络连接与稳定性嵌入式设备的Wi-Fi连接比PC脆弱得多。健壮的连接管理实现一个WiFiManager类它应该在启动时自动连接预设的网络。定期检查连接状态例如ping一个可靠的外网IP如8.8.8.8。如果连接断开自动尝试重连并有指数退避策略等待1秒、2秒、4秒...再重试。保存多个备用的Wi-Fi凭证SSID/密码在主网络不可用时尝试连接备用网络。请求超时与重试所有网络请求ASR、AI、TTS都必须设置合理的超时时间如10秒。请求失败后根据错误类型决定是否重试如网络超时可以重试认证错误则不应重试。离线模式设计一个降级的离线模式。当检测到网络不可用时可以切换到一个本地的问答库或者仅仅播放一段“网络未连接”的提示音而不是让整个系统卡死。5. 效果评估与进阶玩法完成基本功能后我们需要看看这个“K10”到底表现如何以及还能怎么玩。5.1 实际效果与局限性我把自己做的这个原型机放在桌上测试了几天。效果对于简单的问答、闲聊、讲故事它确实能给出有趣且合理的回答反应速度在网络良好的情况下大约在3-5秒录音识别AITTs播放体验上可以接受。OLED屏幕显示对话文字增加了交互的趣味性。但局限性也非常明显网络依赖性强没有网络它就是“哑巴”。这限制了它的使用场景。响应延迟云端API的往返延迟是主要瓶颈不适合需要实时交互的场景。成本长期使用API调用费用不可忽视。功能单一目前只是一个问答机缺乏“行动”能力。5.2 进阶优化方向如果你不满足于此可以尝试以下方向让机器人更强大本地轻量模型集成这是最大的挑战也是终极目标。可以研究TinyLlama或StableLM-3B的4-bit量化版本这些模型经过量化后模型文件可能仍在1GB以上远超ESP32的Flash容量。但你可以探索通过模型分片加载的方式将模型存储在外部SD卡或通过网络文件系统NFS按需加载部分参数到PSRAM中运行。推理框架可以选择TensorFlow Lite Micro或Apache TVM Micro它们对微控制器有较好的支持。专用任务模型如果你的机器人只需要完成特定任务如控制家电、回答产品问题可以训练一个超小型的意图识别Intent Classification和槽位填充Slot Filling模型。这种模型可以做到非常小几十KB完全能在ESP32上实时运行。识别出用户意图后直接执行预设的逻辑无需生成式语言模型。赋予“行动”能力让ESP32的GPIO不再闲置。你可以增加继电器模块通过语音控制开关灯、风扇。“打开客厅的灯” - AI解析意图 - ESP32控制对应GPIO输出高电平 - 继电器吸合。传感器接入温湿度传感器DHT22、光线传感器。机器人不仅可以对话还能主动汇报环境信息。“小K现在温度怎么样” - 读取传感器 - 组织语言 - TTS播放。舵机/电机制作一个能转头、摆手的实体机器人外壳让对话更有形。多模态交互增加一个摄像头如OV2640接入轻量级视觉模型如TinyYOLO MobileNet SSD。这样机器人就能“看见”并描述周围环境或者识别特定物体实现更丰富的交互。例如“小K桌子上有什么” - 拍照 - 运行物体检测 - 生成描述文本 - 播放。5.3 项目总结与心路历程回过头看这个“K10大模型对话机器人”项目与其说是一个产品不如说是一个技术探索的沙盒。它让我深刻体会到在嵌入式设备上实现AI功能就是在性能、成本、功耗和功能之间走钢丝。MicroPython提供的敏捷开发环境让我们能够快速验证想法将重心放在应用逻辑和创新上而不是陷入底层驱动的泥潭。最大的收获不是做出了一个多聪明的机器人而是掌握了在强约束条件下进行系统设计的思维方法如何拆解需求、如何选择技术方案、如何管理有限的内存、如何处理不稳定的网络、如何设计降级策略。这些经验对于任何嵌入式AI或IoT项目都是通用的。如果你也想尝试我的建议是从最简单的云端API方案开始。先打通“录音-识别-AI-TTS-播放”的完整链路获得正反馈。然后再逐一挑战其中的难点比如实现本地唤醒词、集成简单的本地问答库、增加硬件控制功能。每一步的突破都会带来巨大的成就感。这个项目最迷人的地方就在于它的天花板很高你可以根据自己的兴趣和技能无限地往上叠加新的功能模块。