
2026-08-30 | 体验工程
语音输入里,哪些体验不能丢
Helen
从 ai-ask 与 med-agent 两套语音输入方案出发,看一次典型的体验工程取舍:实时字幕、真音波、弱网稳定性都想要时,究竟什么必须保留,什么可以降级。
体验工程不是把所有效果都实现出来,而是先判断哪些反馈承担了用户的控制感、确定感和信任感。核心体验不能丢,反馈形式可以降级。
一开始的问题:实时 ASR 为什么总在吞字?
ai-ask 和 med-agent 最早其实是同一个起点:都用了实时 ASR。
按下
→ 开始录音
→ 建立 ASR 连接
→ PCM 帧实时上传
→ 边说边出字
体验看起来很好,问题也很直接:录音和网络连接绑在了一起。
最典型的是两种吞字:
- 头部吞字:用户已经开始说话,但连接 / 凭证还没准备好
- 尾部吞字:用户已经松手,但最后几帧还没完整送进 ASR
本质上不是识别模型不准,而是:
用户说话的时间线,与网络链路准备完成的时间线产生了竞争。
ai-ask 的选择:继续保留实时
ai-ask 没有放弃实时字幕,而是继续把链路补完整。
包括:
- RecorderManager PCM 帧级录音
- WebSocket 实时 ASR
- recorder 预热
- PrewarmSession 状态管理
- sessionId 防止旧会话污染
- 触摸状态保护
- SentenceEnd 收尾
- 各种异常与设备冷却处理
最终换回来的是一个很明确的体验:
用户说话
↓
文字实时出现
也就是:实时字幕。
但它的代价也很明确。
只要识别输入依赖 WebSocket,那么弱网下就会出现一个结构性问题:
Recorder 已经产帧
↓
WebSocket 还没 ready
↓
这一段帧怎么办?
如果没有完整的本地 buffer + 补发机制,就可能真的丢。
所以 ai-ask 的失败模式是:
弱网不仅会慢,还有可能丢。
med-agent 走了相反的方向
med-agent 后来直接把录音轨和识别轨解耦。
先不考虑 ASR:
按住
↓
RecorderManager 本地完整录音
↓
松手
↓
整包 POST /speechToText
↓
一次性识别
这样最重要的一件事情发生了变化:
用户的声音先完整落在本地,识别只是后续处理。
网络慢:
上传慢一点
ASR 慢:
结果晚一点
但声音本身已经在。
失败模式从:
慢 + 可能丢
变成了:
只慢,不丢
对于短句语音输入来说,这是一个很大的交换。
但去掉实时字幕之后,又冒出一个问题
原来实时字幕其实偷偷承担了很多反馈。
用户看到文字不断出现,会自然得到几个判断:
麦克风工作了
系统听见我了
识别链路正常
它大概听对了
所以把实时字幕删掉,不是简单地少一个 UI。
真正的问题是:
这些确定感,要由谁来补?
这时候才发现,本地录音反而打开了另一个能力:主包现在能拿到 PCM 帧了。
于是可以直接算 RMS:
PCM Frame
↓
RMS / Power Level
↓
真实音量
↓
波形
原来 CSS 假动画的波形,终于可以变成跟真实声音联动的反馈。
真音波又把格式问题带了出来
原本本地整包上传时用的是:
mp3 48kbps
体积很小。
但 onFrameRecorded 的 mp3 帧是压缩数据,并不能直接拿来算 RMS。
如果要真音波,最直接的方案就是:
PCM 录
PCM 画
PCM 传
问题也很明显:
PCM 文件比 mp3 大很多,松手后的上传会不会变慢?
于是中间试过不少方案:
| 方案 | 想解决什么 | 最后为什么没走 |
|---|---|---|
| mp3 帧直接算音量 | 不改录音格式 | 压缩块无法真实反映振幅 |
| mp3 + WASM 解码 | 小文件 + 真音波 | 小程序帧边界与稳定性有问题 |
| PCM + lamejs 边录边转 mp3 | 两边都要 | 增依赖、编码开销和录音栈复杂度 |
| PCM 分块预上传 | 降低松手等待 | 为体积引入新的网络状态管理 |
| mp3 服务端算 RMS | 前端不解码 | 波形反馈延迟,失去实时意义 |
其实所有方案都在回答同一个问题:
为了保住 mp3 这个工程优化,值不值得重新增加一整套复杂度?
最后还是靠实测做决定
真正需要验证的是:
PCM 整包上传,在真实场景里到底慢多少?
真机测试:
| 语音长度 | 文件大小 | 网络 | 上传 + 识别 |
|---|---|---|---|
| ~2s | 64KB | WiFi | ~181ms |
| ~9.5s | 298KB | WiFi | ~547ms |
| ~10s | — | 5G | ~678ms |
结果比一开始按带宽估算乐观很多。
对于日常几秒到十几秒的语音:
松手
↓
200~700ms
↓
结果
仍然处于比较自然的「转一下文字」感受。
因此最终没有为了几百毫秒,再引入额外的转码 / 解码链路。
最后变成:
按住
↓
本地 PCM 完整录音
├─ PCM Frame → RMS → 真音波
└─ 本地文件完整保存
松手
↓
整段 PCM → speechToText
↓
直接发送
PCM 录、PCM 画、PCM 传。
回过头看,真正被舍弃的其实只有一个东西
med-agent 放弃的是:
实时字幕。
换回来的是:
- 录音完整性
- 弱网稳定性
- 真音波
- 更简单的状态模型
- 更容易处理取消 / 后台 / 异常
- 更低的工程维护成本
所以这不是「功能做少了」。
而是一次很明确的体验工程 Trade-off:
把增强体验降级,换核心任务更可靠。
哪些体验是绝对不能丢的?
把整个语音输入拆开,会发现反馈其实有不同优先级。
第一层:任务正确性
用户说的话必须真的被录进去。
Correctness
这个不能为了任何动画、字幕或者实时效果牺牲。
第二层:状态可感知
用户必须知道:
现在有没有在录?
系统有没有收到声音?
现在是不是正在识别?
刚才到底有没有发送?
所以:
- Recording 状态
- 输入反馈
- Processing 状态
- Success / Failure
都不能丢。
但具体长什么样,可以变化。
第三层:控制权
用户必须能:
取消
中断
重新说
从失败状态恢复
比如「上滑取消」。
这个 UI 可以简单,但行为不能不可靠。
哪些东西可以降级?
真正可以根据工程成本降级的,反而是表现形式。
例如波形:
28 根真实柱
↓
12 根
↓
5 根
↓
一个跟随音量变化的形状
都可以。
用户真正需要确认的是:
系统有没有收到我的声音。
而不是必须看到 28 根柱子。
实时字幕也是一样:
逐字实时
↓
分句刷新
↓
延迟 Preview
↓
完全不显示
如果它只是用来告诉用户"系统听到了",那完全可以被真音波替代。
只有当语音本身就是一个文字编辑任务时,例如语音写邮件、写文档、填写内容,实时字幕才会从「增强反馈」变成「核心任务对象」。
可以形成一个简单的体验优先级
L0 Correctness
核心任务必须成功
L1 Observability
系统状态必须可信
L2 Control
用户必须能取消、干预、恢复
L3 Responsiveness
尽量即时、稳定、跟手
L4 Enhancement
实时字幕、复杂波形、精细动画
遇到:
- 弱网
- 低端机
- 小程序 Runtime 限制
- 工程成本过高
应该优先从 L4 往上砍。
而不是为了保住一段漂亮动画,牺牲 L0 / L1。
一个更通用的判断方式
以后再遇到复杂交互,可以先问四个问题。
1. 这个反馈是在告诉用户真实系统状态吗?
是的话,尽量保留。
2. 用户会根据这个反馈改变下一步操作吗?
是的话,不能轻易删除。
3. 如果这个反馈是假的,会不会制造错误信心?
会的话,就必须和真实 Runtime 数据绑定。
4. 它只是让体验更实时、更精致、更"爽"吗?
是的话,可以降级。
所以:
录音状态 不能丢
取消状态 不能丢
识别状态 不能丢
失败恢复 不能丢
真实输入反馈 不能丢
实时字幕 可以降级
波形精度 可以降级
动画复杂度 可以降级
逐字刷新 可以降级
还能继续走一步:Preview 和 Commit 分离
ai-ask 与 med-agent 其实还存在一个中间路线。
不是:
流式
vs
整包
二选一。
而是:
┌→ RMS → 真音波
│
Local PCM ──────────┼→ Streaming ASR → Preview
│
└→ 完整 PCM
↓
松手
↓
Final ASR
↓
Commit
关键在于:
实时 ASR 不再作为最终结果的 Source of Truth。
它只是 Preview。
这样:
WS 正常
→ 有实时字幕
WS 慢
→ 字幕晚一点
WS 挂
→ 不显示字幕
本地 PCM
→ 始终完整
Final ASR
→ 始终以完整文件为准
增强体验可以失败,但核心任务不会一起失败。
小结
这次语音输入真正值得留下来的,不只是 PCM、RMS 或 WebSocket 这些实现细节。
更重要的是一次体验优先级的判断:
核心任务不降级,反馈形式可以降级;系统状态不能骗人,增强能力允许失败。
实时字幕、真音波、漂亮动画都是手段。
真正不能丢的是:
用户知道发生了什么、知道自己的操作有没有成功、始终拥有控制权,并且系统失败后还能回来。
这可能才是体验工程和单纯「把设计效果实现出来」之间最大的区别。