Vue3 + DeepSeek 流式输出:让 AI 回答“边说边写”,别再让用户干等了!

64 阅读4分钟

从 Buffer 到 TextEncoder,从等待焦虑到丝滑如德芙——聊聊前端如何优雅地“接住”AI 的口水话


一、用户不是在等答案,是在等“煎饼果子加几个蛋”

想象一下:你问 AI:“今天适合穿短袖吗?”
结果它沉思三秒,仿佛在推演宇宙命运……
然后,“唰”地一下,整段回答砸你脸上。

这体验,就像你点了个煎饼果子,摊主默默盯着面糊发呆,三分钟后突然把整个摊子塞给你——连锅带铲。

用户要的是“实时感” ,不是“憋大招”。

于是,流式输出(Streaming) 登场了!它让 AI 像真人聊天一样,边想边说,边说边显示。你说一句,它回半句;你刚打完字,它已经开始蹦出第一个字了。

而今天,我们要用 Vue3 + DeepSeek ,配合前端的 Buffer 和 TextEncoder/Decoder,打造一套丝滑到能当洗发水广告的流式交互体验。


二、DeepSeek 说了啥?其实它吐的是“二进制口水”

当你调用 DeepSeek 的流式接口(比如设置 stream: true),后端不会一次性返回完整 JSON,而是通过 HTTP 的 chunked transfer encoding分块推送数据

这些数据块长什么样?
不是字符串,不是 JSON 对象,而是 原始的二进制字节流(Uint8Array)

是的,AI 的智慧,在网络里就是一串 0 和 1 的排列组合,跟你的微信聊天记录本质上没区别——都是字节。

但浏览器收到的是 Raw Bytes,不能直接塞进 <div> 里显示。怎么办?

这时候,HTML5 的 TextEncoder 和 TextDecoder 就像翻译官登场了:

  • TextEncoder:把人类语言(字符串) → 编码成 Uint8Array(给机器看)
  • TextDecoder:把 Uint8Array(机器语) → 解码成人类语言(字符串)

但注意!在流式场景中,我们只用 TextDecoder,因为我们要把 AI 吐出来的二进制“口水”翻译成人话。


三、Buffer:前端的“临时口水盆”

在接收流式数据时,数据是分批到达的。可能第一块只有 "你好" 的前两个字节,第二块才补全。

如果直接 decode 每一块,可能会遇到 “截断字符” 的问题(尤其是 UTF-8 中文,一个字占 2~4 字节)。比如:

// 错误示范
const decoder = new TextDecoder();
decoder.decode(new Uint8Array([0xE4, 0xBD])) // ❌ 不完整的 UTF-8,会变成 

所以,我们需要一个 缓冲区(Buffer) ,把所有收到的字节先存起来,等凑够一个完整字符再解码

在浏览器中,我们可以用:

  • ArrayBuffer + Uint8Array 手动管理
  • 或更聪明的做法:用 TextDecoder 的 stream 模式
const decoder = new TextDecoder('utf-8', { stream: true });

// 每次收到 chunk,直接 decode
let text = decoder.decode(chunk, { stream: true });
// 最后一次调用时不加 stream,表示结束
let final = decoder.decode(); 

这个 { stream: true } 参数,就是 TextDecoder 的“智能拼图模式”——它会自动缓存不完整的字节,等下一块来了再拼!

这就像你吃火锅,毛肚七上八下,TextDecoder 会帮你数着“还差两下”,绝不让你吃到半生不熟的尴尬。


四、Vue3 如何优雅“接住”AI 的口水?

假设我们用 fetch 调用 DeepSeek 的流式接口:

const response = await fetch('/api/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ 
    prompt: "讲个笑话", 
    stream: true 
  })
});

if (!response.body) throw new Error('ReadableStream not supported');

const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8', { stream: true });
let fullText = '';

// 在 Vue3 的 setup() 或方法中
const message = ref('');

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  // 解码当前 chunk
  const chunkText = decoder.decode(value, { stream: true });
  fullText += chunkText;
  
  // 响应式更新!Vue3 自动触发 DOM 更新
  message.value = fullText;
}

// 最后 flush 剩余缓冲
message.value += decoder.decode();

关键点:

  1. ref 响应式变量:每次 message.value 赋值,Vue3 自动更新视图。
  2. TextDecoder({ stream: true }) :避免乱码,智能拼接。
  3. while + await reader.read() :逐块读取,边读边显示。

效果?用户看到文字像打字机一样“哒哒哒”蹦出来,心理等待时间缩短 70% (心理学研究:流动的信息比静止的“加载中”更让人安心)。


五、为什么 Streaming 是用户体验的“作弊器”?

场景非流式流式
用户提问等待 3 秒 → 全文出现0.2 秒 → 第一个字出现,持续输出
心理感受“它是不是卡了?”“哇,它在认真思考!”
可中断性必须等完才能操作看到一半不想听了?直接点停止!
带宽压力一次性传输大 JSON小块传输,更省流量

流式输出,本质是“时间切片”的艺术——把 AI 的思考过程可视化,让用户参与进来,而不是被动等待。


六、Bonus:别忘了错误处理和 Loading 状态!

现实世界总有网络抖动、AI 抽风。记得加上:

try {
  // 上述流式逻辑
} catch (err) {
  message.value += `\n[系统错误: ${err.message}]`;
} finally {
  isLoading.value = false;
}

还可以加个“打字机音效”(开玩笑的,别真加,用户会骂你)。


七、结语:让 AI 像人一样“说话”

技术的本质,是消除等待的焦虑

通过 Vue3 的响应式 + DeepSeek 的流式接口 + TextDecoder 的智能解码,我们不仅实现了功能,更重塑了人与 AI 的对话节奏。

下次当你看到屏幕上文字一个字一个字蹦出来时,请记住:

那不是代码在跑,是 AI 在对你眨眼睛。