Phi-3-mini-128k-instruct赋能前端Vue3项目集成智能对话组件最近在捣鼓一个内部工具项目想给它加点儿“智能”的味道比如让用户能直接跟系统聊聊天问问数据情况或者让它帮忙生成点简单代码。一开始想着用那些大家伙模型但部署成本和响应速度实在让人头疼。直到试了试Phi-3-mini-128k-instruct这个小模型发现它虽然体积小但指令跟随和对话能力相当不错关键是轻量对前端集成特别友好。这篇文章我就来聊聊怎么在一个Vue 3项目里把Phi-3-mini模型的能力接进来从头到尾打造一个流畅的智能对话组件。咱们不搞复杂的理论就聚焦在怎么动手做从调用接口到界面交互再到让回答一个字一个字“流”出来的效果我都会用代码和例子说清楚。如果你也在琢磨怎么给自己的Web应用加个AI聊天功能希望这篇能给你一些直接的参考。1. 项目准备与模型服务对接在开始写组件之前得先把“后勤”安排好。这里假设你已经有一个能提供Phi-3-mini-128k-instruct模型推理能力的后端服务。这个服务可能基于像Ollama、vLLM或者Transformers库搭建它提供了一个HTTP API端点比如http://your-api-server/v1/chat/completions。1.1 安装依赖与配置请求首先在你的Vue 3项目中我们需要一个工具来发送HTTP请求。axios是个常见的选择用起来方便。npm install axios # 或者 yarn add axios接着我们创建一个专门用于和AI模型服务通信的工具文件比如src/utils/aiService.js。这样做的好处是所有关于API的配置、错误处理都集中在一处以后要改接口地址或者换模型都容易。// src/utils/aiService.js import axios from axios; // 创建axios实例统一配置 const aiClient axios.create({ baseURL: process.env.VUE_APP_AI_API_BASEURL || http://localhost:11434, // 你的模型服务地址 timeout: 60000, // 超时时间设长一点AI生成可能需要时间 }); // 定义对话请求的默认参数 const defaultChatOptions { model: phi3:mini-128k-instruct, // 指定模型名称根据你的后端调整 stream: false, // 默认非流式后面我们会改成流式 messages: [], // 对话历史 }; /** * 发送对话请求非流式 * param {Array} messages - 对话消息历史 * param {Object} options - 其他参数 * returns {Promise} - 返回Promiseresolve为完整响应 */ export async function sendChatRequest(messages, options {}) { try { const requestData { ...defaultChatOptions, ...options, messages, }; // 假设后端接口路径为 /api/chat const response await aiClient.post(/api/chat, requestData); return response.data; } catch (error) { console.error(AI对话请求失败:, error); // 这里可以细化错误处理比如网络错误、服务器错误、模型错误等 throw new Error(请求失败: ${error.message}); } }1.2 设计对话数据结构对话界面里每条消息都有角色用户或助手、内容和唯一ID。我们先定义好这个结构。// src/utils/chatData.js // 定义消息角色枚举 export const MessageRole { USER: user, ASSISTANT: assistant, SYSTEM: system, // 系统指令可用于设定助手行为 }; // 生成唯一ID的简单函数 export function generateMessageId() { return msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}; } // 创建一条消息 export function createMessage(role, content) { return { id: generateMessageId(), role, content, timestamp: new Date().toISOString(), }; }2. 构建核心对话组件有了底层服务我们就可以开始构建用户能看见和交互的Vue组件了。我们将创建一个SmartChat.vue组件。2.1 组件基础结构与状态管理这个组件需要管理对话列表、用户输入并处理发送逻辑。!-- src/components/SmartChat.vue -- template div classsmart-chat-container !-- 对话消息展示区域 -- div classmessages-container refmessagesContainer div v-formessage in messages :keymessage.id :class[message-bubble, role-${message.role}] div classmessage-avatar {{ message.role user ? 你 : AI }} /div div classmessage-content !-- 如果是流式响应且是正在接收的助手消息展示动态内容 -- template v-ifmessage.role assistant message.isStreaming {{ message.streamingContent }} span classstreaming-cursor/span /template template v-else {{ message.content }} /template /div /div !-- 加载指示器 -- div v-ifisLoading classthinking-indicator 思考中... /div /div !-- 输入区域 -- div classinput-area textarea v-modeluserInput placeholder输入你的问题... keydown.enter.exact.preventhandleSend rows3 /textarea button clickhandleSend :disabledisLoading || !userInput.trim() classsend-button 发送 /button /div /div /template script setup import { ref, computed, nextTick, watch } from vue; import { sendChatRequest } from /utils/aiService; import { MessageRole, createMessage } from /utils/chatData; // 响应式数据 const messages ref([]); // 所有消息 const userInput ref(); // 用户输入 const isLoading ref(false); // 是否正在请求中 const messagesContainer ref(null); // 用于滚动到最新的DOM引用 // 初始化一条欢迎消息 messages.value.push( createMessage(MessageRole.ASSISTANT, 你好我是基于Phi-3-mini模型的助手有什么可以帮你的) ); // 处理发送消息 const handleSend async () { const inputText userInput.value.trim(); if (!inputText || isLoading.value) return; // 1. 添加用户消息到列表 const userMessage createMessage(MessageRole.USER, inputText); messages.value.push(userMessage); userInput.value ; // 清空输入框 // 2. 开始加载状态 isLoading.value true; try { // 3. 准备发送给后端的消息历史通常包含系统指令和最近的对话 const messagesForAPI [ // 可以在这里添加一条系统指令设定助手行为 { role: MessageRole.SYSTEM, content: 你是一个乐于助人的AI助手。 }, ...messages.value.slice(-10).map(m ({ role: m.role, content: m.content })), // 只发送最近10条作为上下文 ]; // 4. 调用API目前是非流式 const response await sendChatRequest(messagesForAPI); // 5. 添加助手回复到列表 const assistantMessage createMessage(MessageRole.ASSISTANT, response.choices[0]?.message?.content || 无回复); messages.value.push(assistantMessage); } catch (error) { // 6. 错误处理添加一条错误提示消息 const errorMessage createMessage(MessageRole.ASSISTANT, 抱歉我遇到了点问题${error.message}); messages.value.push(errorMessage); } finally { // 7. 结束加载状态 isLoading.value false; // 滚动到底部 scrollToBottom(); } }; // 滚动到消息容器底部 const scrollToBottom () { nextTick(() { if (messagesContainer.value) { messagesContainer.value.scrollTop messagesContainer.value.scrollHeight; } }); }; // 监听消息变化自动滚动 watch(messages, () { scrollToBottom(); }, { deep: true }); /script style scoped .smart-chat-container { display: flex; flex-direction: column; height: 600px; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; } .messages-container { flex: 1; padding: 20px; overflow-y: auto; background-color: #fafafa; } .message-bubble { display: flex; margin-bottom: 16px; } .message-bubble.role-user { flex-direction: row-reverse; } .message-avatar { width: 36px; height: 36px; border-radius: 50%; background-color: #007bff; color: white; display: flex; align-items: center; justify-content: center; font-size: 14px; margin: 0 10px; flex-shrink: 0; } .message-bubble.role-user .message-avatar { background-color: #28a745; } .message-content { max-width: 70%; padding: 12px 16px; border-radius: 18px; background-color: white; box-shadow: 0 1px 2px rgba(0,0,0,0.1); } .message-bubble.role-user .message-content { background-color: #e3f2fd; } .input-area { display: flex; border-top: 1px solid #e0e0e0; padding: 16px; background-color: white; } .input-area textarea { flex: 1; padding: 12px; border: 1px solid #ccc; border-radius: 4px; resize: none; font-family: inherit; } .send-button { margin-left: 12px; padding: 0 24px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } .send-button:disabled { background-color: #cccccc; cursor: not-allowed; } .thinking-indicator { text-align: center; color: #666; font-style: italic; padding: 10px; } /style现在一个基础的、能完成一问一答的聊天组件就完成了。但你会发现AI生成答案时页面会卡住直到全部内容返回后才一下子显示出来体验不够好。接下来我们解决这个关键问题。3. 实现流式输出Streaming提升体验流式输出能让AI的回答像打字一样一个字一个字地显示出来用户体验会流畅很多。这需要后端也支持流式响应Server-Sent Events或类似技术并且前端使用fetch来读取流数据。3.1 改造服务函数支持流式请求我们修改aiService.js增加一个专门处理流式请求的函数。// src/utils/aiService.js (新增函数) /** * 发送流式对话请求 * param {Array} messages - 对话消息历史 * param {Function} onChunk - 收到数据块时的回调函数 * param {Function} onComplete - 流式接收完成时的回调函数 * param {Object} options - 其他参数 * returns {Function} - 返回一个可调用以中止请求的函数 */ export function sendStreamingChatRequest(messages, onChunk, onComplete, options {}) { const controller new AbortController(); const requestData { ...defaultChatOptions, ...options, stream: true, // 关键开启流式 messages, }; fetch(${aiClient.defaults.baseURL}/api/chat, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify(requestData), signal: controller.signal, }) .then(async (response) { if (!response.ok) { throw new Error(HTTP error! status: ${response.status}); } const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); // 最后一行可能是不完整的放回buffer for (const line of lines) { if (line.startsWith(data: )) { const data line.slice(6); // 去掉 data: 前缀 if (data [DONE]) { onComplete(); return; } try { const parsed JSON.parse(data); const chunkContent parsed.choices[0]?.delta?.content || ; if (chunkContent) { onChunk(chunkContent); } } catch (e) { console.warn(解析流数据块失败:, e, 原始数据:, data); } } } } onComplete(); }) .catch((error) { if (error.name ! AbortError) { console.error(流式请求失败:, error); onComplete(error); } }); // 返回一个中止函数 return () controller.abort(); }3.2 升级组件以支持流式响应现在我们需要大幅修改SmartChat.vue组件让它能处理流式数据。!-- src/components/SmartChat.vue (更新script部分) -- script setup import { ref, computed, nextTick, watch } from vue; import { sendStreamingChatRequest, sendChatRequest } from /utils/aiService; import { MessageRole, createMessage } from /utils/chatData; const messages ref([]); const userInput ref(); const isLoading ref(false); const messagesContainer ref(null); // 新增用于存储当前流式响应的中止函数 let currentStreamAborter null; // 初始化欢迎消息 messages.value.push( createMessage(MessageRole.ASSISTANT, 你好我是基于Phi-3-mini模型的助手有什么可以帮你的) ); const handleSend async () { const inputText userInput.value.trim(); if (!inputText || isLoading.value) return; // 如果有正在进行的流式响应先中止它 if (currentStreamAborter) { currentStreamAborter(); currentStreamAborter null; } const userMessage createMessage(MessageRole.USER, inputText); messages.value.push(userMessage); userInput.value ; isLoading.value true; // 为即将到来的AI回复创建一个“占位”消息并标记为流式状态 const assistantMessage createMessage(MessageRole.ASSISTANT, ); assistantMessage.isStreaming true; assistantMessage.streamingContent ; // 用于累积流式内容 messages.value.push(assistantMessage); const messagesForAPI [ { role: MessageRole.SYSTEM, content: 你是一个乐于助人的AI助手。 }, ...messages.value.slice(-10).map(m ({ role: m.role, content: m.content || m.streamingContent })), ]; // 使用流式请求 currentStreamAborter sendStreamingChatRequest( messagesForAPI, // 收到数据块的回调 (chunk) { // 找到最后那条正在流式的助手消息并追加内容 const lastStreamingMsg messages.value.find(m m.id assistantMessage.id); if (lastStreamingMsg lastStreamingMsg.isStreaming) { lastStreamingMsg.streamingContent chunk; } }, // 流式完成的回调 (error) { isLoading.value false; currentStreamAborter null; const finalMsg messages.value.find(m m.id assistantMessage.id); if (finalMsg) { // 将流式内容转移到正式content并清除流式状态 finalMsg.content finalMsg.streamingContent || (未收到回复); delete finalMsg.isStreaming; delete finalMsg.streamingContent; // 如果出错更新内容为错误信息 if (error) { finalMsg.content 抱歉回答生成中断${error.message}; } } } ); }; // 滚动到底部函数保持不变 const scrollToBottom () { nextTick(() { if (messagesContainer.value) { messagesContainer.value.scrollTop messagesContainer.value.scrollHeight; } }); }; watch(messages, () { scrollToBottom(); }, { deep: true }); /script同时需要更新模板中展示流式内容的部分并添加一个“停止生成”按钮。!-- 更新模板部分 -- template div classsmart-chat-container div classmessages-container refmessagesContainer div v-formessage in messages :keymessage.id :class[message-bubble, role-${message.role}] div classmessage-avatar {{ message.role user ? 你 : AI }} /div div classmessage-content template v-ifmessage.role assistant message.isStreaming !-- 流式内容保留空格和换行 -- span classstreaming-text{{ message.streamingContent }}/span span classstreaming-cursor▌/span /template template v-else !-- 非流式或已完成的静态内容 -- {{ message.content }} /template /div /div /div div classinput-area textarea v-modeluserInput placeholder输入你的问题... keydown.enter.exact.preventhandleSend rows3 /textarea div classbutton-group button v-ifisLoading currentStreamAborter clickstopGenerating classstop-button 停止生成 /button button clickhandleSend :disabledisLoading || !userInput.trim() classsend-button 发送 /button /div /div /div /template script setup // ... 其他script部分保持不变新增stopGenerating函数 const stopGenerating () { if (currentStreamAborter) { currentStreamAborter(); currentStreamAborter null; isLoading.value false; // 找到当前流式消息将其状态固定 const streamingMsgIndex messages.value.findIndex(m m.isStreaming); if (streamingMsgIndex ! -1) { const msg messages.value[streamingMsgIndex]; msg.content msg.streamingContent (已停止); delete msg.isStreaming; delete msg.streamingContent; } } }; /script style scoped /* 新增样式 */ .streaming-text { white-space: pre-wrap; /* 保留空格和换行 */ } .streaming-cursor { display: inline-block; width: 8px; height: 1.2em; background-color: #007bff; animation: blink 1s infinite; margin-left: 2px; vertical-align: middle; } keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .button-group { display: flex; flex-direction: column; margin-left: 12px; gap: 8px; } .stop-button { padding: 8px 16px; background-color: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; } .send-button { padding: 8px 16px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } .send-button:disabled { background-color: #cccccc; cursor: not-allowed; } /style4. 功能增强与状态管理一个健壮的对话组件还需要考虑更多细节比如对话历史管理、错误处理优化、以及可能的状态持久化。4.1 使用Pinia管理全局对话状态对于更复杂的应用建议使用PiniaVue官方推荐的状态管理库来集中管理对话状态方便在不同组件间共享。npm install pinia首先创建一个对话Store。// src/stores/chatStore.js import { defineStore } from pinia; import { ref, computed } from vue; import { MessageRole, createMessage } from /utils/chatData; export const useChatStore defineStore(chat, () { // 状态 const sessions ref([]); // 所有对话会话 const currentSessionId ref(null); // 当前活跃会话ID const isLoading ref(false); // 计算属性当前会话的消息列表 const currentMessages computed(() { const session sessions.value.find(s s.id currentSessionId.value); return session ? session.messages : []; }); // 动作 function createNewSession(title 新对话) { const newSession { id: sess_${Date.now()}, title, messages: [ createMessage(MessageRole.ASSISTANT, 你好我是你的AI助手。) ], createdAt: new Date().toISOString(), }; sessions.value.unshift(newSession); // 新会话放在最前面 currentSessionId.value newSession.id; return newSession.id; } function addMessageToCurrentSession(role, content) { const sessionIndex sessions.value.findIndex(s s.id currentSessionId.value); if (sessionIndex ! -1) { const message createMessage(role, content); sessions.value[sessionIndex].messages.push(message); // 可选自动更新会话标题例如用第一条用户消息 if (role MessageRole.USER sessions.value[sessionIndex].messages.length 2) { sessions.value[sessionIndex].title content.substring(0, 20) (content.length 20 ? ... : ); } } } // 初始化时创建一个默认会话 if (sessions.value.length 0) { createNewSession(); } return { sessions, currentSessionId, isLoading, currentMessages, createNewSession, addMessageToCurrentSession, }; });然后在SmartChat.vue组件中使用这个Store。!-- src/components/SmartChat.vue (简化版使用Pinia) -- template !-- 模板基本不变但消息来源改为 store.currentMessages -- div v-formessage in currentMessages :keymessage.id.../div /template script setup import { useChatStore } from /stores/chatStore; import { sendStreamingChatRequest } from /utils/aiService; import { MessageRole } from /utils/chatData; const chatStore useChatStore(); const userInput ref(); const messagesContainer ref(null); let currentStreamAborter null; const handleSend async () { // ... 逻辑类似但操作store const inputText userInput.value.trim(); if (!inputText || chatStore.isLoading) return; chatStore.addMessageToCurrentSession(MessageRole.USER, inputText); userInput.value ; // 为AI回复创建占位消息需要在store中支持流式状态此处略去细节 // 调用流式API... // 在onChunk回调中更新store中对应消息的streamingContent // 在onComplete回调中固定消息内容 }; // 计算属性获取当前消息 const currentMessages computed(() chatStore.currentMessages); /script4.2 错误处理与用户体验优化网络状态提示在组件中添加网络连接状态的检测和提示。重试机制当请求失败时提供重试按钮。输入框防抖在连续快速发送时可以加入防抖逻辑。本地存储使用localStorage或IndexedDB将会话历史保存在浏览器端。5. 总结把Phi-3-mini这样的轻量模型集成到Vue 3前端项目里核心思路其实很清晰前端负责展示和交互通过HTTP API与后端模型服务通信。关键在于流式输出的实现它能极大提升用户感知速度让对话感觉更自然、更即时。整个过程走下来从最基础的请求响应到支持流式输出再到用状态管理库来组织复杂的对话数据每一步都是在解决实际开发中会遇到的问题。代码里我尽量把关键部分都写出来了你可以直接拿去改改用在自己的项目里。当然实际项目中可能还需要考虑更多比如API密钥的管理、对话上下文的长度控制、不同模型的参数调整等等。这种前端集成AI能力的模式特别适合做内部工具、客服辅助、内容生成平台这些场景。Phi-3-mini模型在指令理解和响应速度上表现不错对于很多轻量级应用来说已经足够用了。如果你正在为你的Web应用寻找增加智能交互的方法不妨从这个小模型和这套前端集成方案开始试试。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。