
1. 项目概述与核心设计思路1.1 Rokid Glasses 与 AIUI先搞清楚我们在开发什么Rokid Glasses 是一类带屏幕的智能眼镜设备形态和普通眼镜差不多但镜片上能投射出虚拟信息层戴起来就能看到通知、地图、翻译、提醒这些内容。而 AIUI 是 Rokid 提供的语音交互框架它把“听到语音 → 理解意图 → 执行动作 → 生成反馈”这条链路封装成了一套开发套件。简单说AIUI 承担的是“耳朵嘴巴大脑的部分思考”眼镜承担的是“眼睛”的呈现职责。两者合在一起你就能做出一个完全不需要动手、戴上去就能对话完成任务的智能助理。“今天吃什么”这个项目表面看是一个随机选餐工具实际上它是 AIUI 开发的最佳入门案例。它麻雀虽小但五脏俱全需要处理用户的自由语音表达、需要做意图识别、需要后端逻辑、需要把结果用语音和视觉两种方式反馈给用户。你把这个流程跑通后面做天气查询、日程提醒、语音笔记、外卖跳转全都是同一套骨架换皮。很多新手拿到 AIUI 的文档第一反应是“这 SDK 也太大了吧”。其实你把项目切开之后每个模块都不复杂。我给这个项目定的技术路线是AIUI 负责语音交互和意图识别眼镜本地逻辑负责从预设的菜谱池里随机出结果最后通过 AIUI 的 TTS 播报和眼镜的屏幕渲染把结果呈现给用户。整个项目不需要搭服务器全部在眼镜端完成是最干净的一种入门形态。1.2 为什么选“今天吃什么”这个场景做入门做智能眼镜语音应用最大的认知误区是“先学框架再想场景”。真实开发流程反过来先定一个高频、低复杂度、反馈链路短的小场景用场景倒逼你掌握框架的核心路径。“今天吃什么”就是这种场景。选它有三个理由。第一它是刚需高频每天至少触发两次用户愿意戴眼镜问第二它的语义表达极度多样——“中午吃啥”“今天吃什么”“推荐个菜”“不想吃外卖了”——全都指向同一个意图这对意图泛化能力的训练特别合适第三它的交互结果简单明确要么推荐一个菜品要么推荐三个让用户选反馈时长只有一两秒对眼镜这种近眼设备的体验压力最小。还有一个隐性原因语音交互应用的开发思维和手机 App 完全不同。手机 App 是你设计好页面让用户点语音应用是用户先用嘴告诉你一个模糊需求你再决定怎么做。这个“需求模糊到执行明确”的转化过程是语音开发的核心能力。吃什么这个场景天然的模糊性恰恰是练习这个能力最好的沙盘。2. 开发环境与工程骨架搭建2.1 开发者模式与设备接入Rokid Glasses 的开发不需要专门买一台测试机但你需要确认手里的眼镜固件版本支持 AIUI 开发模式。打开眼镜的配套管理 App找到设备信息页连点版本号五次会激活开发者模式然后在开发者选项里打开“ADB 调试”和“AIUI 调试日志”。这一步不做后面你跑代码的时候根本看不到日志输出出了问题只能靠猜效率极低。设备连接走 ADB 无线通道。眼镜和电脑连同一个 Wi-Fi然后在电脑上执行adb connect 眼镜的IP地址 adb devices看到设备列表里出现device状态说明连接成功。这里有个坑很多开发者的眼镜休眠后会自动断连因为省电策略切掉了调试通道。解决办法是到开发者选项里把“休眠时保持 ADB 连接”打开如果没这个选项就关闭眼镜的自动休眠开发调试阶段那点耗电可以接受。接下来装 Rokid 的开发工具链。核心包括两个东西一个是 AIUI 语音服务的调试工具用来做意图模拟测试另一个是应用打包工具用来把工程代码装到眼镜上。官方文档里有命令行安装方式我建议直接用 IDE 的插件市场搜“Rokid Glasses”安装插件之后项目模板、权限配置、真机调试、日志面板都集成进去了。这一步能节约大约两小时的手工配置时间别跳过。2.2 工程初始化与最小可运行骨架安装完插件后新建项目时选择“Glasses Empty Project”模板。这个模板帮你把最基本的权限、入口、AIUI 初始化都建好了你打开就是一个能跑的空壳。我建议你先不写任何业务代码先把这个空壳跑上眼镜确认三个基本能力应用能启动、眼镜屏幕上渲染出了一个页面、语音服务的回调函数能收到日志。跑通最小骨架之后再逐步加东西这就是“先让管道通水再让水里带内容”。很多新手一上来就写了一千行代码结果连最基本的语音回调都收不到分不清是自己的代码问题还是环境问题。最小骨架最大的价值是给你一个可信任的基线后面所有问题都能往“基线之上少了什么”这个方向排查。骨架里的关键文件结构大致长这样app.json应用配置声明权限、入口页面、语音能力的开启状态entry.js应用入口负责初始化 AIUI 服务pages/home/home.js主页面逻辑承载语音指令的回调和结果渲染确认这三个文件存在且能正常启动工程骨架就完成了。2.3 AIUI 服务初始化的正确姿势AIUI 的初始化是“今天吃什么”项目最容易被看轻的一步。直接在入口文件里写上初始化的调用不难但你需要理解它背后的状态机AIUI 服务存在未连接、连接中、已就绪、会话中四个状态。语音识别只有在已就绪状态才能触发而很多开发者在连接中状态就调了语音识别 API结果毫无反应。标准初始化代码大概长这样// entry.js const { AIUIService } require(rokid/aiui); const aiui new AIUIService(); aiui.on(ready, () { console.log([AIUI] 服务就绪可以开始语音交互); }); aiui.on(error, (err) { console.error([AIUI] 初始化失败, err); }); aiui.init({ appKey: 你的应用Key, deviceId: 眼镜设备ID });这里要强调appKey的获取与录制。在 Rokid 开放平台上创建一个应用拿到对应的 AppKey然后在设备信息里绑定这台眼镜。不同厂商的语音平台机制大同小异但核心逻辑一样语音服务是云边协作的云端要能认出你这台设备、这个应用才会放行识别请求。初始化完成后你会发现日志里刷出[AIUI] 服务就绪。这时候才算真正打通了“眼镜到语音服务”的链路。有个小技巧你可以把这份日志输出封装成一个全局事件界面上用一个状态小圆点显示灰点表示未就绪、绿点表示可以说话。调试阶段极其有用因为语音服务的就绪并不总是瞬间完成Wi-Fi 环境差的时候可能要等好几秒有个可视化状态能省掉大量“怎么又不说话了”的排查时间。3. AIUI 语音交互设计让 AI 听懂“随便整点吃的”3.1 意图、词表与表达泛化AIUI 的交互设计核心是“意图Intent”。你必须把“用户想干什么”和“用户具体怎么说的”分开建模。在“今天吃什么”里用户的意图只有一个叫recommend_food但表达这个意图的说法有无数种今天吃什么、中午吃啥、给我推荐个菜、晚餐有什么好吃的、随便整点吃的。AIUI 的地盘是 Match 引擎你得先建立自定义词表把所有同义表达喂进去。设计词表的时候最忌讳“把句子写死”。如果你只让系统匹配“今天吃什么”五个字那用户说“今天吃点啥好呢”就直接挂掉。正确做法是把常见动词、名词、语气词拆开分别建词表词表词条示例说明时间维度今天、中午、晚上、现在、待会儿用于解析时间场景后续可以做精细推荐动作表达吃、吃点、整点、搞点、推荐、推荐个吃这个动作的多样性表达宾语饭、菜、好吃的、东西、外卖指代食物的口语化名词语气后缀吗、呢、吧、啊、哈辅助消歧去掉语气词后匹配主干这样做的好处是推荐个午餐、今天整点啥这类组合句子也能命中意图。在实际工程里AIUI 的配置面板中有“意图”和“词表”两个独立入口先建意图recommend_food再把词表挂到意图下作为上下文约束。3.2 对话模板与默认兜底建完意图和词表接着要配置对话模板。对话模板是意图里的“必填参数槽位”抽取规则。在“今天吃什么”项目中我们暂时不需要任何必选参数所以模板可以留成空槽位只配置一条utterance示例句子。但你需要同时配好两个东西欢迎语和兜底话术。欢迎语在意图匹配成功时触发比如我可以配置为“好嘞让我想想今天给你翻哪个牌子”。兜底话术则是当用户说了话、但没有命中任何意图时返回的回应我一般配成“这个我还不太会要不要换个说法试试”。千万别省略兜底语音交互最尴尬的时刻就是用户说了一句话眼镜沉默三秒钟。沉默意味着系统死了而一个哪怕有点笨拙的回应至少说明系统还活着。3.3 槽位填值与多轮对话的扩展预留虽然“今天吃什么”的第一版可以设计成一句话直达但你在配置 AIUI 时还是要为多轮对话留好扩展接口。举个具体例子菜系偏好。你可以在意图里增加一个cuisine可选槽位词表包含“辣的”“清淡的”“川菜”“粤菜”“烧烤”等词条。用户在第一次说话时如果带了偏好——“推荐个辣的”——就直接填入槽位如果没带——“今天吃啥”——就保底走随机推荐。在这个项目里我先用“单轮随机”的极简方案落地把cuisine槽位解析逻辑写进代码里但入口先不做多轮追问。这是一个“可以吃亏但不可以不会”的设计思路万一后续想升级推荐逻辑模型骨架已经在了。多轮对话的配置会显著增加初学难度容易陷入闲聊式和任务式的边界混淆。我的建议很直接第一版绝对不上多轮先把一个意图、一个结果跑顺。AIUI 的多轮机制不是加法是乘法任何一个环节的状态管理不严谨整个对话流都会乱。4. 核心逻辑实现菜谱引擎与百搭随机推荐4.1 本地菜谱池的数据结构设计“吃什么”的核心后端其实是一个数据池加一个随机算法。数据池别用数据库直接用静态 JavaScript 对象数组就行够快、够简单。但数据结构不要只放一个菜名每个菜品至少包含名称、类别、热量档位、口味标签四个字段。为什么要四个字段因为后面推荐优化和界面展示都用得上。// data/menu.js const MENU_POOL [ { name: 番茄牛腩饭, category: 米饭, taste: 咸鲜, calories: 标准 }, { name: 麻辣香锅, category: 干锅, taste: 麻辣, calories: 偏高 }, { name: 轻食鸡胸沙拉, category: 沙拉, taste: 清淡, calories: 偏低 }, { name: 兰州拉面, category: 面食, taste: 咸鲜, calories: 标准 }, { name: 鳗鱼饭, category: 日料, taste: 咸甜, calories: 标准 }, // 建议初始准备 20 ~ 30 个菜品太少容易连续抽中同一个 ];实际开发中我最初只放了 8 个菜测试时五分钟内抽到同一个菜两次体验很不好。后来把池子扩到 30 个并引入“最近三次不重复”的防重逻辑。防重逻辑实现很轻let recentHistory []; function recommendFood() { const candidates MENU_POOL.filter(item !recentHistory.includes(item.name)); const pick candidates[Math.floor(Math.random() * candidates.length)]; recentHistory.push(pick.name); if (recentHistory.length 3) recentHistory.shift(); return pick; }这段代码的细节值得讲一句为什么是shift()而不是splice(0, 1)功能上两者一样但shift()用于移除数组首元素时语义更明确别人读代码时一眼就知道这是一个先进先出的滑动窗口而不是中间删改。代码的可读性往往体现在这些小地方。4.2 语音指令处理管线语音指令到达后处理管线分为三段接收、解析、执行。在 AIUI 框架里接收是通过监听intent事件完成的aiui.on(intent, (data) { const intentName data.intent; if (intentName recommend_food) { const selected recommendFood(); renderFood(selected); speak(selected.name); } });这个结构有个大问题同一时间只能有一个指令在跑如果用户在推荐结果播报过程中又追问一句“换个菜”后到的指令会直接打断前面的播报。这在语音交互里叫“抢话”。更健壮的做法是在 intent 事件里维护一个指令队列新指令如果和当前正在处理的指令类型相同先停止当前 TTS 和渲染再执行新指令。因为“今天吃什么”的核心体验就是快速得到一个答案“换一个”这种需求是真实场景里一定会出现的。解析环节我建议加一层“参数抽取代理层”。因为 AIUI 把槽位数据放在data.slots里但不同版本 SDK 的槽位字段名可能不一样。你写一个extractSlots(data)函数专门做字段映射后续升级 SDK 时只需改这一个函数业务代码完全不用动。这种防御性设计的成本极低收益却很大。4.3 视觉反馈在眼镜屏幕上呈现推荐结果眼镜屏幕的 UI 约束比手机严得多。它的视野有限文字不能小而密集更不能一屏堆满列表。“今天吃什么”的结果页我只放三个要素菜品名称大号加粗、口味标签次级字号、一个换一换的语音引导提示。页面渲染逻辑不需要用重型框架轻量数据绑定即可template div classrecommend-screen text classdish-name{{dishName}}/text text classtaste-tag{{tasteTag}}/text text classhint说“换一个”重新推荐/text /div /template在眼镜上做 UI记得一个原则停顿比流畅重要。用户看到推荐结果后需要至少 0.5 秒的纯视觉停留才能把菜名和推荐动作对上。很多开发者把渲染和语音播报同时触发菜名闪现一下就过去了用户只听到语音没看到画面。我的做法是渲染先出现TTS 延迟 300ms 再播报让视觉先把注意力锚定住。4.4 TTS 播报与打断机制TTS 播报“番茄牛腩饭”会暴露一个细节菜名里的多音字和生僻字。比如“肉夹馍”在全国不同地区读音不同、“鳗鱼饭”的“鳗”容易读错。AIUI 的 TTS 引擎对常见菜名的覆盖还可以但保险起见建议在播报前对菜名做一个注音映射。实现思路很朴素function getSpellSafeText(dishName) { const specialMap { 肉夹馍: ròu jiā mó, 鳗鱼饭: mán yú fàn }; return specialMap[dishName] || dishName; }更稳妥的方案是给每个菜品预设一个专门的播报文本字段spokenName在数据池里直接写好正确的读法。这样做的好处是彻底绕过引擎的多音字问题代价只是多维护一个字段。语音应用的主链路必须追求确定性任何“引擎自己发挥”的环节都要尽量消灭。TTS 的打断是另一个必修课。AIUI 提供了stopSpeak()方法但你必须把它和用户的“换一个”指令在时序上绑对。正确顺序是AIUI 识别到“换一个”意图 → 立即stopSpeak()→ 清空当前渲染页 → 执行新的推荐逻辑 → 渲染新菜品 → 延迟 300ms 播报。如果顺序反了就会出现“新菜已经出来旧菜名的声音还在播”的尴尬。5. 常见问题与调试实录5.1 问题速查表我在开发“今天吃什么”的过程中踩过不少坑整理成一张速查表建议收藏症状可能原因解法语音识别无响应AIUI 服务未就绪就调用了识别监听ready事件绿点亮起后再开放语音入口能识别但永远匹配不到意图词表覆盖不够或意图名称大小写不一致在调试工具里把用户原话复制进日志看最终命中的意图名推荐结果和播报菜名不一致TTS 播报的是缓存数据渲染的是新数据统一用一个currentMeal全局对象存目标渲染和播报都从它取值眼镜屏幕偶发白屏页面渲染时机早于 AIUI 就绪UI 层空数据在ready事件后再调用renderFood()或加空状态兜底组件“换一个”指令有时无效指令打断了 TTS 但没打断渲染线程在意图事件开头统一调用resetUI() stopSpeak()菜池明明有 30 个菜却频繁重复随机算法没有做最近历史去重加recentHistory滑动窗口过滤最近 3~5 次推荐5.2 调试工具用的最顺手的三板斧AIUI 调试工具是命令行交互式的它支持三种输入方式真实语音、文本转意图、直接注入意图 JSON。文本转意图对“今天吃什么”这种场景太有用了你可以不开嘴、不戴眼镜直接在电脑上模拟用户说“今天吃点啥”看到匹配结果和槽位数据。大量调意图匹配时我强烈建议用这个方式效率比对着眼镜喊话高十倍。直接注入意图 JSON 是查问题的杀手锏。当实际设备上意图识别一切正常但业务逻辑表现不对时你可以在调试工具里手动构造一份伪造的intent数据发给眼镜。如果伪造数据能正常触发渲染和播报问题就锁定在语音识别环节如果伪造数据也触发不了问题就在业务逻辑。这种“用假数据切分问题域”的排查思路适用于所有语音设备开发甚至所有端侧开发。5.3 一个容易被忽略的玄学问题批量初始化多个 AIUI 实例。我曾在多页面跳转的需求下在每个页面都创建了一个新的AIUIService实例结果发现语音识别间歇性失效。排查了很久最后才意识到是实例之间互相抢占了语音通道。AIUI 服务是全局单例所有页面的语音请求必须共享它。解决办法是在入口文件里创建一次然后通过依赖注入传给各个页面绝不能在页面里二次创建。语音框架和普通 UI 框架的边界就在这里UI 组件可以多实例语音服务必须是单例。5.4 端侧日志的优雅打法以眼镜这种低功耗设备的日志系统直接console.log打进 IDE 控制台没问题但系统会定时批量清理日志缓存信息容易丢。我的经验是给 AIUI 相关日志加一个统一前缀[AIUI]再用脚本把近期的关键事件初始化、ready、intent 匹配、TTS 播报开始单独抽出来落盘到一个日志文件。一旦现场出问题你直接拉这个精简日志看事件序列一下就能看出是“识别到了但没执行”还是“压根没识别到”。“今天吃什么”的逻辑再简单也扛不住“日志靠运气”这种开发方式。6. 实测效果与体验调优实际戴眼镜跑通全流程后整体体验基本顺滑但还是暴露了几个交互细节问题。最大的问题是“唤醒与指令的节奏”。最早我设计的是用户直接说“今天吃什么”AIUI 就会响应。但真机环境下眼镜的麦克风在待机时不会一直开着你需要先通过唤醒词唤醒眼镜等屏幕亮起反馈后再说话。这个“先唤醒、再说话”的节奏对用户来说多了一步习惯了语音助手的用户会下意识直接说指令于是经常丢前两个字。我的解决方案是在欢迎页加上“请先唤醒再提问”的视觉提示同时在用户刚唤醒的 3 秒内让 AIUI 进入最敏感的监听状态尽力捕获完整指令。另一个调优点在推荐策略。纯随机在测试时会连续推过于相似的东西比如上次是麻辣香锅、这次是麻辣烫用户会觉得系统“没动脑子”。后来我把菜池按口味、菜系、烹饪方式做了分类标签随机时先保证类别不连续重复上次麻辣类这次就从清淡或咸鲜类里挑。推荐逻辑的“智能感”并不需要复杂算法几个约束条件就能大幅提升感知质量。还有一个容易被忽视的体验细节TTS 语气词设计。最初的播报是干巴巴的“番茄牛腩饭”听多了觉得像查字典。后来我在播报文本里加了场景化的冗余信息“今天试试番茄牛腩饭吧配米饭刚好”。多出来的几个字并不会让用户烦反而让推荐结果听起来更像是有人在帮你做决定而不是一个随机数生成器在念菜名。至于眼镜上的结果页表现菜名大号展示加口味标签的布局测试下来效果稳定。第一次展示时我把热量信息也放上去了结果发现用户注意力会不自觉地被热量数字带走开始纠结“这个是不是太高了”反而把核心推荐冲淡。后来果断砍掉只留菜名和口味标签。做眼镜端 UI克制是最重要的能力你少放一个元素用户就多一分专注。整个项目从零到跑通如果按我这条路线走一个熟悉 JavaScript 的开发者大概需要两个完整的周末。最快的路径是第一周跑通最小骨架和意图识别第二周做菜谱逻辑和 TTS 渲染打磨。不要一上来就想着做多轮对话、个性化推荐、附近店铺联动第一版就做“一句话到一道菜”的闭环。这个闭环跑通了你对于“佩戴设备上的语音应用怎么开发”这件事就有了一个非常踏实的理解底座。后续加什么、改什么都是在这个底座上做加法。我个人在这个项目里最深的体会是语音交互应用的前期功夫根本不在写代码而在“你愿不愿意花时间去设计那句用户可能说的话”。设计好这句话后面的一切都是水到渠成。