从对话到行动:用 AIUI 构建「喝水节奏教练」

一个戴在眼前的饮水记录助手 ------ 用一句话记录饮水、查看个人目标进度,并随时核对与撤销。


1. 项目简介

「喝水节奏教练」是一款面向 Rokid AI 眼镜 的本地饮水记录智能体。它解决的核心问题是:日常工作、学习或通勤中,用户常常不方便拿出手机记录刚喝的每一口水;而只靠语音对话复述总量,又难以核对"是不是重复记了一次"。

这个项目把「记录 --- 查看 --- 修正」压缩成一张始终出现在眼前的界面:一句话记一笔,页面同步显示今天的累计量、个人目标进度和最近记录;记错了能直接撤销,目标可以自己调整,数据只存在设备本地。

目标用户 是那些习惯用语音交互、希望在不打断手头事情的前提下,轻量、可信地管理自己饮水节奏的人。

项目的定位有三个明确边界,也是贯穿全程的设计原则:

  • 不是健康 App:目标只是用户自设的记录参考,不是医学建议,不推荐目标量、不催促达标、不鼓励过量饮水。

  • 不猜需求:不依据身高、体重、活动量、天气或疾病推算摄水需求,也不采集图像、位置或健康传感器数据。

  • 数据在本地:当天记录与个人目标只保存在设备本地,不上传网络,日期按设备本地日期切换。

2. 场景与用户意图

2.1 用户在什么情况下唤醒

典型的触发场景非常短促:

  • 刚喝了一杯水、不想打断手头的节奏 → 说一句"记一杯"。

  • 下午想检查一下今天喝了多少 → "今天喝了多少?"

  • 发现刚才多记了一次 → "撤销上一笔。"

  • 第一次用,想设定自己的参考值 → "把我的目标设成 1600 毫升。"

  • 想重新开始 → "清空今天的记录。"

2.2 智能体如何理解与承接

用户的表达通常很短且口语化:"记一杯""刚喝了 300 毫升""喝了一大杯"。AIUI 将这些说法映射成少量、清晰的页面意图 和结构化参数,页面再根据动作呈现结果。

具体承接分两层:

  1. 意图映射:把自然语言收敛到 5 类动作 ------ `status`(查看)、`log`(记录)、`undo`(撤销)、`set_goal`(设目标)、`reset`(清空),并提取 `amountMl`(饮水量)与 `goalMl`(目标量)两个参数。

  2. 结果可视化:页面根据动作即时更新总量、进度、最近记录和反馈,让"执行结果可见并可修正"。用户不需要学习固定口令。

同时,系统对"数量可能缺失"做了明确的默认处理:用户只说"喝了一杯"而未给数量时,按 250 ml 记一笔,并在页面上明确说明默认值。

3. 方案设计

3.1 AIUI 智能体定义

智能体以单页面对话式应用实现,核心概念是"意图驱动的界面 "(Intent-driven UI),而非连续聊天。眼镜上的主要诉求是快速记一笔、检查和撤销,而不是阅读长解释,因此采用单页面结构。

两条输入路径写入同一份状态:

  • AIUI 宿主语音意图路由:由系统把语音转成结构化 `query` 参数,分发给页面的 `onLoad(query)`。

  • 页面内语音入口:页面自建的 `SpeechRecognition`,用于"点按语音按钮 / 唤醒词"后识别并本地解析饮水量。

这两条路径是相互独立的,但最终都调用同一套记录处理逻辑,避免累计量不一致。

3.2 页面 / 卡片设计

页面自上而下组织为四个区域:

区域 内容 渲染方式
顶部信息区 当天日期、今日已记录总量 动态(A2UI)
中部状态区 个人目标、进度百分比、分段进度条、最近两笔记录、空状态 动态(A2UI)
中部提示区 语音状态 / 不支持提示 普通组件
底部操作区 记一杯(250 ml)、撤销、目标 ±100、清空今日 普通组件

为什么这样分工 :固定且高频的操作(记、撤销、调目标)用普通 AIUI `button` 组件,交互稳定、无需动态协议;会随 AI 意图和记录数据改变内容的结果区(总量、进度、记录行、反馈)用 A2UI 动态生成。

3.3 状态流转

Plain 复制代码
初始状态(无记录)
   │  用户说"记 300 毫升" / 点"记一杯"
   ▼
记录追加(appendRecord)─────────────────┐
   │                                     │
   ├─ 用户说"撤销" → 移除最后一笔(removeLatestRecord)
   ├─ 用户说"设目标 1600"→ 更新 goalMl(goalIsDemo=false)
   ├─ 用户说"清空"→ records=[](目标保留)
   │
   ▼
重新计算 viewState → 生成 A2UI 更新指令 → context.write() 刷新界面

每个动作都会走同一个 commit(records, goalMl, goalIsDemo, feedback) 流程:先本地存储,再重算视图状态,最后生成 A2UI 指令并刷新。这保证了"语音"和"点按"两条入口的状态始终一致。

