普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月28日首页

如何在老项目中使用AI实现智能问答

作者 菜鸟shuai
2026年2月28日 10:07

老树发新芽:在 Vue 2 老项目中优雅落地 AI 流式对话 (SSE)

前言:随着 DeepSeek、ChatGPT 等大模型的爆火,给现有业务系统装上“AI 大脑”已成为刚需。但对于许多仍坚守在 Vue 2 + Webpack 时代的企业级老项目来说,引入 AI 能力往往面临着技术栈陈旧、依赖冲突等挑战。本文将以实战代码为例,复盘如何在不破坏原有架构的前提下,利用原生技术栈实现丝滑的 AI 流式问答体验。

一、 痛点分析:为什么不用 Axios?

在老项目中,我们通常封装了统一的 Axios 拦截器来处理 Token、错误码和全局 Loading。但在对接 AI 流式接口(Server-Sent Events, SSE)时,Axios 显得有些“水土不服”:

  1. 流式支持弱:老版本 Axios 对 onDownloadProgress 的支持更多是为了进度条,而非真正的流式解析,容易出现“等一坨数据回来再一次性渲染”的伪流式现象。
  2. 配置繁琐:为了通过 Axios 获取原始流,往往需要修改 responseType 并绕过原有的响应拦截器,代码侵入性强。

破局思路:返璞归真。利用浏览器原生支持的 Fetch API + ReadableStream,既能实现真正的流式读取,又能与项目原有的 Axios 逻辑完全解耦,做到零侵入集成。

二、 核心方案:Fetch + ReadableStream 组合拳

1. 封装通用的流式请求器

我们在项目中通过 AbortController 实现了超时控制和请求中断,配合 TextDecoder 完美解决了二进制流转文本时的乱码问题(特别是中文被截断时)。

/**
 * 发起流式请求的核心方法
 * @param {string} url - 接口地址
 * @param {object} payload - 请求参数
 * @param {function} onMessage - 接收消息的回调
 * @param {function} onThinking - 接收思考过程的回调
 */
async function streamRequest(url, payload, onMessage, onThinking) {
  // 1. 也是老项目常被忽略的细节:请求中断控制器
  const controller = new AbortController();
  // 设置 5 分钟超长超时,适应 AI "慢思考" 的特性
  const timeoutId = setTimeout(() => controller.abort(), 300000);

  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // 如果需要鉴权,在此处添加 Token
        // 'Authorization': 'Bearer ' + getToken() 
      },
      body: JSON.stringify(payload),
      signal: controller.signal,
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    // 2. 获取流读取器
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    // 3. 循环读取流数据
    while (true) {
      const { done, value } = await reader.read();
      
      if (done) break;

      // 关键点:流式解码,自动处理 UTF-8 多字节字符被切断的情况
      const chunk = decoder.decode(value, { stream: true });
      buffer += chunk;

      // 4. 处理 SSE 格式数据 (假设以 "data:" 开头)
      // 这一步是为了解决 TCP 粘包问题:一次可能收到多条消息,或者一条消息分两次收到
      if (buffer.includes('data:')) {
        const dataBlocks = buffer.split(/(?=data:)/);
        // 保留最后一个可能未传输完整的块,留到下一次循环处理
        buffer = dataBlocks.pop() || '';

        for (const block of dataBlocks) {
          if (!block.trim()) continue;
          processSSEBlock(block, onMessage, onThinking);
        }
      }
    }
  } catch (error) {
    if (error.name === 'AbortError') {
      console.warn('请求已取消或超时');
    } else {
      throw error;
    }
  } finally {
    clearTimeout(timeoutId);
  }
}

2. 解析 SSE 协议数据

后端通常会定义自定义的事件协议,例如区分“思考中(Think)”和“回答中(Message)”。我们需要精准解析这些状态,给用户更好的反馈。

function processSSEBlock(block, onMessage, onThinking) {
  try {
    // 提取 JSON 内容
    const match = block.match(/data:(.+?)(?=data:|$)/s);
    if (match) {
      const jsonStr = match[1].trim();
      const jsonData = JSON.parse(jsonStr);

      // 状态机分发
      if (jsonData.event === 'think') {
        // AI 正在深度思考
        onThinking(jsonData.answer); 
      } else if (jsonData.event === 'message') {
        // AI 开始输出正文
        onMessage(jsonData.answer);
      }
    }
  } catch (e) {
    // 容错处理:非 JSON 格式的纯文本流
    const textContent = block.replace(/^data:/, '').trim();
    if (textContent && !textContent.startsWith('{')) {
      onMessage(textContent);
    }
  }
}

三、 体验优化:让 AI 更像“人”

在老旧的 UI 框架(如 Element UI)中,如何让 AI 的交互显得现代且灵动?

1. 视觉上的“心跳”机制

AI 的思考过程往往是静默的。为了缓解用户的等待焦虑,我们设计了一个“思考中”的状态机。

  • 痛点:有时候 AI 思考完了,但生成正文还有延迟,导致中间出现尴尬的空白期。
  • 优化:引入智能防抖机制。
// 监听思考流
if (event === 'think') {
  this.isThinking = true;
  this.thinkingContent += content;
  
  // 如果 1.5 秒内没有新的思考内容,自动判定思考结束,转入生成状态
  // 避免后端漏发 'think_end' 事件导致一直显示"思考中"
  clearTimeout(this.thinkTimer);
  this.thinkTimer = setTimeout(() => {
    this.endThinking();
  }, 1500);
}

2. 自动跟随滚动的视窗

当生成内容很长时,用户希望视窗能自动跟随到底部,但在查看上面内容时又不希望被强制拉到底部。

startAutoScroll() {
  // 记录上一次的内容长度
  this.lastLength = 0;
  
  this.scrollInterval = setInterval(() => {
    const currentLength = this.currentContent.length;
    // 只有当内容真正增加时才滚动,避免无意义的 DOM 操作
    if (currentLength > this.lastLength) {
      this.scrollToBottom();
      this.lastLength = currentLength;
    }
  }, 1000); // 1秒的节流频率,既流畅又不占用过多主线程
}

四、 架构思考:Vue 2 老项目的“渐进式”改造

sbs-upms-ui 这种典型的企业级 Vue 2 项目中,我们没有选择重构整个 HTTP 模块,而是采用了插件式的打法:

  1. 独立文件:将 AI 相关的流式请求逻辑封装在单独的 .js 文件中,不污染原有的 API 封装。
  2. 组件复用:将 Markdown 渲染(vue-markdown-it)、打字机效果封装为独立的 Vue 组件,可以在任何业务页面按需引入。
  3. 数据驱动:利用 Vue 的响应式系统,将流式数据直接绑定到 el-inputdiv 上,无需手动操作 DOM 插入文本。

五、 结语

技术不仅仅是追逐新框架。在现有的 Vue 2 老系统中,通过合理利用原生 Fetch API 和流式处理思想,我们依然可以构建出不输给 Next.js/React 的现代化 AI 交互体验。这不仅是功能的叠加,更是对老项目生命力的延续。


相关技术栈:Vue 2.6, Element UI, Fetch API, Server-Sent Events (SSE)

❌
❌