引言
在人工智能技术飞速发展的今天,基于大语言模型的聊天机器人已经成为Web应用中的热门功能。本文将带你从零开始,使用ChatGPT API、Vue3前端框架和Node.js后端服务构建一个完整的智能聊天机器人项目。
通过本教程,你将掌握:
- 如何集成ChatGPT API进行自然语言处理
- Vue3前端组件化开发技巧
- Node.js后端服务架构设计
- WebSocket实时通信实现
- API调用优化策略
项目架构概览
技术栈选择理由
本项目采用Vue3 + Node.js + ChatGPT的组合,具有以下优势:
- Vue3:现代化的前端框架,提供更好的性能和开发体验
- Node.js:适合构建高性能的后端服务,支持异步处理
- 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)