通义千问1.5-1.8B-Chat-GPTQ-Int4与Node.js后端集成:构建全栈AI聊天应用
本文介绍了如何在星图GPU平台上自动化部署通义千问1.5-1.8B-Chat-GPTQ-Int4镜像,并详细阐述了将其与Node.js后端集成以构建全栈AI聊天应用的完整流程。通过该平台,开发者可快速搭建智能对话服务,实现一个具备流式输出功能的Web聊天界面,适用于智能客服、内容创作辅助等场景。
通义千问1.5-1.8B-Chat-GPTQ-Int4与Node.js后端集成:构建全栈AI聊天应用
最近在折腾一些AI应用,发现很多朋友对如何把大模型真正用起来,特别是集成到自己的Web项目里,感觉有点无从下手。正好前段时间在星图GPU平台上部署了通义千问的一个轻量版模型,就想着能不能把它包装成一个聊天服务,然后用Node.js搭个后端,再做个简单的前端界面,串起来玩一玩。
没想到整个过程比预想的要顺畅不少。今天我就把这个从模型API调用到全栈应用搭建的完整过程分享出来,如果你也想给自己的网站或者内部工具加个智能聊天功能,这篇内容应该能给你一个清晰的路线图。我们不用搞得太复杂,就从最基础的开始,一步步来。
1. 项目准备与环境搭建
在开始写代码之前,我们需要先把“战场”准备好。这里主要分两块:一是确保你的Node.js环境是OK的,二是你得有一个已经部署好并能提供API服务的通义千问模型。
1.1 Node.js环境检查与配置
首先,打开你的终端,检查一下Node.js的版本。这个项目对版本要求不算苛刻,但建议用比较新的长期支持版。
node --version
如果显示版本号(比如v18.x或v20.x),那就可以继续了。如果还没安装,可以去Node.js官网下载安装包,过程很简单,一路下一步就行。
安装好后,我们新建一个项目目录,并初始化它。
mkdir qwen-chat-app && cd qwen-chat-app
npm init -y
这个命令会生成一个package.json文件,它就像我们项目的“身份证”和“购物清单”,记录了项目信息和需要的工具包。
接下来,安装我们马上要用到的几个核心工具包。我们用一个命令搞定:
npm install express axios cors dotenv
我简单说一下这几个包是干嘛的:
express:这是Node.js里最流行的Web框架,我们用它来快速搭建后端服务器和定义API接口。axios:一个非常好用的HTTP客户端,我们靠它去调用星图平台上那个通义千问模型的API。cors:一个处理跨域资源共享的中间件。因为我们的前端和后端通常运行在不同的端口或地址上,浏览器会有安全限制,这个包能帮我们解决这个问题。dotenv:用来管理环境变量。我们把像API地址、密钥这类敏感信息放在一个单独的.env文件里,而不是硬写在代码中,这样更安全,也方便切换不同环境。
1.2 获取模型API访问信息
这一步的前提是,你已经在星图GPU平台上成功部署了“通义千问1.5-1.8B-Chat-GPTQ-Int4”这个镜像。部署成功后,平台通常会提供一个API访问的端点(Endpoint)和必要的认证信息(比如API Key)。
你需要找到并记下这两样东西:
- API Base URL:看起来像
https://your-instance-id.region.example.com/v1这样的一个网址。 - API Key:一长串字符,用于验证你的请求身份。
为了安全起见,我们不在代码里直接写死这些信息。在项目根目录下创建一个名为 .env 的文件:
touch .env
然后,用文本编辑器打开这个.env文件,把刚才的信息填进去,格式如下:
QWEN_API_BASE_URL=https://your-actual-api-endpoint/v1
QWEN_API_KEY=your-actual-api-key-here
PORT=3000
注意把 your-actual-api-endpoint 和 your-actual-api-key-here 替换成你从星图平台获取的真实信息。PORT 是我们后端服务要监听的端口号,这里先设为3000。
2. 构建Node.js后端服务
环境准备好之后,我们就可以动手搭建服务的“大脑”——后端了。后端主要负责接收前端的聊天请求,然后去调用真正的模型API,拿到回复后再传回给前端。
2.1 创建Express服务器与基础路由
我们在项目根目录下创建一个 server.js 文件,这是后端服务的入口。
// server.js
require('dotenv').config(); // 加载.env文件中的环境变量
const express = require('express');
const axios = require('axios');
const cors = require('cors');
const app = express();
const port = process.env.PORT || 3000;
// 使用中间件
app.use(cors()); // 允许跨域请求
app.use(express.json()); // 解析JSON格式的请求体
// 从环境变量读取模型API配置
const QWEN_API_BASE = process.env.QWEN_API_BASE_URL;
const QWEN_API_KEY = process.env.QWEN_API_KEY;
// 配置axios实例,用于调用模型API
const qwenApiClient = axios.create({
baseURL: QWEN_API_BASE,
headers: {
'Authorization': `Bearer ${QWEN_API_KEY}`,
'Content-Type': 'application/json',
},
});
// 一个简单的健康检查路由,用来测试服务是否启动
app.get('/health', (req, res) => {
res.json({ status: 'OK', message: 'Chat backend is running.' });
});
// 启动服务器
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
现在,你可以运行 node server.js 来启动服务。如果看到控制台打印出运行在3000端口的信息,并且在浏览器访问 http://localhost:3000/health 能看到返回的JSON数据,说明基础服务搭建成功了。
2.2 实现核心聊天API接口
健康检查通了,我们来写最关键的聊天接口。这个接口需要做两件事:接收用户发来的消息,然后转发给通义千问模型。
在 server.js 文件中,在健康检查路由后面添加新的路由:
// server.js (接上文)
// 核心聊天接口 - 非流式(一次性返回完整回复)
app.post('/api/chat', async (req, res) => {
try {
const userMessage = req.body.message;
if (!userMessage || userMessage.trim() === '') {
return res.status(400).json({ error: 'Message cannot be empty.' });
}
// 构造发送给通义千问API的请求体
const requestBody = {
model: 'qwen1.5-1.8b-chat', // 根据实际部署的模型名称调整
messages: [
{
role: 'user',
content: userMessage
}
],
stream: false // 非流式输出
};
// 调用模型API
const response = await qwenApiClient.post('/chat/completions', requestBody);
// 从模型响应中提取回复内容
const aiReply = response.data.choices[0]?.message?.content || 'No response from model.';
// 将回复返回给前端
res.json({
reply: aiReply,
model: response.data.model,
usage: response.data.usage // 可选,包含token消耗信息
});
} catch (error) {
console.error('Error calling Qwen API:', error.response?.data || error.message);
res.status(500).json({
error: 'Failed to get response from AI model.',
details: error.message
});
}
});
这个接口已经可以工作了。你可以在终端里用 curl 命令测试一下:
curl -X POST http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "你好,请介绍一下你自己。"}'
如果一切配置正确,你应该能收到一段来自通义千问模型的自我介绍。
2.3 实现流式对话输出接口
一次性返回所有内容虽然简单,但用户需要等待模型完全生成完毕才能看到回复,体验不够好。流式输出可以让回复像打字一样一个字一个字地显示出来,体验更自然。这对于一个聊天应用来说,算是个“加分项”。
实现流式输出的原理是,模型API会返回一个数据流(Server-Sent Events),我们的后端需要把这个流转发(pipe)给前端。我们来添加这个流式接口:
// server.js (接上文)
// 流式聊天接口
app.post('/api/chat/stream', async (req, res) => {
const userMessage = req.body.message;
if (!userMessage || userMessage.trim() === '') {
return res.status(400).json({ error: 'Message cannot be empty.' });
}
const requestBody = {
model: 'qwen1.5-1.8b-chat',
messages: [{ role: 'user', content: userMessage }],
stream: true // 关键:开启流式输出
};
try {
// 设置响应头,告诉前端这是一个事件流
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 向模型API发起流式请求
const apiResponse = await qwenApiClient.post('/chat/completions', requestBody, {
responseType: 'stream' // 告诉axios我们期待一个流式响应
});
// 将模型API返回的数据流,直接转发给前端
apiResponse.data.pipe(res);
// 处理流错误
apiResponse.data.on('error', (err) => {
console.error('Stream error from model API:', err);
if (!res.headersSent) {
res.status(500).end();
}
});
} catch (error) {
console.error('Error setting up stream:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Failed to establish stream.' });
}
}
});
这样,后端服务就具备了两种聊天模式。你可以根据前端的需求选择使用哪个接口。
3. 开发前端聊天界面
后端搞定了,我们得有个界面让用户能输入和看到对话。为了简单快速,我们不用复杂的Vue或React框架,就用最基础的HTML、CSS和一点JavaScript,写一个单页应用。这样你也能更清楚地看到前后端是如何交互的。
在项目根目录下创建一个 public 文件夹,然后在里面创建 index.html 和 app.js。
3.1 构建聊天界面HTML
public/index.html 文件内容如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>通义千问聊天演示</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
body { background-color: #f5f5f7; color: #1d1d1f; line-height: 1.6; padding: 20px; max-width: 800px; margin: 0 auto; }
header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #d2d2d7; }
h1 { color: #0066cc; margin-bottom: 10px; }
.subtitle { color: #86868b; font-size: 0.95em; }
.chat-container { background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); overflow: hidden; margin-bottom: 20px; }
#chat-history { height: 400px; overflow-y: auto; padding: 20px; }
.message { margin-bottom: 16px; padding: 12px 16px; border-radius: 18px; max-width: 80%; clear: both; }
.user-message { background-color: #007aff; color: white; float: right; border-bottom-right-radius: 4px; }
.ai-message { background-color: #e5e5ea; color: black; float: left; border-bottom-left-radius: 4px; }
.ai-thinking { color: #8e8e93; font-style: italic; }
.input-area { display: flex; padding: 20px; background: #fbfbfd; border-top: 1px solid #d2d2d7; }
#message-input { flex-grow: 1; padding: 12px 16px; border: 1px solid #c7c7cc; border-radius: 10px; font-size: 16px; margin-right: 10px; }
#message-input:focus { outline: none; border-color: #007aff; }
button { padding: 12px 24px; background-color: #007aff; color: white; border: none; border-radius: 10px; font-size: 16px; cursor: pointer; transition: background-color 0.2s; }
button:hover { background-color: #0056cc; }
button:disabled { background-color: #a7a7ad; cursor: not-allowed; }
.controls { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; font-size: 0.9em; color: #86868b; }
.mode-switch label { margin-right: 15px; cursor: pointer; }
footer { text-align: center; margin-top: 30px; color: #86868b; font-size: 0.85em; }
</style>
</head>
<body>
<header>
<h1>🤖 通义千问聊天演示</h1>
<p class="subtitle">基于 Qwen-1.8B-Chat 模型与 Node.js 全栈应用</p>
</header>
<main>
<div class="chat-container">
<div id="chat-history">
<!-- 聊天记录会动态插入到这里 -->
<div class="message ai-message">
你好!我是通义千问,一个AI助手。有什么可以帮你的吗?
</div>
</div>
<div class="input-area">
<input type="text" id="message-input" placeholder="输入你的问题..." autocomplete="off">
<button id="send-button">发送</button>
</div>
</div>
<div class="controls">
<div class="mode-switch">
<label><input type="radio" name="mode" value="stream" checked> 流式输出(打字机效果)</label>
<label><input type="radio" name="mode" value="normal"> 普通输出(一次性返回)</label>
</div>
<button id="clear-button">清空对话</button>
</div>
</main>
<footer>
<p>模型:通义千问 1.5-1.8B-Chat-GPTQ-Int4 | 后端:Node.js + Express</p>
</footer>
<script src="app.js"></script>
</body>
</html>
3.2 实现前端交互逻辑
现在来写JavaScript逻辑,让这个静态页面活起来。创建 public/app.js 文件:
// public/app.js
document.addEventListener('DOMContentLoaded', function() {
const chatHistory = document.getElementById('chat-history');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const clearButton = document.getElementById('clear-button');
const modeRadios = document.querySelectorAll('input[name="mode"]');
// 当前对话模式,默认为流式
let currentMode = 'stream';
// 当前正在进行的流式响应处理函数,用于中止请求
let currentStreamController = null;
// 监听模式切换
modeRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
currentMode = e.target.value;
console.log(`切换到 ${currentMode} 模式`);
});
});
// 发送消息函数
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
// 禁用输入和按钮,防止重复发送
messageInput.disabled = true;
sendButton.disabled = true;
// 在界面上显示用户消息
appendMessage(message, 'user');
// 清空输入框
messageInput.value = '';
// 根据模式调用不同的后端接口
if (currentMode === 'stream') {
await handleStreamResponse(message);
} else {
await handleNormalResponse(message);
}
// 重新启用输入和按钮
messageInput.disabled = false;
sendButton.disabled = false;
messageInput.focus(); // 聚焦到输入框
}
// 处理普通(非流式)响应
async function handleNormalResponse(userMessage) {
// 先显示一个“思考中”的占位符
const thinkingId = appendThinkingIndicator();
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage })
});
const data = await response.json();
// 移除“思考中”占位符
removeElement(thinkingId);
if (response.ok) {
// 显示AI回复
appendMessage(data.reply, 'ai');
} else {
// 显示错误信息
appendMessage(`抱歉,出错了:${data.error || '未知错误'}`, 'ai');
}
} catch (error) {
removeElement(thinkingId);
appendMessage(`网络请求失败:${error.message}`, 'ai');
console.error('Request failed:', error);
}
}
// 处理流式响应
async function handleStreamResponse(userMessage) {
// 为AI回复创建一个新的消息容器,初始为空
const aiMessageId = `ai-msg-${Date.now()}`;
const aiMessageElem = document.createElement('div');
aiMessageElem.className = 'message ai-message';
aiMessageElem.id = aiMessageId;
chatHistory.appendChild(aiMessageElem);
// 滚动到底部
chatHistory.scrollTop = chatHistory.scrollHeight;
try {
// 使用AbortController以便可以中止请求
const controller = new AbortController();
currentStreamController = controller;
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage }),
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 处理服务器发送的事件流
const reader = response.body.getReader();
const decoder = new TextDecoder();
let aiReply = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码数据块
const chunk = decoder.decode(value);
// 事件流格式是 "data: {...}\n\n",我们需要解析它
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6); // 去掉 "data: " 前缀
if (dataStr === '[DONE]') {
// 流结束
currentStreamController = null;
return;
}
try {
const data = JSON.parse(dataStr);
const content = data.choices[0]?.delta?.content || '';
if (content) {
aiReply += content;
// 更新DOM,显示累积的回复
aiMessageElem.textContent = aiReply;
// 滚动到底部,确保新内容可见
chatHistory.scrollTop = chatHistory.scrollHeight;
}
} catch (e) {
console.warn('Failed to parse stream data:', e);
}
}
}
}
currentStreamController = null;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Stream request was aborted.');
aiMessageElem.textContent += ' (已中止)';
} else {
console.error('Stream error:', error);
aiMessageElem.textContent = `流式响应出错:${error.message}`;
}
currentStreamController = null;
}
}
// 工具函数:在聊天记录中添加一条消息
function appendMessage(text, sender) {
const messageElem = document.createElement('div');
messageElem.className = `message ${sender}-message`;
messageElem.textContent = text;
chatHistory.appendChild(messageElem);
// 滚动到底部
chatHistory.scrollTop = chatHistory.scrollHeight;
}
// 工具函数:添加“思考中”指示器
function appendThinkingIndicator() {
const thinkingId = `thinking-${Date.now()}`;
const thinkingElem = document.createElement('div');
thinkingElem.className = 'message ai-message ai-thinking';
thinkingElem.id = thinkingId;
thinkingElem.textContent = '思考中...';
chatHistory.appendChild(thinkingElem);
chatHistory.scrollTop = chatHistory.scrollHeight;
return thinkingId;
}
// 工具函数:移除指定ID的元素
function removeElement(id) {
const elem = document.getElementById(id);
if (elem) elem.remove();
}
// 清空对话历史
function clearChat() {
// 如果有正在进行的流式请求,中止它
if (currentStreamController) {
currentStreamController.abort();
currentStreamController = null;
}
// 保留第一条AI欢迎消息
const welcomeMsg = chatHistory.querySelector('.ai-message:first-child');
chatHistory.innerHTML = '';
if (welcomeMsg) {
chatHistory.appendChild(welcomeMsg);
} else {
// 如果没有欢迎消息,加一条
appendMessage('你好!我是通义千问,一个AI助手。有什么可以帮你的吗?', 'ai');
}
}
// 事件监听
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
clearButton.addEventListener('click', clearChat);
// 初始聚焦到输入框
messageInput.focus();
});
3.3 让Express提供静态文件
最后,我们需要修改一下 server.js,让Express能够把我们刚写好的 public 目录作为静态资源服务出去。这样,当我们访问后端根地址时,就能看到这个聊天页面了。
在 server.js 文件的开头(在定义 app 之后),添加这行代码:
// server.js (在 app.use(cors()) 附近添加)
app.use(express.static('public')); // 提供public目录下的静态文件
现在,重启你的后端服务 (node server.js),然后在浏览器中访问 http://localhost:3000。你应该能看到一个简洁的聊天界面。试着输入一些问题,切换一下“流式输出”和“普通输出”模式,感受一下两者的区别。
4. 项目优化与部署建议
一个能跑起来的Demo完成了,但如果你想把它用在更正式的场景,或者分享给别人用,可能还需要考虑下面几点。
4.1 添加对话历史与上下文管理
我们现在的实现,每次对话都是独立的,模型不知道之前的聊天内容。这对于多轮对话来说体验不好。优化思路是,在后端维护一个简单的会话存储(比如用内存对象或者Redis),把每次对话的用户消息和AI回复都存起来。下次请求时,把整个历史记录都发给模型,这样它就能根据上下文来回答了。
这需要修改后端的 /api/chat 接口,让它能接受一个 sessionId 参数,并根据这个ID来获取和存储历史消息列表。
4.2 增加基础的安全与限流措施
开放给公网的API,安全是必须考虑的。
- API Key保护:确保你的
.env文件不被提交到Git等代码仓库(记得把它加入.gitignore)。在部署时,使用服务器环境变量来配置。 - 请求限流:可以使用
express-rate-limit这样的中间件,防止同一个IP地址在短时间内发送大量请求,消耗你的API额度或服务器资源。 - 输入验证与清理:对用户输入的消息内容做基本的检查和清理,防止注入攻击或处理异常数据。
4.3 前端体验优化点
- 支持Markdown渲染:如果模型回复中包含了代码块或列表,前端可以用
marked.js这样的库将其渲染成富文本,更美观。 - 添加发送快捷键:我们已经支持了回车发送,可以再加一个
Ctrl+Enter换行的功能。 - 对话导出:增加一个按钮,允许用户将当前对话历史以文本或JSON格式导出。
- 响应中断:在流式输出时,提供一个“停止生成”按钮,点击后可以中止正在进行的请求。
4.4 部署到生产环境
当你准备把这个应用部署到云服务器让更多人访问时:
- 使用进程管理工具:不要直接用
node server.js运行。推荐使用pm2,它可以守护进程,在应用崩溃时自动重启,还能做日志管理。npm install -g pm2 pm2 start server.js --name "qwen-chat-app" - 配置反向代理:通常我们不会让Node.js服务直接暴露在80或443端口。可以用Nginx或Caddy这样的Web服务器作为反向代理,处理静态文件、SSL加密(HTTPS)、负载均衡等,再把请求转发给Node.js应用。
- 环境变量管理:在服务器上,通过
export命令或使用.env文件(确保其权限安全)来设置QWEN_API_BASE_URL和QWEN_API_KEY。
5. 写在最后
走完这一趟,你会发现把一个AI模型集成到自己的全栈应用里,并没有想象中那么神秘。核心就是三步:后端作为“中间人”去调用模型API,前端提供一个友好的交互界面,中间通过HTTP协议把两者连接起来。
通义千问1.5-1.8B这个版本虽然参数量不大,但在轻量级对话、创意写作、代码辅助这些场景下,表现已经足够让人惊喜,而且推理速度很快,对硬件要求也友好。通过这种集成方式,你可以把它变成你网站的一个智能客服,内部工具的一个问答助手,或者任何你需要对话交互的地方。
这次我们用最基础的技术栈实现了一个完整链路,你可以在这个基础上,根据自己的需求去添加更多功能,比如用户登录、多会话管理、更精美的UI组件等等。最重要的是,你亲手打通了从模型到应用的“最后一公里”,这其中的经验和理解,是光看文档学不来的。希望这个实践能给你带来一些启发,如果有问题,欢迎随时交流。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐

所有评论(0)