行业资讯
AI对话打字机效果卡顿崩溃?1套Vue3流式代码搞定逐Token输出,彻底解决断包报错
做AI对话时我被流式输出坑到崩溃之前开发AI聊天页面想实现文字逐字弹出的打字机效果踩满一堆致命问题网络分片导致JSON半截页面直接报错白屏中文多字节截断渲染一堆乱码方块分不清delta.content和完整message.content拿不到输出文字不处理[DONE]结束标记循环卡死页面。试过EventSource但它只支持GET请求没法携带鉴权Header和长上下文body完全不适合大模型接口。最后用原生fetch ReadableStream手撸流式解析兼容POST、自带缓冲区兜底几十行代码稳定跑通DeepSeek、OpenAI全系接口。读完这篇你能收获彻底搞懂LLM流式输出底层原理二进制流、SSE协议、ReadableStream完整可复制Vue3script setup实战页面支持切换流式/一次性返回生产环境必避5大高频坑断包、乱码、解析失败全覆盖缓冲区buffer核心逻辑拆解看懂为什么必须缓存半截数据先搞懂大模型流式输出底层是什么1. 普通接口 vs 流式接口普通请求模型完整生成全部文字后一次性返回完整JSON用户只能干等体验极差。流式stream请求参数设置stream: true模型每生成1个Token就立刻推送一段二进制数据前端边收边渲染实现打字机效果。2. 后端返回数据流格式后端返回二进制Uint8Array字节流遵循SSE规范每条消息以data:开头\n换行符分隔单次网络包可能返回1行/多行/半行数据分片边界完全随机流结束会单独推送一行data: [DONE]标记终止单条消息内部是JSON包含增量文本choices[0].delta.content。3. 前端核心处理链路二进制Uint8Array → TextDecoder转文本 → buffer拼接残缺分片 → 按\n分割完整行 → JSON解析 → 增量追加到页面响应式变量。完整实战Vue3 DeepSeek流式对话页面1. 环境准备Vite Vue3项目根目录新建.env.local存放密钥避免硬编码泄露VITE_DEEPSEEK_API_KEY你的DeepSeek密钥2. 完整可运行 App.vue 代码script setup // vue3 composition API逻辑内聚热更新局部刷新 import { ref } from vue; // 页面响应式状态 const question ref(讲一个中国龙的故事); const content ref(); const stream ref(true); // 开关流式输出/一次性返回 // 核心请求函数 const update async () { if (!question.value.trim()) return; content.value 思考中...; // 大模型流式接口地址 const endpoint https://api.deepseek.com/chat/completions; const headers { Content-Type: application/json, Authorization: Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY} }; const response await fetch(endpoint, { method: POST, headers, body: JSON.stringify({ model: deepseek-v4-flash, messages: [ { role: user, content: question.value } ], stream: stream.value // 开启流式开关 }) }); // 流式模式逐Token打字机效果 if (stream.value) { content.value ; // 获取二进制流读取器水管取水逻辑 const reader response.body?.getReader(); // 二进制转UTF8文本解码器stream:true解决中文截断乱码 const decoder new TextDecoder(utf-8, { stream: true }); let done false; // buffer缓存上一轮未解析完整的半截JSON行解决断包报错 let buffer ; while (!done) { // 读取一小块二进制数据无数据时await阻塞等待 const { value, done: doneReading } await reader?.read(); done doneReading; if (!value) continue; // 拼接上一轮残留buffer 本轮新解码文本 const chunkValue buffer decoder.decode(value); buffer ; // 已拼接完成清空缓冲区 // 按换行分割只保留以data:开头的有效SSE行 const lines chunkValue.split(\n).filter(line line.startsWith(data: )); for (const line of lines) { // 切掉前缀 data: 6个字符 const incoming line.slice(6); // 流结束标记终止循环 if (incoming [DONE]) { done true; break; } try { // 正常完整JSON直接解析 const data JSON.parse(incoming); const deltaText data.choices[0].delta.content; if (deltaText) { // 增量追加Vue细粒度响应式局部更新 content.value deltaText; } } catch (err) { // JSON不完整存入buffer下一轮读取拼接后再解析 buffer data: ${incoming}; } } } } else { // 非流式等待全部生成完毕一次性渲染 const data await response.json(); content.value data.choices[0].message.content; } }; /script template div classcontainer div classinput-bar label提问/label input classinput v-modelquestion placeholder输入你的问题 / button clickupdate发送提问/button /div div classstream-switch label开启流式输出(打字机效果)/label input typecheckbox v-modelstream / /div div classoutput-box h4AI回答/h4 div classanswer-text{{ content }}/div /div /div /template style scoped .container { display: flex; flex-direction: column; gap: 12px; padding: 20px; height: 100vh; font-size: 0.9rem; } .input-bar { display: flex; align-items: center; gap: 8px; } .input { width: 320px; padding: 6px 8px; } .stream-switch { display: flex; align-items: center; gap: 6px; } .output-box { margin-top: 10px; width: 100%; } .answer-text { min-height: 300px; padding: 12px; border: 1px solid #eee; border-radius: 6px; white-space: pre-wrap; } button { padding: 6px 14px; cursor: pointer; } /style核心代码关键点拆解stream: true请求参数大模型接口强制开启流式返回关闭则一次性返回完整回答。ReadableStream getReader()浏览器原生流式API像水管一样分段读取二进制数据不用等全部响应下载完成。TextDecoder(utf-8, { stream: true })解决中文多字节截断乱码解码器会缓存跨分片的残缺字符下次拼接完整解码。buffer缓冲区变量网络分片可能把一行JSON拦腰切断解析直接报错残缺行存入buffer下一轮读取拼接后再解析。delta.content增量文本流式专用增量字段不要误用一次性返回的message.content会拿不到文字。[DONE]结束标记必须提前判断否则JSON.parse([DONE])直接抛出语法错误页面卡死。⚠️流式开发5个致命坑完整避坑方案坑1网络分片截断JSON页面频繁报错现象控制台持续SyntaxError: Unexpected end of JSON input原因一次网络包只传输半行data: {}直接解析半截JSON。✅ 方案维护buffer缓冲区捕获JSON解析异常时存入残缺字符串下一轮拼接完整再处理。坑2中文渲染出现乱码方块 现象中文偶尔变成问号/方块乱码英文正常。原因UTF-8中文占3字节分片刚好切在字符中间解码器无缓存直接解码。✅ 方案new TextDecoder(utf-8, { stream: true })开启流式解码缓存。坑3EventSource无法携带POST请求与鉴权Header现象想用EventSource简化代码但是接口需要传body、token鉴权。✅ 方案放弃EventSource使用fetch ReadableStream原生支持POST、自定义请求头。坑4混淆delta.content与message.content拿不到输出文字现象流式模式下页面空白无任何文字输出。原因一次性返回用message.content流式增量只能取choices[0].delta.content。✅ 方案两种分支分开处理流式逻辑只读取delta增量文本。坑5不处理[DONE]标记循环无限阻塞现象AI输出完成后页面卡死无法再次发送提问。原因流结束会单独推送data: [DONE]直接丢给JSON.parse会报错循环无法退出。✅ 方案切片后优先判断incoming [DONE]直接标记done终止循环。拓展流式输出能落地哪些场景推荐使用流式输出AI对话、知识库问答、RAG智能检索页面长文本生成文案、小说、方案实时预览实时日志、服务端进度推送、文件流式上传下载。不推荐使用流式输出短文本一次性查询字符极少没必要增加解析复杂度低版本老旧浏览器不支持ReadableStream需降级一次性返回后台纯接口同步处理不需要前端实时渲染反馈。全文总结LLM流式输出本质是HTTP分块二进制流通过ReadableStream实现边收边渲染大幅提升用户等待体验生产级流式解析三要素TextDecoder流式解码、buffer残缺分片缓存、[DONE]结束标记判断Vue3用Composition API维护响应式文本每次增量追加只局部更新DOM性能无损耗避开断包、乱码、字段混淆、循环卡死四大核心坑代码可直接对接OpenAI/DeepSeek/通义千问等主流模型接口流式与一次性返回双分支兼容一套页面支持两种交互模式适配不同业务需求。
郑州网站建设
网页设计
企业官网