QwQ-32B与Vue3前端框架的交互实现
QwQ-32B与Vue3前端框架的交互实现最近在做一个智能问答项目需要在前端页面里集成一个推理能力强的AI模型。找了一圈发现QwQ-32B这个模型挺有意思的它专门针对推理任务做了优化效果据说很不错。不过问题来了怎么把这个模型跟我的Vue3前端项目结合起来呢传统的做法是把模型部署在服务器端前端通过API调用。但这样有几个问题网络延迟、服务器成本、还有隐私安全考虑。后来我发现其实可以用Ollama在本地运行QwQ-32B然后前端直接跟本地服务交互这样既快又安全。1. 为什么选择QwQ-32B和Vue3的组合先说说为什么选这两个技术。QwQ-32B是Qwen系列里的推理专用模型跟普通的对话模型不太一样。它有个特点就是会先“思考”再回答。你问它一个问题它会在内部先推理一番然后再给出最终答案。这种模式特别适合需要逻辑推理的场景比如代码生成、数学解题、数据分析这些。Vue3呢现在是前端开发的主流框架之一它的组合式API用起来特别顺手响应式系统也很成熟。更重要的是Vue3的生态很丰富各种工具库都有跟后端服务集成起来很方便。把这两个结合起来就能在前端项目里直接调用本地的AI模型实现各种智能功能。比如你可以做个代码助手在IDE里直接问AI怎么实现某个功能或者做个数据分析工具让AI帮你分析数据趋势。2. 环境准备与模型部署要开始之前得先把环境搭好。这里我用的是Ollama来管理本地模型因为它用起来简单一条命令就能搞定。2.1 安装Ollama如果你还没装Ollama可以去官网下载安装包或者用命令行安装。我是在Mac上用的安装命令很简单curl -fsSL https://ollama.com/install.sh | shWindows用户可以直接下载exe安装包Linux用户也可以用类似的脚本安装。装好之后Ollama服务会自动启动你可以在终端里验证一下ollama --version能看到版本号就说明安装成功了。2.2 拉取QwQ-32B模型Ollama装好后拉取模型就是一句话的事ollama pull qwq:32b这个命令会下载QwQ-32B的量化版本大概20GB左右。下载速度取决于你的网络我这边用了大概半小时。下载完成后你可以运行一下试试ollama run qwq:32b然后随便问个问题比如“Hello!”看看模型能不能正常响应。如果能看到回复说明模型部署成功了。这里有个小细节要注意QwQ-32B是推理模型它的回复格式跟普通对话模型不太一样。它会先输出思考过程然后再给最终答案。比如你问它一个数学题它可能会先写一段推理然后再给出答案。3. Vue3项目搭建与基础配置模型准备好了接下来就是前端部分了。我新建了一个Vue3项目用的是Vite因为启动快配置也简单。3.1 创建Vue3项目npm create vuelatest my-ai-project创建过程中我选了TypeScript、Pinia、Vue Router这些常用工具。项目创建好后安装一些必要的依赖cd my-ai-project npm install3.2 配置Ollama API客户端Ollama提供了HTTP API我们可以在前端直接调用。我创建了一个专门的service来处理跟Ollama的通信// src/services/ollamaService.ts import axios from axios; const OLLAMA_BASE_URL http://localhost:11434/api; export interface ChatMessage { role: user | assistant | system; content: string; } export interface ChatRequest { model: string; messages: ChatMessage[]; stream?: boolean; options?: { temperature?: number; top_p?: number; top_k?: number; }; } export interface ChatResponse { model: string; created_at: string; message: { role: string; content: string; }; done: boolean; } class OllamaService { private client axios.create({ baseURL: OLLAMA_BASE_URL, timeout: 30000, // 30秒超时因为模型推理可能需要时间 }); async chat(request: ChatRequest): PromiseChatResponse { try { const response await this.client.postChatResponse(/chat, { ...request, stream: false, // 先不用流式响应简化处理 }); return response.data; } catch (error) { console.error(Ollama API调用失败:, error); throw error; } } async streamChat(request: ChatRequest, onChunk: (chunk: string) void) { const response await fetch(${OLLAMA_BASE_URL}/chat, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ ...request, stream: true, }), }); const reader response.body?.getReader(); if (!reader) return; const decoder new TextDecoder(); while (true) { const { done, value } await reader.read(); if (done) break; const chunk decoder.decode(value); const lines chunk.split(\n).filter(line line.trim()); for (const line of lines) { try { const data JSON.parse(line); if (data.message?.content) { onChunk(data.message.content); } } catch (e) { // 忽略解析错误 } } } } async listModels() { try { const response await this.client.get(/tags); return response.data.models; } catch (error) { console.error(获取模型列表失败:, error); return []; } } } export const ollamaService new OllamaService();这个服务类封装了跟Ollama API的交互提供了聊天和流式聊天两种方式。流式聊天适合需要实时显示生成内容的场景用户体验更好。4. 实现智能聊天界面有了基础服务接下来就是实现用户界面了。我设计了一个简单的聊天界面包含消息列表、输入框和发送按钮。4.1 聊天组件实现!-- src/components/ChatInterface.vue -- template div classchat-container div classchat-header h2QwQ-32B智能助手/h2 div classmodel-info span当前模型: {{ currentModel }}/span button clickrefreshModels classrefresh-btn刷新模型/button /div /div div classmessages-container refmessagesContainer div v-for(message, index) in messages :keyindex :class[message, message.role] div classmessage-avatar span v-ifmessage.role user/span span v-else/span /div div classmessage-content div classmessage-role{{ roleNames[message.role] }}/div div classmessage-text v-htmlformatContent(message.content)/div div v-ifmessage.thinking classthinking-section div classthinking-label思考过程:/div div classthinking-content v-htmlformatContent(message.thinking)/div /div /div /div div v-ifisLoading classmessage assistant div classmessage-avatar/div div classmessage-content div classmessage-roleAI助手/div div classloading-indicator div classloading-dots span/spanspan/spanspan/span /div div classloading-textQwQ正在思考中.../div /div /div /div /div div classinput-container div classinput-header label input typecheckbox v-modelshowThinking / 显示思考过程 /label div classtemperature-control span温度: {{ temperature.toFixed(1) }}/span input typerange v-modeltemperature min0 max1 step0.1 classtemp-slider / /div /div div classinput-area textarea v-modelinputText keydown.enter.exact.preventsendMessage placeholder输入您的问题... (按Enter发送ShiftEnter换行) rows3 classmessage-input /textarea button clicksendMessage :disabledisLoading || !inputText.trim() classsend-btn {{ isLoading ? 思考中... : 发送 }} /button /div div classquick-actions button v-foraction in quickActions :keyaction.label clickuseQuickAction(action) classquick-btn {{ action.label }} /button /div /div /div /template script setup langts import { ref, computed, onMounted, nextTick, watch } from vue; import { ollamaService, type ChatMessage } from /services/ollamaService; interface Message extends ChatMessage { thinking?: string; timestamp: Date; } const messages refMessage[]([]); const inputText ref(); const isLoading ref(false); const currentModel ref(qwq:32b); const showThinking ref(true); const temperature ref(0.6); const messagesContainer refHTMLElement(); const roleNames { user: 您, assistant: QwQ助手, system: 系统 }; const quickActions [ { label: 解释代码, prompt: 请解释以下代码的功能和工作原理 }, { label: 数学解题, prompt: 请分步骤解答以下数学问题 }, { label: 写代码, prompt: 请用JavaScript实现以下功能 }, { label: 分析问题, prompt: 请分析以下问题的关键点和解决方案 }, ]; const formatContent (content: string) { // 简单的Markdown格式处理 return content .replace(/\*\*(.*?)\*\*/g, strong$1/strong) .replace(/\n/g, br) .replace(/([^])/g, code$1/code) .replace(/(\w)?\n([\s\S]*?)/g, precode$2/code/pre); }; const scrollToBottom () { nextTick(() { if (messagesContainer.value) { messagesContainer.value.scrollTop messagesContainer.value.scrollHeight; } }); }; const sendMessage async () { const text inputText.value.trim(); if (!text || isLoading.value) return; // 添加用户消息 const userMessage: Message { role: user, content: text, timestamp: new Date(), }; messages.value.push(userMessage); inputText.value ; // 添加占位符消息 const assistantMessage: Message { role: assistant, content: , timestamp: new Date(), }; messages.value.push(assistantMessage); isLoading.value true; scrollToBottom(); try { // 构建完整的对话历史 const chatHistory: ChatMessage[] messages.value .slice(0, -1) // 排除刚添加的占位符消息 .map(msg ({ role: msg.role, content: msg.content, })); // 调用Ollama API const response await ollamaService.chat({ model: currentModel.value, messages: [ ...chatHistory, { role: user, content: text } ], options: { temperature: temperature.value, top_p: 0.95, }, }); // 处理QwQ的特殊响应格式 const fullResponse response.message.content; let thinkingContent ; let finalAnswer fullResponse; // 尝试提取思考过程QwQ会在think标签中输出思考 const thinkMatch fullResponse.match(/think([\s\S]*?)\/think/); if (thinkMatch) { thinkingContent thinkMatch[1].trim(); finalAnswer fullResponse.replace(/think[\s\S]*?\/think/, ).trim(); } // 更新最后一条消息 const lastIndex messages.value.length - 1; messages.value[lastIndex] { ...messages.value[lastIndex], content: finalAnswer, thinking: showThinking.value ? thinkingContent : undefined, }; } catch (error) { console.error(发送消息失败:, error); const lastIndex messages.value.length - 1; messages.value[lastIndex].content 抱歉请求失败。请检查Ollama服务是否运行正常。; } finally { isLoading.value false; scrollToBottom(); } }; const useQuickAction (action: typeof quickActions[0]) { inputText.value action.prompt; }; const refreshModels async () { const models await ollamaService.listModels(); console.log(可用模型:, models); // 这里可以更新模型选择器 }; // 初始化时添加欢迎消息 onMounted(() { messages.value.push({ role: assistant, content: 您好我是QwQ-32B助手擅长推理和问题分析。请问有什么可以帮您, timestamp: new Date(), }); }); // 监听消息变化自动滚动 watch(messages, () { scrollToBottom(); }, { deep: true }); /script style scoped .chat-container { display: flex; flex-direction: column; height: 100vh; max-width: 800px; margin: 0 auto; background: #f5f5f5; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); } .chat-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; } .model-info { display: flex; justify-content: center; align-items: center; gap: 15px; margin-top: 10px; font-size: 14px; } .refresh-btn { background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.3); color: white; padding: 5px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; transition: background 0.3s; } .refresh-btn:hover { background: rgba(255, 255, 255, 0.3); } .messages-container { flex: 1; overflow-y: auto; padding: 20px; background: #fafafa; } .message { display: flex; margin-bottom: 20px; animation: fadeIn 0.3s ease; } keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .message-avatar { width: 40px; height: 40px; border-radius: 50%; background: white; display: flex; align-items: center; justify-content: center; margin-right: 15px; font-size: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .message.user .message-avatar { background: #e3f2fd; } .message.assistant .message-avatar { background: #f3e5f5; } .message-content { flex: 1; background: white; padding: 15px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .message.user .message-content { background: #e3f2fd; border-top-left-radius: 4px; } .message.assistant .message-content { background: white; border-top-right-radius: 4px; } .message-role { font-weight: 600; font-size: 12px; color: #666; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; } .message-text { line-height: 1.6; color: #333; } .message-text code { background: #f5f5f5; padding: 2px 6px; border-radius: 4px; font-family: Courier New, monospace; font-size: 0.9em; } .message-text pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; overflow-x: auto; margin: 10px 0; } .message-text pre code { background: none; padding: 0; color: inherit; } .thinking-section { margin-top: 15px; padding-top: 15px; border-top: 1px dashed #ddd; } .thinking-label { font-size: 12px; color: #888; font-weight: 600; margin-bottom: 8px; } .thinking-content { font-size: 13px; color: #666; line-height: 1.5; font-style: italic; background: #f9f9f9; padding: 10px; border-radius: 6px; border-left: 3px solid #667eea; } .loading-indicator { display: flex; align-items: center; gap: 10px; } .loading-dots { display: flex; gap: 4px; } .loading-dots span { width: 8px; height: 8px; border-radius: 50%; background: #667eea; animation: bounce 1.4s infinite ease-in-out both; } .loading-dots span:nth-child(1) { animation-delay: -0.32s; } .loading-dots span:nth-child(2) { animation-delay: -0.16s; } keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } } .loading-text { color: #666; font-size: 14px; } .input-container { background: white; padding: 20px; border-top: 1px solid #eee; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); } .input-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; font-size: 14px; color: #666; } .temperature-control { display: flex; align-items: center; gap: 10px; } .temp-slider { width: 100px; height: 6px; -webkit-appearance: none; background: #e0e0e0; border-radius: 3px; outline: none; } .temp-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #667eea; cursor: pointer; transition: background 0.3s; } .temp-slider::-webkit-slider-thumb:hover { background: #5a67d8; } .input-area { display: flex; gap: 10px; margin-bottom: 15px; } .message-input { flex: 1; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; line-height: 1.5; resize: none; transition: border-color 0.3s; font-family: inherit; } .message-input:focus { outline: none; border-color: #667eea; } .send-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 0 24px; border-radius: 8px; font-weight: 600; cursor: pointer; transition: transform 0.2s, opacity 0.3s; min-width: 80px; } .send-btn:hover:not(:disabled) { transform: translateY(-1px); } .send-btn:disabled { opacity: 0.5; cursor: not-allowed; } .quick-actions { display: flex; gap: 10px; flex-wrap: wrap; } .quick-btn { background: #f0f0f0; border: 1px solid #ddd; padding: 6px 12px; border-radius: 6px; font-size: 12px; color: #555; cursor: pointer; transition: all 0.3s; } .quick-btn:hover { background: #e0e0e0; transform: translateY(-1px); } /style这个聊天界面实现了几个关键功能实时显示对话、支持Markdown格式渲染、显示QwQ的思考过程、温度调节、快速操作按钮等。界面设计也比较现代有动画效果用户体验不错。4.2 处理QwQ的特殊响应格式QwQ-32B作为推理模型它的响应格式比较特殊。它会在think标签里输出思考过程然后再给出最终答案。我们的代码需要处理这种格式// 在sendMessage函数中处理响应 const fullResponse response.message.content; let thinkingContent ; let finalAnswer fullResponse; // 尝试提取思考过程 const thinkMatch fullResponse.match(/think([\s\S]*?)\/think/); if (thinkMatch) { thinkingContent thinkMatch[1].trim(); finalAnswer fullResponse.replace(/think[\s\S]*?\/think/, ).trim(); }这样用户就可以选择是否显示AI的思考过程对于学习和技术交流很有帮助。5. 实际应用场景演示有了基础框架我们来看看在实际项目中怎么用这个组合。我做了几个演示场景都是开发中常见的需求。5.1 代码解释与优化假设你有一段复杂的代码看不懂可以直接贴给QwQ让它解释// 用户输入 const complexCode function deepClone(obj, hash new WeakMap()) { if (obj null || typeof obj ! object) return obj; if (hash.has(obj)) return hash.get(obj); const clone Array.isArray(obj) ? [] : {}; hash.set(obj, clone); for (let key in obj) { if (obj.hasOwnProperty(key)) { clone[key] deepClone(obj[key], hash); } } return clone; } ; // 问QwQ请解释这段代码的功能和实现原理QwQ会先思考“这是一个深拷贝函数使用了WeakMap处理循环引用...”然后给出详细的解释包括每行代码的作用、WeakMap的用途、循环引用的处理等。5.2 数学问题求解对于数学或算法问题QwQ的推理能力特别有用// 用户一个楼梯有10级台阶每次可以走1级或2级有多少种走法 // QwQ的思考过程 // 这是一个典型的动态规划问题类似于斐波那契数列 // 设f(n)为走到第n级台阶的方法数 // f(1) 1, f(2) 2 // f(n) f(n-1) f(n-2) // 计算f(10) f(9) f(8) ...5.3 业务逻辑分析在实际业务开发中经常需要分析复杂的业务逻辑// 用户我们的电商系统需要实现一个优惠券系统支持满减、折扣、包邮等多种类型 // 还要考虑叠加规则、有效期、使用限制等请帮我设计数据库表和核心逻辑 // QwQ会分析 // 1. 优惠券表设计类型、面值、使用条件、有效期等字段 // 2. 用户优惠券表关联用户和优惠券记录使用状态 // 3. 优惠规则引擎如何计算最优优惠组合 // 4. 并发处理防止同一优惠券被重复使用6. 性能优化与实践建议在实际使用中我发现了一些可以优化的地方分享给大家6.1 流式响应优化上面的代码用的是完整响应等模型全部生成完才显示。其实可以用流式响应让用户看到实时生成的内容// 在ollamaService中添加流式聊天方法 async streamChat(request: ChatRequest, onChunk: (chunk: string) void) { const response await fetch(${OLLAMA_BASE_URL}/chat, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ ...request, stream: true, }), }); const reader response.body?.getReader(); if (!reader) return; const decoder new TextDecoder(); while (true) { const { done, value } await reader.read(); if (done) break; const chunk decoder.decode(value); const lines chunk.split(\n).filter(line line.trim()); for (const line of lines) { try { const data JSON.parse(line); if (data.message?.content) { onChunk(data.message.content); } } catch (e) { // 忽略解析错误 } } } }然后在组件中使用const sendMessageStream async () { // ... 前面的代码类似 let accumulatedContent ; let accumulatedThinking ; await ollamaService.streamChat( { model: currentModel.value, messages: [ ...chatHistory, { role: user, content: text } ], options: { temperature: temperature.value, top_p: 0.95, }, }, (chunk) { accumulatedContent chunk; // 实时更新显示 const lastIndex messages.value.length - 1; messages.value[lastIndex].content accumulatedContent; scrollToBottom(); } ); // 处理完成后再提取思考过程 // ... };6.2 上下文管理QwQ-32B支持很长的上下文128K tokens但实际使用时要注意管理对话历史。太长的历史会影响性能和效果。我建议摘要历史对较旧的对话进行摘要只保留关键信息滑动窗口只保留最近N轮对话重要信息提取提取对话中的关键信息如用户偏好、任务目标等单独保存// 简单的上下文管理 const manageContext (messages: Message[], maxTokens 4000) { if (messages.length 5) return messages; // 保留最近5轮 // 保留系统消息、最近3轮对话和第一条用户消息 const systemMessages messages.filter(m m.role system); const recentMessages messages.slice(-3); const firstUserMessage messages.find(m m.role user); const managedMessages [ ...systemMessages, ...(firstUserMessage !recentMessages.includes(firstUserMessage) ? [firstUserMessage] : []), ...recentMessages, ]; return managedMessages; };6.3 错误处理与重试网络请求可能会失败需要完善的错误处理const sendMessageWithRetry async (retries 3) { for (let i 0; i retries; i) { try { return await sendMessage(); } catch (error) { if (i retries - 1) throw error; // 等待一段时间后重试 await new Promise(resolve setTimeout(resolve, 1000 * (i 1))); console.log(第${i 1}次重试...); } } };6.4 本地存储对话历史为了方便用户可以添加本地存储功能// 保存对话历史 const saveConversation () { const conversation { id: Date.now().toString(), title: messages.value[0]?.content?.substring(0, 50) || 新对话, messages: messages.value, createdAt: new Date(), }; const saved JSON.parse(localStorage.getItem(conversations) || []); saved.push(conversation); localStorage.setItem(conversations, JSON.stringify(saved.slice(-20))); // 最多保存20个 }; // 加载对话历史 const loadConversation (id: string) { const saved JSON.parse(localStorage.getItem(conversations) || []); const conversation saved.find((c: any) c.id id); if (conversation) { messages.value conversation.messages; } };7. 总结把QwQ-32B和Vue3结合起来在前端项目里实现智能交互这个方案用下来感觉挺不错的。最大的优点是响应速度快因为模型跑在本地没有网络延迟。而且数据隐私有保障敏感信息不用上传到云端。QwQ-32B的推理能力确实强特别是对于需要逻辑分析的任务比普通对话模型表现更好。不过它也有个特点就是思考时间比较长毕竟要内部推理一番。这时候用流式响应就很关键让用户能看到生成过程不会觉得卡住了。Vue3的响应式系统和组件化开发让集成AI功能变得很顺畅。你可以把聊天组件做成一个通用组件在不同的页面里复用。状态管理用Pinia也很方便可以统一管理对话历史、模型设置这些状态。实际用的时候我发现温度设置对输出质量影响挺大的。温度太高比如0.8以上回答会比较发散温度太低0.3以下又可能太死板。一般设置在0.5-0.7之间比较合适既有创造性又不失准确性。还有一点QwQ-32B对提示词比较敏感。如果你想要它输出特定格式的内容最好在系统消息里明确说明。比如让它用JSON格式回复或者分步骤解答问题。总的来说这个技术组合适合需要强推理能力的应用场景比如代码助手、学习辅导、数据分析工具这些。如果你正在做这类项目不妨试试这个方案。当然第一次部署可能会遇到些小问题比如模型下载慢、内存不够这些但解决后体验还是很不错的。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。