3.4 能力调用关系

  • LanguageModel(端侧语言模型):当页面内语音识别到文本但无法靠规则确定数量时,用来估算饮水量;不可用时回退到 parseAmountFromText 正则解析。

  • SpeechRecognition(页面语音):监听、转写、错误与超时处理。

  • wx.setStorageSync / getStorageSync:本地持久化目标与当天记录。

  • a2ui:动态渲染与更新结果区。

4. AIUI 实现

4.1 项目结构

Plain 复制代码
喝水节奏教练/
├── AGENTS.md               # 智能体能力与边界约定
├── app.js                  # 应用入口(onLaunch)
├── app.json                # 页面与权限声明
├── package.json            # 工程元信息
├── README.md               # 使用与实现说明
└── pages/
    └── index/
        └── index.ink       # AIUI 单文件 Page(唯一的页面)

4.2 app.json:一个页面 + 最小权限

app.json 只注册一个全屏页面,并声明页面语音识别所需的 `RECORD_AUDIO` 权限;不申请摄像头或定位权限。

JSON 复制代码
{
  "name": "喝水节奏教练",
  "description": "用自然语言记录饮水、查看个人目标进度,并在眼前即时核对和撤销。",
  "pages": ["pages/index/index"],
  "permissions": ["RECORD_AUDIO"],
  "window": { "navigationBarTitleText": "喝水节奏教练" }
}

4.3 页面 Schema:自然语言描述 + 结构化意图

index.ink 的 <script def> 声明了页面的自然语言描述和 JSON Schema。Schema 里定义了 5 类动作和两个数值参数:

JSON 复制代码
{
  "action": { "enum": ["status", "log", "undo", "set_goal", "reset"] },
  "amountMl": { "type": "integer", "minimum": 1, "maximum": 5000 },
  "goalMl": { "type": "integer", "minimum": 100, "maximum": 10000 }
}

amountMl 用于"本次饮水量",未给出时留空、页面默认 250;goalMl 只在 set_goal 动作中使用。

4.4 状态核心:纯函数状态机

视图状态由 makeViewState(records, goalMl, goalIsDemo, feedback, storageAvailable) 统一计算,产出总量、目标、进度百分比、最近两笔记录、记录数、反馈文案和存储状态。记录的增加 / 撤销是纯函数:

JavaScript 复制代码
function appendRecord(records, amountMl, source) {
  return records.concat([{ amountMl: Math.round(Number(amountMl)), source }]);
}

function removeLatestRecord(records) {
  if (!records.length) return { records, removed: null };
  return { records: records.slice(0, -1), removed: records[records.length - 1] };
}

4.5 A2UI:动态渲染结果区

首次渲染时,页面把 createSurface 与初始 updateComponents 指令序列化成字符串传给 <a2ui commands="{{initialCommands}}">;之后每次状态变化,通过 a2ui.createA2UIContext('hydration-dashboard') 获取组件上下文,再用 context.write() 推送更新指令。

JavaScript 复制代码
function initialA2UICommands(view) {
  return JSON.stringify([
    { version: 'v0.9', createSurface: { surfaceId: 'main' } },
    { version: 'v0.9', updateComponents: { surfaceId: 'main', components: buildA2UIComponents(view) } }
  ]);
}

flushA2UI() {
  if (!this.a2uiReady || !this.pendingA2UI) return;
  const context = a2ui.createA2UIContext('hydration-dashboard');
  context.write(this.pendingA2UI);
  this.pendingA2UI = null;
}

组件树会依记录状态动态生成空状态或多条记录行;新数据只更新 A2UI 页面内容,不执行动态 JavaScript(安全边界)。

4.6 本地存储与日期隔离

用日期键隔离每天记录,个人目标在跨天时保留。存储不可用时页面仍能在当前会话内操作,并在反馈中提示"仅本次打开有效"。

JavaScript 复制代码
function localDayKey() {
  return new Date().toDateString();
}

function normalizeSavedRecords(value, today) {
  if (!value || typeof value !== 'object' || value.dayKey !== today || !Array.isArray(value.records)) return [];
  return value.records
    .filter((r) => r && Number.isFinite(Number(r.amountMl)) && Number(r.amountMl) > 0)
    .map((r) => ({ amountMl: Number(r.amountMl), source: String(r.source || '记录') }));
}

onShow 里检测日期变化,若已跨天则重新开始当天记录、保留目标。

4.7 数量智能解析:LLM + 规则双保险

页面内语音识别到文本后,先尝试用端侧 LanguageModel 估算饮水量,失败或结果无效时回退到正则规则(支持"300 毫升""两杯""一大杯""半瓶"等口语表达)。

JavaScript 复制代码
async handleTranscript(transcript) {
  this.setData({ voiceState: 'processing', feedback: '解析中...' });
  let amount = await this.parseAmountWithLLM(transcript);
  if (amount === null) amount = parseAmountFromText(transcript);
  if (amount !== null && amount > 0 && amount <= 5000) {
    this.addWater(amount, '语音记录');
    this.endVoiceSession(`已识别"${transcript}",记录 ${Math.round(amount)} 毫升。`);
  } else {
    this.endVoiceSession(`听到"${transcript}",未识别出饮水量。可说"300 毫升"或"一杯"。`);
  }
}

