我用AIUI给Rokid眼镜做了个随身换算智能体

1. 前言

楼主平时算是个"单位困难户":烤箱温度说华氏度、跑步配速用英里、海淘价格是英镑、菜谱写一杯牛奶。每次卡住都是掏手机解锁搜索等结果,一套下来十几秒。我正戴着Rokid眼镜呢,为什么不张嘴问一句"100华氏度是多少摄氏度",让AI眼镜直接给答案?

于是楼主在Rokid AIUI Studio上做了"随身换算":对着眼镜说一句换算的话,页面打开就是结果,左边原始数值,右边一个大字结果,页面底下三个按钮:反向换算、换一组示例、算一个。

整个思路一句话总结就是,固定单位的日常换算,用一张系数表维护在本地,不需要调用模型、也不涉及联网存储。

2. 项目背景

AIUI Studio里的智能体,一个app.json声明名称、触发说法和权限,一个.ink页面文件写界面和逻辑,脚本是标准JavaScript。楼主的做法是:解析和换算全部收进modules/convert.js,modules/voice.js负责控制收音,pages/convert/convert.ink管展示,页面自己一个数都不算。app.json里pages只挂convert一个页面,标题"随身换算",permissions全文件就RECORD_AUDIO一项,没有网络权限,没有存储权限。

agentPrompt楼主拆成四层:第一层给触发例句,六组单位各点一个名,从"100华氏度是多少摄氏度"到"1英亩是多少平方米";第二层定参数怎么传,有明确数值和单位就传value、fromUnit、toUnit,只有原话就传text;第三层把20个中文单位名列全,宿主不用猜;第四层是纪律------缺单位、超出支持范围或只说"度"时页面会提示,不要替用户猜测单位。为什么不调模型?20个固定单位的换算本质就是一张系数表加一个温度公式,大模型来算又慢又贵还可能编个系数,本地一张表快、稳、轻量。

3. 界面与操作

页面按480×352布局,黑底荧光绿,从上到下:分组标签、方向文字、左右两个数值框、状态行、三个按钮、底部提示。结果字号分三档:超过十个字26像素,超过七个字34像素,其余44像素,换算结果通常就几个字符,平时看到的基本都是最大字号。

「反向换算」把当前结果当成新的原始值换回去;「换一组示例」在八组预设示例里循环,八组把六组单位各覆盖到;「语音说」进收音流程,后面单说。按过反向换算之后是这样:

状态行是这个页面的"解说员",从"已按宿主传入的数值换算""已解析原话并换算"到"已反向换算""示例N / 8"各有台词,出错显示"提示:"加具体原因,涉及货币结尾自动补" · 参考汇率,非实时"。台词有42个字截断上限,普通状态半透明荧光绿,警告状态实色加粗。空打开时回落第一组示例,状态行提示"示例换算,可以说"3英里是多远""。键盘也能用:确认键触发焦点按钮,上下键换焦点,左右键故意没接------三个按钮横着排,左右键容易和滚动手势打架。

4. 单位设计

六个分组二十个单位:温度(摄氏度、华氏度)、长度(公里、英里、米、英尺)、重量(公斤、磅)、货币(英镑、人民币)、体积(升、毫升、加仑、立方米、杯)、面积(平方米、平方英尺、英亩、公顷、亩)。温度之外每个单位只存一个"对基准的系数",基准是每组里最常用的那个,任意两组单位之间换算都是先乘源系数到基准、再除以目标系数,20个单位只要存18个系数加2个温度函数,不用维护20乘19的互转大表,往后加单位也只动一行。全表贴在这,"也认这些说法"一栏是页面能听懂的全部叫法:

