ChatGPT+Vue3+Node.js构建智能聊天机器人:从零到一的完整技术栈分享

Oliver5
Oliver5 2026-02-08T06:07:09+08:00
0 0 0

引言

在人工智能技术飞速发展的今天,基于大语言模型的聊天机器人已经成为Web应用中的热门功能。本文将带你从零开始,使用ChatGPT API、Vue3前端框架和Node.js后端服务构建一个完整的智能聊天机器人项目。

通过本教程,你将掌握:

  • 如何集成ChatGPT API进行自然语言处理
  • Vue3前端组件化开发技巧
  • Node.js后端服务架构设计
  • WebSocket实时通信实现
  • API调用优化策略

项目架构概览

技术栈选择理由

本项目采用Vue3 + Node.js + ChatGPT的组合,具有以下优势:

  1. Vue3:现代化的前端框架,提供更好的性能和开发体验
  2. Node.js:适合构建高性能的后端服务,支持异步处理
  3. ChatGPT API:强大的自然语言理解能力,提供高质量的对话体验

项目架构图

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Vue3 UI   │    │   Node.js   │    │  ChatGPT    │
│   前端应用  │───▶│   后端服务  │───▶│   API调用   │
└─────────────┘    └─────────────┘    └─────────────┘
      │                  │                  │
      ▼                  ▼                  ▼
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  WebSocket  │    │  API路由    │    │   请求处理  │
│ 实时通信    │    │   路由管理  │    │   响应返回  │
└─────────────┘    └─────────────┘    └─────────────┘

后端服务开发

Node.js项目初始化

首先创建Node.js后端项目:

mkdir chatbot-backend
cd chatbot-backend
npm init -y

安装必要的依赖包:

npm install express cors helmet morgan dotenv socket.io
npm install --save-dev nodemon

项目目录结构

chatbot-backend/
├── src/
│   ├── config/
│   │   └── index.js
│   ├── controllers/
│   │   └── chatController.js
│   ├── middleware/
│   │   └── rateLimit.js
│   ├── routes/
│   │   └── chatRoutes.js
│   ├── services/
│   │   └── chatGPTService.js
│   ├── utils/
│   │   └── logger.js
│   └── app.js
├── .env
├── server.js
└── package.json

环境配置

创建.env文件:

# 端口配置
PORT=3000

# ChatGPT API配置
OPENAI_API_KEY=your_openai_api_key_here
OPENAI_API_BASE=https://api.openai.com/v1

# WebSocket配置
WEBSOCKET_TIMEOUT=30000

# 限流配置
RATE_LIMIT_WINDOW_MS=15*60*1000
RATE_LIMIT_MAX_REQUESTS=100

核心服务实现

ChatGPT服务封装

// src/services/chatGPTService.js
const OpenAI = require('openai');
const logger = require('../utils/logger');

class ChatGPTService {
  constructor() {
    this.openai = new OpenAI({
      apiKey: process.env.OPENAI_API_KEY,
      baseURL: process.env.OPENAI_API_BASE
    });
  }

  async generateResponse(messages) {
    try {
      const completion = await this.openai.chat.completions.create({
        model: "gpt-3.5-turbo",
        messages: messages,
        temperature: 0.7,
        max_tokens: 1000,
        top_p: 1,
        frequency_penalty: 0,
        presence_penalty: 0
      });

      return completion.choices[0].message.content.trim();
    } catch (error) {
      logger.error('ChatGPT API Error:', error);
      throw new Error('Failed to generate response from ChatGPT');
    }
  }

  async getChatHistory(messages, maxMessages = 10) {
    // 确保只保留最近的对话历史
    if (messages.length > maxMessages) {
      return messages.slice(-maxMessages);
    }
    return messages;
  }
}

module.exports = new ChatGPTService();

聊天控制器

// src/controllers/chatController.js
const chatGPTService = require('../services/chatGPTService');
const logger = require('../utils/logger');

