Phi-3 Forest Laboratory 集成Vue3前端项目:打造AI对话管理界面
Phi-3 Forest Laboratory 集成Vue3前端项目打造AI对话管理界面最近在做一个AI小工具后端用上了Phi-3 Forest Laboratory这个轻量级模型推理速度挺快。但光有后端还不够用户需要一个能看得见、摸得着的界面来和AI对话。这就轮到前端上场了。我选择了Vue 3因为它现在的组合式API写起来特别顺手响应式系统管理状态也很清晰。整个目标很明确做一个简洁、好用的对话界面能发送问题能看到AI一个字一个字“思考”出来的回答还能方便地管理之前的聊天记录。这篇文章我就来分享一下怎么把Phi-3的后端API“塞进”一个Vue 3项目里从头搭建一个属于你自己的AI对话前台。你会发现把模型能力变成用户指尖的交互这个过程其实挺有意思的。1. 项目初始化与核心思路在动手写代码之前我们先聊聊这个项目要干成什么样。核心目标就一个在浏览器里创建一个能和Phi-3模型流畅对话的界面。这意味着前端需要处理好几件事一是怎么和后端API“说话”发送请求和接收响应二是怎么把对话的历史记录有条理地存起来、展示出来三是怎么实现那种“逐字输出”的效果让等待回答的过程不那么枯燥最后还得加上一些基本的交互比如清空对话、标记重要信息等。Vue 3的组合式API和响应式系统恰好能非常优雅地处理这些状态管理和UI更新的需求。我们整体的技术栈很简单Vue 3 TypeScript 构建界面Axios 处理HTTP请求再用Vue的内置响应式工具如ref,reactive来管理所有会变化的数据。假设你的Phi-3 Forest Laboratory后端服务已经跑起来了API地址是http://localhost:8000提供了一个/v1/chat/completions的端点来接收对话请求。那么前端的任务就是围绕这个端点构建一个友好的交互层。2. 构建对话状态与核心组件首先我们来定义这个应用最核心的“状态”——对话数据。在src/composables/useChat.ts文件里我们用组合式函数来封装这块逻辑。// src/composables/useChat.ts import { ref, reactive } from vue; import type { Ref } from vue; // 定义一条消息的接口 export interface Message { id: string; role: user | assistant; content: string; timestamp: number; isBookmarked?: boolean; // 是否被标记 } // 定义对话状态 export interface ChatState { messages: Message[]; isLoading: boolean; error: string | null; inputText: string; } export function useChat() { // 使用 reactive 创建响应式状态对象 const state: ChatState reactive({ messages: [], isLoading: false, error: null, inputText: , }); // 添加一条新消息 const addMessage (role: Message[role], content: string) { const newMessage: Message { id: Date.now().toString(), role, content, timestamp: Date.now(), }; state.messages.push(newMessage); }; // 清空所有消息 const clearMessages () { state.messages []; state.error null; }; // 切换某条消息的标记状态 const toggleBookmark (messageId: string) { const message state.messages.find(msg msg.id messageId); if (message) { message.isBookmarked !message.isBookmarked; } }; return { state, addMessage, clearMessages, toggleBookmark, }; }这个useChat组合函数成了我们整个应用的数据中枢。state对象是响应式的里面装着所有的对话记录、加载状态、错误信息和用户正在输入的文本。任何对这些数据的修改都会自动触发依赖它们的UI组件更新。接下来我们创建主要的页面组件src/views/ChatView.vue。这个组件负责把状态渲染成用户看到的界面。!-- src/views/ChatView.vue -- template div classchat-container header classchat-header h1 Phi-3 对话实验室/h1 button clickhandleClear classbtn-clear :disabledstate.messages.length 0 清空对话 /button /header !-- 消息列表区域 -- div classmessages-container div v-formessage in state.messages :keymessage.id :class[message-bubble, role-${message.role}] div classmessage-header span classmessage-role{{ message.role user ? 你 : AI助手 }}/span button clicktoggleBookmark(message.id) classbtn-bookmark :class{ bookmarked: message.isBookmarked } {{ message.isBookmarked ? ★ : ☆ }} /button /div div classmessage-content{{ message.content }}/div div classmessage-time {{ new Date(message.timestamp).toLocaleTimeString() }} /div /div !-- 加载指示器 -- div v-ifstate.isLoading classloading-indicator AI正在思考中... /div !-- 错误显示 -- div v-ifstate.error classerror-alert {{ state.error }} /div /div !-- 输入区域 -- div classinput-area textarea v-modelstate.inputText keydown.enter.exact.preventhandleSend placeholder输入你的问题... (按Enter发送) rows3 /textarea button clickhandleSend classbtn-send :disabledstate.isLoading || !state.inputText.trim() 发送 /button /div /div /template script setup langts import { useChat } from /composables/useChat; import { sendMessageToAPI } from /api/chatApi; // 稍后我们会实现这个API函数 const { state, addMessage, clearMessages, toggleBookmark } useChat(); const handleSend async () { const userInput state.inputText.trim(); if (!userInput || state.isLoading) return; // 1. 添加用户消息到界面 addMessage(user, userInput); const userMessage state.inputText; state.inputText ; // 清空输入框 state.isLoading true; state.error null; try { // 2. 调用API获取AI回复这里先写个占位下一节实现流式 const aiResponse await sendMessageToAPI(userMessage); // 3. 添加AI回复到界面 addMessage(assistant, aiResponse); } catch (err: any) { state.error err.message || 请求失败请稍后重试; } finally { state.isLoading false; } }; const handleClear () { if (confirm(确定要清空所有对话记录吗)) { clearMessages(); } }; /script style scoped /* 这里省略具体的CSS样式主要包含布局、气泡框、按钮等基础样式 */ .chat-container { max-width: 800px; margin: 0 auto; height: 100vh; display: flex; flex-direction: column; } .messages-container { flex: 1; overflow-y: auto; padding: 20px; } .message-bubble { margin-bottom: 16px; padding: 12px 16px; border-radius: 12px; max-width: 80%; } .role-user { background-color: #e3f2fd; align-self: flex-end; margin-left: auto; } .role-assistant { background-color: #f5f5f5; align-self: flex-start; } .input-area { border-top: 1px solid #eee; padding: 16px; display: flex; gap: 12px; } .input-area textarea { flex: 1; padding: 12px; border: 1px solid #ccc; border-radius: 8px; resize: none; font-family: inherit; } .btn-send, .btn-clear { padding: 10px 20px; background-color: #1976d2; color: white; border: none; border-radius: 6px; cursor: pointer; } .btn-send:disabled, .btn-clear:disabled { background-color: #ccc; cursor: not-allowed; } /style现在一个基础的对话界面骨架就有了。你可以输入文字点击发送界面会暂时模拟添加一条AI回复因为我们还没连真正的API。清空和标记功能也已经可以工作。接下来我们要解决最关键的一步如何与后端的Phi-3模型进行真正的、支持流式输出的对话。3. 实现API通信与流式响应普通的API调用是一次性返回完整结果但对于大语言模型等待它生成一整段话可能需要几秒甚至十几秒用户体验会很差。流式输出Server-Sent Events, SSE能让模型边生成边传输前端可以逐字渲染感觉就像AI在实时“打字”体验好很多。首先我们创建API层。在src/api/chatApi.ts中我们将实现两种请求方式普通请求和流式请求。// src/api/chatApi.ts import axios from axios; // 配置后端API基础地址 const API_BASE_URL import.meta.env.VITE_API_BASE_URL || http://localhost:8000; // 定义请求和响应类型 interface ChatMessage { role: user | assistant | system; content: string; } interface ChatCompletionRequest { messages: ChatMessage[]; stream?: boolean; max_tokens?: number; temperature?: number; } // 普通请求一次性返回 export async function sendMessageToAPI( userMessage: string, history: ChatMessage[] [] ): Promisestring { const requestPayload: ChatCompletionRequest { messages: [...history, { role: user, content: userMessage }], stream: false, max_tokens: 500, temperature: 0.7, }; try { const response await axios.post(${API_BASE_URL}/v1/chat/completions, requestPayload); // 假设后端返回结构为 { choices: [{ message: { content: string } }] } return response.data.choices[0]?.message?.content || 模型未返回有效内容; } catch (error: any) { console.error(API请求失败:, error); throw new Error(error.response?.data?.error || 网络请求异常); } } // 流式请求核心 export async function streamMessageFromAPI( userMessage: string, history: ChatMessage[] [], onChunk: (chunk: string) void, // 收到数据块时的回调 onComplete: (fullText: string) void, // 流式完成时的回调 onError: (error: string) void // 错误处理回调 ) { const requestPayload: ChatCompletionRequest { messages: [...history, { role: user, content: userMessage }], stream: true, // 关键参数开启流式 max_tokens: 500, temperature: 0.7, }; try { const response await fetch(${API_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify(requestPayload), }); if (!response.ok || !response.body) { throw new Error(HTTP error! status: ${response.status}); } const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let accumulatedText ; while (true) { const { done, value } await reader.read(); if (done) break; // 解码数据块 const chunk decoder.decode(value, { stream: true }); // 处理SSE格式数据行以 data: 开头 const lines chunk.split(\n); for (const line of lines) { if (line.startsWith(data: ) line ! data: [DONE]) { try { const jsonStr line.slice(6); // 去掉 data: 前缀 if (jsonStr.trim()) { const parsed JSON.parse(jsonStr); const contentDelta parsed.choices[0]?.delta?.content || ; if (contentDelta) { accumulatedText contentDelta; onChunk(contentDelta); // 实时推送每一个新生成的字符/词 } } } catch (e) { console.warn(解析流式数据块失败:, e, line); } } } } onComplete(accumulatedText); // 流式传输完毕 reader.releaseLock(); } catch (error: any) { console.error(流式请求失败:, error); onError(error.message || 流式连接失败); } }有了这个流式API函数我们就可以改造之前的ChatView.vue组件让AI的回答能够逐字显示了。我们需要修改handleSend函数并引入一个新的响应式变量来临时存储流式生成的内容。!-- 在 ChatView.vue 的 script 部分进行修改 -- script setup langts import { ref } from vue; import { useChat } from /composables/useChat; import { streamMessageFromAPI } from /api/chatApi; // 导入流式函数 const { state, addMessage, clearMessages, toggleBookmark } useChat(); // 新增一个ref用于临时存储正在流式接收的AI消息 const streamingMessage ref(); const handleSend async () { const userInput state.inputText.trim(); if (!userInput || state.isLoading) return; // 添加用户消息 addMessage(user, userInput); const userMessage state.inputText; state.inputText ; state.isLoading true; state.error null; streamingMessage.value ; // 清空上一次的流式内容 // 准备对话历史格式化为API需要的格式 const historyForAPI state.messages .filter(msg msg.role ! user || msg.content ! userMessage) // 过滤掉刚添加的当前用户消息 .map(msg ({ role: msg.role, content: msg.content })); try { // 先添加一条空的AI消息占位用于后续更新内容 const aiMessageId Date.now().toString(); state.messages.push({ id: aiMessageId, role: assistant, content: , // 初始内容为空 timestamp: Date.now(), }); // 调用流式API await streamMessageFromAPI( userMessage, historyForAPI, // 收到数据块的回调更新临时变量和界面上的占位消息 (chunk) { streamingMessage.value chunk; // 找到刚才添加的那条空AI消息更新其内容 const targetMessage state.messages.find(msg msg.id aiMessageId); if (targetMessage) { targetMessage.content streamingMessage.value; } }, // 流式完成后的回调最终确定消息内容 (fullText) { streamingMessage.value fullText; const targetMessage state.messages.find(msg msg.id aiMessageId); if (targetMessage) { targetMessage.content fullText; } }, // 错误回调 (errorMsg) { state.error 流式请求出错: ${errorMsg}; // 如果出错移除那条空的AI占位消息 const index state.messages.findIndex(msg msg.id aiMessageId); if (index -1) { state.messages.splice(index, 1); } } ); } catch (err: any) { state.error err.message || 请求发送失败; } finally { state.isLoading false; streamingMessage.value ; // 清空临时流式存储 } }; /script现在当你发送一个问题后AI的回答就会像真人打字一样逐字逐句地出现在对话框里。这种即时反馈的体验远比等待一个旋转的加载图标要好得多。4. 功能增强与体验优化基础功能跑通了我们可以再给它加点儿“料”让这个对话界面更好用、更健壮。1. 对话历史持久化用户关掉浏览器再打开肯定不希望对话记录全没了。我们可以利用浏览器的localStorage简单实现这个功能。// 在 useChat.ts 中增加功能 import { watch } from vue; export function useChat() { const state: ChatState reactive({ messages: JSON.parse(localStorage.getItem(phi3-chat-history) || []), // 初始化时读取 isLoading: false, error: null, inputText: , }); // 监听 messages 变化自动保存到 localStorage watch( () state.messages, (newMessages) { localStorage.setItem(phi3-chat-history, JSON.stringify(newMessages)); }, { deep: true } // 深度监听确保标记状态变化也能触发保存 ); // ... 其他函数不变 }2. 优化流式输出的视觉体验我们可以给正在接收的文字添加一个闪烁的光标动画让它看起来更像是在输入中。!-- 在 ChatView.vue 的模板中修改AI消息的显示部分 -- div classmessage-content {{ message.content }} !-- 如果这条消息是正在接收的流式消息且处于加载中显示光标动画 -- span v-ifstate.isLoading message.id currentStreamingMessageId classstreaming-cursor ▋ /span /div style scoped /* 添加光标闪烁动画 */ .streaming-cursor { display: inline-block; animation: blink 1s infinite; color: #1976d2; } keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } /style3. 处理网络中断与重试网络环境复杂我们需要处理连接意外中断的情况。可以为流式请求增加一个超时和重试机制。// 在 chatApi.ts 的 streamMessageFromAPI 函数中增加超时控制 export async function streamMessageFromAPI( // ... 参数不变 onError: (error: string) void ) { const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), 30000); // 30秒超时 try { const response await fetch(${API_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(requestPayload), signal: controller.signal, // 关联AbortController }); clearTimeout(timeoutId); // ... 后续流式处理逻辑不变 } catch (error: any) { clearTimeout(timeoutId); if (error.name AbortError) { onError(请求超时请检查网络或稍后重试); } else { onError(error.message || 流式连接失败); } } }4. 支持对话上下文管理对于更长的对话我们可以选择只发送最近N条消息给后端以节省token并保持上下文相关性。这可以在准备historyForAPI时处理。// 在 ChatView.vue 的 handleSend 函数中修改 // 只取最近5轮对话作为上下文10条消息 const recentMessages state.messages.slice(-10); const historyForAPI recentMessages.map(msg ({ role: msg.role, content: msg.content }));5. 总结回过头来看我们把一个Phi-3 Forest Laboratory的后端API通过Vue 3前端项目变成了一个具有实时交互感的AI对话界面。这个过程涉及了几个关键点首先是状态管理用Vue的响应式系统来管理对话列表、加载状态这些动态数据让UI能自动跟着数据变。然后是API集成特别是实现了流式输出SSE这是提升大模型对话体验的核心让等待过程变得可视化用户体验流畅很多。最后我们围绕这个核心添加了历史记录保存、消息标记、网络健壮性处理等实用功能让这个小工具更像一个完整的产品。代码本身其实不复杂更多的是对前端如何与AI后端协同工作的思路梳理。你完全可以基于这个基础继续扩展更多功能比如支持多轮对话主题切换、调整模型参数temperature、max_tokens、甚至集成语音输入输出。前端在这里扮演的角色就是模型能力的“翻译官”和“放大器”把冰冷的API响应转化成温暖、直观的用户交互。动手试试吧看着自己搭建的界面能和AI模型流畅对话那种成就感还是挺足的。如果你在集成过程中遇到问题或者有了新的创意不妨多看看社区里的实践前端生态里总有现成的轮子或思路能帮到你。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。