分组 单位 也认这些说法 对基准的系数 默认换到
温度 摄氏度 摄氏、°c、c 基准 华氏度
温度 华氏度 华氏、°f、f 仿射,见下节 摄氏度
长度 公里 千米、km 1000米 英里
长度 英里 mile、mi 1609.344米 公里
长度 米 公尺、m 基准 英尺
长度 英尺 呎、ft、feet 0.3048米 米
重量 公斤 千克、kg 基准 磅
重量 磅 lb、lbs、pound 0.45359237公斤 公斤
货币 英镑 gbp、£ 9.18元(参考值) 人民币
货币 人民币 元、rmb、cny、¥ 基准 英镑
体积 升 l、L 基准 加仑
体积 毫升 ml、mL 0.001升 升
体积 加仑 美加仑、gal、gallon 3.78541升 升
体积 立方米 m3、m³ 1000升 升
体积 杯 cup 0.236588升 毫升
面积 平方米 m2、m²、平米 基准 平方英尺
面积 平方英尺 平方呎、sqft、ft2、ft² 0.092903平方米 平方米
面积 英亩 acre、ac 4046.86平方米 公顷
面积 公顷 ha 10000平方米 英亩
面积 亩 市亩 666.667平方米 平方米

系数有讲究的交代两句:英里1609.344、英尺0.3048、磅0.45359237都是国际标准的精确值;加仑按美制3.78541升;杯按美制量杯折的0.236588升;英镑那格是参考汇率,不是计量值。最后一列是默认目标,"3英里是多远"只提到一个单位也知道往哪换,两个单位都提了就按用户说的来。

单位名匹配先把别名表建好,查表前做NFKC归一化再全部转小写,全角半角、大小写都不至于失配。"度"是个特例,单独映射成歧义标记,解析时看另一半单位:另一半是温度单位就自动补全,两边都是"度"或另一半不是温度就抛出追问。楼主的原则是只挑日常高频不贪全:二十个够覆盖九成场景,再往里堆维护和测试的负担都会涨。

5. 温度换算

温度单独说,因为它是表里唯一不能"乘个系数"搞定的一组:华氏度和摄氏度的关系是 (F减32) 乘5除9,那个32是冰点偏移,换算前要先平移,所以这对单位在表里不是factor,是一对toBase和fromBase函数,原文是:

css 复制代码
fahrenheit: {group: 'temperature', label: '华氏度', aliases: ['华氏度', '华氏', '°f', 'f'],
             toBase: v => (v - 32) * 5 / 9, fromBase: v => v * 9 / 5 + 32}

摄氏度做基准,别的十八个单位走同一条factor通道,页面上感觉不到差别。温度还有一条别的组没有的底线:绝对零度。区间检查用的是换算成基准单位之后的值,基准值低于零下273.15摄氏度,直接报"温度不能低于绝对零度(零下273.15摄氏度)"。上限各组分两档,温度最高10万摄氏度,其余五组放到1亿,都是"日常换算"的合理边界;其他分组只挡负数,温度挡的是物理下限。

仿射和系数两套路子,最后都汇进同一个换算主函数convertValue:查表、挡跨组、挡边界、进基准、出目标,一共十一行,原文抄在这:

vbnet 复制代码
export function convertValue(value, fromId, toId) {
  const from = UNITS[fromId];
  const to = UNITS[toId];
  if (!from || !to) throw new Error('单位暂不支持,支持:' + UNIT_LABELS);
  if (from.group !== to.group) throw new Error(from.label + '和' + to.label + '属于不同类型,不能互转');
  const num = Number(value);
  if (!Number.isFinite(num)) throw new Error('数值不是有效的数字');
  const base = from.toBase(num);
  checkRange(from.group, base);
  return to.fromBase(base);
}

6. 边界划分

做单位换算的时候最怕的不是不认识,是自作聪明,楼主定的规矩:拿不准就明说,绝不猜。最典型的是"度":口语里"40度"可能是摄氏度也可能是华氏度,页面直接追问是哪一种;但"100度是多少摄氏度"问题本身就排除了摄氏度,"度"就按华氏度处理得37.78,不用用户重复。不认识的单位也一样直说,"3光年等于多少米"返回"不认识"加单位,后面跟着支持单位清单;跨组混搭如米换磅,返回"属于不同类型,不能互转"。

"度"的补全、追问和"不认识"的拒绝,本体都在resolveUnit里,原文抄在这:

