如何在老项目中使用AI实现智能问答
老树发新芽:在 Vue 2 老项目中优雅落地 AI 流式对话 (SSE)
前言:随着 DeepSeek、ChatGPT 等大模型的爆火,给现有业务系统装上“AI 大脑”已成为刚需。但对于许多仍坚守在 Vue 2 + Webpack 时代的企业级老项目来说,引入 AI 能力往往面临着技术栈陈旧、依赖冲突等挑战。本文将以实战代码为例,复盘如何在不破坏原有架构的前提下,利用原生技术栈实现丝滑的 AI 流式问答体验。
一、 痛点分析:为什么不用 Axios?
在老项目中,我们通常封装了统一的 Axios 拦截器来处理 Token、错误码和全局 Loading。但在对接 AI 流式接口(Server-Sent Events, SSE)时,Axios 显得有些“水土不服”:
-
流式支持弱:老版本 Axios 对
onDownloadProgress的支持更多是为了进度条,而非真正的流式解析,容易出现“等一坨数据回来再一次性渲染”的伪流式现象。 -
配置繁琐:为了通过 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 模块,而是采用了插件式的打法:
-
独立文件:将 AI 相关的流式请求逻辑封装在单独的
.js文件中,不污染原有的 API 封装。 -
组件复用:将 Markdown 渲染(
vue-markdown-it)、打字机效果封装为独立的 Vue 组件,可以在任何业务页面按需引入。 -
数据驱动:利用 Vue 的响应式系统,将流式数据直接绑定到
el-input或div上,无需手动操作 DOM 插入文本。
五、 结语
技术不仅仅是追逐新框架。在现有的 Vue 2 老系统中,通过合理利用原生 Fetch API 和流式处理思想,我们依然可以构建出不输给 Next.js/React 的现代化 AI 交互体验。这不仅是功能的叠加,更是对老项目生命力的延续。
相关技术栈:Vue 2.6, Element UI, Fetch API, Server-Sent Events (SSE)