5. 交互与体验

5.1 重要界面

  • 有记录时:显示"已记录 X ml"、个人目标(标注"我的目标"或"演示目标 · 可调整")、进度百分比、10 段进度条,以及最近两笔记录。

  • 无记录时:显示独立的空状态提示"今天还没有记录。说出饮水量后,这里会生成记录。"

  • 语音状态:聆听中(脉冲圆点)/ 解析中 / 不支持语音,均有明确文字反馈。

5.2 操作路径

操作 语音 点按 / 按键
记录一杯 "记一杯" 「记一杯」按钮(250 ml)/ Enter
记录定量 "记 300 毫升" 语音入口 + 文本解析
撤销 "撤销上一笔" 「撤销」按钮
调目标 "设成 1600 毫升" 「−100 / +100」按钮
清空 "清空今天的记录" 「清空今日」按钮

页面还支持方向键移动选中、Enter 激活,方便在不方便触控/语音时完成操作。

5.3 反馈机制

  • 每次动作后立即反馈结果,例如"已记录 300 毫升;可随时撤销"。

  • 默认值透明化:未给数量时反馈"已按默认一杯 250 毫升记录"。

  • 目标边界透明化:反馈里明确"它只是你的记录参考,不是健康建议"。

  • 存储状态透明化:显示"保存在此设备"或"仅本次打开有效"。

5.4 可读性设计(面向 AR 眼镜窄视野)

  • 采用单色(绿色 `#40ff5e`)+ 细分隔线 + 短状态文字,不依赖颜色语义。

  • 同步使用文字、数字、分段线和符号表达状态,色弱用户也能辨识。

  • 明亮操作边界、紧凑字号,适配眼镜窄视野与透明显示。

6. 调试与验证

6.1 AIUI Studio 预览

在 AIUI Studio 中国站 用"本地导入"加载项目文件夹,在设备模拟预览中逐条试说:

  • "今天喝了多少?"

  • "记录 300 毫升水。"

  • "我刚刚喝了一杯。"(应提示按 250 ml 默认记录)

  • "撤销上一笔。"

  • "把我的目标设成 1600 毫升。"

  • "清空今天的记录。"

同时点按页面上的「记一杯」「撤销」和目标调整按钮,验证按钮与语音是否写入同一份状态、反馈是否同步。

6.2 关键验证点

  • 结构化参数:确认 `action` / `amountMl` / `goalMl` 是否正确落到 `onLoad(query)`。

  • A2UI 刷新:记录/撤销/调目标后,总量、进度、记录行、反馈是否即时更新。

  • 跨天行为:修改设备日期,确认当天记录重新开始、个人目标保留。

  • 存储异常:模拟 `wx` 存储失败,确认页面仍能用、且提示"仅本次打开有效"。

  • 数量解析边界:口语"两杯""一大杯""半瓶"与纯数字"300"的解析结果是否符合预期。

7. 成果与复盘

7.1 可运行成果

  • 一个可导入 AIUI Studio、可模拟预览的完整 AIUI 单页面智能体工程。

  • 支持 5 类语音意图 + 5 类页面操作,语音与点按共用同一份状态逻辑。

  • A2UI 动态渲染总量、进度、最近记录与反馈;本地持久化目标与当天记录;跨天自动重置记录、保留目标。

7.2 遇到的问题与解决方案

问题 解决方案
用户说"喝了一杯"没给数量,无法记账 明确 250 ml 默认值,并在反馈与说明中透明化
误记后难以恢复 保存每笔数量与来源,撤销只删除最后一笔,无需重置整天
目标易被误解成健康建议 目标标记为"用户自设 / 演示值",且不自动估算、不催促达标
真机本地存储可能受限 对 wx 读写加异常处理;失败时明确提示"仅本次打开有效"
语音与点按各自维护状态导致累计量不一致 两条入口共用同一份记录状态与新增/撤销辅助逻辑
页面内语音对口语数量解析不准 LLM 估算 + 正则规则双保险,LLM 不可用时优雅回退
AR 眼镜窄视野下可读性差 单色 + 分段线 + 短文字 + 明亮边界,不依赖颜色语义
相关推荐
nutriu10 个月前
从UE5.6DNA 导出指定LOD层级的ARkit52个表情或者Metahuman263个表情教程 #BlendShapeExporter
ue5·数字人·arkit·blendshape·虚拟角色·meta human·dna
新缸中之脑2 年前
ARKit读取LiDAR点云
arkit
冬冬吧3 年前
AR场景实践(2 ) - RealityKit探索
swift·visionos·arkit
冬冬吧3 年前
AR场景实践(1) - ARKit探索
visionos·arkit
AlanHou3 年前
visionOS空间计算实战开发教程Day 2 使用RealityKit显示3D素材
swiftui·visionos·arkit
和时间赛跑ing3 年前
ARKit
arkit
XR基地3 年前
AR 开发入门:与苹果的 AR 工具一起玩转增强现实
ios·arkit
BBLv3 年前
RealityKit深入浅出之ARView
swift·visionos·arkit