ini 复制代码
function resolveUnit(raw, otherRaw) {
  const key = normalizeUnit(raw);
  if (!key) return null;
  const hit = ALIAS_MAP[key];
  if (hit === AMBIGUOUS_DEGREE) {
    const otherKey = normalizeUnit(otherRaw);
    const otherId = otherKey ? ALIAS_MAP[otherKey] : null;
    if (otherId && otherId !== AMBIGUOUS_DEGREE && UNITS[otherId].group === 'temperature') {
      return otherId === 'celsius' ? 'fahrenheit' : 'celsius';
    }
    throw new Error('"度"可能指摄氏度或华氏度,请说明是哪一种');
  }
  if (hit) return hit;
  throw new Error('不认识"' + String(raw).trim() + '"这个单位,支持:' + UNIT_LABELS);
}

这三条行为app.json的提示词里都写了,宿主和页面在这一点上是说好了的。

7. 口语解析

真实说话没人念参数,都是"帮我看看3英里是多远"这种,楼主给解析定了三步。第一步剥前缀:开场词清单从"请问""帮我换算"到"我想知道""麻烦"一共16个,放循环里反复试,"请帮我换算"这种叠罗汉也能一层层剥干净。第二步从中间切开:先找数值,阿拉伯数字直接用,中文数字走"零一二两三四五六七八九十百"的位值换算,"两"按2算;数值后面找一个连接词把原话劈成两半,连接词从"等于多少""是多少"这种长词一路排到"是""合""=",长的排前面。第三步两头清理:左边剥"的距离""的长度"这类缀词,右边先剥"吧呢吗啊呀"语气词,再循环剥"多少""多远"这类问词。"3英里是多远"走完三步就是源单位英里、值3、默认目标公里,哪一步卡住都有明确报错。三步全串在parseText一个函数里,原文抄在这:

javascript 复制代码
export function parseText(text) {
  if (typeof text !== 'string' || !text.trim()) throw new Error('没有收到要换算的原话');
  let source = text.trim().normalize('NFKC').replace(/[??。!!~~\s]+$/, '').replace(/[吧呢吗啊呀]+$/, '').trim();
  source = stripPrefixes(source).replace(/[??。!!~~\s]+$/, '').trim();
  if (!source) throw new Error('没有听清要换算的内容,请说"100华氏度是多少摄氏度"这样的问法');
  const match = source.match(NUMBER_RE);
  if (!match) throw new Error('没有听清数值,请说"100华氏度是多少摄氏度"这样的问法');
  const value = match[1] != null ? Number(match[1]) : cnToNumber(match[2]);
  if (!Number.isFinite(value)) throw new Error('"' + match[0] + '"不像一个能换算的数值');
  const after = source.slice(match.index + match[0].length);
  const connector = after.match(CONNECTOR_RE);
  const fromRaw = cleanLeft(connector ? after.slice(0, connector.index) : after);
  const toRaw = cleanRight(connector ? after.slice(connector.index + connector[0].length) : '');
  if (!fromRaw) throw new Error('没有识别出原始单位,请说"100华氏度是多少摄氏度"这样的问法');
  return {value, fromRaw, toRaw};
}

参数进来有优先级:宿主的value和fromUnit齐了走结构化路径;缺了看原话,有text就解析;都没有回落第一组示例。结构化参数残缺不会立刻报错------原话里还有内容就继续走解析,原话也没有才报错:给了数值没给单位、只给了单位没给数值、两边同一个单位,各有各的提示。每个出口都有话说。

8. 反向换算

反向换算有个小讲究:回换用的是内部全精度值,不是屏幕上显示的四位有效数字。以100华氏度为例,内部算出37.7778循环小数,反向换算用那个没取整的值,换回去正好是100,楼主实测分毫不差;偷懒用显示值37.78回乘,连换几个来回误差就会渗进来。显示归显示,计算归计算,中间这道门不能开。

格式化统一取四位有效数字:3英里是4.828,5公斤是11.02,1英亩是4047;绝对值小于0.01且不为零的,改取六位小数再剥尾零。函数全文就六行:

