AI聊天界面开发实战:流式输出与多轮对话
这是一份关于AI聊天界面开发全流程的实战教程。我们将从零开始手把手构建一个支持流式输出和真多轮对话的现代聊天界面。我会用口语化的方式穿插大量实战代码和核心知识点帮你彻底搞懂。一、 项目蓝图我们要做什么想象一下ChatGPT的聊天界面。我们的目标就是做一个简化版核心功能如下一个漂亮的聊天窗口能显示对话历史用户消息和AI回复。支持流式输出AI的回复不是“憋”好几秒然后一下子全出来而是一个字一个字“流”出来像真人打字一样。支持多轮对话AI能记住我们之前的对话上下文而不是“金鱼记忆”每次只回答当前问题。一个输入框和发送按钮让用户可以输入问题。技术栈选择前端Vue 3 TypeScript Element Plus (UI库)。Vue的响应式特性非常适合处理实时流式数据。后端Node.js Express。轻量适合快速构建API。AI能力调用大模型API如 OpenAI GPT, 国内可选用智谱、月之暗面等。本教程将模拟一个流式接口并讲解如何对接真实API。二、 核心概念口语化解读在写代码前必须搞懂两个核心概念不然就是瞎写。1. 流式输出 (Streaming Output)传统方式 (非流式)你问“讲个笑话”。前端发送请求后端调用AIAI在后台想啊想花了3秒把整个笑话编完然后后端把完整的笑话文本一次性返回给前端。这3秒内用户屏幕是空白的体验很差。流式方式同样的问题AI想到第一个词“从前”就立刻把这个词传回前端显示出来想到“有个”再传回并显示... 这样用户几乎感觉不到等待就看到文字一个个“流”出来体验流畅自然。技术本质后端和前端建立一个长连接比如SSE或WebSocket后端可以在这个连接上多次、分段地发送数据。前端则像接水管里的水滴一样一段段接收并实时渲染。2. 多轮对话 (Multi-turn Conversation)伪多轮前端只是把当前这一条用户消息发给AI。AI完全不知道之前聊过什么。你问“《三体》的作者是谁”它答“刘慈欣”。你再问“他还有哪些作品”它就懵了因为不知道“他”指代谁。真多轮前端需要把整个对话历史一个消息数组发给AI。AI模型看到整个上下文才能理解指代关系进行连贯的对话。这就是“真多轮”与“伪多轮”的本质区别。关键数据结构messages: Array{role: ‘user’ | ‘assistant’ content: string}。role表明说话者是用户还是AIcontent是内容。三、 后端实战构建流式对话API我们先搭建一个Node.js后端它有两个核心任务1) 处理多轮对话的历史管理2) 模拟或真实对接流式AI响应。1. 项目初始化与依赖安装mkdir ai-chat-backend cd ai-chat-backend npm init -y npm install express cors body-parser # 我们用一个简单的库来模拟流式响应实际项目中你可能需要安装 openai, zhipuai/sdk 等 npm install eventsource-parser # 用于解析SSE流2. 核心服务器代码 (server.js)const express require(express); const cors require(cors); const bodyParser require(body-parser); const app express(); const PORT 3000; // 启用CORS允许前端跨域访问 app.use(cors()); app.use(bodyParser.json()); // 在内存中存储对话会话简单示例生产环境需用数据库 const sessions new Map(); // key: sessionId, value: { messages: [...] } // 1. 创建新会话或获取历史消息的接口 app.post(/api/session, (req, res) { const { sessionId } req.body; if (!sessionId) { const newSessionId session_${Date.now()}; sessions.set(newSessionId, { messages: [] }); return res.json({ sessionId: newSessionId, messages: [] }); } const session sessions.get(sessionId) || { messages: [] }; res.json({ sessionId, messages: session.messages }); }); // 2. 核心流式对话接口 (模拟SSE - Server-Sent Events) app.post(/api/chat/stream, (req, res) { const { sessionId, message } req.body; if (!sessionId || !message) { return res.status(400).json({ error: Missing sessionId or message }); } // 获取或创建会话 let session sessions.get(sessionId); if (!session) { session { messages: [] }; sessions.set(sessionId, session); } // **多轮对话核心将用户新消息加入历史** session.messages.push({ role: user, content: message }); console.log([Session ${sessionId}] 用户消息: ${message}); console.log(当前对话历史:, JSON.stringify(session.messages, null, 2)); // 设置SSE响应头 res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); // 模拟AI的流式思考过程 const aiThinkingText 这是关于“${message}”的思考回复; const simulatedResponse aiThinkingText 流式输出让我们看到了每个字是如何产生的。多轮对话的关键在于维护完整的 messages 数组上下文。; let index 0; const streamInterval setInterval(() { if (index simulatedResponse.length) { // SSE格式 data: {内容} const chunk simulatedResponse.charAt(index); // 发送一个数据块前端会收到这个事件 res.write(data: ${JSON.stringify({ content: chunk })} ); index; } else { // 流式输出结束 const finalMessage { role: assistant, content: simulatedResponse }; // **多轮对话核心将AI的完整回复也加入历史** session.messages.push(finalMessage); console.log([Session ${sessionId}] AI回复已加入历史。); // 发送结束信号 res.write(data: ${JSON.stringify({ done: true })} ); clearInterval(streamInterval); res.end(); // 结束响应 } }, 50); // 每50毫秒发送一个字模拟打字效果 // 如果客户端断开连接清理定时器 req.on(close, () { clearInterval(streamInterval); console.log(客户端断开连接停止流式输出。); }); }); // 3. 清空会话历史接口 app.post(/api/session/clear, (req, res) { const { sessionId } req.body; if (sessionId sessions.has(sessionId)) { sessions.set(sessionId, { messages: [] }); } res.json({ success: true }); }); app.listen(PORT, () { console.log(后端服务器运行在 http://localhost:${PORT}); });知识点与实战解析会话管理我们用Map在内存中模拟了一个简单的会话存储。每个sessionId对应一个对话messages数组。这是实现多轮对话的基石。生产环境要用Redis或数据库。SSE (Server-Sent Events)这是一种服务器向客户端推送数据的简单协议。我们设置了Content-Type: text/event-stream然后通过res.write不断发送 data: ...格式的数据。前端用EventSource或fetch 来接收。流式模拟我们用setInterval逐字发送模拟的AI回复让前端能看到“打字”效果。真实项目中这里应该调用大模型API如OpenAI的createChatCompletion并设置stream: true并将API返回的流实时转发给前端。上下文维护注意看代码在流式输出开始前我们把用户消息push进历史。在流式输出结束后我们把AI的完整回复push进历史。这样下次用户再提问时发送的整个messages数组就包含了之前的所有对话AI便能理解上下文。四、 前端实战构建响应式聊天界面现在我们来构建一个能看到效果的前端界面。1. 初始化Vue项目并安装依赖npm create vuelatest ai-chat-frontend # 根据提示选择 TypeScript, Vue Router(否), Pinia(否), ESLint(是) cd ai-chat-frontend npm install npm install element-plus axios npm run dev2. 核心组件代码 (src/components/ChatWindow.vue)我们将创建一个完整的聊天组件。template div classchat-container el-container directionvertical styleheight: 100vh; !-- 头部 -- el-header styleborder-bottom: 1px solid #eee; display: flex; align-items: center; h2 AI聊天助手 (支持流式多轮)/h2 el-button clickclearHistory sizesmall stylemargin-left: auto; typewarning plain 清空对话 /el-button span stylemargin-left: 10px; font-size: 12px; color: #666;会话ID: {{ sessionId }}/span /el-header !-- 聊天消息区域 -- el-main refmessageContainer styleoverflow-y: auto; padding: 20px; div v-for(msg, index) in messages :keyindex classmessage-wrapper !-- 用户消息 -- div v-ifmsg.role user classmessage user-message div classavatar/div div classbubble{{ msg.content }}/div /div !-- AI消息 -- div v-else classmessage ai-message div classavatar/div div classbubble !-- 关键如果是当前正在接收的流式消息显示动态内容 -- span v-ifindex messages.length - 1 isStreaming {{ streamingContent }} span classcursor|/span !-- 打字光标 -- /span span v-else {{ msg.content }} /span /div /div /div !-- 加载指示器 -- div v-ifisLoading !isStreaming classloadingAI正在思考.../div /el-main !-- 底部输入区域 -- el-footer styleborder-top: 1px solid #eee; padding: 20px; el-form submit.preventsendMessage el-input v-modelinputMessage :disabledisLoading placeholder输入您的问题...(按Enter发送ShiftEnter换行) typetextarea :autosize{ minRows: 2, maxRows: 4 } keydown.enter.exact.preventsendMessage / div stylemargin-top: 10px; display: flex; justify-content: flex-end; el-button typeprimary clicksendMessage :loadingisLoading :disabled!inputMessage.trim() {{ isLoading ? 发送中... : 发送 }} /el-button /div /el-form div stylemargin-top: 10px; font-size: 12px; color: #999; 提示这是一个模拟演示。流式输出是逐字生成的多轮对话历史保存在后端。 /div /el-footer /el-container /div /template script setup langts import { ref, onMounted, nextTick, watch } from vue import axios from axios import { ElMessage } from element-plus // --- 核心状态定义 --- const sessionId refstring() // 会话ID用于标识多轮对话 const messages refArray{role: string, content: string}([]) // 对话消息历史 const inputMessage ref() // 用户输入 const isLoading ref(false) // 是否正在加载非流式请求时用 const isStreaming ref(false) // 是否正在流式接收中 const streamingContent ref() // 当前流式接收到的内容 const messageContainer refHTMLElement() // 用于自动滚动到最新的消息 // 后端API地址 const API_BASE http://localhost:3000/api // --- 生命周期初始化时创建或获取会话 --- onMounted(async () { // 尝试从本地存储获取已有的sessionId const savedSessionId localStorage.getItem(ai_chat_session_id) if (savedSessionId) { sessionId.value savedSessionId // 获取该会话的历史消息 try { const resp await axios.post(${API_BASE}/session, { sessionId: savedSessionId }) messages.value resp.data.messages || [] } catch (error) { console.error(获取历史消息失败:, error) // 如果失败创建新会话 createNewSession() } } else { createNewSession() } }) // 创建新会话的函数 const createNewSession async () { try { const resp await axios.post(${API_BASE}/session, {}) sessionId.value resp.data.sessionId messages.value resp.data.messages || [] localStorage.setItem(ai_chat_session_id, sessionId.value) } catch (error) { console.error(创建会话失败:, error) ElMessage.error(无法连接服务器请检查后端是否运行) } } // --- 核心函数发送消息并处理流式响应 --- const sendMessage async () { const userMessage inputMessage.value.trim() if (!userMessage || isLoading.value) return // 1. 更新UI将用户消息添加到列表清空输入框 messages.value.push({ role: user, content: userMessage }) inputMessage.value // 为AI回复占位 messages.value.push({ role: assistant, content: }) isLoading.value true isStreaming.value true streamingContent.value // 滚动到底部 scrollToBottom() try { // 2. 关键使用 fetch 来处理 Server-Sent Events (SSE) 流 const response await fetch(${API_BASE}/chat/stream, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ sessionId: sessionId.value, message: userMessage, }), }) if (!response.ok || !response.body) { throw new Error(HTTP error! status: ${response.status}) } // 3. 读取流式响应的数据 const reader response.body.getReader() const decoder new TextDecoder(utf-8) let done false while (!done) { const { value, done: doneReading } await reader.read() done doneReading if (value) { // 解码流数据块 const chunk decoder.decode(value, { stream: true }) // 4. 解析SSE格式的数据行 (data: {...} ) const lines chunk.split( ).filter(line line.startsWith(data: )) for (const line of lines) { const dataStr line.replace(data: , ) if (dataStr.trim() ) continue try { const parsedData JSON.parse(dataStr) // 5. 处理流式数据块 if (parsedData.done) { // 流式输出结束 isStreaming.value false // 注意后端已经在流结束时将完整消息加入了历史这里我们只需更新本地最后一条消息的显示 // 实际上streamingContent已经包含了完整内容 // 我们更新messages中最后一条即占位的那条为完整内容 messages.value[messages.value.length - 1].content streamingContent.value } else if (parsedData.content) { // 接收到一个内容块追加到当前流式内容中 streamingContent.value parsedData.content // 实时更新占位消息的内容用于显示 messages.value[messages.value.length - 1].content streamingContent.value // 每次收到新内容都滚动一下提升体验 scrollToBottom() } } catch (e) { console.error(解析SSE数据失败:, e, 原始数据:, dataStr) } } } } } catch (error) { console.error(请求失败:, error) ElMessage.error(发送消息失败请检查网络或后端服务) // 出错时移除AI的占位消息 messages.value.pop() } finally { isLoading.value false // 确保流式状态被重置 if (isStreaming.value) { isStreaming.value false } scrollToBottom() } } // --- 工具函数 --- // 清空对话历史 const clearHistory async ---- ## 参考来源 - [GLM-4.7-Flash多轮对话实战打造智能聊天机器人](https://blog.csdn.net/weixin_42230607/article/details/157629159) - [鸿蒙系统基于大模型的界面开发与实践](https://developer.huawei.com/consumer/cn/blog/topic/03208714748606057) - [AI 智能分类 实时反馈打造高效客服培训新范式](https://www.cnblogs.com/weimaoyun/p/18796402)