
设计一个会说话的儿童学习伴学机器人涉及空净、提醒、交互等功能这是很多机器人爱好者都绕不开的一个项目。做硬件创客这几年我前后做了几个版本的桌面机器人有的买现成开发板拼装有的直接用了安卓板加屏幕但始终没有找到一个既稳定又开源、还能把语音交互、传感器、网络通信串在一起的完整方案。去年底我决定自己搭一套真正能跑起来的教学原型今天就把整个项目从需求拆解、硬件选型、软件架构、语音链路、传感器接入到后续可以扩展的方向一次性讲清楚。1. 这个“伴学机器人”到底要解决什么问题以及核心功能拆分1.1 从“智能音箱”到“伴学机器人”的差距在哪里市面上的智能音箱能放儿歌、能报天气、能定闹钟但它本质上是一个“云端对话终端”本地几乎没有感知能力也没有肢体反馈更不会主动根据环境变化做出行为调整。而伴学机器人要做到的不只是“听懂话”它还需要感知环境比如检测空气质量、温湿度、光照判断孩子学习环境是否合适。主动提醒比如到了设定的学习时间机器人主动发出语音提示而不是被动等指令。多模态反馈语音交互之外最好还有屏幕表情、LED灯带、舵机肢体动作让孩子觉得“它是一个活着的伙伴”。离线兜底家庭网络不稳定时基础对话和提醒功能不能完全瘫痪。这两个定位之间的差距正是这个项目的核心价值所在。1.2 功能需求拆分与优先级排序我把需求拆成四个层次按照“先跑通、再优化”的原则排序基础语音对话优先级最高本地唤醒词 云端大模型对话允许断网时使用预设话术兜底。环境感知与主动提醒使用传感器采集温度、湿度、空气质量PM2.5/CO2、光照强度当指标异常时主动语音播报。视觉交互与表情系统通过显示屏显示表情结合舵机构造的头部动作形成简单的“情绪反馈”。家长配置与后台管理通过Web界面配置学习计划、查看环境历史数据、远程下发提醒。注意这里我刻意把“视觉识别”比如用摄像头看孩子有没有在看书放到了后面的迭代版本第一版先不碰因为视觉链路会大幅增加硬件成本和软件复杂度。1.3 技术选型的基本思路语音方案本地唤醒词用开源工具大模型对话走云端API。之所以不全部走云端是因为唤醒词如果每次都上传音频延迟和隐私都是问题。主控板选择双芯片架构树莓派做上层逻辑对话、传感器数据处理、网络通信ESP32或STM32做底层外设控制舵机、LED、按键。这样即使上层系统崩溃底层设备功能依然可用。传感器环境传感器统一走I2C总线减少接线语音模块单独走串口方便调试。机器人本体3D打印外壳设计阶段预留传感器窗口和舵机安装位。这套选型的核心逻辑是把不稳定的事情云端对话、网络、大模型和稳定的事情传感器采集、舵机控制、本地提醒尽量隔离开。2. 硬件搭建从树莓派到传感器模块的完整清单与接线避坑2.1 硬件清单与选型理由模块型号/规格选型理由主控板上层树莓派4B4GB版社区资料多、支持Python生态、跑得动语音识别和网络请求主控板底层ESP32 DevKit便宜、支持Arduino和MicroPython适合舵机控制和传感器采集语音唤醒模块离线唤醒词模块如SU-03T本地唤醒延迟低不依赖网络可自定义唤醒词麦克风阵列双麦克风降噪板既能用于唤醒也能用于近场语音采集扬声器3W/5W小喇叭 I2S音频功放声音清晰度比PWM直驱好很多环境传感器SHT30温湿度 SGP30eCO2/VOC都是I2C接口精度够用功耗低PM2.5传感器PMS5003激光散射原理数据稳定串口输出舵机2个SG90 1个MG90S用于头部左右转动、上下点头、眼部表情联动显示屏2.8寸TFT LCDSPI接口显示表情和基础菜单成本可控外壳3D打印PLA打印成本低迭代修改方便避坑树莓派4B的功耗需求比较高必须使用支持5V/3A的电源适配器尽量不要用普通手机充电头电流不够会导致USB外设随机掉线排查起来非常痛苦。2.2 接线要点与常见错误系统接线图遵循低压逻辑所有模块供电电压必须统一规划树莓派与ESP32之间使用USB转TTL串口连接波特率设置为115200实现双向通信。树莓派发指令给ESP32控制舵机ESP32把传感器数据定期回传。传感器统一挂I2C总线SHT30和SGP30都挂在树莓派的I2C引脚Pin3SDA、Pin5SCL地址分别为0x44和0x58接线时注意上拉电阻树莓派内部默认已经有上拉不需要额外外接。PMS5003接串口PMS5003是5V供电、3.3V逻辑必须做电平转换否则树莓派GPIO会烧。我第一次接线时就把PMS5003的TX直接接到了树莓派RX上读到的数据全是乱码后来才发现是电平不匹配。强烈建议所有串口模块都加一个逻辑电平转换器几块钱的成本能省掉一整天的排查时间。2.3 3D打印外壳的结构设计重点外壳分三个部分头部壳体内部预留显示屏窗口、舵机支架、扬声器出音孔。扬声器出音孔要设计成扩散孔阵列避免声音闷在壳内。身体壳体放置树莓派、ESP32、传感器模块、电源管理板。侧面开Type-C供电口和USB调试口。底座预留走线槽和散热孔树莓派发热量不小底部不能完全封死。设计时最好把传感器窗口朝前或朝上不要朝下贴着桌面否则灰尘和遮挡会影响读数。打印层高0.2mm填充率20%就够外壳不承重30%填充纯属浪费耗材。3. 软件架构设计上层逻辑与底层控制的“分工与合作”3.1 为什么必须采用双芯片架构实际开发中如果全部逻辑都压在树莓派上会遇到几个痛点语音对话过程中网络阻塞会导致树莓派CPU吃满此时如果还要直接驱动舵机做动作动作必然卡顿。标准Linux系统没有硬实时能力舵机PWM波形的jitter会很明显肢体动作看起来“发抖”。树莓派关机或系统崩溃时底层设备全部失联连“紧急停止”都做不到。所以我的设计原则是树莓派负责“大脑”ESP32负责“小脑反射”。3.2 树莓派侧的软件框架树莓派侧使用Python 3 FastAPI架设一个轻量级服务模块划分如下audio_service.py负责录音、播放、唤醒词触发回调。dialog_service.py负责与云端大模型对话维护多轮会话上下文。sensor_service.py读取I2C传感器数据做简单滤波和异常检测。action_service.py将上层逻辑产生的“动作意图”翻译成指令发送给ESP32。scheduler_service.py维护学习计划表到点触发提醒事件。核心代码结构大致如下import threading from fastapi import FastAPI from pydantic import BaseModel app FastAPI() app.get(/health) def health_check(): return {status: ok, sensors: sensor_service.summary()} app.post(/remind) def create_remind(item: BaseModel): scheduler_service.add_task(item.dict()) return {code: 0}3.3 ESP32侧的固件逻辑ESP32侧用Arduino框架开发主要工作循环void loop() { // 1. 接收上位机串口指令 if (Serial.available()) { String cmd Serial.readStringUntil(\n); if (cmd.startsWith(MOVE)) { moveServo(cmd); } else if (cmd.startsWith(LED)) { setLedEffect(cmd); } } // 2. 处理本地的紧急开关和传感器兜底逻辑 if (digitalRead(EMERGENCY_PIN) LOW) { stopAllServos(); } }ESP32上独立维护一个“超时保护”如果连续10秒没有收到树莓派的任何指令就自动让舵机回到安全位置。这个机制在树莓派死机时能避免机器人摆出奇怪姿势也算是个保险。3.4 核心协议JSON-RPC over Serial树莓派和ESP32之间的通信协议需要支持结构化指令又不至于太重。我这里采用了简化版JSON-RPC每条指令一行JSON以\n作为帧结束符{cmd:servo_move,target:head_yaw,angle:15,duration:500,id:1} {cmd:report_sensor,temp:26.3,humidity:45,pm25:12,eco2:680,id:2}之所以选择JSON而不是自定义二进制协议主要原因是调试方便你可以在串口监视器里直接看到树莓派发给ESP32的内容出问题了立刻能定位是协议解析错误还是逻辑错误。4. 语音链路实战唤醒词、录音降噪、云端大模型对话4.1 唤醒词方案选择语音交互的第一步不是“识别全部内容”而是“只识别唤醒词”。我第一版尝试在树莓派上用openWakeWord做本地唤醒效果不错但CPU占用偏高每次唤醒后响应延迟在1到2秒之间。后来改用专用的离线唤醒模块SU-03T效果稳定很多——它自己负责监听麦克风检测到唤醒词后通过串口发一个高电平脉冲给树莓派。SU-03T的使用方式很简单在配套上位机里自定义唤醒词比如“小伴同学”“嗨小伴”烧录固件即可。它同时还能自定义几组固定命令词像“打开提醒”“关闭提醒”这种本地指令不经过云端直接执行。4.2 录音与播放链路唤醒之后树莓派需要录制一段用户语音。推荐直接用sounddevice库录音它会挂到树莓派USB声卡或I2S声卡上。import sounddevice as sd import numpy as np import wave def record_audio(duration3, sample_rate16000): # 录音期间显示音量便于调试 frames sd.rec(int(duration * sample_rate), sampleratesample_rate, channels1, dtypeint16) sd.wait() return frames.flatten()录音后需要做降噪处理。如果麦克风直接裸露在桌面环境中回声和风扇声很严重我用两招解决采用双麦克风差分降噪板硬件级过滤一部分环境噪声。上传前做简单的能量检测和静音截断把“用户没说话”的静音段切掉既省流量又让云端识别更准。4.3 云端大模型对话的接入细节对话服务我用的是云端大模型API需要处理的关键点是上下文管理。直接每次把全部历史对话都发给大模型既浪费token又可能超出上下文长度。我的做法是构造一个轻量级记忆窗口只保留最近6轮对话。每轮对话压缩成(role, content, timestamp)的元组。系统提示词里固定写入“你是小伴一个陪伴孩子学习的机器人朋友回答简洁、正面、有耐心”。请求部分核心代码from openai import OpenAI client OpenAI(api_keyYOUR_API_KEY) def get_reply(user_text, history): messages [{role: system, content: SYSTEM_PROMPT}] messages.extend(history) messages.append({role: user, content: user_text}) resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, temperature0.7, max_tokens300 ) reply resp.choices[0].message.content return reply避坑树莓派4B使用Python的requests模块访问HTTPS API时偶尔会因为系统证书过期报SSL错误。用pip install --upgrade certifi升级证书库即可这个问题在很多树莓派镜像里都存在。4.4 离线兜底策略家里网络断了或者云端API超时不能让机器人变“哑巴”。我写了一个离线话术映射表场景离线话术唤醒后网络不可用“我现在网络有点卡不过可以帮你简单的提醒哦。”用户询问今天课程从本地SQLite数据库读取课表直接播报用户说“关灯”通过ESP32直接关闭灯光走本地链路这个兜底策略的意义在于即使云端服务不可用机器人的“基础可用性”仍然存在这恰恰是伴学类设备最重要的信任感来源。5. 环境感知与主动提醒从传感器数据到“主动说话”5.1 传感器数据采集与滤波我用SHT30读温湿度、SGP30读CO2和VOC、PMS5003读PM2.5。SGP30有一个明显的坑它在首次上电的前12秒里读数会漂移必须等它完成初始化否则初始CO2数值会明显偏高。在代码里加一个初始化等待def init_sgp30(): sgp30 adafruit_sgp30.Adafruit_SGP30(i2c) # 等待内部算法稳定 time.sleep(15) return sgp30读数滤波上我用了滑动窗口均值对每个指标保留最近10个采样点取平均再上报避免单次噪声导致误报。5.2 空气质量判断逻辑空气中CO2浓度是一个典型指标设定阈值CO2 800ppm状态“优”正常学习。CO2 800-1200ppm状态“注意”建议开窗。CO2 1200ppm状态“差”主动播报建议休息。PM2.5阈值类似35μg/m³为优35-75为轻度污染75为重度污染。环境判断代码把它封装成一个函数def evaluate_air_quality(co2, pm25): if co2 800 and pm25 35: return good elif co2 1200 and pm25 75: return notice else: return bad5.3 主动提醒怎么做到“不烦人”主动提醒最容易犯的错误是太频繁。如果传感器数据一超标就播报孩子会觉得烦最终把机器人的语音关掉。我的做法是引入“冷却时间”机制同一类提醒两次播报之间至少间隔30分钟。提醒语气采用建议式不说“空气质量太差”而说“我们可以打开窗户透透气”。如果当前正在播放故事或正在对话中提醒会排队等到对话结束后再播报。这个设计背后是交互礼仪问题伴学机器人不是监控器它的定位是“伙伴”所以提醒方式要像朋友一样温和不能像报警器。6. 表情系统与动作反馈如何让机器人看起来“有情绪”6.1 表情系统的数据驱动设计显示屏上画表情可以用现成图形库渲染。我用的是Pillow在树莓派上生成表情图像再通过SPI把图像推送到LCD屏。表情状态机空闲态眼睛缓慢眨眼每4秒眨一次。对话态眼睛张大嘴部出现波形给人“正在认真听”的感觉。思考态眼睛眯起视线偏移模拟“思考中”。提醒态眼睛变成感叹号样式配合黄色背景灯。低电量态眼睛变成半月形显示电量图标。表情切换由action_service根据当前对话状态和传感器状态触发核心代码from PIL import Image, ImageDraw def draw_face(modeidle): img Image.new(RGB, (240, 240), (255, 255, 255)) draw ImageDraw.Draw(img) if mode idle: draw.ellipse((60, 80, 100, 120), fill(0, 0, 0)) draw.ellipse((140, 80, 180, 120), fill(0, 0, 0)) elif mode happy: draw.arc((60, 80, 100, 120), start0, end360, fill(0, 0, 0)) draw.arc((140, 80, 180, 120), start0, end360, fill(0, 0, 0)) draw.arc((80, 120, 160, 160), start180, end360, fill(0, 0, 0)) return img6.2 舵机动作与表情联动头部动作和表情同步才能产生“情绪感”。比如开心时头轻微左右摇摆眼睛弯弯。提醒时头抬高眼睛睁大。思考时头微微低下、偏向一侧。这些动作通过ESP32驱动舵机实现。树莓派侧只需要发送动作指令动作的插值运动在ESP32侧完成保证动作平滑void smoothMove(Servo servo, int from, int to, int duration) { int steps 50; int delayMs duration / steps; for (int i 1; i steps; i) { int angle from (to - from) * i / steps; servo.write(angle); delay(delayMs); } }6.3 LED灯带的效果反馈我在机器人底座装了一圈可编程LED灯带WS2812B不同的灯光颜色表达不同状态蓝色呼吸灯待机。绿色流动灯正在听。橙色常亮环境提醒。红色闪烁系统异常。灯带如果直接接树莓派的GPIO驱动电流不够最好通过ESP32控制再由树莓派发送命令。如果要接树莓派必须使用独立的5V电源供电并将信号线串联一个330欧电阻防止过冲。7. 家长配置后台用Web界面管理提醒计划与查看历史数据7.1 为什么需要后台管理一台放置在孩子书桌上的学习机器人必然需要家长参与配置。如果每次修改提醒时间都要拆机改代码这个产品就没法用。所以我把交互分成两层孩子使用语音直接操控机器人。家长通过手机浏览器或电脑访问树莓派上的Web页面配置提醒计划、查看环境数据曲线。后端直接复用FastAPI构建前端用简单的HTML Chart.js绘制曲线不引入重量级框架。7.2 Web界面功能清单功能说明提醒计划配置按周设置学习/休息/喝水的时间点与播报内容环境状态查看实时显示温湿度、PM2.5、CO2数值历史曲线按小时/天/周查看环境变化趋势日志查看查看机器人的对话记录、提醒记录、异常记录网络设置配置WiFi账号密码支持DHCP或静态IP7.3 Web后台简易实现FastAPI后端中加一组路由app.get(/api/history) def get_history(hours: int 24): data db.query(EnvRecord).filter(EnvRecord.ts time.time() - hours * 3600).all() return {items: data} app.post(/api/schedule) def set_schedule(item: ScheduleItem): scheduler_service.update_schedule(item) return {code: 0}前端显示历史曲线用Chart.js几行代码就能画出动态折线图fetch(/api/history?hours24) .then(r r.json()) .then(data { new Chart(ctx, { type: line, data: { labels: data.items.map(i new Date(i.ts * 1000).toLocaleTimeString()), datasets: [{ label: PM2.5, data: data.items.map(i i.pm25) }] } }); });7.4 数据存储方案环境历史数据保存在SQLite中文件不大但要注意写入频率传感器每10秒采集一次直接写入SQLite会导致大量写入于是我先在内存中缓存最近5分钟的数据每5分钟批量写入一次。数据库记录保留最近30天超过的自动清理。这个设计虽然没有用上时序数据库但对这种单机项目来说完全够用而且内存占用极低树莓派4B跑起来完全没压力。8. 实测情况与踩坑记录那些文档里不会告诉你的问题8.1 语音识别准确率与延迟实测我连续测试了一周使用的测试场景包括访客在1米内正常音量说话。孩子在播放儿歌的情况下说话。家里开着风扇时说话。结果记录如下场景唤醒成功率云端识别成功率平均响应时间安静环境95%90%2.8秒背景音乐88%75%3.5秒风扇噪声80%60%4.2秒实测下来背景音乐对云端识别影响比较大尤其是人声类内容。后来我在录音前端做了简单的频谱减法和动态降噪识别率提升到80%左右但也没法做到100%这类消费级设备在噪声环境下的识别能力本身有上限。8.2 舵机抖动与供电不足问题第一次组装完外壳后我发现头部舵机在转动时会产生明显抖动甚至偶发抽搐。排查过程如下怀疑程序问题检查舵机控制代码剔除所有非必要的串口打印抖动依旧。怀疑舵机电压不稳用万用表量SG90供电端舵机启动瞬间电压跌到4.2V明显偏低。换独立5V电源将舵机供电从树莓派5V引脚改到独立5V/2A电源抖动消失。查了SG90的手册才知道堵转时电流可达500mA以上树莓派的GPIO 5V根本吃不消。舵机要单独供电并且电源地和树莓派地必须共地否则PWM信号参考电压不一致舵机一样会乱动。8.3 ESP32串口数据乱码与波特率不一致树莓派和ESP32的串口通信初期经常出现乱码原因有两个树莓派的UART默认被系统控制台占用需要开启/boot/config.txt里的enable_uart1并禁用串口控制台功能。ESP32的串口波特率默认是115200但树莓派的pyserial如果不指定timeout参数会导致半包数据读取。解决方式是把读取封装成按行读取import serial ser serial.Serial(/dev/serial0, 115200, timeout0.1) def read_line(): if ser.in_waiting 0: line ser.readline() if line: return line.decode(utf-8, errorsignore).strip() return None8.4 树莓派温度过高导致降频夏天室温30度时树莓派4B如果安装在没有风扇的全封闭3D打印外壳里核心温度会飙到85度以上出现强制降频语音对话延迟明显增加。解决方案外壳设计时增加顶部风扇安装位。使用主动散热风扇通过GPIO控制温度超过60度时启动。软件层面限制不必要的后台服务减少CPU占用。9. 后续可扩展方向与个人总结9.1 断网情况下的完全离线对话当前版本的离线兜底只支持预设话术如果想完全离线对话可以接一个本地小模型。比较现实的路径是使用whisper.cpp做离线语音识别。使用一个轻量级大模型如Qwen-7B量化版做回复生成。把两者跑在树莓派5或带NPU的板子上如瑞芯微RK3588。我试过在树莓派4B上跑7B量化模型速度太慢不具备可用性所以这个方向更适合预算更高的硬件平台。9.2 视觉感知能力增强第二版我计划加入摄像头和人脸检测通过人脸检测判断孩子是否在桌前结合学习计划做“注意力提醒”。通过OCR识别孩子是否在阅读纸质书不过这种需求会让成本明显上升。摄像头画面在本地处理不上传保护孩子隐私。9.3 多机器人联动如果家里两个孩子、两个房间多台伴学机器人可以组成一个局域网集群。家长的Web后台可以统一管理所有机器人机器人之间还能互相传递消息——比如哥哥的机器人提醒妹妹该写作业了。这个方向在数据同步和消息总线上有更多可玩性。9.4 一点个人经验这个项目从立项到跑通第一版我用了大概三周其中将近一半时间花在排障上。最大的体会是做这类“多硬件多软件”集成的项目稳定性永远是第一位的而不是功能多花哨。先把语音对话、主动提醒、表情反馈这条最小链路跑得足够稳再逐步加功能整个开发轨迹才会可控。代码和硬件方案还在持续迭代中欢迎同样在做伴学机器人的朋友一起交流这些踩坑记录如果能帮到后来者少走弯路就很有意义了。