尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从零构建AI聊天应用:Vue+Node.js+OpenAI全栈实战指南

从零构建AI聊天应用:Vue+Node.js+OpenAI全栈实战指南 1. 从“豆包”到“菜包”一个AI全栈新手的真实起点最近“豆包”挺火的身边不少朋友都在讨论。作为一个对AI应用开发有点兴趣但一直停留在“看教程”阶段的程序员我总感觉隔着一层纱。那些成熟的AI产品功能强大但背后的技术栈、工程化细节对新手来说就像黑盒。直到有一天我盯着“豆包”的界面脑子里蹦出一个念头它叫“豆包”那我能不能从零开始搞一个属于自己的“菜包”不是那种调用个API就完事的玩具而是一个能跑通前后端、有基础对话逻辑、能部署上线的、完整的AI聊天助手。这个想法听起来有点“菜”但恰恰是“菜”的真实。我不打算一开始就追求多模态、长上下文、复杂记忆这些高级特性。我的目标是用最直接的方式理解一个现代AI聊天应用从构思到上线的全链路。工具我选择了最近在开发者圈子里口碑不错的Cursor它集成了强大的AI辅助编程能力正好可以充当我这个“新手”的“外挂大脑”帮我跨越一些初始的工程门槛。所以这篇内容不是什么高深的技术分享而是一个“菜包”开发者的实战踩坑记录我会把从环境搭建、技术选型、核心实现到最终部署的每一步包括那些看似愚蠢但绕不过去的坑都摊开来聊聊。如果你也对“自己动手做一个AI应用”感兴趣但又觉得无从下手那咱们可以一起试试。2. 项目蓝图与技术栈选型为什么是这些“配料”在真正动手写代码之前花点时间想清楚“菜包”要做什么、以及用什么工具来做至关重要。盲目开始很容易陷入“写到一半推倒重来”的困境。2.1 定义“菜包”的最小可行产品一个聊天助手最核心的功能是什么我的定义很简单用户能输入文字需要一个聊天界面。助手能回复文字需要调用大语言模型的API。对话有连续性需要能记住上下文不能每句话都独立。能部署访问不能只在我本地电脑跑。基于这四点我画出了一个极简的架构图前端页面负责展示和输入后端服务负责处理逻辑和调用AI中间通过API通信最后找个地方把前后端都放上去。功能上第一期就实现纯文本对话支持清空上下文界面干净就行。先追求“跑通”再考虑“做好”。2.2 技术栈的“家常菜”选择选技术栈就像配菜不求山珍海味但求顺手、易得、能快速出活。前端Vue 3 Element Plus为什么选它我对Vue的语法更熟悉其响应式系统和组件化开发对于构建一个动态的聊天界面非常高效。Element Plus提供了丰富且美观的UI组件像输入框、按钮、布局容器、消息气泡等几乎可以拿来即用能让我把精力集中在业务逻辑而非样式调试上。用Vite作为构建工具启动和热更新速度极快开发体验流畅。后端Node.js Express为什么选它JavaScript/TypeScript全栈的诱惑力很大前后端语言统一心智负担小。Express是Node.js生态里最轻量、最灵活的Web框架足够用来快速搭建RESTful API。对于“菜包”这种IO密集型主要是网络请求AI API的应用Node.js的异步非阻塞特性很合适。后期如果需要加入TypeScript也能提供更好的类型安全。AI能力核心OpenAI API (GPT-3.5-Turbo)为什么选它这是当前最稳定、文档最完善的主流LLM API之一。虽然国内访问需要一些配置但其可靠性和效果经过了广泛验证。对于新手项目稳定性和可预测性比追求最新模型更重要。我也可以很方便地切换为其他兼容OpenAI API格式的服务如一些国内合规的模型平台只需修改API地址和密钥。开发环境与辅助Cursor GitCursor是本项目的“秘密武器”。它不仅仅是一个编辑器其深度集成的AI能力如通过CmdK进行自然语言编程、自动补全、代码解释、生成测试等能极大提升开发效率尤其是在我不熟悉的领域快速生成代码框架或排查错误。Git用于版本控制这是现代软件开发的基本素养哪怕是一个人开发。部署Vercel (前端) Railway / 云服务器 (后端)Vercel对前端项目的部署体验是无与伦比的关联Git仓库后自动部署自带CDN、HTTPS非常适合部署我们的Vue应用。后端的选择更灵活。对于演示级项目Railway或Render这类平台即服务非常友好能自动配置环境、部署Node.js应用。如果想更自主购买一台最基础的云服务器如各大云厂商的轻量应用服务器自己用PM2守护进程也是经典且可控的方案。这个技术栈组合每一项都是当前领域内成熟、社区活跃的选择意味着遇到问题很容易找到解决方案。它构成了我们“菜包”的厨房和基础厨具。3. 搭建开发环境与项目骨架工欲善其事必先利其器。这一步的目标是把代码仓库、本地开发环境、以及项目的基本目录结构建立起来。3.1 初始化项目与目录结构首先我在本地创建一个项目总目录比如my-ai-chatbot。然后分别初始化前端和后端子项目。前端项目初始化# 在项目根目录下 npm create vuelatest frontend # 根据提示选择TypeScript, JSX, Vue Router, Pinia, ESLint等可按需本项目暂不需要路由和状态管理可以先不选。 cd frontend npm install npm install element-plus element-plus/icons-vue # 安装Element Plus及其图标库后端项目初始化# 回到项目根目录 mkdir backend cd backend npm init -y npm install express express-rate-limit cors dotenv npm install --save-dev types/express types/cors typescript ts-node nodemon # 安装核心依赖和开发依赖初始化完成后我的项目结构大致如下my-ai-chatbot/ ├── frontend/ # Vue前端项目 │ ├── public/ │ ├── src/ │ │ ├── assets/ │ │ ├── components/ # 存放聊天组件等 │ │ ├── App.vue │ │ └── main.ts │ ├── index.html │ ├── package.json │ └── vite.config.ts ├── backend/ # Node.js后端项目 │ ├── src/ │ │ ├── index.ts # 主入口文件 │ │ └── routes/ # 路由文件 │ ├── .env # 环境变量API密钥等 │ ├── package.json │ └── tsconfig.json └── README.md3.2 配置Cursor与基础代码生成打开Cursor将整个my-ai-chatbot文件夹作为项目打开。Cursor的优势在这里开始体现。例如当我在backend/src/index.ts中新建文件后我可以直接使用CmdK输入提示“创建一个基本的Express服务器监听3000端口启用CORS并添加一个健康检查端点/health。”Cursor很快会生成类似下面的代码import express from express; import cors from cors; import dotenv from dotenv; dotenv.config(); const app express(); const port process.env.PORT || 3000; // 中间件 app.use(cors()); // 允许前端跨域请求 app.use(express.json()); // 解析JSON请求体 // 健康检查端点 app.get(/health, (req, res) { res.json({ status: OK, message: 菜包后端服务运行正常 }); }); app.listen(port, () { console.log( 菜包后端服务已启动监听端口: ${port}); });同样在前端我可以让Cursor帮我快速搭建一个基于Element Plus的聊天界面骨架。在frontend/src/components/ChatWindow.vue中通过提示生成包含消息列表、输入框和发送按钮的模板代码。注意Cursor生成的代码是很好的起点但绝不能无脑信任。你必须理解每一行代码的作用。例如它可能不会自动添加请求超时处理、错误边界或者安全相关的中间件如速率限制这些都需要我们后续手动补充和完善。把Cursor当作一个强大的代码建议工具而非自动编程机。3.3 解决初始依赖与配置问题在前后端关联开发时第一个坑往往是跨域问题。虽然我们在后端使用了cors()中间件但在开发环境下前端Vite服务器运行在localhost:5173后端运行在localhost:3000端口不同属于跨域。我们的CORS配置已经解决了这个问题。另一个常见问题是环境变量。后端的OpenAI API密钥绝对不能硬编码在代码里。我们在后端项目根目录创建.env文件OPENAI_API_KEYsk-your-actual-api-key-here PORT3000并在代码中通过process.env.OPENAI_API_KEY读取。记得将.env添加到.gitignore中防止密钥泄露。至此我们的“厨房”已经准备就绪灶台本地服务器可以点火了。分别在前端和后端目录运行npm run dev具体命令看package.json中的scripts配置如果能在浏览器中看到前端页面并且访问http://localhost:3000/health能看到{“status”: “OK”}的返回那么基础环境就搭建成功了。4. 核心功能实现对话逻辑与前后端联调环境搭好接下来就是烹饪的核心环节实现聊天功能。这部分需要前后端紧密配合。4.1 后端API连接OpenAI并管理对话上下文后端的核心任务是提供一个API端点比如POST /api/chat接收用户消息将其与历史对话组合成符合OpenAI API要求的格式发送请求再将AI的回复返回给前端。首先安装OpenAI官方Node.js库cd backend npm install openai然后创建处理聊天的路由逻辑。在backend/src/routes/chat.ts中import { Router, Request, Response } from express; import OpenAI from openai; import { rateLimit } from express-rate-limit; const router Router(); // 配置速率限制每个IP每15分钟最多100次请求 const chatLimiter rateLimit({ windowMs: 15 * 60 * 1000, max: 100, message: 请求过于频繁请稍后再试。, }); // 初始化OpenAI客户端 const openai new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); // 内存中存储对话上下文仅用于演示生产环境需用数据库 const userSessions: Mapstring, ArrayOpenAI.ChatCompletionMessageParam new Map(); router.post(/, chatLimiter, async (req: Request, res: Response) { try { const { message, sessionId default } req.body; if (!message || typeof message ! string) { return res.status(400).json({ error: 无效的请求消息内容不能为空且必须为字符串 }); } // 获取或初始化当前会话的历史记录 if (!userSessions.has(sessionId)) { userSessions.set(sessionId, [ { role: system, content: 你是一个乐于助人的AI助手名字叫“菜包”。请用友好、简洁的语气回答用户的问题。 }, ]); } const conversationHistory userSessions.get(sessionId)!; // 将用户新消息加入历史 conversationHistory.push({ role: user, content: message }); // 调用OpenAI API const completion await openai.chat.completions.create({ model: gpt-3.5-turbo, // 可根据需要更换模型 messages: conversationHistory, temperature: 0.7, // 控制创造性0-2之间 max_tokens: 1000, // 限制回复长度 }); const aiResponse completion.choices[0]?.message?.content || 抱歉我没有收到回复。; // 将AI回复加入历史 conversationHistory.push({ role: assistant, content: aiResponse }); // 可选限制历史记录长度防止token超限 if (conversationHistory.length 20) { // 保留最近10轮对话 conversationHistory.splice(1, conversationHistory.length - 20); // 保留system prompt } // 返回AI回复 res.json({ reply: aiResponse }); } catch (error: any) { console.error(调用AI API出错, error); // 更友好的错误信息 let errorMessage 服务器内部错误; if (error.response) { errorMessage AI服务错误 (${error.response.status}): ${error.response.data.error?.message || 未知}; } else if (error.request) { errorMessage 网络错误无法连接到AI服务; } res.status(500).json({ error: errorMessage }); } }); // 清空上下文的端点 router.post(/clear, (req: Request, res: Response) { const { sessionId default } req.body; userSessions.delete(sessionId); res.json({ message: 对话上下文已清空 }); }); export default router;关键点解析与踩坑记录速率限制使用express-rate-limit非常重要。OpenAI API本身有调用频率和额度限制后端再加一层限制可以防止恶意刷接口保护你的API密钥和预算。上下文管理这里为了简单使用了内存Map来存储不同会话的历史。这是一个巨大的坑点服务器重启后所有对话记忆都会消失且无法在多实例部署下共享。生产环境必须使用数据库如Redis、MongoDB来持久化存储会话和消息。System Prompt在历史记录开头插入一个role为system的消息用于设定AI助手的角色和行为。这是塑造“菜包”性格的关键。错误处理对OpenAI API调用进行了详细的错误捕获和分类处理向前端返回更明确的错误信息而不是笼统的“500错误”。Token限制max_tokens参数和手动修剪历史记录的长度都是为了控制每次请求的token消耗避免超出模型上下文长度或产生过高费用。4.2 前端界面构建交互式聊天窗口前端需要创建一个美观且交互流畅的聊天界面。我们使用Vue 3的Composition API和Element Plus。在frontend/src/components/ChatWindow.vue中template div classchat-container el-container directionvertical styleheight: 600px; border: 1px solid #ebeef5; border-radius: 8px; !-- 头部 -- el-header styleborder-bottom: 1px solid #e4e7ed; display: flex; align-items: center; justify-content: space-between; div stylefont-weight: bold; color: #409EFF; 菜包 AI 助手/div el-button sizesmall clickclearHistory :disabledisLoading清空对话/el-button /el-header !-- 消息区域 -- el-main stylepadding: 20px; overflow-y: auto; refmessageContainer div v-for(msg, index) in messages :keyindex classmessage-wrapper :class{ user-message: msg.role user } div classmessage-avatar el-avatar v-ifmsg.role user你/el-avatar el-avatar v-else stylebackground-color: #67c23a;菜/el-avatar /div div classmessage-bubble :class{ user-bubble: msg.role user } div v-htmlformatMessage(msg.content)/div div classmessage-time{{ msg.timestamp }}/div /div /div div v-ifisLoading classmessage-wrapper div classmessage-avatarel-avatar stylebackground-color: #67c23a;菜/el-avatar/div div classmessage-bubble el-icon classis-loadingLoading //el-icon 菜包正在思考... /div /div /el-main !-- 输入区域 -- el-footer styleborder-top: 1px solid #e4e7ed; padding: 15px; el-input v-modelinputMessage typetextarea :rows3 placeholder和菜包聊点什么吧... keydown.enter.exact.preventsendMessage :disabledisLoading template #append el-button typeprimary clicksendMessage :loadingisLoading :disabled!inputMessage.trim()发送/el-button /template /el-input div stylefont-size: 12px; color: #909399; margin-top: 5px; 按 Enter 发送Shift Enter 换行。当前会话ID: {{ sessionId }} /div /el-footer /el-container /div /template script setup langts import { ref, computed, nextTick, onMounted } from vue; import { ElMessage } from element-plus; import { Loading } from element-plus/icons-vue; // 定义消息类型 interface ChatMessage { role: user | assistant; content: string; timestamp: string; } const inputMessage ref(); const messages refChatMessage[]([]); const isLoading ref(false); const messageContainer refHTMLElement(); // 生成一个简单的会话ID实际应用中可能来自用户登录信息 const sessionId ref(session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}); // 格式化消息内容简单处理换行 const formatMessage (content: string) { return content.replace(/\n/g, br); }; // 滚动到底部 const scrollToBottom () { nextTick(() { if (messageContainer.value) { messageContainer.value.scrollTop messageContainer.value.scrollHeight; } }); }; // 发送消息 const sendMessage async () { const text inputMessage.value.trim(); if (!text || isLoading.value) return; // 添加用户消息到界面 const userMsg: ChatMessage { role: user, content: text, timestamp: new Date().toLocaleTimeString(), }; messages.value.push(userMsg); inputMessage.value ; scrollToBottom(); isLoading.value true; try { const response await fetch(http://localhost:3000/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message: text, sessionId: sessionId.value }), }); if (!response.ok) { const errorData await response.json(); throw new Error(errorData.error || 请求失败 (${response.status})); } const data await response.json(); // 添加AIÿ
返回列表