时笺:一个 AIUI 提醒智能体的实现解析
一、这个项目是怎么来的
理由很朴素:我记性差,手机上的待办又坚持不下来。消息一多,通知栏就是个摆设。我想要的是抬头能看见、张嘴能确认的提醒。
还有个私心:家里老人吃药这件事。几点吃、吃什么,我手机里存了一堆闹钟,但老人不会用手机。要是眼镜能替我说这一句,事情就简单了。
核心诉求三条:
-
说一句话就能设提醒,别让我在界面里点点点
-
到点真能叫到人,不是弹个通知就完事
-
状态全存本地、离线能跑,不整云同步那一套
二、工程结构
├── app.js # AIUI 应用入口
├── app.json # 页面注册
├── package.json # node --test 单测脚本
├── pages/remind/index.ink # 页面 + 语音链路 + tick 调度 + TTS 队列
└── lib/
├── reminder.js # 核心纯函数(解析/调度/状态机/持久化)
├── llmParser.js # LLM 解析与意图识别(含 prompt 定义)
依赖关系很干净:reminder.js、llmParser.js 不碰任何宿主 API,可以在 Node 下直接跑测试;index.ink 只负责把语音识别、语音播报和这些纯函数串起来。这也是我能把大部分时间花在逻辑正确而不是界面调试上的原因。
三、数据模型与状态机
核心字段:
|
字段 |
说明 |
|---|---|
|
|
|
|
|
事项文案,落库前裁剪到 60 字并做脏数据兜底 |
|
|
排期 |
|
|
状态机见下 |
|
|
升级提醒档位 |
|
|
延后唤醒时刻 |
|
|
相对时间( |
状态流转:
pending ──到点──> (升级提醒 escalateLevel 0→1→2)
│
├──> 语音确认(ack) ──> done(重复型自动补排下一周期)
├──> 延后(snooze) ──> snoozed ──到时──> pending
├──> 取消(cancel) ──> cancelled
└──> 120s 无人理 ──> missed(记入补报账本;重复型同时补排下一次)
状态集:pending / snoozed / missed / done / cancelled。同一句话重复设置时,findDuplicateReminder 会按 同内容 + 同排期且仍有效 判重,直接拒建并回辅 duplicate,防止手滑说两遍建两条。
四、自然语言解析:双轨方案
一开始我也天真过,想着「时间这种具象的东西直接丢给大模型不就完了」。实践下来发现只靠 LLM 是虚的:它会一本正经地算错 dayOffset,偶尔还给你一个用 markdown 代码块包着的 JSON,最气的是不稳定——同一个句子上午答对下午翻车。
所以解析做成了双轨:
-
本地正则
parseReminder先跑一遍。纯函数、确定、快,支持相对/绝对时间、中文数字时刻(三点一刻/十点半)、星期区间(周一到周五)、间隔/每月/每年等频率; -
能调 LLM 时再走
parseWithLLM:调用前把当前日期拼进输入(【当前日期:2026年9月20日】,不然它算「25号是几号」全靠蒙),带 10s 超时保护; -
交叉校验:对
once类型核对 dayOffset,两边不一致以正则为准,并重算到点。LLM 超时 / JSON 解析失败 / 格式无效,一律静默回退,用户无感知。
// llmParser.js:方案 B 交叉校验(摘录)
if (parsed.ok && parsed.repeat === ‘once’ && typeof parsed.dayOffset === ‘number’) {
const regexParsed = parseReminder(text, now); // 本地精确算日期偏移
if (regexParsed && regexParsed.ok && regexParsed.repeat === ‘once’ &&
regexParsed.dayOffset !== parsed.dayOffset) {
parsed.dayOffset = regexParsed.dayOffset; // 模型猜错?听正则的
}
}
意图识别同套路:parseIntentWithLLM 失败返回 null,回退本地 detectIntent 正则,覆盖 set / query / snooze / cancel / ack / reschedule 六种。
五、调度与升级机制
index.ink 以 TICK_RATE_MS = 1000 定时调 tick(state, now) 扫描到期项。升级节奏:
|
到点后时长 |
档位 |
提醒方式 |
|---|---|---|
|
0 |
0 |
气泡轻提示 |
|
≥30s 无人确认 |
1 |
轻声语音念一遍 |
|
≥60s |
2 |
大字 + 完整内容播报 |
|
≥120s |
missed |
记入补报账本,稍后集中补报 |
重复型提醒在 ack 和 missed 两个分支都会补排下一周期,保证每天 8 点吃药不会因为一次漏确认就断档。
六、查询与补报
-
todayPending:今天(北京时间)待触发项按时刻排序 -
pendingOnDate:按目标日挑出各项并排序,重复任务从创建日起显示,不回溯历史 -
parseTimeRange+pendingInRange:支持今天/明天/本周/下周/本月/上月等区间查询,跨度上限 93 天 -
missedList→markReported→ackIds:错过项统一报账,确认后消账
七、持久化与容量控制
-
存储键
remind-agent.state.v1,saveStateToJSON/loadStateFromJSON负责序列化与反序列化 -
上限
MAX_REMINDERS = 50,超出按最旧淘汰 -
坑:脏数据。 有次把字段拼错,content 直接存成了字面量
"undefined"。后来所有落库文本统一过safeContentFor:空串、undefined、null、甚至"undefined"这几个字面量,一律回退默认文案
八、测试与验证
核心逻辑全是纯函数,npm test(node --test lib/test/)一键运行 42 个用例,覆盖:北京时间墙钟/日界、六种意图分类与反误判、相对/绝对/中文数字时刻、7 种排期、升级与错过判定、重复提醒判重与补排、容量上限、持久化 round-trip、脏数据兜底。
实际验证:
3点 -> false hour=undefined missing=[‘time’]
3点半 -> true hour=3
3点15 -> true hour=3
晚上9点 -> false missing=[‘time’]
主要是测试任务设置和查询是否存在问题,验证不同的提示话术出现的结果是否与预期一致

九、交互体验:主要靠嘴,能动手
页面主视角是一屏 HUD:顶部「时笺」标题配一枚实时时钟芯片,中间是语音状态气泡 + 今日安排列表,底部一条五个动作的操作栏。交互核心是语音,按键只做辅助兜底。
唤醒与倾听。 唤醒词把助手叫醒,回复「在的。说吧,几点提醒你做什么?」。进入倾听后,气泡会实时显示语音转写文字(「我在听你说…… 明天下午3点」),让你确认它听对了。这次实时转写给交互加了不少分——之前都是等一句话说完再回你,现在至少能看出它有没有在聋。
到点提醒的呈现。 到点后弹出绿色主题的提醒横幅,带「提醒中」标签,下方提示「说『好的』或『延后』处理」。
今日安排列表。 每项显示时刻、事项、以及频率标签(错过 / 稍后 / 每天 / 每周五 / 每2h / 每月15号 / 每年10月1号 / 待办),用户瞄一眼就知道自己今天欠了多少事。列表为空时给一句提示语,顺便教用户怎么用。
按键辅助。 空行操作栏五个 chip:说话 / 查提醒 / 延后 / 完成 / 取消。上下键移动高亮焦点,回车执行;倾听状态再按一次回车等于「说完了」。底部一行「选:说话 · 上下键选择 · 单击执行」的提示,带眼镜不开口也能完成一轮操作。
设置提醒的多轮对话(一条完整链路)是这样的:
用户:明天下午3点提醒我开会
时笺:明天下午3点提醒你开会,只提醒一次,对吗?
用户:好的
时笺:好,记下了:明天下午3点提醒你开会
查询是独立的「查询态」。 结果少就直接念完;多的时候每页念两条,说「下一页 / 上一页」翻,4~10 条时还会先问「要不要逐条念」。退出说「取消 / 退出」就行。查询期间屏幕切成查询列表,也保留了翻页的语音快捷通道。
十、成果与后续计划
-
已经落地的东西:四项硬成果(语音闭环、7 种排期+分级升级、双轨解析、本地离线),收尾到「都是能跑能测的代码」
-
还欠着的债:如实保留 26/42、TTS 估算、否定词边界、真机未系统验证
-
后续计划(按优先级):修到全绿 → 真机联调 → 口语变体扩量 → 抽 Craft 模板 → 上架智能体商店,每一步都可落地
取消
取消
写在最后
做完最大的感受:智能体这东西,「听懂人话」只是小头,「别听错」才是大头。你让大模型写诗很浪漫,但让它帮你记吃药这种关乎健康的事,错一次你就不敢用了。所以时笺宁可多追问一句,也不自作主张;宁可回退到一板一眼的正则,也不收一个看起来很聪明的错误答案。
更多推荐


所有评论(0)