通义千问1.5-1.8B-Chat-GPTQ-Int4 WebUI开发:Node.js后端服务调用实战
本文介绍了如何在星图GPU平台上自动化部署通义千问1.5-1.8B-Chat-GPTQ-Int4 WebUI镜像,并利用Node.js构建后端服务调用其AI能力。通过搭建代理服务器,开发者可以轻松将该大语言模型集成到智能客服、问答助手等应用场景中,实现工程化落地。
通义千问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 lang="zh-CN">
<head>
<meta charset="UTF-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>
<label><input type="checkbox" id="streamCheckbox"> 使用流式输出</label>
<button onclick="startNewSession()">开始新会话</button>
</div>
<div id="chatBox"></div>
<div id="inputArea">
<input type="text" id="userInput" placeholder="输入你的问题..." onkeypress="handleKeyPress(event)">
<button onclick="sendMessage()">发送</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-streaming="true"]');
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 class="message 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星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐



所有评论(0)