AI助手流式输出实现方案

一:技术原理

采用Server-Sent Events (SSE) 技术实现AI助手的流式输出,具体原理如下:

  1. 流式响应机制 :通过设置API请求的 stream: true 参数,使通义千问API以流式方式返回数据
  2. ReadableStream处理 :使用Fetch API的 response.body.getReader() 获取可读流
  3. 实时数据解析:通过TextDecoder逐块解码服务器返回的数据
  4. 增量UI更新:通过回调函数实时更新前端界面,实现打字机效果
  5. 节流优化:通过节流函数控制DOM更新频率,减少性能消耗

二:遇到的问题:大模型流式响应渲染卡顿

1.场景:用户提问后,大模型返回长文本回答(比如几百上千字的技术说明、流程介绍),需要在聊天框里做逐字流式输出时,页面会出现明显的卡顿和布局抖动。

2.具体表现是:

  1. 渲染卡顿:每收到一个 token 就触发一次 DOM更新,当模型返回速度快、文字量大时,短时间内高频触发重渲染,主线程被占满,页面会掉帧、输入框无法操作,甚至整个页面卡死几秒。
  2. 布局抖动:因为聊天框是自适应高度,逐字拼接会导致高度频繁变化,页面其他元素跟着一起上下跳,用户体验很差。
  3. 性能累积问题:当对话轮数多、历史消息量大时,DOM 节点越来越多,后面的流式渲染会越来越卡,甚至出现浏览器内存占用过高的情况。

3.解决方案:

  1. 节流 + 批量渲染:把收到的 token 先缓存起来,每 10-20ms 做一次批量更新
    DOM,减少重渲染次数,主线程压力直接降下来了。
  2. 固定容器 + 虚拟滚动:给聊天框设置最大高度 + 虚拟滚动,避免 DOM 节点无限增长,也解决了布局抖动问题。
  3. 使用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 协议。

是全双工通信:客户端和服务端可以同时、双向发送数据。

参考:大模型 API 调用中的流式输出与非流式输出全面对比:原理、场景与最佳实践

相关推荐
Huang2601082 小时前
Claude Code:让编程变得更简单的 VS Code 插件
ai
考勤技术解析2 小时前
外包技术人员打卡管理的技术痛点与轻量化解决方案
大数据·人工智能·ai
ofoxcoding3 小时前
DeepSeek V4 预览版实测:Agent、世界知识、推理能力,跟 V3 和 GPT-5.5/Claude 4.6 比到底什么水平?
大数据·人工智能·gpt·ai
huisheng_qaq3 小时前
【01-AI入门篇】深入理解AI感知智能和认知智能
人工智能·ai·chatgpt·认知智能·感知智能
薛定谔的猫3693 小时前
深入浅出 MCP (Model Context Protocol):开启 AI Agent 的标准化连接时代
ai·llm·agent·技术分享·mcp
老陈跨境记3 小时前
电商出海效率革命:萤火AI批量图片翻译的技术原理与实战测评
人工智能·ai
leikooo3 小时前
Skills 实战:Unsplash → COS 自动化配图
运维·ai·自动化
企业架构师老王3 小时前
注册审批申报材料自动校验:如何利用实在Agent构建非侵入式架构并降低数据误报率?
大数据·人工智能·ai·架构
陌殇殇3 小时前
004 Spring AI Alibaba框架整合百炼大模型平台 — MCP服务
java·spring·ai