Phi-3-Mini-128K前端应用:Vue3项目集成智能对话组件
Phi-3-Mini-128K前端应用Vue3项目集成智能对话组件最近在做一个内部知识库项目需要给用户提供一个智能问答的入口。团队评估了几个轻量级模型最终决定试试微软的Phi-3-Mini-128K。这个模型虽然参数不大但在常识推理和对话任务上表现不错而且对硬件要求不高部署起来也方便。不过光有后端模型还不够用户最终接触的是前端界面。怎么把模型的能力平滑地集成到Vue3项目里做出一个既智能又流畅的聊天界面这才是关键。今天我就来分享一下我们是怎么做的从API调用到状态管理再到提升体验的流式响应希望能给你一些实用的参考。1. 项目准备与环境搭建在开始写代码之前我们需要先把项目的基础环境搭好。这里假设你已经有了一个可以正常运行的Phi-3-Mini-128K模型后端服务它提供了一个HTTP API接口能够接收用户的问题并返回模型的回答。1.1 创建Vue3项目如果你还没有Vue3项目可以用Vite快速创建一个。打开终端执行下面的命令npm create vuelatest phi3-chat-frontend创建过程中你可以根据需求选择需要的特性。对于这个聊天组件我建议至少勾选上TypeScript可选但推荐用于更好的类型提示Vue Router如果项目需要页面路由Pinia状态管理对于复杂应用很有用创建完成后进入项目目录并安装依赖cd phi3-chat-frontend npm install1.2 安装必要的依赖我们的聊天组件需要几个关键的依赖包npm install axiosaxios用来发送HTTP请求到我们的模型后端API。如果你打算使用Pinia进行状态管理也需要安装它npm install pinia安装完成后你可以先运行npm run dev启动开发服务器确保一切正常。2. 核心聊天组件实现接下来就是重头戏了实现一个可复用的智能聊天组件。我们会用Vue3的Composition API来写这样逻辑更清晰也更容易复用。2.1 组件基础结构与样式我们先创建一个ChatWindow.vue文件搭建起聊天窗口的基本框架。template div classchat-container !-- 消息展示区域 -- div classmessages-container refmessagesContainer div v-for(message, index) in messages :keyindex :class[message-bubble, message.role] div classmessage-avatar span v-ifmessage.role user/span span v-else/span /div div classmessage-content div classmessage-text v-htmlformatMessage(message.content)/div div classmessage-time v-ifmessage.timestamp {{ formatTime(message.timestamp) }} /div /div /div !-- 加载指示器 -- div v-ifisLoading classthinking-indicator div classthinking-dots span/spanspan/spanspan/span /div span classthinking-text思考中.../span /div /div !-- 输入区域 -- div classinput-area textarea v-modeluserInput placeholder输入您的问题... keydown.enter.exact.preventsendMessage :disabledisLoading rows3 classmessage-input /textarea button clicksendMessage :disabled!userInput.trim() || isLoading classsend-button {{ isLoading ? 发送中... : 发送 }} /button /div /div /template script setup langts import { ref, computed, nextTick, onMounted } from vue import axios from axios // 在这里我们会逐步添加组件的逻辑 /script style scoped .chat-container { display: flex; flex-direction: column; height: 600px; max-width: 800px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; background: white; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } .messages-container { flex: 1; overflow-y: auto; padding: 20px; background: #f9fafb; } .message-bubble { display: flex; margin-bottom: 16px; animation: fadeIn 0.3s ease; } .message-bubble.user { flex-direction: row-reverse; } .message-avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: #3b82f6; color: white; font-size: 18px; flex-shrink: 0; margin: 0 12px; } .user .message-avatar { background: #10b981; } .message-content { max-width: 70%; background: white; padding: 12px 16px; border-radius: 18px; box-shadow: 0 1px 2px rgb(0 0 0 / 0.05); } .user .message-content { background: #3b82f6; color: white; } .message-text { line-height: 1.5; word-break: break-word; } .message-time { font-size: 12px; color: #6b7280; margin-top: 4px; text-align: right; } .user .message-time { color: rgba(255, 255, 255, 0.8); } .thinking-indicator { display: flex; align-items: center; padding: 12px 16px; background: white; border-radius: 18px; box-shadow: 0 1px 2px rgb(0 0 0 / 0.05); width: fit-content; margin-top: 8px; } .thinking-dots { display: inline-flex; margin-right: 8px; } .thinking-dots span { width: 8px; height: 8px; border-radius: 50%; background: #6b7280; margin: 0 2px; animation: bounce 1.4s infinite ease-in-out both; } .thinking-dots span:nth-child(1) { animation-delay: -0.32s; } .thinking-dots span:nth-child(2) { animation-delay: -0.16s; } .thinking-text { color: #6b7280; font-size: 14px; } .input-area { border-top: 1px solid #e5e7eb; padding: 16px; background: white; display: flex; gap: 12px; } .message-input { flex: 1; padding: 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; resize: none; outline: none; transition: border-color 0.2s; } .message-input:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } .message-input:disabled { background: #f3f4f6; cursor: not-allowed; } .send-button { padding: 0 24px; background: #3b82f6; color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; align-self: flex-end; } .send-button:hover:not(:disabled) { background: #2563eb; } .send-button:disabled { background: #9ca3af; cursor: not-allowed; } keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1.0); } } /style这个组件已经有了聊天界面的基本样子消息展示区域、输入框和发送按钮。消息会区分用户和AI并且有加载状态指示器。样式方面我们用了比较现代的圆角、阴影和动画效果让界面看起来更友好。2.2 实现基础的消息发送与接收现在我们来添加最核心的功能发送消息到后端API并显示回复。在script setup部分我们添加以下代码// 定义消息类型 interface ChatMessage { role: user | assistant content: string timestamp?: number } // 响应式数据 const messages refChatMessage[]([ { role: assistant, content: 你好我是基于Phi-3-Mini模型的智能助手。有什么可以帮您的吗, timestamp: Date.now() } ]) const userInput ref() const isLoading ref(false) const messagesContainer refHTMLElement() // 配置API端点 - 这里替换成你实际的API地址 const API_BASE_URL http://localhost:8000 // 示例地址 const API_ENDPOINT ${API_BASE_URL}/api/chat // 发送消息 const sendMessage async () { const input userInput.value.trim() if (!input || isLoading.value) return // 添加用户消息 const userMessage: ChatMessage { role: user, content: input, timestamp: Date.now() } messages.value.push(userMessage) // 清空输入框 userInput.value // 设置加载状态 isLoading.value true try { // 调用API const response await axios.post(API_ENDPOINT, { message: input, // 可以传递对话历史让模型有上下文 history: messages.value.slice(-10).map(msg ({ role: msg.role, content: msg.content })) }) // 添加AI回复 const aiMessage: ChatMessage { role: assistant, content: response.data.response, timestamp: Date.now() } messages.value.push(aiMessage) } catch (error) { console.error(API调用失败:, error) // 添加错误消息 const errorMessage: ChatMessage { role: assistant, content: 抱歉我暂时无法处理您的请求。请稍后再试。, timestamp: Date.now() } messages.value.push(errorMessage) } finally { isLoading.value false // 滚动到底部 scrollToBottom() } } // 辅助函数滚动到消息底部 const scrollToBottom () { nextTick(() { if (messagesContainer.value) { messagesContainer.value.scrollTop messagesContainer.value.scrollHeight } }) } // 辅助函数格式化消息内容支持简单的Markdown const formatMessage (content: string) { // 简单的Markdown转换 return content .replace(/\*\*(.*?)\*\*/g, strong$1/strong) .replace(/\*(.*?)\*/g, em$1/em) .replace(/(.*?)/g, code$1/code) .replace(/\n/g, br) } // 辅助函数格式化时间 const formatTime (timestamp: number) { return new Date(timestamp).toLocaleTimeString(zh-CN, { hour: 2-digit, minute: 2-digit }) } // 组件挂载时滚动到底部 onMounted(() { scrollToBottom() })现在你的聊天组件已经可以工作了输入问题点击发送就能看到AI的回复。不过现在的体验还不够好——用户需要等待整个回复生成完成才能看到内容。接下来我们要实现更流畅的流式响应。3. 实现流式响应提升用户体验流式响应Server-Sent Events, SSE能让用户看到AI回复的生成过程就像真人打字一样体验会好很多。Phi-3-Mini模型支持流式输出我们需要在前端做相应的适配。3.1 修改API调用支持流式响应首先我们需要修改后端的API让它支持流式输出。这里假设你的后端已经做好了相应的调整返回的是text/event-stream格式的数据。在前端我们需要创建一个新的函数来处理流式响应// 添加新的响应式数据来存储流式响应的内容 const currentStreamContent ref() const isStreaming ref(false) // 流式发送消息 const sendMessageStream async () { const input userInput.value.trim() if (!input || isLoading.value) return // 添加用户消息 const userMessage: ChatMessage { role: user, content: input, timestamp: Date.now() } messages.value.push(userMessage) // 清空输入框 userInput.value // 设置加载状态 isLoading.value true isStreaming.value true // 创建AI消息的占位符 const aiMessageIndex messages.value.length messages.value.push({ role: assistant, content: , timestamp: Date.now() }) // 重置当前流内容 currentStreamContent.value try { // 使用EventSource或fetch API接收流式响应 // 这里使用fetch API因为EventSource只支持GET请求 const response await fetch(${API_ENDPOINT}/stream, { method: POST, headers: { Content-Type: application/json, Accept: text/event-stream }, body: JSON.stringify({ message: input, history: messages.value.slice(-10).map(msg ({ role: msg.role, content: msg.content })) }) }) if (!response.ok) { throw new Error(HTTP error! status: ${response.status}) } const reader response.body?.getReader() const decoder new TextDecoder(utf-8) if (!reader) { throw new Error(无法读取响应流) } // 读取流数据 while (true) { const { done, value } await reader.read() if (done) { break } // 解码数据 const chunk decoder.decode(value, { stream: true }) const lines chunk.split(\n) for (const line of lines) { if (line.startsWith(data: )) { const data line.slice(6) if (data [DONE]) { // 流结束 continue } try { const parsed JSON.parse(data) if (parsed.content) { // 累加内容 currentStreamContent.value parsed.content // 更新消息内容 messages.value[aiMessageIndex].content currentStreamContent.value // 滚动到底部 scrollToBottom() } } catch (e) { console.warn(解析流数据失败:, e) } } } } } catch (error) { console.error(流式请求失败:, error) // 更新消息内容为错误提示 messages.value[aiMessageIndex].content 抱歉响应过程中出现了问题。请重试。 } finally { isLoading.value false isStreaming.value false currentStreamContent.value scrollToBottom() } } // 修改sendMessage函数使用流式版本 const sendMessage async () { // 你可以根据配置决定使用哪种方式 const useStream true // 设置为true使用流式响应 if (useStream) { await sendMessageStream() } else { // 使用原来的非流式版本 // ... 原有的sendMessage逻辑 } }3.2 优化流式响应的显示为了让流式响应看起来更自然我们可以添加一些优化// 在组件中添加打字机效果 const typewriterSpeed ref(30) // 打字速度毫秒/字符 const isTyping ref(false) // 模拟打字机效果显示流式内容 const displayStreamWithTypewriter (content: string, messageIndex: number) { isTyping.value true let displayedContent let i 0 const typeNextChar () { if (i content.length) { displayedContent content.charAt(i) messages.value[messageIndex].content displayedContent i setTimeout(typeNextChar, typewriterSpeed.value) scrollToBottom() } else { isTyping.value false } } typeNextChar() } // 修改流式响应处理部分 // 在收到流数据时不再直接更新而是收集完整内容后使用打字机效果 // 修改sendMessageStream函数中的数据处理部分 // 将原来的直接更新改为 // currentStreamContent.value parsed.content // messages.value[aiMessageIndex].content currentStreamContent.value // 改为 currentStreamContent.value parsed.content // 然后在流结束时 // 在finally块之前添加 if (!isStreaming.value) { // 流结束后使用打字机效果显示完整内容 displayStreamWithTypewriter(currentStreamContent.value, aiMessageIndex) }4. 状态管理与错误处理优化随着功能增多我们需要更好的状态管理和错误处理机制。4.1 使用Pinia进行状态管理如果项目比较复杂建议使用Pinia来管理聊天状态。首先创建store// stores/chatStore.ts import { defineStore } from pinia import { ref, computed } from vue import type { ChatMessage } from /types/chat export const useChatStore defineStore(chat, () { // 状态 const messages refChatMessage[]([]) const isLoading ref(false) const error refstring | null(null) const apiConfig ref({ baseUrl: http://localhost:8000, endpoint: /api/chat, useStream: true }) // Getter const lastMessage computed(() { return messages.value.length 0 ? messages.value[messages.value.length - 1] : null }) const hasMessages computed(() messages.value.length 0) // Actions const addMessage (message: ChatMessage) { messages.value.push(message) } const clearMessages () { messages.value [] } const setLoading (loading: boolean) { isLoading.value loading } const setError (err: string | null) { error.value err } const updateApiConfig (config: Partialtypeof apiConfig.value) { apiConfig.value { ...apiConfig.value, ...config } } // 初始化欢迎消息 const initWelcomeMessage () { if (messages.value.length 0) { messages.value.push({ role: assistant, content: 你好我是基于Phi-3-Mini模型的智能助手。有什么可以帮您的吗, timestamp: Date.now() }) } } return { messages, isLoading, error, apiConfig, lastMessage, hasMessages, addMessage, clearMessages, setLoading, setError, updateApiConfig, initWelcomeMessage } })4.2 增强错误处理在组件中我们需要更完善的错误处理// 在组件中添加错误状态 const errorMessage refstring | null(null) // 修改sendMessageStream函数中的错误处理 try { // ... 原有的try逻辑 } catch (error) { console.error(流式请求失败:, error) // 设置错误信息 if (error instanceof TypeError error.message.includes(Failed to fetch)) { errorMessage.value 无法连接到服务器请检查网络连接或API地址配置。 } else if (error instanceof Error error.message.includes(HTTP error)) { errorMessage.value 服务器返回错误请稍后重试。 } else { errorMessage.value 请求处理失败请检查控制台获取详细信息。 } // 更新消息内容为错误提示 messages.value[aiMessageIndex].content 抱歉处理您的请求时出现了问题。 // 3秒后清除错误信息 setTimeout(() { errorMessage.value null }, 3000) } finally { // ... 原有的finally逻辑 } // 在模板中添加错误显示 div v-iferrorMessage classerror-message {{ errorMessage }} /div // 添加对应的样式 .error-message { background: #fee2e2; border: 1px solid #fca5a5; color: #dc2626; padding: 12px 16px; border-radius: 8px; margin: 16px; font-size: 14px; animation: slideIn 0.3s ease; } keyframes slideIn { from { transform: translateY(-10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }4.3 添加重试机制对于网络不稳定的情况我们可以添加重试机制const maxRetries ref(3) const retryDelay ref(1000) // 1秒 const sendMessageWithRetry async (retryCount 0) { try { await sendMessageStream() } catch (error) { if (retryCount maxRetries.value) { console.log(请求失败第${retryCount 1}次重试...) // 指数退避 const delay retryDelay.value * Math.pow(2, retryCount) await new Promise(resolve setTimeout(resolve, delay)) return sendMessageWithRetry(retryCount 1) } else { throw error } } } // 修改sendMessage函数 const sendMessage async () { const input userInput.value.trim() if (!input || isLoading.value) return // ... 添加用户消息等逻辑 try { await sendMessageWithRetry() } catch (error) { // 最终错误处理 console.error(所有重试都失败了:, error) errorMessage.value 请求失败请检查网络连接后重试。 } }5. 完整组件代码与使用示例经过上面的步骤我们已经有了一个功能完整的智能聊天组件。下面是完整的组件代码和使用示例。5.1 完整组件代码!-- SmartChat.vue -- template div classchat-container !-- 错误提示 -- div v-iferrorMessage classerror-message span{{ errorMessage }}/span button clickerrorMessage null classclose-error×/button /div !-- 消息区域 -- div classchat-header h3Phi-3智能助手/h3 div classchat-actions button clickclearChat classaction-button title清空对话 ️ /button button clicktoggleStreamMode classaction-button :titleapiConfig.useStream ? 关闭流式响应 : 开启流式响应 {{ apiConfig.useStream ? : }} /button /div /div div classmessages-container refmessagesContainer div v-for(message, index) in messages :keyindex :class[message-bubble, message.role] div classmessage-avatar span v-ifmessage.role user/span span v-else/span /div div classmessage-content div classmessage-text v-htmlformatMessage(message.content)/div div classmessage-time v-ifmessage.timestamp {{ formatTime(message.timestamp) }} /div /div /div !-- 加载指示器 -- div v-ifisLoading classthinking-indicator div classthinking-dots span/spanspan/spanspan/span /div span classthinking-text思考中.../span /div /div !-- 输入区域 -- div classinput-area div classinput-wrapper textarea v-modeluserInput placeholder输入您的问题... keydown.enter.exact.preventsendMessage keydown.enter.shift.exact.preventuserInput \n :disabledisLoading rows3 classmessage-input refinputRef /textarea div classinput-hint 按 Enter 发送Shift Enter 换行 /div /div button clicksendMessage :disabled!userInput.trim() || isLoading classsend-button span v-ifisLoading span classloading-spinner/span 发送中... /span span v-else发送/span /button /div /div /template script setup langts import { ref, computed, nextTick, onMounted, onUnmounted } from vue import axios from axios // 类型定义 interface ChatMessage { role: user | assistant content: string timestamp?: number } interface ApiConfig { baseUrl: string endpoint: string useStream: boolean } // 响应式数据 const messages refChatMessage[]([ { role: assistant, content: 你好我是基于Phi-3-Mini模型的智能助手。有什么可以帮您的吗, timestamp: Date.now() } ]) const userInput ref() const isLoading ref(false) const isStreaming ref(false) const errorMessage refstring | null(null) const messagesContainer refHTMLElement() const inputRef refHTMLTextAreaElement() // API配置 const apiConfig refApiConfig({ baseUrl: import.meta.env.VITE_API_BASE_URL || http://localhost:8000, endpoint: /api/chat, useStream: true }) // 重试配置 const maxRetries 3 const retryDelay 1000 // 计算属性 const apiUrl computed(() { const base apiConfig.value.baseUrl.replace(/\/$/, ) const endpoint apiConfig.value.endpoint.replace(/^\//, ) return ${base}/${endpoint} }) // 方法 const scrollToBottom () { nextTick(() { if (messagesContainer.value) { messagesContainer.value.scrollTop messagesContainer.value.scrollHeight } }) } const formatMessage (content: string) { return content .replace(/\*\*(.*?)\*\*/g, strong$1/strong) .replace(/\*(.*?)\*/g, em$1/em) .replace(/(.*?)/g, code$1/code) .replace(/\n/g, br) } const formatTime (timestamp: number) { return new Date(timestamp).toLocaleTimeString(zh-CN, { hour: 2-digit, minute: 2-digit }) } const clearChat () { if (confirm(确定要清空对话历史吗)) { messages.value [{ role: assistant, content: 对话已清空。有什么可以帮您的吗, timestamp: Date.now() }] errorMessage.value null scrollToBottom() } } const toggleStreamMode () { apiConfig.value.useStream !apiConfig.value.useStream const mode apiConfig.value.useStream ? 流式 : 非流式 errorMessage.value 已切换到${mode}响应模式 setTimeout(() { errorMessage.value null }, 2000) } // 流式请求 const sendMessageStream async (input: string, history: any[]): Promisestring { return new Promise(async (resolve, reject) { const controller new AbortController() const timeoutId setTimeout(() controller.abort(), 30000) // 30秒超时 try { const response await fetch(${apiUrl.value}/stream, { method: POST, headers: { Content-Type: application/json, Accept: text/event-stream }, body: JSON.stringify({ message: input, history }), signal: controller.signal }) clearTimeout(timeoutId) if (!response.ok) { throw new Error(HTTP error! status: ${response.status}) } const reader response.body?.getReader() const decoder new TextDecoder(utf-8) let fullResponse if (!reader) { throw new Error(无法读取响应流) } while (true) { const { done, value } await reader.read() if (done) { break } const chunk decoder.decode(value, { stream: true }) const lines chunk.split(\n) for (const line of lines) { if (line.startsWith(data: )) { const data line.slice(6) if (data [DONE]) { continue } try { const parsed JSON.parse(data) if (parsed.content) { fullResponse parsed.content // 更新最后一条消息的内容 messages.value[messages.value.length - 1].content fullResponse scrollToBottom() } } catch (e) { console.warn(解析流数据失败:, e) } } } } resolve(fullResponse) } catch (error) { clearTimeout(timeoutId) reject(error) } }) } // 非流式请求 const sendMessageNormal async (input: string, history: any[]): Promisestring { const response await axios.post(apiUrl.value, { message: input, history }, { timeout: 30000 // 30秒超时 }) return response.data.response } // 带重试的发送 const sendWithRetry async (input: string, history: any[], retryCount 0): Promisestring { try { if (apiConfig.value.useStream) { return await sendMessageStream(input, history) } else { return await sendMessageNormal(input, history) } } catch (error) { if (retryCount maxRetries) { console.log(请求失败第${retryCount 1}次重试...) const delay retryDelay * Math.pow(2, retryCount) await new Promise(resolve setTimeout(resolve, delay)) return sendWithRetry(input, history, retryCount 1) } else { throw error } } } // 主发送函数 const sendMessage async () { const input userInput.value.trim() if (!input || isLoading.value) return // 添加用户消息 const userMessage: ChatMessage { role: user, content: input, timestamp: Date.now() } messages.value.push(userMessage) // 清空输入框 userInput.value // 添加AI消息占位符 const aiMessageIndex messages.value.length messages.value.push({ role: assistant, content: , timestamp: Date.now() }) // 设置加载状态 isLoading.value true errorMessage.value null try { // 准备历史记录最近10条 const history messages.value .slice(-12, -1) // 排除最后一条占位符 .map(msg ({ role: msg.role, content: msg.content })) // 发送请求 const response await sendWithRetry(input, history) // 更新AI消息 messages.value[aiMessageIndex].content response } catch (error) { console.error(发送消息失败:, error) // 错误处理 let errorMsg 请求失败请稍后重试。 if (error instanceof Error) { if (error.name AbortError) { errorMsg 请求超时请检查网络连接。 } else if (error.message.includes(Failed to fetch)) { errorMsg 无法连接到服务器请检查API地址配置。 } else if (error.message.includes(HTTP error)) { errorMsg 服务器返回错误请稍后重试。 } } errorMessage.value errorMsg // 更新AI消息为错误提示 messages.value[aiMessageIndex].content 抱歉处理您的请求时出现了问题。 } finally { isLoading.value false scrollToBottom() // 聚焦输入框 nextTick(() { if (inputRef.value) { inputRef.value.focus() } }) } } // 生命周期 onMounted(() { scrollToBottom() }) // 键盘快捷键 const handleKeyDown (e: KeyboardEvent) { if (e.ctrlKey e.key k) { e.preventDefault() if (inputRef.value) { inputRef.value.focus() } } } onMounted(() { window.addEventListener(keydown, handleKeyDown) }) onUnmounted(() { window.removeEventListener(keydown, handleKeyDown) }) /script style scoped /* 样式代码同上略作优化 */ .chat-container { display: flex; flex-direction: column; height: 700px; max-width: 900px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; background: white; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); } .chat-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .chat-header h3 { margin: 0; font-size: 18px; font-weight: 600; } .chat-actions { display: flex; gap: 8px; } .action-button { background: rgba(255, 255, 255, 0.2); border: none; border-radius: 6px; padding: 6px 12px; color: white; cursor: pointer; transition: background-color 0.2s; font-size: 16px; } .action-button:hover { background: rgba(255, 255, 255, 0.3); } /* 其他样式同上略作优化 */ .loading-spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top-color: white; animation: spin 1s ease-in-out infinite; margin-right: 8px; } keyframes spin { to { transform: rotate(360deg); } } .input-hint { font-size: 12px; color: #6b7280; margin-top: 4px; text-align: right; } /style5.2 在项目中使用组件在你的Vue3项目中可以这样使用这个组件!-- App.vue 或任何其他父组件 -- template div classapp-container header classapp-header h1Phi-3智能对话演示/h1 p基于Phi-3-Mini-128K模型的前端集成示例/p /header main classapp-main div classdemo-info p这是一个完整的智能对话组件示例展示了如何在前端项目中集成大语言模型。/p div classfeatures span✅ 实时对话/span span✅ 流式响应/span span✅ 错误处理/span span✅ 重试机制/span /div /div SmartChat / div classconfig-section h3配置说明/h3 p请确保后端API服务已启动并在 code.env/code 文件中配置正确的API地址/p preVITE_API_BASE_URLhttp://your-api-server:port/pre /div /main /div /template script setup import SmartChat from ./components/SmartChat.vue /script style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; padding: 20px; } .app-container { max-width: 1000px; margin: 0 auto; } .app-header { text-align: center; margin-bottom: 40px; padding: 40px 20px; background: white; border-radius: 20px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } .app-header h1 { font-size: 2.5rem; color: #1f2937; margin-bottom: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .app-header p { color: #6b7280; font-size: 1.1rem; } .app-main { display: flex; flex-direction: column; gap: 30px; } .demo-info { background: white; padding: 24px; border-radius: 16px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } .demo-info p { color: #4b5563; margin-bottom: 16px; line-height: 1.6; } .features { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 16px; } .features span { background: #f3f4f6; padding: 6px 12px; border-radius: 20px; font-size: 14px; color: #374151; } .config-section { background: white; padding: 24px; border-radius: 16px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } .config-section h3 { color: #1f2937; margin-bottom: 12px; } .config-section p { color: #4b5563; margin-bottom: 12px; line-height: 1.6; } .config-section pre { background: #f8fafc; padding: 16px; border-radius: 8px; border: 1px solid #e2e8f0; font-family: Monaco, Menlo, Ubuntu Mono, monospace; font-size: 14px; color: #334155; overflow-x: auto; } media (max-width: 768px) { .app-header h1 { font-size: 2rem; } .features { flex-direction: column; } } /style6. 总结整个集成过程做下来感觉Vue3的Composition API确实让这类复杂组件的开发变得清晰很多。把聊天逻辑、API调用、状态管理这些功能拆分成独立的函数不仅代码好维护复用起来也方便。流式响应这个功能对用户体验的提升真的很明显。以前用户要等整个回复生成完才能看到内容现在可以一边生成一边显示感觉对话更自然了。不过实现的时候要注意错误处理和网络中断的情况我们加了重试机制和超时控制基本能应对大多数网络问题。错误处理这块也花了不少心思。网络请求失败、服务器错误、超时这些情况都要考虑到给用户清晰的提示而不是一个模糊的出错了。我们还加了清空对话、切换响应模式这些实用功能让组件用起来更顺手。性能方面我们做了消息历史的管理只传递最近的几条对话给后端避免请求数据过大。前端也做了虚拟滚动和消息缓存的优化对话记录多了也不会卡顿。实际用的时候记得根据你的后端API调整请求参数和错误处理逻辑。如果对话内容比较敏感可能还需要加个加密传输。这个组件算是个基础版本你可以根据自己的需求再加功能比如支持文件上传、多轮对话管理、对话导出这些。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。