上篇我们搭建了基于 Vue 3 的交互界面,本篇将聚焦核心技术:如何接收并解析大模型的流式响应。这涉及浏览器底层的流处理能力、二进制数据解码,以及对不完整 JSON 片段的容错拼接。
发起流式请求
调用支持流式输出的大模型 API(如 DeepSeek)时,只需在请求体中设置 stream: true:
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
model: 'deepseek-chat',
stream: true,
messages: [{ role: 'user', content: question.value }]
})
})
关键在于,此时 response.body 不再是一个完整的 JSON 对象,而是一个 ReadableStream——一个可被逐步读取的数据流。
读取二进制流
浏览器通过 getReader() 获取流的读取器,每次 read() 返回一个包含 value(Uint8Array 二进制数据)和 done(是否结束)的对象。
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
// ...
}
由于网络传输以二进制为单位,我们需要使用 TextDecoder 将 Uint8Array 解码为可读字符串。这是 HTML5 提供的标准 API,兼容性良好。
处理 SSE 格式的分块数据
主流 LLM 的流式接口通常遵循 Server-Sent Events (SSE) 协议:每条消息以 data: {...}\n\n 格式发送。但一次 read() 可能包含多个完整消息,也可能只包含半个 JSON 字符串。
因此,我们必须维护一个 buffer,用于暂存未解析完的片段:
let buffer = '';
const chunkValue = buffer + decoder.decode(value);
buffer = '';
const lines = chunkValue.split('\n')
.filter(line => line.startsWith('data: '));
我们将当前块与缓冲区拼接,按行分割,并筛选出以 data: 开头的有效行。
安全解析 JSON 片段
对每一行 data: {...},我们尝试解析其后的 JSON 内容:
for (const line of lines) {
const incoming = line.slice(6);
if (incoming === '[DONE]') break;
try {
const data = JSON.parse(incoming);
const delta = data.choices[0].delta.content;
if (delta) content.value += delta;
} catch (err) {
buffer += `data: ${incoming}`;
}
}
若解析失败(说明 JSON 不完整),则将该行重新放回 buffer,等待下一次读取补全。这种“试错+回退”机制,确保了即使在网络抖动或分块不均的情况下,也能正确拼接出完整内容。
非流式模式的兼容处理
为便于对比,我们保留非流式选项:
if (!stream.value) {
const data = await response.json();
content.value = data.choices[0].message.content;
}
这展示了同一接口在不同模式下的处理差异:流式注重增量更新,非流式追求一次性获取。两者共存,方便调试与体验对比。
总结
通过结合 Vue 3 的响应式系统与浏览器原生的流处理能力,我们成功实现了一个支持 AI 流式输出的前端应用。整个过程无需第三方库,仅依赖标准 Web API,体现了现代浏览器的强大能力。
更重要的是,这种实现方式具有普适性——无论是 OpenAI、DeepSeek 还是其他兼容 SSE 的 LLM 接口,只需调整字段路径,即可无缝接入。掌握这一模式,意味着你已具备构建下一代智能交互界面的核心技能。