在 Rokid 眼镜上,把一块碑拆成三层释文
先说结论:这是一个戴着眼镜、对准碑文、按一下确认键,就能逐字看到简体释文 / 字义 / 字源考据的小智能体,下面是它怎么长出来的,包括我踩过的几个坑。有些坑挺蠢的,但都真实。
一、项目简介:一块看不懂的碑
几年前在碑林,我站在一方唐碑前看了十来分钟,只认出三个字。手机拍下来放大,笔画能看清,但还是不认识;用识图软件搜,它告诉我这是「一块石碑」。
后来查资料才知道那是方墓志,写的是墓主人家世。当时的感觉不是「学到了」,而是「白站了十分钟」。
「字溯」解决的就是这十分钟。
戴上眼镜,对准石碑、摩崖石刻或者拓片,按一下确认,它拍一张照,把照片交给 LanguageModel 识读,然后把结果拆成三层摆在眼前:
-
L1:简体释文 + 拼音——先告诉你这是哪个字
-
L2:字义——这个字在这里大概什么意思
-
L3:字源、书体、典故——为什么是这个意思,它以前长什么样
目标用户有三拨:跟研学团看碑的学生、临帖前要对照原碑的书法爱好者、以及到了现场才想弄明白的文博爱好者。他们有个共同点——手上都有别的事,拿着手机、扶着眼镜、跟着队伍,能腾出来的只有一句话和一根手指。
所以这个产品的形态从一开始就定了:不是让你坐下来研究,是让你站在原地,用最少的动作看完。
二、场景与用户意图:他其实只想说一句话
真实的现场大概是这样:
站在碑前,看不懂,想知道写的是什么。
这句话可以拆成几个动作:掏出手机 → 打开相机 → 拍 → 打开识图 → 等 → 发现结果是一段百科 → 再搜单字 → 放弃。
眼镜上的链路要短得多:抬头、说一句(或者直接按确认)、看结果。中间那些步骤应该由界面自己组织,而不是交给用户去点。
我把它理解成一组有先后关系的意图:
|
用户想干的 |
界面上的承接方式 |
|---|---|
|
这段写的是什么 |
拍照 → 识读 → 出字卡 |
|
这个字什么意思 |
选中字卡 → 展开 L2 |
|
为什么这么写 |
再展开 → L3 |
|
念给我听 |
语音「朗读」/ 按钮「朗读」 |
|
记一下,回头查 |
保存笔记(照片 + 三层释文 + 时间) |
|
考考我 |
开启研学 → 从已识别的字里出题 |
有一点需要说清楚:唤醒词这一层我做不了主。onVoiceWakeup 只接收平台预置的唤醒词,没有任何接口能注册「你好,字溯」。所以实际链路是「平台预置唤醒 → SpeechRecognition 收音 → 我自己的关键词路由」。这也是为什么代码里有一张 11 条规则的意图表,而不是一个唤醒词。
眼镜的输入方式也决定了交互不能照搬手机。onHeadGesture 只有点头和摇头,没有滑动、没有精确触摸。所以整套操作最后落在四个键上:方向键选、Enter 确认、Backspace 返回。
想清楚这一点之后,很多设计就不用纠结了:凡是需要「拖、滑、长按」的交互,全部不做。
三、方案设计:四个页面和一个状态机
3.1 智能体定义
AGENTS.md 里我写死了一条边界,这条边界后来救了我很多次:
古文字考释存在多种学术分歧,释读内容仅作科普参考,不作为学术定论。
它不只是免责声明,它还是行为约束:模型不能输出「定论」「确切无疑」这种词,遇到有分歧的考释必须写「一说」「或释」。界面上,取景页常驻一行小字;L3 展开时再加一句「不作为学术定论」。
另外三条主动放弃的能力,也写进了 manifest:
-
不做 SLAM 空间锚定。
enableWorldAwareness不含 SLAM,释文卡片不做物理坐标绑定,就在取景界面里分层展示。 -
不做自定义唤醒词,原因上面说了。
-
不做上滑/下滑手势,只做方向键。
3.2 页面分工
四个页面,各管一件事:
|
页面 |
职责 |
|---|---|
|
|
取景、拍照、识读、三层字卡、语音指令入口 |
|
|
研学答题:出题、作答、校验、发徽章,页内连答 |
|
|
笔记列表 + 详情,看完整三层释文 |
|
|
徽章墙,4 列网格 |
主页面为什么把「取景」和「释文」放在同一屏?因为在现场,用户最怕的是「点进去之后找不到刚才那块碑」。取景框和字卡共用一块 240px 高的主区,有释文就显示字卡,没有就显示取景区提示——不跳页,不丢上下文。
3.3 状态流转
主页状态不多,但每个都得给用户一个明确的表情:
idle ──拍照──> recognizing ──成功──> success
│ ├──失败──> fail / anchor_lost(保留上次释文)
└──按确认──> cancel(回到 idle 或 success)
失败态这里我改过一次:最早失败就清空 glyphs,用户看到一片空白,第一反应是「刚才识别成功的字呢」。现在失败只改状态文字,上一次的释文和照片都留着,顶部加一句「本次识别未成功,以下为上次释文」。
3.4 能力调用
整条链路只有四类外部能力,全部是平台给的:
wx.media.createCameraContext().takePhoto()
↓ ArrayBuffer → wx.arrayBufferToBase64
LanguageModel.availability() / create() / session.prompt([text, image_url])
↓ 文本(一行 JSON)
parseGlyphs() ← 自己写的容错解析
↓ 结构化字形数组
speechSynthesis.speak() / wx.setStorageSync()
没有后端,没有自建服务。识读完全靠端上的 LanguageModel。
四、AIUI 实现:几个真正花时间的点
项目结构就是标准 AIUI 的样子,ES module,.ink 单文件四块(script def / script setup / page / style):
字溯/
├── AGENTS.md 智能体定义
├── app.json 4 个页面路由 + 相机/录音权限
├── app.js globalData(研学退出标志)
├── pages/
│ ├── camera/camera.ink
│ ├── quiz/quiz.ink
│ ├── notes/notes.ink
│ └── badges/badges.ink
└── utils/
├── promptBuilder.js prompt 构造
├── resultParser.js 模型返回容错解析
└── voiceRouter.js 意图路由 / TTS / 本地存储
4.1 从拍照到 base64
坑不多,但要按平台的方式来:
const photo = await this._takePhotoWithTimeout();
const ab = photo.data instanceof ArrayBuffer ? photo.data : /* 处理 TypedArray */;
const base64 = 'data:' + photo.mimeType + ';base64,' + wx.arrayBufferToBase64(ab);
navigator.mediaDevices.getUserMedia + ImageCapture 是浏览器 API,在这套运行时里会直接卡死页面,别碰。
4.2 让模型少说话
第一版 prompt 只写了「识读图片中的古文字,输出 JSON 数组」。结果模型很热情:先夸一句碑文,再解释自己的识读过程,最后给一段带 markdown 代码块的 JSON。用户等 20 秒,拿到一堆不能渲染的文字。
第二版把话说死:
只输出一行 JSON 数组,不要输出解释、标题、思考过程、前后缀或 markdown 代码块。
数组每个元素对应一个字,最多 20 个字。
所有字段都是字符串,每个字段不超过 30 字。
无法识读的字不要输出;整幅无石刻古文字时只输出 []。
还顺手修了原示例里的一个低级错误:示例 JSON 里的双引号没转义,模型照着抄,抄一次错一次。
改完之后,返回长度从几百字降到一百多字,响应也快了不少。
4.3 模型不听话怎么办
Prompt 再严也不能保证 100% 合规。所以解析这一层必须自己扛:
-
去掉 ``` 代码围栏
-
前面有解释文字?不管,从第一个
[或{开始做括号平衡扫描,找到完整的 JSON 片段 -
返回对象?在
glyphs/chars/data/list里找数组 -
字段名不一样?
simplified/char/character/hanzi/meaning挨个试 -
单个对象、字符串数组也认
核心那几行大概是这样:
for (let i = 0; i < src.length; i++) {
const open = src[i];
if (open !== '[' && open !== '{') continue;
const close = open === '[' ? ']' : '}';
let depth = 0, inStr = false, esc = false;
for (let j = i; j < src.length; j++) {
// 跳过字符串内部,避免把字符串里的括号当结构
// ...
if (depth === 0) { out.push(src.slice(i, j + 1)); break; }
}
}
写完之后我拿 17 组样本跑了一遍:纯 JSON、代码块、前后带解释、对象包装、字段别名、字符串数组、单对象、空数组、纯闲聊、超长字段……全部通过。这一步比在真机上瞎试有用得多。
4.4 按键时序(这个坑最贵)
研学答题做完一题,界面上写着「确认或单击继续」。按一下确认,应用直接退出了。
排查了半天,问题不在 quiz 页的业务逻辑,在事件时序:
原来是在 onKeyDown 里直接 wx.navigateBack()。页面在 onKeyUp 之前就被卸载了,卸载之后那段 event.preventDefault() 永远不会执行——宿主把同一个 Enter 当成默认确认,又返回了一次。栈底被弹穿,应用退出。
修正后的规则,四个页面统一:
keydown 只记意图,keyup 先
preventDefault,再执行导航。
onKeyDown(e) {
if (e.code === 'Enter') { this._pendingConfirm = true; return; }
if (e.code === 'Backspace') { this._pendingBack = true; return; }
// 方向键:只做选中项移动,不动页面
}
onKeyUp(e) {
if (e.code === 'Enter' && this._pendingConfirm) {
this._pendingConfirm = false;
e.preventDefault(); // 先拦
this._onConfirm(); // 再干
}
}
顺手把研学也改了:原来答完一题要 navigateBack 回主页,由主页再 navigateTo 进答题页出下一题,一题一闪。现在直接在 quiz 页里出下一题;只有按返回,才回主页并把研学模式关掉。
五、交互与体验:走一遍完整流程
戴上眼镜,唤醒,说帮我释读这段碑文。
界面先变成识别中…(按确认取消)。这行字很重要——模型偶尔会慢,没有这行字,用户会以为死机了;有了这行字,他还知道可以按确认取消,不用干等 30 秒。
识读完成,主区换成字卡列表,顶栏显示12 字。方向键往下,选中第一张卡,按确认:
-
第一次:展开 L2,日,太阳
-
第二次:展开 L3,甲骨文象太阳之形,后加点+ 书体 + 典故
-
第三次:收回 L1
永远只有一张卡是展开的。 这不是技术限制,是屏幕限制:480×352 里塞两张展开的卡,什么都看不清。
按朗读,TTS 从第一个字念到最后一个;再按一次停。播报做了 1.2 秒同内容防抖和 96 字截断——眼镜贴着头骨,重复播报比不播报更烦人。
按 笔记,底部弹一条已记笔记,两秒后自己消失。笔记里存的是照片 base64、识别原文、三层释文、时间戳和标签,下次打开还在。
按 研学,进入答题。题目不是写死的题库,是模型根据刚才那个字现出的四选一。答对了,徽章 +1,顶栏数字跟着变。
整个流程里,用户没有打开过一次键盘,没有输过一个字。
六、调试与验证:四个实机问题
模拟器里跑通不代表能用。实机第一轮,用户报了四个问题,每一个都值得记一笔。

问题一:识别成功,但主区是空的。
现象是取景框还在,字卡不出来。这里其实叠了两个原因:一是模型偶尔返回超长文本,旧解析器只认顶层数组 + simplified,返回 [],页面就走失败分支;二是 scroll-view 的高度是 flex: 1 1 auto + min-height: 0,真机上塌成了 0——数据有了,但没地方画。
两处都改了。解析改成容错解析;scroll-view 的父链全部改成固定 flex-basis。为了下次能一眼分清是没识」还是没渲染,我加了一行调试信息:
step=response photo=312KB raw=847 parsed=0
raw 有值、parsed 是 0,就是解析问题;两个都是 0,才是模型没返回。这行字在第一轮排查里省了我很多来回。
问题二:等太久,返回的还是一大段话。
Prompt 收紧 + 30 秒超时 + 识别中可取消。超时文案也分开写:超时说「识别超时,请重试或换更清晰的画面」,光线问题说「光线较弱」,取景异常说「取景不可用」,不让用户对着同一句「识别失败」猜。
问题三:研学点「继续」就退出。 上面 4.4 讲过了,keydown/keyup 的锅。
问题四:尺寸乱了。 按钮换行、免责提示压住按钮、笔记提示跑到屏幕外。按 4.5 的固定栅格重排,改完就没再动过。
验证这块我做了两件事:
-
写了一份
docs/VERIFY.md,从拍照、三层展开、朗读、记笔记、连答五题、返回退出,一条条勾。真机测试最怕「感觉能用」,勾完清单才知道哪里不能用。 -
脚本层先做静态检查。把每个
.ink的<script setup>抽出来跑node --check,解析器单独写 17 组用例。这些都不能替代真机,但能把「语法写错」这类低级问题挡在上机之前。
七、成果与复盘:能做什么,不能做什么
现在能做的:拍照识读甲骨文、金文、小篆、隶书(载体限碑刻、摩崖、拓片);逐字三层释文;语音指令覆盖释读、朗读、视图、笔记、模式切换、答题;研学答题页内连答,答对收徽章;笔记本地保存,随时回看;全程中文 TTS 反馈。
做不到的,我也写清楚:
-
没有 SLAM,字卡不会钉在碑上,它只在取景界面里分层显示。这是平台能力边界,不是设计取巧。
-
唤醒词只能用平台预置的,没法叫「你好,字溯」。
-
单次识读上限 40 字。碑文动辄几百字,这是刻意的取舍:眼镜上没人能读完 300 字。
-
考释有分歧的地方,它只能说「一说」「或释」,不能给结论。这不是能力不足,是这门学问本来的样子。
接下来的打算:
一是把「词组」做出来。现在是一字一卡,但碑文里「之」「其」「也」这种字单看没意义,得连着上下文。
二是把 resultParser.js 抽成模板。模型返回不规范是普遍问题,不只古文字会遇到,这套括号平衡 + 字段别名的写法换个场景也能用,放到社区里比留在自己项目里值钱。
三是补一次真正的现场测试。现在的验证都是「在能拍清楚的地方拍」,实际碑刻有反光、有苔痕、有玻璃罩。这些不是调 prompt 能解决的,得带着眼镜去碑林蹲一下午。
最后说回「意图驱动」这四个字。
做这个东西之前,我以为意图驱动的界面就是把聊天框搬到眼镜上:用户说一句,AI 回一段。做完之后发现不是。真正的难点在于——用户说的是「这写的啥」,他真正想要的是「别让我掏手机」。
界面要做的,是把这一句话翻译成尽量少的按键、尽量短的字、尽量明确的状态。三层释文是这么来的,两个字按钮是这么来的,失败时保留上次结果也是这么来的。
更多推荐

所有评论(0)