通义千问1.5-1.8B-Chat-GPTQ-Int4 WebUI开发:Node.js后端服务调用实战
通义千问1.5-1.8B-Chat-GPTQ-Int4 WebUI开发Node.js后端服务调用实战最近在折腾一些AI应用的原型发现很多有意思的模型都提供了WebUI界面比如通义千问的这个轻量级版本。WebUI用起来是方便点一点就行但如果你想把它集成到自己的项目里比如做个智能客服机器人、或者给内部系统加个问答助手总不能每次都让用户去打开那个网页界面吧这时候后端服务调用就派上用场了。通过Node.js写个服务直接和模型的WebUI后端“对话”把AI能力变成你应用里的一行API调用这才是真正的工程化落地。今天我就结合自己的踩坑经验聊聊怎么用Node.js来搞定这件事从环境搭建到流式响应处理给你一个能直接抄作业的实战方案。1. 项目起点环境准备与模型服务假设在开始写代码之前我们得先把“舞台”搭好。这里有两个前提需要明确因为本文重点在于Node.js如何调用而不是模型本身的部署。首先你需要有一个已经启动并运行起来的通义千问WebUI服务。这通常意味着你已经通过Docker、源码安装或者其他方式成功部署了模型并且可以通过浏览器访问一个本地地址比如http://localhost:7860来使用聊天界面。我们的Node.js服务就是要和这个地址背后的API打交道。其次我们来准备Node.js的开发环境。这件事现在简单多了。1.1 Node.js环境安装与配置如果你还没装Node.js可以去官网下载最新的长期支持版本。安装过程就是一路下一步没什么坑。安装完成后打开终端用下面两行命令验证一下node --version npm --version能看到版本号比如v18.x.x和9.x.x就说明安装成功了。接下来为我们的项目创建一个新的目录并初始化它mkdir qwen-nodejs-integration cd qwen-nodejs-integration npm init -y这个npm init -y命令会快速生成一个package.json文件里面记录了项目的基本信息和依赖。1.2 安装核心依赖包我们这个项目主要需要两个依赖一个用来创建Web服务器另一个用来发送HTTP请求。这里我选择Express和Axios因为它们生态丰富用的人多遇到问题也容易找到解决方案。在项目根目录下运行npm install express axios如果你更喜欢Koa或者Fetch API思路也是完全相通的只是语法稍有不同。安装完成后你的package.json的dependencies部分应该能看到这两个包。环境这就准备好了接下来我们进入正题看看怎么跟WebUI的API“对话”。2. 核心交互调用WebUI聊天接口WebUI通常会在后端暴露一个标准的API接口供前端调用我们的目标就是模拟这个调用过程。经过对常见WebUI如Ollama、OpenAI WebUI等的观察这类接口通常是一个接收POST请求的聊天端点。2.1 理解API请求格式我们先来写一个最简单的调用函数看看如何与WebUI服务通信。在项目根目录创建一个apiClient.js文件const axios require(axios); // 假设你的通义千问WebUI服务运行在本地7860端口 const API_BASE_URL http://localhost:7860; async function sendChatMessage(message) { try { const response await axios.post(${API_BASE_URL}/api/chat, { // 这是最常见的请求体结构具体字段可能需要根据实际WebUI调整 model: Qwen1.5-1.8B-Chat-GPTQ-Int4, // 指定模型名称 messages: [ { role: user, content: message } ], stream: false // 先关闭流式响应看看一次性返回的结果 }); console.log(AI回复:, response.data); return response.data; } catch (error) { console.error(调用API失败:, error.message); if (error.response) { // 服务器返回了错误状态码4xx, 5xx console.error(错误详情:, error.response.data); } throw error; } } // 测试一下 (async () { try { const reply await sendChatMessage(你好请介绍一下你自己。); console.log(成功收到回复:, reply.choices[0].message.content); } catch (e) { console.log(测试失败); } })();这段代码做了几件事定义了API的基础地址。创建了一个sendChatMessage函数它用Axios向/api/chat发送POST请求。请求体里包含了模型名、消息历史这里只有用户最新的一条以及是否流式输出的标志。用了一个自执行的异步函数来立即测试。关键点/api/chat和请求体的具体格式如model,messages的键名需要根据你实际使用的通义千问WebUI的API文档来确定。如果不对通常会返回404或400错误根据错误信息调整即可。stream: false意味着我们让服务器一次性生成完整回复再返回适合调试。2.2 处理流式响应上面是一次性获取回复但AI生成文字通常是一点点“吐”出来的为了更好的用户体验像打字机效果我们需要处理流式响应。这稍微复杂一点但原理是监听数据块。修改apiClient.js增加一个流式对话函数const axios require(axios); const { Readable } require(stream); const API_BASE_URL http://localhost:7860; async function sendChatMessageStream(message, onDataChunk) { try { const response await axios({ method: post, url: ${API_BASE_URL}/api/chat, data: { model: Qwen1.5-1.8B-Chat-GPTQ-Int4, messages: [{ role: user, content: message }], stream: true // 关键开启流式输出 }, responseType: stream // 关键告诉Axios我们期待一个流 }); const stream response.data; let fullContent ; stream.on(data, (chunk) { // 流式数据通常是一行行的JSON文本以 data: 开头 const chunkStr chunk.toString(); const lines chunkStr.split(\n).filter(line line.trim()); for (const line of lines) { if (line.startsWith(data: )) { const dataStr line.slice(6); // 去掉 data: 前缀 if (dataStr [DONE]) { console.log(流式传输结束); return; } try { const parsed JSON.parse(dataStr); // 假设回复内容在 parsed.choices[0].delta.content const contentChunk parsed.choices?.[0]?.delta?.content || ; if (contentChunk) { fullContent contentChunk; // 调用回调函数将新的内容块传递出去 if (onDataChunk) { onDataChunk(contentChunk); } } } catch (e) { // 忽略非JSON行或解析错误 } } } }); stream.on(end, () { console.log(Stream ended. Full reply:, fullContent); if (onDataChunk) { onDataChunk(null, fullContent); // 传递结束信号和完整内容 } }); stream.on(error, (err) { console.error(Stream error:, err); if (onDataChunk) { onDataChunk(err); } }); } catch (error) { console.error(请求失败:, error.message); throw error; } } // 测试流式调用 (async () { console.log(开始流式对话...); await sendChatMessageStream(写一首关于春天的短诗, (chunk, full) { if (chunk typeof chunk string) { process.stdout.write(chunk); // 逐块打印到控制台模拟打字效果 } else if (chunk instanceof Error) { console.error(出错:, chunk.message); } else if (full) { console.log(\n\n完整回复已生成。); } }); })();这段代码的核心变化是设置了responseType: stream和stream: true。我们不再等待整个响应而是监听data事件像接水管一样来一块数据就处理一块。这里假设WebUI返回的是Server-Sent Events格式每行以data:开头。你需要根据实际API返回的数据格式来调整解析逻辑。3. 构建业务层用Express创建代理服务器直接从前端调用模型服务会有跨域问题而且把API密钥或内部服务地址暴露给浏览器也不安全。更好的做法是搭建一个Node.js后端作为代理统一处理这些请求并可以在这里添加认证、限流、日志等业务逻辑。3.1 创建基础的Express服务器在项目根目录创建server.js文件const express require(express); const axios require(axios); const app express(); const port 3000; // 中间件解析JSON格式的请求体 app.use(express.json()); // 中间件处理跨域请求根据需求调整 app.use((req, res, next) { res.header(Access-Control-Allow-Origin, *); // 生产环境应指定具体域名 res.header(Access-Control-Allow-Headers, Content-Type, Authorization); res.header(Access-Control-Allow-Methods, GET, POST, OPTIONS); if (req.method OPTIONS) { return res.sendStatus(200); } next(); }); // 健康检查端点 app.get(/health, (req, res) { res.json({ status: ok, service: Qwen API Proxy }); }); // 核心聊天代理接口 app.post(/api/proxy/chat, async (req, res) { const userMessage req.body.message; const useStream req.body.stream || false; if (!userMessage) { return res.status(400).json({ error: 缺少 message 参数 }); } console.log(收到用户消息: ${userMessage}, 流式模式: ${useStream}); try { // 这里是调用我们之前封装的函数或者直接写Axios调用 // 为了演示这里直接写调用逻辑 const response await axios.post(http://localhost:7860/api/chat, { model: Qwen1.5-1.8B-Chat-GPTQ-Int4, messages: [{ role: user, content: userMessage }], stream: useStream }, { responseType: useStream ? stream : json // 根据前端需求决定响应类型 }); if (useStream) { // 设置流式响应头 res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); // 将模型服务的流直接管道到客户端响应 response.data.pipe(res); // 处理源流错误防止服务器崩溃 response.data.on(error, (err) { console.error(模型流错误:, err); if (!res.headersSent) { res.status(500).end(); } }); } else { // 一次性响应 res.json(response.data); } } catch (error) { console.error(代理请求失败:, error.message); const statusCode error.response?.status || 500; const errorData error.response?.data || { error: Internal Server Error }; res.status(statusCode).json(errorData); } }); app.listen(port, () { console.log(Node.js代理服务器运行在 http://localhost:${port}); });这个服务器做了几件有用的事提供了一个/api/proxy/chat接口前端只需向它发送请求。它内部去调用真正的模型服务解决了跨域问题。根据前端传来的stream参数智能地处理一次性或流式响应。添加了错误处理将模型服务的错误信息合理地返回给前端。3.2 添加对话状态管理简单的问答没问题但真正的对话需要记忆上下文。我们可以在服务器端维护一个简单的会话状态。注意这是一个简单的内存存储示例生产环境需要用数据库如Redis来持久化。在server.js中增加会话管理// 简单的内存存储用于演示。生产环境请使用数据库。 const sessions new Map(); // 生成唯一会话ID function generateSessionId() { return sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}; } // 增强的聊天代理接口支持多轮对话 app.post(/api/proxy/chat-with-context, async (req, res) { const { message, sessionId: clientSessionId, newSession } req.body; const useStream req.body.stream || false; if (!message) { return res.status(400).json({ error: 缺少 message 参数 }); } let sessionId clientSessionId; let messages []; // 处理会话逻辑 if (newSession || !sessionId || !sessions.has(sessionId)) { sessionId generateSessionId(); messages [{ role: system, content: 你是一个乐于助人的AI助手。 }]; sessions.set(sessionId, messages); console.log(创建新会话: ${sessionId}); } else { messages sessions.get(sessionId); console.log(继续会话: ${sessionId}, 历史消息数: ${messages.length}); } // 将用户新消息加入历史 messages.push({ role: user, content: message }); try { const response await axios.post(http://localhost:7860/api/chat, { model: Qwen1.5-1.8B-Chat-GPTQ-Int4, messages: messages, // 发送整个对话历史 stream: useStream }, { responseType: useStream ? stream : json }); if (useStream) { res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); res.write(data: ${JSON.stringify({ sessionId })}\n\n); // 先发送sessionId let fullAIContent ; response.data.on(data, (chunk) { // 简化处理实际应像之前一样解析SSE const chunkStr chunk.toString(); // ... 解析逻辑提取 contentChunk ... // fullAIContent contentChunk; res.write(chunk); // 将模型流直接转发 }); response.data.on(end, () { // 流结束后将AI回复加入内存中的历史 // messages.push({ role: assistant, content: fullAIContent }); // sessions.set(sessionId, messages); res.end(); }); response.data.pipe(res); // 更简单的管道方式但需注意格式兼容 } else { const aiReply response.data.choices[0].message.content; // 将AI回复加入历史并保存 messages.push({ role: assistant, content: aiReply }); sessions.set(sessionId, messages); res.json({ sessionId, reply: aiReply, fullHistory: messages // 可选返回完整历史用于调试 }); } } catch (error) { console.error(对话请求失败:, error.message); res.status(500).json({ error: 对话处理失败, details: error.message }); } });这样前端只需要在第一次请求时或想开始新对话时不传sessionId或设置newSession: true后续请求带上返回的sessionId就能进行连续的多轮对话了。服务器会维护这个会话的历史记录。4. 前端调用示例与项目整合后端服务准备好了前端怎么用呢这里给一个非常简单的HTML示例展示如何调用我们刚搭建的代理接口。在项目根目录创建public文件夹并在里面创建一个index.html文件。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title通义千问Node.js集成测试/title style body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 1rem; } #chatBox { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 1rem; margin-bottom: 1rem; } .message { margin-bottom: 0.8rem; } .user { color: #0066cc; text-align: right; } .ai { color: #009933; } #inputArea { display: flex; gap: 0.5rem; } #userInput { flex-grow: 1; padding: 0.5rem; } button { padding: 0.5rem 1.5rem; } label { margin-right: 1rem; } /style /head body h2与通义千问对话 (通过Node.js代理)/h2 div labelinput typecheckbox idstreamCheckbox 使用流式输出/label button onclickstartNewSession()开始新会话/button /div div idchatBox/div div idinputArea input typetext iduserInput placeholder输入你的问题... onkeypresshandleKeyPress(event) button onclicksendMessage()发送/button /div script let currentSessionId null; function addMessage(text, sender) { const chatBox document.getElementById(chatBox); const msgDiv document.createElement(div); msgDiv.className message ${sender}; msgDiv.textContent ${sender user ? 你 : AI}: ${text}; chatBox.appendChild(msgDiv); chatBox.scrollTop chatBox.scrollHeight; } function appendToLastAIMessage(chunk) { const chatBox document.getElementById(chatBox); const aiMessages chatBox.querySelectorAll(.ai); const lastAiMsg aiMessages[aiMessages.length - 1]; if (lastAiMsg lastAiMsg.dataset.streaming true) { lastAiMsg.textContent lastAiMsg.textContent.replace(/AI: /, ) chunk; lastAiMsg.textContent AI: lastAiMsg.textContent; } else { // 创建新的AI消息块 const msgDiv document.createElement(div); msgDiv.className message ai; msgDiv.dataset.streaming true; msgDiv.textContent AI: ${chunk}; chatBox.appendChild(msgDiv); } chatBox.scrollTop chatBox.scrollHeight; } async function sendMessage() { const input document.getElementById(userInput); const message input.value.trim(); const useStream document.getElementById(streamCheckbox).checked; if (!message) return; addMessage(message, user); input.value ; const payload { message: message, sessionId: currentSessionId, stream: useStream }; try { if (useStream) { // 流式请求 addMessage(, ai); // 先占位 const response await fetch(http://localhost:3000/api/proxy/chat-with-context, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }); const reader response.body.getReader(); const decoder new TextDecoder(); 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(); // 最后一行可能不完整 for (const line of lines) { if (line.startsWith(data: )) { const dataStr line.slice(6); if (dataStr.startsWith({)) { try { const data JSON.parse(dataStr); if (data.sessionId) { currentSessionId data.sessionId; console.log(Session ID:, currentSessionId); } } catch(e) { /* 忽略非JSON数据 */ } } else if (dataStr.trim()) { // 假设这是AI回复的文本块 appendToLastAIMessage(dataStr); } } } } // 流结束标记完成 const lastAiMsg document.querySelector(.ai[data-streamingtrue]); if (lastAiMsg) lastAiMsg.dataset.streaming false; } else { // 非流式请求 const response await fetch(http://localhost:3000/api/proxy/chat-with-context, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }); const data await response.json(); if (data.sessionId) currentSessionId data.sessionId; addMessage(data.reply, ai); } } catch (error) { console.error(请求出错:, error); addMessage(抱歉请求出错: ${error.message}, ai); } } function handleKeyPress(event) { if (event.key Enter) { sendMessage(); } } function startNewSession() { currentSessionId null; document.getElementById(chatBox).innerHTML div classmessage ai已开始新会话。/div; } /script /body /html为了让Express能提供这个静态页面需要在server.js开头添加一行app.use(express.static(public)); // 提供静态文件服务现在运行node server.js打开浏览器访问http://localhost:3000就能看到一个简单的聊天界面通过你的Node.js代理与背后的通义千问模型对话了。5. 总结走完这一趟你会发现把通义千问这样的AI模型通过WebUI集成到Node.js应用里核心思路就是“代理”和“适配”。我们搭建的Node.js服务就像一个翻译官和调度员把前端友好的请求转换成模型服务能理解的格式再把模型生成的结果以合适的方式一次性或流式返回给前端。整个过程的关键点有几个一是理解并正确调用WebUI暴露的API接口格式要对二是熟练处理流式响应这是提升用户体验的重点三是在代理层实现会话管理让对话有记忆最后是做好错误处理和日志让整个流程更稳健。这个方案的好处是灵活你可以在代理服务器上做很多文章比如加个API密钥验证、给请求限个流、或者把对话记录存到数据库里做分析。代码里用的内存存储只是图个方便真要上线的话换成Redis或者MySQL更靠谱。模型部署和API调用有时候会有点小波折主要是文档和实际接口可能对不上多看看日志调整一下请求参数一般都能解决。希望这个实战指南能帮你顺利地把AI能力接进自己的项目里。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2438596.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!