class ChatController {
  async sendMessage(req, res) {
    try {
      const { message, conversationId } = req.body;
      
      if (!message) {
        return res.status(400).json({
          success: false,
          error: 'Message is required'
        });
      }

      // 构建消息历史
      let messages = [
        {
          role: "system",
          content: "You are a helpful assistant. Keep your responses concise and accurate."
        },
        {
          role: "user",
          content: message
        }
      ];

      // 如果有对话ID,添加历史记录
      if (conversationId) {
        // 这里可以实现从数据库获取历史消息的逻辑
        logger.info(`Processing conversation: ${conversationId}`);
      }

      const response = await chatGPTService.generateResponse(messages);
      
      res.json({
        success: true,
        message: response,
        timestamp: new Date().toISOString()
      });

    } catch (error) {
      logger.error('Chat controller error:', error);
      res.status(500).json({
        success: false,
        error: 'Internal server error'
      });
    }
  }

  async getConversationHistory(req, res) {
    try {
      // 这里实现获取对话历史的逻辑
      res.json({
        success: true,
        history: []
      });
    } catch (error) {
      logger.error('Get conversation history error:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to retrieve conversation history'
      });
    }
  }
}

module.exports = new ChatController();

API路由配置

// src/routes/chatRoutes.js
const express = require('express');
const router = express.Router();
const chatController = require('../controllers/chatController');
const rateLimit = require('../middleware/rateLimit');

// 限流中间件应用到所有聊天API
router.use(rateLimit);

// 发送消息接口
router.post('/send', chatController.sendMessage);

// 获取对话历史接口
router.get('/history/:conversationId', chatController.getConversationHistory);

module.exports = router;

限流中间件

// src/middleware/rateLimit.js
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS),
  max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS),
  message: {
    success: false,
    error: 'Too many requests from this IP, please try again later.'
  },
  standardHeaders: true,
  legacyHeaders: false,
});

module.exports = limiter;

主应用启动文件

// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const dotenv = require('dotenv');

// 加载环境变量
dotenv.config();

// 路由导入
const chatRoutes = require('./routes/chatRoutes');

const app = express();

// 中间件配置
app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// 路由注册
app.use('/api/chat', chatRoutes);

// 健康检查端点
app.get('/health', (req, res) => {
  res.json({
    status: 'OK',
    timestamp: new Date().toISOString()
  });
});

// 404处理
app.use('*', (req, res) => {
  res.status(404).json({
    success: false,
    error: 'Route not found'
  });
});

module.exports = app;

WebSocket实时通信

// server.js
const http = require('http');
const express = require('express');
const socketIo = require('socket.io');
const app = require('./src/app');

const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
});