javascript 复制代码
export function formatNumber(value) {
  const num = Number(value);
  if (!Number.isFinite(num)) return '---';
  const rounded = Number(num.toPrecision(4));
  if (rounded !== 0 && Math.abs(rounded) < 0.01) return rounded.toFixed(6).replace(/0+$/, '').replace(/.$/, '');
  return String(rounded);
}

9. 语音输入

「语音说」按钮点下去,状态行先显示"正在连接语音...",连上后变"正在听,说完单击结束",识别的字实时回显,只留最后28个字符,说完再点一下结束,或20秒不说话自动收尾。收音模块挂三个兜底:启动10秒连不上报"语音未就绪";空闲20秒自动结束;手动结束后再等1.8秒让最终结果落回来,防有些引擎停得慢。麦克风权限被拒单独提示。一个细节和"随口算"不一样:那边报错让用户再点一次按钮,这边失败提示是"可用按键操作",因为这个页面光靠按键就能完成全部功能,语音挂了用户不必干等。页面被盖住时立刻停麦,每次开收音发运行序号,旧识别器晚到的回调自动作废。

10. 验证情况

楼主在AIUI Studio平台进行了如下的验证:

  • 结构化路径:100华氏度换37.78摄氏度,进页面到出结果没有一步是页面编的
  • 原话解析:"3英里是多远"得4.828;"2加仑等于多少升"得7.571;"1英亩是多少平方米"得4047
  • 常用组抽测:5公斤得11.02磅;100英镑得918人民币,状态行补"参考汇率,非实时";10米得32.81英尺;12杯得2839毫升
  • 歧义与拒绝:"100度是多少摄氏度"自动按华氏度处理得37.78;"40度等于多少度"追问;"光年"和"斤"被拒并附支持清单
  • 往返一致性:100华氏度反向再反向,显示值分毫不差还是100;空参数回落第一组示例

在不断调试和迭代过程中终于将智能体上架,在这期间也是借用了平台的AI辅助开发的能力帮我进行了代码调优,AI辅助编程的便捷性拉满了,完全不用懂代码逻辑就能做出属于自己的智能体,当然楼主还是懂一点编程基础的~

11. 结语

楼主做这个智能体最大的感受是:日常换算的难点从来不在计算,在边界的划分。哪些单位认、哪些不认、拿不准怎么办,这些都要提前有个清楚的划分和规范,只有理清了这些才能开始写代码,剩下的就是一张表一个页面的事。眼镜上一句"3英里是多远",本地一毫秒出4.828,这种感觉比什么都实在。

欢迎各位体验我的智能体,要是觉得还缺哪个高频单位,回帖告诉楼主,下一版加进去。

相关推荐
夕水1 小时前
我用trae重构了ew-color-picker,一个插件化架构的现代颜色选择器
前端·javascript·开源
FYKJ_20103 小时前
springboot同人周边订单管理系统48955-计算机毕业设计、课程设计
java·javascript·vue.js·spring boot·django·php·课程设计
总要学点什么3 小时前
第106篇:三维特效-流动线效果——让一颗光点沿整条线一路滚到头
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
2分钟速写快排10 小时前
孔乙己的茴字有多少种写法?|AI 对话实时通信的三种实现
前端·javascript·github
用户00447295382517 小时前
练歌镜:让眼镜听懂“循环这一句”
javascript
yuyuxun120 小时前
协同过滤的校园二手物品智能推荐系统的设计与实现 毕业设计-附源码40082
javascript·vue.js·spring boot·python·数据分析·django·课程设计
总要学点什么1 天前
第105篇:三维特效-流动图片墙体效果——让箭头沿墙长滚,它俩竟是同一份代码
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
Agent世界1 天前
用 CDP 做发布自动化:四道机器闸门与产物验收
前端·javascript
总要学点什么1 天前
第104篇:三维特效-流动墙体效果——让贴图沿墙流动,它却只肯走一条对角线
开发语言·javascript·ecmascript·gis·webgl·材质·cesium