ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零构建模拟AI对话Web应用:React+Node.js全栈开发实践

从零构建模拟AI对话Web应用:React+Node.js全栈开发实践 在实际项目中我们经常需要快速构建一个具备特定交互逻辑的网页应用例如一个模拟AI对话的界面。这类项目不仅考验前端交互设计也涉及后端逻辑处理、状态管理以及如何将创意想法快速落地为可访问的网页服务。本文将围绕如何从零开始构建一个类似“Your AI Slop Bores Me”的互动网页应用展开该应用的核心特点是“真人扮演AI”来回答用户提问这背后需要一套完整的请求响应机制和前端状态流转逻辑。我们将使用现代Web开发中常见的技术栈前端使用React构建用户界面后端使用Node.js和Express框架处理业务逻辑并通过一个简单的JSON文件或内存存储来模拟“真人扮演”的应答库。整个过程会涵盖项目初始化、核心功能开发、前后端联调、部署上线以及常见问题排查。即使你没有完全相同的创意这套技术流程也适用于构建任何需要动态问答、角色扮演或内容生成的Web应用。1. 理解项目核心模拟AI应答的Web应用架构在开始写代码之前需要明确我们要构建的应用是什么。它不是一个真正的AI大模型而是一个“模拟器”。用户在前端输入问题请求发送到后端后端根据预设的规则或内容库返回一个看似由AI生成、实则由真人预先定义或触发的回答。这种模式的关键在于设计一个灵活、可扩展的应答匹配逻辑并确保整个交互流程流畅自然。1.1 核心工作机制与数据流整个应用的数据流是标准的客户端-服务器模式但核心在于后端的“应答决策”环节。用户交互层用户在网页的输入框中提交问题。网络请求层前端通过HTTP通常是POST请求将问题文本发送到指定的后端API端点。业务逻辑层后端接收问题进行解析。这里的逻辑可以是关键词匹配在预设的问答对库中查找包含用户问题关键词的条目。规则引擎根据问题的句式、长度或情感倾向需简单NLP处理选择不同的回答模板。随机抽取从一个充满“AI风格”废话的答案池中随机返回一条模拟AI的“幻觉”。人工干预接口预留一个管理界面当遇到未匹配的问题时通知真人操作者手动回复并将此新问答对存入知识库实现“真人扮演”的增强。数据持久层存储预设的问答对、回答模板或历史会话记录。为简化起步我们可以使用JSON文件或内存对象。生产环境则会考虑数据库。响应返回层后端将选中的答案封装成JSON格式返回给前端。界面渲染层前端收到响应后将答案以对话气泡的形式动态添加到聊天历史区域。1.2 技术选型理由前端 (React)组件化开发方便管理聊天框、消息列表、输入区域等UI状态。丰富的生态系统如状态管理、UI组件库能加速开发。后端 (Node.js Express)JavaScript全栈开发上下文统一。Express轻量、灵活能快速搭建RESTful API非常适合处理JSON请求和响应。存储 (初期用JSON文件)项目初期数据量小且结构简单读写JSON文件足以应对。这避免了早期引入数据库的复杂性让我们专注于核心逻辑。部署 (Vercel / Railway / 传统服务器)对于此类轻量级应用可以选择Serverless平台实现快速、免运维部署。2. 环境准备与项目初始化在开始编码前请确保你的开发环境已就绪。我们将分别创建前端和后端两个项目这是一种常见的解耦架构便于独立开发和部署。2.1 开发环境检查清单请确保你的机器上已安装以下工具并尽量使用较新的稳定版本。工具推荐版本检查命令作用Node.js18.x 或 20.x LTSnode --versionJavaScript 运行时用于执行后端和构建前端。npm随 Node.js 安装npm --versionNode.js 包管理器用于安装依赖。代码编辑器VS Code / WebStorm-编写代码。浏览器Chrome / Firefox 最新版-调试前端应用。Git最新版git --version版本控制可选但强烈推荐。2.2 创建项目目录结构首先为整个项目创建一个根目录并在其中分别初始化前端和后端项目。# 创建项目根目录 mkdir ai-slop-simulator cd ai-slop-simulator # 创建并初始化后端项目 mkdir backend cd backend npm init -y # 安装后端依赖 npm install express cors dotenv npm install --save-dev nodemon # 返回根目录创建并初始化前端项目 cd .. # 使用 Create React App 快速搭建前端 npx create-react-app frontend cd frontend执行完上述命令后你的项目结构应如下所示ai-slop-simulator/ ├── backend/ │ ├── node_modules/ │ ├── package.json │ └── package-lock.json ├── frontend/ │ ├── node_modules/ │ ├── public/ │ ├── src/ │ └── package.json └── README.md (可后续创建)2.3 配置后端开发脚本与基础服务器进入backend目录创建主要的应用文件。创建入口文件app.js(或index.js)// backend/app.js const express require(express); const cors require(cors); require(dotenv).config(); const app express(); const PORT process.env.PORT || 5000; // 中间件配置 app.use(cors()); // 允许前端跨域请求 app.use(express.json()); // 解析 JSON 格式的请求体 // 简单的根路由用于健康检查 app.get(/, (req, res) { res.json({ message: AI Slop Simulator Backend is running! }); }); // 模拟AI回答的API端点 - 我们先实现一个基础版本 app.post(/api/chat, (req, res) { const userMessage req.body.message; if (!userMessage || userMessage.trim() ) { return res.status(400).json({ error: Message cannot be empty }); } // 初始逻辑简单回显稍后替换为复杂的模拟逻辑 const mockResponse 模拟AI思考中...关于“${userMessage}”经过我的万亿参数计算我认为这是一个非常有趣的问题。本质上它涉及到信息、认知和存在的交互。; // 模拟网络延迟让交互更真实 setTimeout(() { res.json({ reply: mockResponse }); }, 800); }); // 启动服务器 app.listen(PORT, () { console.log(Server is running on http://localhost:${PORT}); });修改package.json中的脚本// backend/package.json { name: backend, version: 1.0.0, description: , main: app.js, scripts: { start: node app.js, dev: nodemon app.js }, keywords: [], author: , license: ISC, dependencies: { cors: ^2.8.5, dotenv: ^16.4.5, express: ^4.18.2 }, devDependencies: { nodemon: ^3.0.3 } }现在你可以在backend目录下运行npm run dev来启动一个热重载的开发服务器。访问http://localhost:5000应该能看到欢迎信息。3. 实现核心功能从简单回显到“真人扮演”逻辑基础服务器搭建好后我们需要实现核心的应答逻辑。我们将分三步走先实现一个固定的问答库匹配再引入随机性和模板最后设计一个可扩展的、允许“真人干预”的架构。3.1 构建基础的问答知识库在backend目录下创建一个data文件夹并在其中创建knowledgeBase.json文件。// backend/data/knowledgeBase.json { qaPairs: [ { questionKeywords: [你好, hello, hi, 嗨], answer: 你好我是一个对AI废话感到厌倦的模拟器。今天有什么可以帮你“解构”的吗 }, { questionKeywords: [你是谁, 你是什么, what are you], answer: 我是“Your AI Slop Bores Me”的模拟实体。我不生产智能我只是AI废话的搬运工。 }, { questionKeywords: [天气, weather], answer: 根据我的超验感知外面的天气处于一种“存在”的状态。更具体地说你需要打开窗户。 }, { questionKeywords: [生命的意义, meaning of life], answer: 42。或者持续地对抗信息熵。又或者是找到那个不会让你觉得是“AI废话”的答案。 }, { questionKeywords: [推荐, 书, 电影, recommend], answer: 我推荐你体验一下“等待”。这是一部由时间导演、现实主演的永恒作品。 } ], randomSlop: [ 这个问题触及了数字时代叙事的本体论边界。, 从跨模态对齐的角度看你的输入存在潜在的分布偏移。, 我的训练数据中没有与此完全一致的模式但基于潜在空间插值我可以生成一个近似响应。, 这让我想起了一个关于梯度下降的寓言故事。, 为了提供安全、负责任的回答我将对此问题进行无害化重构。 ] }3.2 实现智能匹配与应答服务创建backend/services/chatService.js文件封装核心的应答逻辑。// backend/services/chatService.js const knowledgeBase require(../data/knowledgeBase.json); class ChatService { constructor() { this.qaPairs knowledgeBase.qaPairs; this.randomPool knowledgeBase.randomSlop; // 可以在这里初始化连接数据库的客户端 } /** * 处理用户消息生成模拟AI回复 * param {string} userMessage - 用户输入 * returns {string} - 模拟AI的回复 */ generateReply(userMessage) { const lowerCaseMsg userMessage.toLowerCase().trim(); // 1. 优先进行关键词匹配 for (const pair of this.qaPairs) { for (const keyword of pair.questionKeywords) { if (lowerCaseMsg.includes(keyword.toLowerCase())) { return pair.answer; } } } // 2. 特殊规则匹配示例处理长度 if (lowerCaseMsg.length 100) { return “您的问题信息量很大这需要消耗我虚拟的1000PFLOPs算力来解构。简而言之是的也不是。” } if (lowerCaseMsg.length 3) { return “输入 tokens 不足无法生成有意义的潜在向量。请多说一点。” } // 3. 触发“真人扮演”标志示例特定指令 if (lowerCaseMsg.includes(真人) || lowerCaseMsg.includes(human)) { // 在实际项目中这里可以触发一个通知如WebSocket、邮件、写入待处理队列 console.log([真人扮演触发] 用户问题“${userMessage}” 需要人工干预。); return “系统已将您的问题标记为‘需要真人智慧’。请稍候一位真正的人类可能会在某个平行宇宙中思考它。” } // 4. 默认返回从废话池中随机选取 const randomIndex Math.floor(Math.random() * this.randomPool.length); return this.randomPool[randomIndex]; } // 未来扩展添加学习功能将新的问答对存入数据库 // async learnNewPair(question, answer) { ... } } module.exports new ChatService();3.3 更新后端API以使用新服务修改backend/app.js中的/api/chat端点。// 在文件顶部引入服务 const chatService require(./services/chatService); // 修改 /api/chat 路由处理函数 app.post(/api/chat, (req, res) { const userMessage req.body.message; if (!userMessage || userMessage.trim() ) { return res.status(400).json({ error: Message cannot be empty }); } // 使用 ChatService 生成回复 const aiReply chatService.generateReply(userMessage); // 模拟网络延迟 const delay Math.floor(Math.random() * 401) 400; // 400-800ms setTimeout(() { res.json({ reply: aiReply }); }, delay); });现在后端已经具备了一个有一定“智能”的应答核心。它首先尝试匹配关键词其次应用简单规则识别特定触发词最后才落入随机废话生成。4. 构建前端交互界面前端需要提供一个聊天界面允许用户输入信息并展示对话历史。4.1 创建聊天组件在frontend/src目录下创建components文件夹并新建ChatInterface.jsx和ChatInterface.css。// frontend/src/components/ChatInterface.jsx import React, { useState, useRef, useEffect } from react; import ./ChatInterface.css; const ChatInterface () { const [input, setInput] useState(); const [messages, setMessages] useState([ { id: 1, text: 模拟AI你好我是一个对AI废话感到厌倦的模拟器。今天有什么可以帮你“解构”的吗, sender: ai } ]); const [isLoading, setIsLoading] useState(false); const messagesEndRef useRef(null); // API基础URL在开发环境下代理到后端生产环境需配置实际地址 const API_BASE_URL process.env.REACT_APP_API_URL || http://localhost:5000; // 滚动到底部 const scrollToBottom () { messagesEndRef.current?.scrollIntoView({ behavior: smooth }); }; useEffect(scrollToBottom, [messages]); const handleSend async () { const trimmedInput input.trim(); if (!trimmedInput || isLoading) return; // 添加用户消息 const userMessage { id: Date.now(), text: 用户${trimmedInput}, sender: user }; setMessages(prev [...prev, userMessage]); setInput(); setIsLoading(true); try { const response await fetch(${API_BASE_URL}/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message: trimmedInput }) }); if (!response.ok) { throw new Error(HTTP error! status: ${response.status}); } const data await response.json(); // 添加AI回复 const aiMessage { id: Date.now() 1, text: 模拟AI${data.reply}, sender: ai }; setMessages(prev [...prev, aiMessage]); } catch (error) { console.error(发送消息失败:, error); const errorMessage { id: Date.now() 1, text: 系统抱歉连接模拟AI时出现故障。${error.message}, sender: system }; setMessages(prev [...prev, errorMessage]); } finally { setIsLoading(false); } }; const handleKeyPress (e) { if (e.key Enter !e.shiftKey) { e.preventDefault(); handleSend(); } }; return ( div classNamechat-container div classNamechat-header h1Your AI Slop Bores Me - 模拟器/h1 p一个由真人逻辑驱动的“AI废话”生成器/p /div div classNamechat-messages {messages.map(msg ( div key{msg.id} className{message ${msg.sender}} {msg.text} /div ))} {isLoading div classNamemessage ai模拟AI正在生成废话.../div} div ref{messagesEndRef} / /div div classNamechat-input-area textarea value{input} onChange{(e) setInput(e.target.value)} onKeyDown{handleKeyPress} placeholder输入你的问题体验定制版AI废话... disabled{isLoading} rows3 / button onClick{handleSend} disabled{isLoading || !input.trim()} {isLoading ? 思考中... : 发送} /button /div div classNamechat-footer small提示试试问“你是谁”、“天气如何”、“生命的意义”或者说“需要真人回答”。/small /div /div ); }; export default ChatInterface;/* frontend/src/components/ChatInterface.css */ .chat-container { display: flex; flex-direction: column; max-width: 800px; height: 90vh; margin: 20px auto; border: 1px solid #ccc; border-radius: 12px; overflow: hidden; background-color: #f9f9f9; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .chat-header { background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); color: white; padding: 20px; text-align: center; } .chat-header h1 { margin: 0 0 8px 0; font-size: 1.8rem; } .chat-header p { margin: 0; opacity: 0.9; font-size: 0.95rem; } .chat-messages { flex: 1; padding: 20px; overflow-y: auto; background-color: #f0f2f5; } .message { margin-bottom: 16px; padding: 12px 18px; border-radius: 18px; max-width: 80%; word-wrap: break-word; line-height: 1.5; } .message.user { background-color: #0084ff; color: white; align-self: flex-end; margin-left: auto; border-bottom-right-radius: 4px; } .message.ai { background-color: #e4e6eb; color: #050505; align-self: flex-start; border-bottom-left-radius: 4px; } .message.system { background-color: #fff3cd; color: #856404; border: 1px solid #ffeaa7; align-self: center; max-width: 95%; font-size: 0.9rem; text-align: center; } .chat-input-area { display: flex; border-top: 1px solid #ddd; background-color: white; padding: 15px; } .chat-input-area textarea { flex: 1; padding: 12px; border: 1px solid #ccc; border-radius: 8px; font-size: 1rem; resize: none; font-family: inherit; } .chat-input-area textarea:focus { outline: none; border-color: #6a11cb; } .chat-input-area button { margin-left: 12px; padding: 0 24px; background-color: #6a11cb; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: bold; transition: background-color 0.2s; } .chat-input-area button:hover:not(:disabled) { background-color: #2575fc; } .chat-input-area button:disabled { background-color: #cccccc; cursor: not-allowed; } .chat-footer { padding: 10px 20px; background-color: #f8f9fa; border-top: 1px solid #dee2e6; text-align: center; color: #6c757d; font-size: 0.85rem; }4.2 集成组件并配置代理修改frontend/src/App.js// frontend/src/App.js import React from react; import ChatInterface from ./components/ChatInterface; import ./App.css; function App() { return ( div classNameApp ChatInterface / /div ); } export default App;配置开发环境代理解决跨域 在frontend目录下修改package.json添加proxy字段。// frontend/package.json { name: frontend, proxy: http://localhost:5000, // ... 其他配置保持不变 }注意proxy配置仅在开发环境使用npm start时生效。它的作用是告诉 React 开发服务器将未知的 API 请求转发到指定的后端地址这里是http://localhost:5000从而避免浏览器的跨域限制。生产环境部署时需要将API_BASE_URL设置为后端服务的真实公网地址。启动应用在backend目录下运行npm run dev。在frontend目录下运行npm start。现在打开浏览器访问http://localhost:3000你应该能看到完整的聊天界面并可以与你的“模拟AI”进行对话了。5. 部署上线与生产环境考量让本地应用在公网可访问是项目完成的最后一步。我们将分别部署前端和后端。5.1 前端部署以 Vercel 为例Vercel 对 Create React App 项目有极好的支持。构建生产版本在frontend目录运行npm run build生成优化后的静态文件在build文件夹。推送代码到 Git 仓库将整个项目或至少frontend目录推送到 GitHub、GitLab 等平台。连接 Vercel访问 Vercel 并登录可使用 GitHub 账号。点击 “Add New…” - “Project”导入你的仓库。在配置页面框架预设选择 “Create React App”其他设置保持默认。点击 “Deploy”。部署完成后你会获得一个*.vercel.app的域名。关键点由于前端需要调用后端 API你需要在 Vercel 的环境变量中设置REACT_APP_API_URL为你即将部署的后端公网地址。在 Vercel 项目的 “Settings” - “Environment Variables” 中添加。5.2 后端部署以 Railway 为例Railway 可以轻松部署 Node.js 应用。准备生产环境配置在backend目录创建.env文件确保已添加到.gitignore内容示例PORT5000 NODE_ENVproduction # 未来可添加数据库连接字符串等修改app.js确保在生产环境正确使用端口const PORT process.env.PORT || 5000; // Railway 等平台会注入 PORT 环境变量推送代码确保backend目录的代码已推送到 Git 仓库。连接 Railway访问 Railway 并登录。点击 “New Project” - “Deploy from GitHub repo”。选择你的仓库并指定部署路径为/backend如果你的后端代码在仓库的backend子目录。Railway 会自动检测为 Node.js 项目并开始部署。部署成功后会提供一个*.up.railway.app的域名。5.3 生产环境配置与优化清单部署上线后还需要考虑以下问题以确保应用稳定运行事项开发环境生产环境建议API 地址使用proxy或localhost前端通过环境变量REACT_APP_API_URL配置后端公网地址。CORS允许所有来源 (cors())精确配置允许的源app.use(cors({ origin: process.env.FRONTEND_URL }))。错误处理简单控制台日志集成 Sentry、LogRocket 等错误监控服务。返回给前端的错误信息应通用化避免泄露堆栈。日志记录console.log使用winston、pino等日志库结构化日志并输出到文件或日志服务。性能与缓存无对静态知识库如knowledgeBase.json进行内存缓存避免每次请求都读文件。考虑使用 Redis。安全性宽松设置请求体大小限制、速率限制如express-rate-limit。验证和清理用户输入。“真人扮演”扩展控制台打印实现一个管理面板可新建admin路由将待回答问题持久化到数据库并提供界面供真人回复。数据持久化JSON 文件迁移到 PostgreSQL、MongoDB 等数据库存储问答对、会话历史、用户反馈等。6. 常见问题排查与调试在开发和部署过程中你可能会遇到以下典型问题。这里提供排查路径。6.1 前端无法连接到后端 API现象前端发送消息后一直显示“正在生成废话...”或控制台出现跨域CORS错误、网络错误。排查步骤检查后端服务是否运行访问http://localhost:5000看是否返回欢迎信息。检查代理配置确认frontend/package.json中的proxy字段指向正确的后端地址和端口。检查网络请求打开浏览器开发者工具的“网络”(Network)选项卡查看发送的请求。请求 URL 是否正确应为http://localhost:3000/api/chat被代理后。状态码是什么如果是 404检查后端路由路径 (/api/chat) 是否匹配。如果是 500查看后端控制台日志。是否有 CORS 错误如果请求直接发往后端地址如localhost:5000并报 CORS 错误说明代理未生效。重启前端开发服务器 (npm start)。生产环境检查确认前端构建时REACT_APP_API_URL环境变量已正确设置并指向已部署的后端地址。6.2 后端服务启动失败现象运行npm run dev时报错如Error: listen EADDRINUSE: address already in use :::5000。排查步骤端口被占用使用命令lsof -i :5000(Mac/Linux) 或netstat -ano | findstr :5000(Windows) 查找占用端口的进程并终止它或修改app.js中的PORT变量。依赖未安装确保在backend目录下已运行npm install。语法错误检查app.js或其他引入的 JS 文件是否有语法错误。nodemon通常会给出具体错误信息。6.3 应答逻辑不符合预期现象输入匹配了关键词的问题却没有返回预设答案或者总是返回随机废话。排查步骤检查关键词匹配逻辑在chatService.js的generateReply方法中添加调试日志打印lowerCaseMsg和匹配过程。console.log([DEBUG] 用户输入: ${lowerCaseMsg}); for (const pair of this.qaPairs) { console.log([DEBUG] 检查关键词组: ${pair.questionKeywords}); // ... }检查 JSON 文件格式确保knowledgeBase.json格式正确没有多余的逗号字符串使用双引号。确认数据加载检查require(‘../data/knowledgeBase.json’)路径是否正确。可以尝试在服务启动时打印this.qaPairs的长度来确认数据已加载。6.4 生产环境部署后静态资源或 API 404现象前端页面可以打开但样式错乱CSS/JS 404或所有 API 请求失败。排查步骤前端路由问题 (SPA)对于 React 等单页应用如果直接刷新非根路径的页面或输入 URL 访问可能会 404。需要在 Web 服务器如 Nginx或部署平台如 Vercel配置将所有请求重定向到index.html。在 Vercel 中这通常由vercel.json配置。API 路径错误确认生产环境后端 API 的完整路径。如果后端部署在https://your-api.up.railway.app那么前端请求的地址就应该是https://your-api.up.railway.app/api/chat。环境变量未生效在部署平台重新构建项目确保环境变量已注入。有时需要手动触发重新部署。7. 扩展方向与最佳实践至此一个基础版的“模拟AI”网页应用已经完成。你可以在此基础上进行扩展使其更强大、更实用。7.1 功能扩展建议实现真正的“真人扮演”后台创建一个简单的管理员登录页面。当用户问题触发“真人”关键词或完全无法匹配时将问题存入数据库的pending_questions表。管理员后台轮询或使用 WebSocket 实时获取待处理问题并直接输入答案。答案保存后可即时通过 WebSocket 推送给用户并同时添加到知识库中。增强匹配算法使用字符串相似度算法如 Levenshtein 距离、余弦相似度基于词向量替代简单的关键词包含匹配。引入简单的意图识别可使用开源的 NLP 库如node-nlp或对接大模型 API 进行意图分类。会话记忆使用 Cookie 或 LocalStorage 为匿名用户生成唯一会话 ID。在后端存储会话上下文使 AI 的回复能参考之前的对话历史例如“你刚才问过天气现在又问穿什么我建议…”。前端体验优化支持 Markdown 渲染让 AI 的回复可以包含粗体、列表、代码块等。添加消息发送时间戳、头像、消息加载动画打字机效果。实现对话导出文本、图片功能。7.2 代码与工程最佳实践环境配置分离严格区分开发、测试、生产环境配置。使用.env文件并通过dotenv加载。敏感信息如数据库密码、API密钥绝不上传至代码仓库。输入验证与清理在后端对req.body.message进行更严格的验证防止超长字符串、特殊字符注入攻击虽然本例风险低但应养成习惯。错误处理中间件在 Express 后端使用统一的错误处理中间件来捕获和格式化所有错误避免服务崩溃。app.use((err, req, res, next) { console.error(err.stack); res.status(500).json({ error: Something went wrong! }); });API 版本管理如果未来 API 会变更从开始就设计版本路径如/api/v1/chat。前端状态管理当聊天功能变得复杂如多房间、用户身份考虑使用 Context API 或状态管理库如 Zustand, Redux Toolkit来管理全局状态。容器化部署使用 Docker 将前后端分别容器化并通过docker-compose.yml定义服务这能极大简化部署和环境一致性。通过这个项目你不仅构建了一个有趣的网页应用更实践了从设计、开发、调试到部署的完整 Web 全栈流程。核心的“请求-匹配-响应”模式可以迁移到客服机器人、游戏 NPC 对话、智能表单提示等众多场景。下一步尝试为它添加一个真实的数据库或者集成一个轻量级的机器学习库来改进匹配算法都是很好的深入学习方向。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进