作为前端/后端新手,是不是也想拥有一个专属的AI聊天工具?不用复杂操作,基于Node.js搭建后端,对接通义千问API,半小时就能做出一个可直接使用的AI对话工具,本文全程手把手教学,复制代码就能跑通!
本文适合:Node.js 入门者、想快速对接AI大模型的开发者、需要专属AI接口的个人用户,全程无冗余操作,重点解决新手常踩的坑(依赖安装、API配置、Git提交避坑等),看完直接上手实战。
一、项目前言:为什么选通义千问API?
做个人AI项目,选对API很关键,对比了多个平台后,最终选择通义千问(DashScope),原因很简单:
-
✅ 新手友好:免费额度充足(90天内输入/输出各100万Token),个人开发完全够用,不用初期就花钱
-
✅ 性价比高:免费额度用完后,按Token计费,单价极低(输入0.3元/百万Token,输出0.6元/百万Token)
-
✅ 稳定可靠:阿里云旗下,接口响应快,很少出现报错,适合长期使用
-
✅ 配置简单:接口格式清晰,对接Node.js难度低,新手也能快速上手
本文核心目标:搭建一个基于Express的后端接口,对接通义千问API,搭配完整前端页面,实现“前端输入→后端转发→AI响应”的完整流程,可直接部署使用。
二、前置准备(3步搞定,新手无压力)
在开始写代码前,先做好3个前置准备,避免后续踩坑,全程不复杂,跟着做就好。
1. 环境准备:安装Node.js
首先确保电脑安装了Node.js(建议版本16+,本文使用v20.19.5测试通过),安装完成后,打开终端输入以下命令,验证是否安装成功:
node -v # 显示版本号即成功
npm -v # 显示npm版本号
2. 获取通义千问API Key
API Key是对接千问API的关键,必须先获取,步骤如下(全程免费):
-
打开阿里云百炼控制台:dashscope.console.aliyun.com/,用手机号注册/登录阿里云账号
-
首次登录会提示开通“百炼服务”,直接开通即可,系统会自动赠送90天免费额度(输入/输出各100万Token)
-
开通后,左侧菜单点击「API-Key」→「创建API Key」,命名后生成,注意:Key仅显示一次,务必复制保存好(格式:sk-xxxxxxxxxxxxxxxx)
重点提醒:API Key是个人隐私,禁止泄露(比如上传到Git仓库、公开分享),否则会被他人盗用,消耗你的额度/余额!
3. 项目初始化
打开VS Code,新建项目文件夹(本文命名为ai-chat-server),打开终端,执行以下命令初始化项目:
# 初始化package.json(一路回车即可)
npm init -y
# 安装核心依赖(express、cors、axios)
npm install express cors axios
依赖说明:
-
express:搭建Node.js后端服务,处理接口请求
-
cors:解决跨域问题,让前端页面能正常调用后端接口
-
axios:发送HTTP请求,对接通义千问API
三、完整代码实现(直接复制可用)
项目结构说明:根目录下创建3个文件,分别是后端服务文件、前端页面文件、Git忽略文件,结构如下:
ai-chat-server/
├─ server.js # 后端服务(对接API、提供接口)
├─ index.html # 前端页面(可直接打开使用)
└─ .gitignore # 忽略不必要的文件(避免上传依赖、配置等)
1. 后端服务:server.js(核心文件)
// 引入核心依赖
const express = require('express');
const cors = require('cors');
const axios = require('axios');
// 初始化Express服务
const app = express();
// 解决跨域问题(必须加,否则前端无法调用接口)
app.use(cors());
// 解析JSON格式请求体(必须加,否则无法获取前端传参)
app.use(express.json());
// ===================== 替换成你的配置 =====================
const QWEN_API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // 替换成你自己的API Key
const QWEN_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
// ==========================================================
// 核心AI对话接口(POST请求,前端可直接调用)
app.post('/api/chat', async (req, res) => {
try {
// 接收前端传入的对话消息(格式:[{role: "user", content: "聊天内容"}])
const { messages } = req.body;
// 发送请求到通义千问API
const response = await axios.post(
QWEN_API_URL,
{
model: "qwen-turbo", // 选用qwen-turbo模型(性价比最高,适合日常对话)
input: {
messages: messages // 传入前端的对话消息
},
parameters: {
temperature: 0.7, // 随机性(0~1,越小越严谨,越大越灵活)
max_tokens: 2000 // 最大响应长度(避免AI回复过长)
}
},
{
headers: {
// 身份验证,固定格式:Bearer + 你的API Key(Bearer后面有一个空格)
"Authorization": `Bearer ${QWEN_API_KEY}`,
"Content-Type": "application/json" // 请求体格式为JSON
}
}
);
// 提取AI回复内容(千问API返回格式固定,重点注意这一行)
const reply = response.data.output.choices[0].message.content;
// 提取本次调用的Token消耗(方便查看用量,避免超支)
const usage = response.data.usage;
// 打印用量信息(终端查看,心里有数)
console.log('=== 本次API调用用量 ===');
console.log('输入Token:', usage.input_tokens);
console.log('输出Token:', usage.output_tokens);
console.log('总计Token:', usage.total_tokens);
// 估算本次费用(qwen-turbo单价)
const cost = (usage.input_tokens * 0.3 / 1000000 + usage.output_tokens * 0.6 / 1000000).toFixed(4);
console.log('预估费用:', cost, '元');
console.log('======================\n');
// 向前端返回AI回复和用量信息
res.json({ reply, usage, cost });
} catch (error) {
// 捕获错误,打印错误信息(方便排查问题)
console.error("接口调用错误:", error.response?.data || error.message);
// 向前端返回错误提示
res.status(500).json({ error: "AI服务请求失败,请稍后再试" });
}
});
// 启动服务,监听3000端口
const PORT = 3000;
app.listen(PORT, () => {
console.log(`AI对话服务已启动,运行在:http://localhost:${PORT}`);
console.log(`可调用接口:POST http://localhost:${PORT}/api/chat`);
console.log(`提示:请先打开 index.html 页面,即可开始使用AI对话`);
});
2. 前端页面:index.html(完整可直接打开)
这是可直接打开使用的前端页面,支持新建对话、删除对话、历史对话保存,界面简洁美观,适配电脑和手机端:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>小明AI 对话助手</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: system-ui, -apple-system, sans-serif;
}
body {
background: #f5f7fa;
display: flex;
height: 100vh;
overflow: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: 260px;
background: #ffffff;
border-right: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid #e8e8e8;
}
.new-chat-btn {
width: 100%;
padding: 12px;
background: #4e83fd;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.new-chat-btn:hover {
background: #3a70e0;
}
.chat-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.chat-item {
padding: 12px;
margin-bottom: 4px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
position: relative;
}
.chat-item:hover {
background: #f0f2f5;
}
.chat-item.active {
background: #e8f0fe;
}
.chat-item-title {
flex: 1;
font-size: 14px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-item-delete {
opacity: 0;
background: none;
border: none;
color: #999;
cursor: pointer;
padding: 4px;
border-radius: 4px;
}
.chat-item:hover .chat-item-delete {
opacity: 1;
}
.chat-item-delete:hover {
background: #ffebee;
color: #f44336;
}
/* 主聊天区域 */ /* 主聊天区域 */
.main-container {
flex: 1;
display: flex;
flex-direction: column;
background: #f9f9f9;
}
.chat-header {
padding: 16px 20px;
background: white;
color: #333;
font-size: 18px;
font-weight: 500;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid #e8e8e8;
}
.menu-toggle {
display: none;
background: none;
border: none;
color: #333;
font-size: 20px;
cursor: pointer;
}
.messages {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
max-width: 900px;
margin: 0 auto;
width: 100%;
}
.message {
max-width: 75%;
padding: 10px 14px;
border-radius: 14px;
line-height: 1.5;
}
.user {
align-self: flex-end;
background: #4e83fd;
color: white;
}
.bot {
align-self: flex-start;
background: #eef1f5;
color: #333;
}
.input-area {
display: flex;
padding: 14px 20px;
gap: 10px;
border-top: 1px solid #eee;
align-items: flex-end;
max-width: 900px;
margin: 0 auto;
width: 100%;
background: #f9f9f9;
}
#userInput {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 12px;
outline: none;
font-size: 15px;
min-height: 50px;
max-height: 200px;
resize: vertical;
font-family: inherit;
line-height: 1.5;
}
#sendBtn {
padding: 12px 20px;
background: #4e83fd;
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
height: 50px;
align-self: flex-end;
}
#sendBtn:disabled {
background: #ccc;
}
/* 主聊天区域 */
.main-container {
flex: 1;
display: flex;
flex-direction: column;
background: #f9f9f9;
}
.chat-header {
padding: 16px 20px;
background: white;
color: #333;
font-size: 18px;
font-weight: 500;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid #e8e8e8;
}
.menu-toggle {
display: none;
background: none;
border: none;
color: #333;
font-size: 20px;
cursor: pointer;
}
.messages {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
max-width: 900px;
margin: 0 auto;
width: 100%;
}
.message {
max-width: 75%;
padding: 10px 14px;
border-radius: 14px;
line-height: 1.5;
}
.user {
align-self: flex-end;
background: #4e83fd;
color: white;
}
.bot {
align-self: flex-start;
background: #eef1f5;
color: #333;
}
.input-area {
display: flex;
padding: 14px 20px;
gap: 10px;
border-top: 1px solid #eee;
align-items: flex-end;
max-width: 900px;
margin: 0 auto;
width: 100%;
background: #f9f9f9;
}
#userInput {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 12px;
outline: none;
font-size: 15px;
min-height: 50px;
max-height: 200px;
resize: vertical;
font-family: inherit;
line-height: 1.5;
}
#sendBtn {
padding: 12px 20px;
background: #4e83fd;
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
height: 50px;
align-self: flex-end;
}
#sendBtn:disabled {
background: #ccc;
flex: 1;
display: flex;
flex-direction: column;
background: white;
}
.chat-header {
padding: 16px 20px;
background: white;
color: #333;
font-size: 18px;
font-weight: 500;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid #e8e8e8;
}
.menu-toggle {
display: none;
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
}
.messages {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
max-width: 75%;
padding: 10px 14px;
border-radius: 14px;
line-height: 1.5;
}
.user {
align-self: flex-end;
background: #4e83fd;
color: white;
}
.bot {
align-self: flex-start;
background: #eef1f5;
color: #333;
}
.input-area {
display: flex;
padding: 14px;
gap: 10px;
border-top: 1px solid #eee;
}
#userInput {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 24px;
outline: none;
font-size: 15px;
}
#sendBtn {
padding: 12px 20px;
background: #4e83fd;
color: white;
border: none;
border-radius: 24px;
cursor: pointer;
}
#sendBtn:disabled {
background: #ccc;
}
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
gap: 16px;
}
.empty-state-icon {
font-size: 48px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
z-index: 1000;
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.menu-toggle {
display: block;
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.overlay.show {
display: block;
}
}
</style>
</head>
<body>
<!-- 遮罩层(移动端) -->
<div class="overlay" id="overlay"></div>
<!-- 侧边栏 -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<button class="new-chat-btn" id="newChatBtn">
<span>+</span>
<span>新建对话</span>
</button>
</div>
<div class="chat-list" id="chatList"></div>
</div>
<!-- 主聊天区域 -->
<div class="main-container">
<div class="chat-header">
<button class="menu-toggle" id="menuToggle">☰</button>
<div id="chatTitle">小明AI 对话助手</div>
</div>
<div class="messages" id="messages">
<div class="empty-state" id="emptyState">
<div class="empty-state-icon">💬</div>
<div>开始新的对话吧</div>
</div>
</div>
<div class="input-area">
<textarea id="userInput" placeholder="输入你的问题..." autocomplete="off" disabled rows="1"></textarea>
<button id="sendBtn" disabled>发送</button>
</div>
</div>
<script>
const messagesEl = document.getElementById('messages');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const chatList = document.getElementById('chatList');
const newChatBtn = document.getElementById('newChatBtn');
const chatTitle = document.getElementById('chatTitle');
const emptyState = document.getElementById('emptyState');
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('overlay');
const menuToggle = document.getElementById('menuToggle');
// 存储所有对话会话
let conversations = [];
let currentChatId = null;
// 初始化:从 localStorage 加载历史对话
function init() {
const saved = localStorage.getItem('conversations');
if (saved) {
conversations = JSON.parse(saved);
}
if (conversations.length === 0) {
createNewChat();
} else {
renderChatList();
loadChat(conversations[0].id);
}
}
// 创建新对话
function createNewChat() {
const newChat = {
id: Date.now(),
title: '新对话',
messages: [],
createdAt: new Date().toISOString()
};
conversations.unshift(newChat);
saveConversations();
renderChatList();
loadChat(newChat.id);
}
// 保存对话到 localStorage
function saveConversations() {
localStorage.setItem('conversations', JSON.stringify(conversations));
}
// 渲染对话列表
function renderChatList() {
chatList.innerHTML = '';
conversations.forEach(chat => {
const item = document.createElement('div');
item.className = `chat-item ${chat.id === currentChatId ? 'active' : ''}`;
item.innerHTML = `
<span>💬</span>
<span class="chat-item-title">${escapeHtml(chat.title)}</span>
<button class="chat-item-delete" data-id="${chat.id}">🗑️</button>
`;
item.addEventListener('click', (e) => {
if (!e.target.classList.contains('chat-item-delete')) {
loadChat(chat.id);
closeSidebar();
}
});
const deleteBtn = item.querySelector('.chat-item-delete');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteChat(chat.id);
});
chatList.appendChild(item);
});
}
// 加载指定对话
function loadChat(chatId) {
currentChatId = chatId;
const chat = conversations.find(c => c.id === chatId);
if (!chat) return;
chatTitle.textContent = chat.title;
renderMessages(chat.messages);
renderChatList();
userInput.disabled = false;
sendBtn.disabled = false;
userInput.focus();
}
// 渲染消息
function renderMessages(messages) {
messagesEl.innerHTML = '';
if (messages.length === 0) {
messagesEl.appendChild(emptyState);
emptyState.style.display = 'flex';
return;
}
emptyState.style.display = 'none';
messages.forEach(msg => {
addMessageToDOM(msg.content, msg.role, false);
});
scrollToBottom();
}
// 添加消息到 DOM
function addMessageToDOM(text, sender, scroll = true) {
const div = document.createElement('div');
div.className = `message ${sender}`;
div.innerText = text;
messagesEl.appendChild(div);
if (scroll) {
scrollToBottom();
}
}
// 滚动到底部
function scrollToBottom() {
messagesEl.scrollTop = messagesEl.scrollHeight;
}
// 发送消息
async function sendMessage() {
const content = userInput.value.trim();
if (!content) return;
const chat = conversations.find(c => c.id === currentChatId);
if (!chat) return;
// 如果是第一条消息,更新标题
if (chat.messages.length === 0) {
chat.title = content.substring(0, 20) + (content.length > 20 ? '...' : '');
renderChatList();
chatTitle.textContent = chat.title;
}
// 显示用户消息
addMessageToDOM(content, 'user');
userInput.value = '';
userInput.style.height = 'auto';
sendBtn.disabled = true;
// 加入历史
chat.messages.push({ role: 'user', content });
saveConversations();
try {
const res = await fetch('http://localhost:3000/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: chat.messages })
});
const data = await res.json();
const reply = data.reply;
addMessageToDOM(reply, 'bot');
chat.messages.push({ role: 'assistant', content: reply });
saveConversations();
} catch (err) {
addMessageToDOM('AI 服务异常,请稍后重试', 'bot');
}
sendBtn.disabled = false;
userInput.focus();
}
// 自动调整 textarea 高度
userInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
});
// 删除对话
function deleteChat(chatId) {
if (!confirm('确定要删除这个对话吗?')) return;
conversations = conversations.filter(c => c.id !== chatId);
saveConversations();
if (currentChatId === chatId) {
if (conversations.length > 0) {
loadChat(conversations[0].id);
} else {
createNewChat();
}
} else {
renderChatList();
}
}
// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 移动端侧边栏控制
function openSidebar() {
sidebar.classList.add('open');
overlay.classList.add('show');
}
function closeSidebar() {
sidebar.classList.remove('open');
overlay.classList.remove('show');
}
// 事件监听
sendBtn.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
newChatBtn.addEventListener('click', () => {
createNewChat();
closeSidebar();
});
menuToggle.addEventListener('click', openSidebar);
overlay.addEventListener('click', closeSidebar);
// 初始化
init();
</script>
</body>
</html>
3. Git忽略文件:.gitignore(避免上传无用文件)
新建.gitignore文件,避免把依赖、配置等无用文件上传到Git,内容如下:
# 依赖文件夹(不上传)
node_modules/
# 环境变量和配置文件(不上传,避免泄露API Key)
.env
.env.local
# 日志文件
*.log
# VS Code配置文件(不上传)
.vscode/
# 系统文件
.DS_Store
四、运行步骤(新手必看)
-
将上面3个文件(server.js、index.html、.gitignore)放在同一个文件夹下(如ai-chat-server);
-
打开VS Code,打开该文件夹,打开终端,执行
npm install express cors axios安装依赖; -
替换server.js中的QWEN_API_KEY,换成你自己的通义千问API Key;
-
终端执行
node server\.js,启动后端服务(出现“服务已启动”提示即成功); -
双击打开index.html文件,即可开始使用AI对话(输入问题,点击发送即可收到回复)。
五、关键说明(避坑重点)
1. 关于免费额度
通义千问API免费额度:输入/输出各100万Token,90天有效期,日常个人使用完全足够(每天聊几十次,90天也用不完),用完后可充值继续使用(按Token计费,非常便宜)。
2. 关于自动扣费
只有当你账户有余额,且免费额度用完后,才会自动扣费;如果账户没有余额,API调用会直接报错,不会产生欠费,放心使用。
3. 关于前端页面功能
本文提供的index.html包含以下实用功能,无需额外开发:
-
✅ 新建对话、删除对话,支持多会话切换;
-
✅ 历史对话保存在本地(localStorage),刷新页面不丢失;
-
✅ 适配电脑、手机端,移动端可通过侧边栏切换对话;
-
✅ 输入框自适应高度,支持回车发送(Shift+回车换行)。
六、常见问题排查
-
问题1:启动服务报错“MODULE_NOT_FOUND” → 解决:执行
npm install express cors axios安装依赖; -
问题2:接口调用报错401 → 解决:检查API Key是否正确,重新创建API Key并替换;
-
问题3:前端无法调用接口 → 解决:确保后端服务已启动(终端显示“服务已启动”),且接口地址正确;
-
问题4:聊天记录丢失 → 解决:页面使用localStorage存储,清除浏览器缓存会丢失,可备份对话内容。
七、总结
整个项目从0到1,只用3个文件,就能实现一个可直接使用的AI对话工具,新手也能快速上手。核心流程是:前端页面输入问题 → 后端接口转发请求 → 通义千问API返回回复 → 前端展示结果,全程免费、稳定、易操作。
如果需要扩展功能(比如部署到云服务器、增加更多对话配置),可以留言,后续会补充对应的教程~
附:相关资源
-
通义千问API控制台:dashscope.console.aliyun.com/
-
通义千问API文档:help.aliyun.com/document_de…
-
项目完整代码(可直接下载使用):gitee.com/xiaoming511…