你有没有这样的体验:在对话框里敲下一行字,然后盯着屏幕,等待AI的回复像一篇文章一样"啪"地整段出现?那种空白期间的焦虑,就像看着网页加载的转圈圈,一秒像一年。
但如果换一种方式:AI像一个打字员,一边思考一边把答案一个字一个字敲给你看,你的感受会完全不同——你不再干等,而是跟着它的思路走,甚至能提前捕捉到它的语气和倾向。这就是流式输出的魔力。
今天,我想和你聊聊我们在AI客服产品中,如何用技术把"等一屏字"变成"边打边看",以及背后那些让你感觉"又快又稳"的小心机。
一、感知速度:比快更快的心理学
先说一个反常识的事实:同样一段话,逐字出现比一次性出现,用户会觉得更快。
为什么?因为用户的等待心理被"填充"了。一次性加载时,用户看到的是从0到100%的跃迁,中间是未知的空白;而逐字出现时,用户始终有东西可看,大脑在持续处理信息,等待感被稀释了。
所以,流式输出的核心目标不是真的把延迟降到0(这不可能),而是用连续反馈掩盖延迟,让用户感觉系统"正在努力思考",而不是"死机了"。
二、选型:为什么是SSE,而不是WebSocket?
很多同学第一反应:实时推送?上WebSocket啊!但我们在项目里选了Server-Sent Events(SSE)。
原因很简单:客服场景是"一问一答"的半双工通信,不是实时游戏那种全双工。
具体实现上,我们用了微软的 `@microsoft/fetch-event-source` 库。它支持用POST发起请求,建立单向下行连接,完美适配我们"发送问题->接收流式回复"的场景。
而且,不同的业务(普通问答、翻译、菜单OCR、推荐)虽然接口不同,但都复用了同一套流式接收逻辑——一次封装,到处复用,这是代码洁癖者的福音。
三、三条主线:连接、解析、渲染
流式输出的实现可以拆成三个核心环节,每个环节都有坑,也有巧思。
1. 连接与请求控制:别让多条消息打架
想象一下:用户问了一个问题,AI正在逐字回复,结果用户不耐烦又发了一条。这时候如果同时存在两个流,UI就会乱套——A的回复和B的回复混在一起,神仙都分不清。
我们的解决方案很简单:每次发送新消息前,先中止上一次的请求。
利用浏览器的 `AbortController`,在发起新请求时调用 `abort()`,保证同一时刻只有一条有效连接。这样既避免竞态,也防止重复订阅导致的性能浪费。
// 伪代码示意
let abortController = null;
async function sendMessage(text) {
if (abortController) abortController.abort();
abortController = new AbortController();
await fetchEventSource('/api/chat', {
signal: abortController.signal,
onmessage: handleMessage
});
}2. 数据解析:从字节流到有意义的片段
服务端推送的是SSE格式的数据,我们需要在 `onmessage` 里解析。我们约定每个消息体是一个JSON,包含 `event` 和 `data` 字段:
此外,还要处理转义字符。比如某些接口返回 `\u4e2d\u6587` 这种Unicode,需要用 `decodeUnicode()` 转换成可读的中文。
这一步的核心是稳定地切分数据流,把乱糟糟的字节拼成一条条完整的消息。
3. 文本累积与UI更新:别让用户看到跳帧
收到一个chunk后,我们需要把内容追加到当前正在显示的AI消息上。这里有两个关键点:
为了让不同业务共用同一套UI,我们把"追加逻辑"抽象成 `addAIMessage(data, 'ai', isFirst)`,内部根据数据类型决定是替换还是追加。这样,无论后端是实时流还是一次性返回,前端都能正确渲染。
四、差异化策略:同一个"流",不同的"戏法"
虽然都叫流式输出,但不同业务后端的实现天差地别。我们做了针对性处理:
但这有个坑:频繁的 `setTimeout` 会阻塞事件循环,如果答案很长,用户会感觉卡顿。更好的做法是用字符队列 + `requestAnimationFrame` 非阻塞渲染,这是我们后续优化的方向。
你看,同样是"流",后端不给力时,前端就得自己演。但演也要演得专业,不能让用户察觉。
五、小结与可优化点
目前这套方案已经在线上平稳运行,优点明显:
当然,还有改进空间:
六、写在最后
流式输出的本质,是在网络和算力延迟的现实下,用"边算边送、边送边显"把等待感转移给后端和网络,让用户在前端几乎感觉不到等待。
这不仅是技术实现,更是对用户体验的深刻理解。就像你和朋友聊天,如果对方总是等想好全部才开口,你会觉得他反应慢;但如果他一边想一边说,你会觉得他思维敏捷,甚至能参与他的思考过程。
我们做的,就是让AI学会"一边想一边说"。而这背后,是前端对网络、对渲染、对人性的把控。
希望这篇文章能给你一些启发。如果你也在做类似的功能,欢迎交流,我们一起踩坑,一起进步。