为什么流式 AI 响应感觉比实际更快 (Android + SSE)
Source: Dev.to
(请提供您希望翻译的正文内容,我将按照要求保留原始链接、格式和技术术语,仅翻译文本部分。)
真正的问题:AI 聊天应用 感觉 慢
当用户发送消息,而界面哪怕短暂保持空白时,大脑会把这种沉默解读为延迟。
从用户的视角
- 我的消息发送成功了吗?
- 应用卡住了吗?
- 模型运行慢吗?
在大多数情况下,这些都不是真的。但感知往往比现实更重要。
AI 应用中的延迟首先是心理层面的,而非技术层面的。
Source:
为什么等待完整响应会破坏用户体验
许多 AI 聊天应用遵循一个简单的模式:
- 发送提示
- 等待完整响应
- 一次性渲染所有内容
从技术上讲这可以工作,但从用户体验的角度来看,它会失败。人类对交互系统中的沉默极为敏感。即使是几百毫秒没有可见反馈也会产生不确定感。加载动画有所帮助,但仍然让人感觉与响应本身脱节。
实际延迟 → 系统实际花费的时间
感知延迟 → 人们感觉花费的时间
大多数 AI 应用优化前者,却忽视了后者。
流式传输是显而易见的解决方案,但仍不足
逐 token 流式返回可以立刻提升响应速度。只要文字开始出现,用户就会感知到:
- 系统正在工作
- 已收到他们的输入
- 正在进行进度
Server‑Sent Events (SSE) 等技术让实现这一点变得简单。
然而,朴素的流式传输会带来新问题。现代模型生成文本的速度极快。随着 token 到达而渲染会导致:
- 文本更新呈突发式
- 句子形成时出现抖动
- 阅读流畅性被打断
完整的单词或从句可能一次性出现,破坏自然的阅读节奏。界面虽然变快,却会让人感到疲惫。流式传输解决了 速度,但如果不慎使用,可能会损害 可读性。
核心洞见:将网络速度与视觉速度解耦
网络速度和人类阅读速度根本不同。
- 服务器以毫秒为单位运行
- 人类以块、停顿和模式阅读
如果 UI 完全镜像网络,用户就被迫适应机器行为。更好的做法是相反:
让 UI 适应人类,而不是服务器。
不要立即渲染文本:
- 将传入的 token 缓冲
- UI 以受控的节奏消费它们
体验会显得平静、刻意且易读。
为实现此目标,我引入了 StreamingTextController,这是一个小但关键的层,位于网络与 UI 之间。流式传输不仅仅是更早显示文本;更在于以 合适的节奏 显示它。
StreamingTextController 的工作原理(概念)
StreamingTextController 将 到达速度 与 渲染速度 分离。将此逻辑放在 ViewModel 之外,可防止时序问题泄漏到状态管理中。
- 通过 SSE 接收 Token
- Token 被缓冲
- 以稳定、符合人类阅读习惯的速率进行受控消费
- 通过状态更新实现渐进式 UI 渲染
从 UI 的视角来看:
- 文本平滑增长
- 句子自然形成
- 网络波动不可见
这与人类处理信息的方式相似:
- 我们以突发的方式阅读,而不是逐字符
- 可预测的节奏提升理解力
- 减少抖动降低认知负荷
此控制器 不 是
- 不是打字动画
- 不是人为的延迟
- 不是对慢速模型的变通办法
它是一个 UX 边界,将机器输出转化为人类交互。
Source:
架构决策:让流式处理达到生产就绪水平
流式处理只有在长期保持稳定且可测试时才有意义。职责划分明确:
- 网络层 → 发出原始 token
- StreamingTextController → 节奏控制与缓冲
- ViewModel(MVVM) → 生命周期与不可变状态
- UI(Jetpack Compose) → 声明式渲染
有意使用的技术
- Kotlin Coroutines + Flow
- Jetpack Compose
- Hilt
- Clean Architecture
目标不是新奇,而是 在负载和不同设备下的可预测行为。

构建流式 UI 时的常见错误
- 在每个 token 上更新 UI
- 将渲染速度绑定到模型速度
- 没有缓冲或背压
- 在 UI 代码中嵌入时序逻辑
- 将流式视为动画
流式处理不是关于视觉炫耀。它旨在 降低认知负荷.
超越聊天应用
相同的原则适用于:
- 实时转录
- AI 摘要
- 代码助手
- 搜索解释器
- 多模态副驾驶
随着 AI 系统变得更快,用户体验——而非模型速度——成为差异化因素。
演示与源代码
该项目是开源的,旨在作为参考实现。它包括:
- SSE 流式设置
StreamingTextController- Jetpack Compose 聊天 UI
- 干净的、可用于生产的结构
关键要点
- 用户并不在乎你的模型有多快。
- 他们在乎你的产品的感受有多快。
流式传输降低不确定性。
- 节奏恢复清晰度。
优秀的 AI 用户体验位于两者的交叉点。
