Node.js+ 通义千问API 搭建专属AI聊天接口(附完整代码+前端页面)# 新手也能上手!Node.js + 通义千问API

0 阅读7分钟

作为前端/后端新手,是不是也想拥有一个专属的AI聊天工具?不用复杂操作,基于Node.js搭建后端,对接通义千问API,半小时就能做出一个可直接使用的AI对话工具,本文全程手把手教学,复制代码就能跑通!

本文适合:Node.js 入门者、想快速对接AI大模型的开发者、需要专属AI接口的个人用户,全程无冗余操作,重点解决新手常踩的坑(依赖安装、API配置、Git提交避坑等),看完直接上手实战。

index.png

一、项目前言:为什么选通义千问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的关键,必须先获取,步骤如下(全程免费):

  1. 打开阿里云百炼控制台:dashscope.console.aliyun.com/,用手机号注册/登录阿里云账号

  2. 首次登录会提示开通“百炼服务”,直接开通即可,系统会自动赠送90天免费额度(输入/输出各100万Token)

  3. 开通后,左侧菜单点击「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

四、运行步骤(新手必看)

  1. 将上面3个文件(server.js、index.html、.gitignore)放在同一个文件夹下(如ai-chat-server);

  2. 打开VS Code,打开该文件夹,打开终端,执行 npm install express cors axios 安装依赖;

  3. 替换server.js中的QWEN_API_KEY,换成你自己的通义千问API Key;

  4. 终端执行 node server\.js,启动后端服务(出现“服务已启动”提示即成功);

  5. 双击打开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返回回复 → 前端展示结果,全程免费、稳定、易操作。

如果需要扩展功能(比如部署到云服务器、增加更多对话配置),可以留言,后续会补充对应的教程~

附:相关资源