// WebSocket连接处理
io.on('connection', (socket) => {
  console.log('User connected:', socket.id);

  // 处理聊天消息
  socket.on('chat_message', async (data) => {
    try {
      const { message, conversationId } = data;
      
      // 这里可以调用后端API处理消息
      // 模拟API调用
      setTimeout(() => {
        const response = `Received: ${message}`;
        socket.emit('chat_response', {
          message: response,
          timestamp: new Date().toISOString()
        });
      }, 1000);
    } catch (error) {
      socket.emit('error', { error: 'Failed to process message' });
    }
  });

  // 处理断开连接
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

module.exports = server;

前端Vue3应用开发

Vue3项目初始化

npm create vue@latest chatbot-frontend
cd chatbot-frontend
npm install

安装必要的依赖:

npm install axios socket.io-client

项目目录结构

chatbot-frontend/
├── src/
│   ├── assets/
│   ├── components/
│   │   ├── ChatContainer.vue
│   │   ├── MessageBubble.vue
│   │   └── InputArea.vue
│   ├── composables/
│   │   └── useChat.js
│   ├── services/
│   │   └── chatService.js
│   ├── store/
│   │   └── chatStore.js
│   ├── views/
│   │   └── HomeView.vue
│   ├── App.vue
│   └── main.js
├── vite.config.js
└── package.json

聊天服务封装

// src/services/chatService.js
import axios from 'axios';

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';

const api = axios.create({
  baseURL: API_BASE_URL,
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器
api.interceptors.request.use(
  (config) => {
    // 添加认证token等
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
api.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (error) => {
    console.error('API Error:', error);
    return Promise.reject(error);
  }
);

export const chatService = {
  // 发送消息
  async sendMessage(message, conversationId = null) {
    try {
      const response = await api.post('/chat/send', {
        message,
        conversationId
      });
      
      return response;
    } catch (error) {
      throw new Error(`Failed to send message: ${error.message}`);
    }
  },

  // 获取对话历史
  async getConversationHistory(conversationId) {
    try {
      const response = await api.get(`/chat/history/${conversationId}`);
      return response;
    } catch (error) {
      throw new Error(`Failed to fetch conversation history: ${error.message}`);
    }
  }
};

组合式函数实现

// src/composables/useChat.js
import { ref, reactive } from 'vue';
import { chatService } from '../services/chatService';

export function useChat() {
  const messages = ref([]);
  const isLoading = ref(false);
  const conversationId = ref(null);
  const inputMessage = ref('');

  // 添加消息到聊天记录
  const addMessage = (message, sender) => {
    messages.value.push({
      id: Date.now(),
      text: message,
      sender,
      timestamp: new Date()
    });
  };

  // 发送消息
  const sendMessage = async () => {
    if (!inputMessage.value.trim() || isLoading.value) return;

    const userMessage = inputMessage.value.trim();
    addMessage(userMessage, 'user');
    inputMessage.value = '';
    isLoading.value = true;

    try {
      const response = await chatService.sendMessage(userMessage, conversationId.value);
      
      if (response.success) {
        addMessage(response.message, 'bot');
        
        // 更新对话ID(如果需要)
        if (!conversationId.value) {
          conversationId.value = Date.now().toString();
        }
      } else {
        addMessage('Sorry, I encountered an error. Please try again.', 'bot');
      }
    } catch (error) {
      console.error('Error sending message:', error);
      addMessage('Sorry, I encountered an error. Please try again.', 'bot');
    } finally {
      isLoading.value = false;
    }
  };

  // 清空聊天记录
  const clearChat = () => {
    messages.value = [];
    conversationId.value = null;
  };

  return {
    messages,
    isLoading,
    inputMessage,
    sendMessage,
    clearChat,
    addMessage
  };
}

消息气泡组件

<!-- src/components/MessageBubble.vue -->
<template>
  <div :class="['message-bubble', { 'user-message': isUser, 'bot-message': !isUser }]">
    <div class="message-content">
      <p>{{ message.text }}</p>
      <span class="timestamp">{{ formattedTime }}</span>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  message: {
    type: Object,
    required: true
  }
});

const isUser = computed(() => props.message.sender === 'user');

const formattedTime = computed(() => {
  return new Date(props.message.timestamp).toLocaleTimeString([], {
    hour: '2-digit',
    minute: '2-digit'
  });
});
</script>

<style scoped>
.message-bubble {
  margin: 10px 0;
  max-width: 80%;
}

.user-message {
  margin-left: auto;
  text-align: right;
}

.bot-message {
  margin-right: auto;
  text-align: left;
}

.message-content {
  display: inline-block;
  padding: 12px 16px;
  border-radius: 18px;
  position: relative;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.user-message .message-content {
  background-color: #007bff;
  color: white;
  border-bottom-right-radius: 4px;
}

.bot-message .message-content {
  background-color: #f8f9fa;
  color: #333;
  border-bottom-left-radius: 4px;
}

.timestamp {
  font-size: 0.75rem;
  opacity: 0.7;
  display: block;
  margin-top: 4px;
}
</style>

输入区域组件

<!-- src/components/InputArea.vue -->
<template>
  <div class="input-area">
    <div class="input-container">
      <textarea
        v-model="inputMessage"
        @keyup.enter="handleEnter"
        placeholder="Type your message here..."
        :disabled="isLoading"
        class="message-input"
        rows="1"
      />
      <button 
        @click="sendMessage" 
        :disabled="!inputMessage.trim() || isLoading"
        class="send-button"
      >
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
          <path d="M15.854.146a.5.5 0 0 1 .11.397l-2.5 4.75a.5.5 0 0 1-.774.097L7.5 3.226 7.5 8.5a.5.5 0 0 1-.5.5h-1.5a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5 0 0 1 .5.5v3.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 .5-.5h3.5a.5.5
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000