一:技术原理
采用Server-Sent Events (SSE) 技术实现AI助手的流式输出,具体原理如下:
- 流式响应机制 :通过设置API请求的
stream: true参数,使通义千问API以流式方式返回数据 - ReadableStream处理 :使用Fetch API的
response.body.getReader()获取可读流 - 实时数据解析:通过TextDecoder逐块解码服务器返回的数据
- 增量UI更新:通过回调函数实时更新前端界面,实现打字机效果
- 节流优化:通过节流函数控制DOM更新频率,减少性能消耗
二:遇到的问题:大模型流式响应渲染卡顿
1.场景:用户提问后,大模型返回长文本回答(比如几百上千字的技术说明、流程介绍),需要在聊天框里做逐字流式输出时,页面会出现明显的卡顿和布局抖动。
2.具体表现是:
- 渲染卡顿:每收到一个 token 就触发一次 DOM更新,当模型返回速度快、文字量大时,短时间内高频触发重渲染,主线程被占满,页面会掉帧、输入框无法操作,甚至整个页面卡死几秒。
- 布局抖动:因为聊天框是自适应高度,逐字拼接会导致高度频繁变化,页面其他元素跟着一起上下跳,用户体验很差。
- 性能累积问题:当对话轮数多、历史消息量大时,DOM 节点越来越多,后面的流式渲染会越来越卡,甚至出现浏览器内存占用过高的情况。
3.解决方案:
- 节流 + 批量渲染:把收到的 token 先缓存起来,每 10-20ms 做一次批量更新
DOM,减少重渲染次数,主线程压力直接降下来了。 - 固定容器 + 虚拟滚动:给聊天框设置最大高度 + 虚拟滚动,避免 DOM 节点无限增长,也解决了布局抖动问题。
- 使用requestIdleCallback:把非紧急的渲染任务放到浏览器空闲时间执行,不阻塞主线程。
三:核心实现代码
1. API层实现 (src/api/chat.js)
javascript
/**
* 发送聊天消息到通义千问 API
* @param {Array} messages - 消息数组
* @param {string} model - 模型名称
* @param {Function} onStream - 流式响应回调函数
* @returns {Promise}
*/
export async function sendChatMessage(messages, model = 'qwen-vl-plus', onStream = null) {
const requestBody = {
model,
messages,
stream: !!onStream // 动态设置stream参数
}
if (onStream) {
// 流式响应处理
const apiKey = import.meta.env.VITE_QWEN_API_KEY || localStorage.getItem('qwen_api_key')
const response = await fetch(`${QWEN_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify(requestBody)
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let result = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
const lines = chunk.split('\n').filter(line => line.trim() !== '')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6)
if (data === '[DONE]') {
onStream(result, true)
return result
}
try {
const parsed = JSON.parse(data)
const content = parsed.choices[0]?.delta?.content || ''
result += content
onStream(result, false)
} catch (e) {
// 忽略解析错误
}
}
}
}
return result
} else {
// 非流式响应
const response = await qwenRequest.post('/chat/completions', requestBody)
return response.choices[0]?.message?.content || ''
}
}
2. 前端层实现 (src/views/mine/Chat.vue)
javascript
// 节流函数
const throttle = (func, delay) => {
let timeoutId = null
return (...args) => {
if (timeoutId) return
timeoutId = setTimeout(() => {
func(...args)
timeoutId = null
}, delay)
}
}
// 批量渲染函数(节流处理)
const batchRender = throttle((content, done, lastMessage) => {
if (lastMessage?.role === 'assistant') {
lastMessage.content = content
} else {
messages.value.push({
role: 'assistant',
content: content,
time: new Date().toLocaleTimeString()
})
}
scrollToBottom()
if (done) {
// 保存消息到本地存储
saveMessages()
}
}, 15) // 15ms节流,平衡响应速度和性能
// 发送消息
const sendMessage = async () => {
if (!inputMessage.value.trim() || loading.value) return
const userMessage = inputMessage.value.trim()
inputMessage.value = ''
// 添加用户消息
messages.value.push({
role: 'user',
content: userMessage,
time: new Date().toLocaleTimeString()
})
scrollToBottom()
loading.value = true
try {
// 构建消息历史
const chatMessages = [
{ role: 'system', content: '你是一个有帮助的 AI 助手,请用中文回答用户的问题。' },
...messages.value.map(m => ({
role: m.role,
content: m.content
}))
]
// 流式响应
let assistantContent = ''
await sendChatMessage(chatMessages, 'qwen-plus', (content, done) => {
assistantContent = content
// 更新或添加助手消息(批量渲染)
const lastMessage = messages.value[messages.value.length - 1]
if (done) {
// 完成时立即更新,确保所有内容都显示
if (lastMessage?.role === 'assistant') {
lastMessage.content = content
} else {
messages.value.push({
role: 'assistant',
content: content,
time: new Date().toLocaleTimeString()
})
}
scrollToBottom()
saveMessages()
} else {
// 未完成时使用节流批量渲染
batchRender(content, done, lastMessage)
}
})
} catch (error) {
// 错误处理...
} finally {
loading.value = false
}
}
四:流式输出与非流式输出基本原理
1.流式输出:逐步生成的实时交互
流式输出是一种增量式的数据传输方式,它允许大模型在生成内容的同时,将已生成的部分立即发送给客户端,而不必等待整个响应完成。这种方式的核心特点是:
- 实时性:模型生成一小段内容就立即传输,用户几乎可以实时看到生成过程
- 增量传输:通过 SSE(Server-Sent Events)或 WebSocket 协议实现服务器到客户端的持续数据流
- 低感知延迟:用户通常在 100ms 内就能看到首批内容,大幅降低等待感
2.非流式输出:完整生成的一次性返回
非流式输出采用传统的请求-响应模式,模型会等待完整内容生成后再一次性返回给客户端:
- 完整性:返回的是经过完全处理和验证的完整响应
- 单次传输:采用标准 HTTP 请求-响应模式,一次性传输所有数据
- 等待时间:用户需要等待整个生成过程完成(可能需要数秒甚至更长)
3.流式输出与非流式输出技术实现差异

4.流式输出常见问题
-
问:流式输出会增加 API 调用成本吗?
答:从 token 计费角度看,流式输出与非流式输出的成本相同,都是基于生成的 token 数量计费。但从基础设施角度,流式输出可能会增加服务器连接维护成本,特别是在高并发场景下。
-
问:流式输出是否会影响模型生成的质量?
答:不会。流式输出只是改变了内容传输的方式,不会影响模型生成内容的质量或完整性。模型的思考过程和生成结果与非流式模式相同。
-
问:如何处理流式输出中的连接中断问题?
答:应实现重连机制,包括:保存已接收内容的状态、设置合理的超时参数、实现指数退避重试策略,以及在客户端提供友好的错误提示和恢复选项。
5.非流式输出常见问题
-
问:如何优化非流式输出的等待体验?
答:可以通过实现加载动画、分阶段请求、提供取消选项、预估完成时间等方式改善用户等待体验。对于特别长的生成任务,可以考虑异步处理并通知用户。
-
问:非流式输出是否更适合移动应用?
答:通常是的。非流式输出对网络连接的要求较低,且资源消耗更可控,更适合移动环境。但如果用户体验是首要考虑因素,且网络条件允许,流式输出仍然可以在移动应用中提供更好的交互体验。
-
问:如何处理非流式输出中的超时问题?
答:设置合理的超时参数、实现请求重试机制、考虑将大型请求拆分为多个小请求,以及在服务端优化处理速度都是有效的策略。
五:SSE 和 WebSocket 的区别
SSE(Server-Sent Events)
基于标准 HTTP/HTTPS 协议,通过一个普通的 GET 请求建立连接。
是单向通信:只能由服务端向客户端推送数据,客户端只能被动接收,不能主动发送消息。
WebSocket
基于 TCP 协议,通过 HTTP 握手建立连接后,升级为 WebSocket 协议。
是全双工通信:客户端和服务端可以同时、双向发送数据。