人生中第一个全栈项目:好像真有点东西

260 阅读7分钟

引言

这次我们来搞一个完整的全栈项目:在网页上显示后端的数据,能够结合AI回答用户提出的问题。下面,我们将从零开始规划这样一个项目,确保每个步骤都清晰明了,适合不同技术水平的朋友跟随学习,相信每个人在看完后都能有所收获。

完成的结果: image.png

不多废话,下面就开始。

56556.jpg

全栈项目

介绍一下本次任务的架构设计:

  1. 在前端中,浏览器向本地服务器(127.0.0.1)发送请求。后端使用json-sever搭建服务器接受请求,读取数据并通过http发送到浏览器,浏览器解析json并显示出后端数据。
  2. 前端接收用户提出的问题,传给后端,后端通过API接口请求AI回答问题,并将回答传回后端,最后返回给前端并在页面上显示回答。

我们需要分别创建三个文件夹:

  • backend(后端):应用程序中负责处理数据逻辑、业务规则以及与数据库交互的部分。它不直接与用户交互,而是通过前端界面间接地进行。
  • frontend(前端):指用户可以直接看到并与之互动的应用程序部分。这部分关注的是用户体验,包括视觉设计、页面布局、用户交互等方面。
  • LLM(大模型):大型语言模型是一种人工智能技术,能够理解和生成自然语言文本。这类模型基于深度学习技术训练而成,可以用于各种任务,比如自动问答、内容创作、翻译等。

Backend 后端

后端的使命是要提供数据接口

  • 首先,在后端文件夹命令行中
npm init -y         // 初始化后端项目
// 安装 json-server  来创建服务器
// 快速地搭建一个 RESTful API 服务器,它基于你的 JSON 数据文件来提供数据。它允许开发者在没有实际后端的情况下测试和开发前端应用。
npm i json-server   
  • 并在package.json文件中的 "scripts" 脚本项中添加以下内容。在运行npm run dev时将会dev中的命令。使用json-server,监听user.json文件变化并自动重启服务器(可以简单理解为把user.json作为服务器),使用3001端口。
"scripts": {
    "dev": "json-server --watch user.json --port 3001"
  },
  • 在后端文件夹中创建一个user.json文件用来存放后端数据,我们将把它作为一个简单的服务器。这里是我的user.json中的数据:
{
    "users": [
        {
            "id": 1,
            "name": "张三",
            "hometown": "北京"
        },
        {
            "id": 2,
            "name": "李四",
            "hometown": "上海"
        },
        {
            "id": 3,
            "name": "王五",
            "hometown": "北京"
        }
    ]
}

至此,我们backend的配置就完成。我们运行一下

npm run dev  //运行开发环境下的dev脚本

image.png 可以看到已经成功了(不要关闭服务器),我们接下来就可以在前端通过fetch(Endpoints)拿取后端的数据了。

Fronted 前端

先来构建HTML的基本结构,这里使用了Bootstrap前端框架快速构建页面。如果想要了解Bootstrap,可以看看我之前的文章通过Bootstrap设计的PC端网格布局,给你的代码加加速

  • 我们使用表格来展示后端数据,先在<tbody>中随便添加一些内容来看一看最后的页面显示结果。一会儿将使用DOM根据数据动态创建每一行。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Users Rag chatbot</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <!-- 占页面一半并居中 -->
        <div class="row col-md-6 col-md-offset-3">
            <!-- 表格使用斑马纹样式 -->
            <table class="table table-striped" id="user_table">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>姓名</th>
                        <th>家乡</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>1</td>
                        <td>1</td>
                        <td>1</td>
                    </tr>
                    <tr>
                        <td>2</td>
                        <td>2</td>
                        <td>2</td>
                    </tr>
                    <tr>
                        <td>3</td>
                        <td>3</td>
                        <td>3</td>
                    </tr>
                </tbody>
            </table>
            <!-- 用户提问部分 -->
            <div class="row">
                <form name="aiForm" action="http://www.baidu.com">
                    <div class="from-group">
                        <label for="questionInput">提问</label>
                        <input type="text" id="questionInput" class="form-control" name="question" placeholder="请输入问题"
                            required>
                    </div>
                    <button type="submit" class="btn btn-default" name="btn">查询</button>
                </form>
            </div>
            <div class="row" id="message"></div>
        </div>
    </div>
    <script>
    </script>
</body>
</html>

下面是页面展示 image.png 我们现在来进行js部分

  • js 主动向后端发送数据接口请求,前端向后端拉取数据,并打印到页面上。我们要通过fetch向后本地服务器的3001端口发出请求,请求users内容,.then用来等待。但fetch比较底层,请求的数据为二进制,还需要使用.json将二进制转化为 json 。使用let of遍历json数组,并将其打印在createElement创建的tr和td中。
      // js 主动向后端发送数据接口请求
      // 前端向后端拉去数据
      // DOM获取tbody元素,准备在tbody中间动态添加 tr 和 td
        const tableBody = document.querySelector('table tbody');
        let usersData;
        // 请求本地服务器的 3001 端口中的 users 数组
        fetch('http://localhost:3001/users')
            // 数据到达前端之后  二进制 -> json  
            .then(res => res.json())
            .then(users => {
                usersData = users;
                // console.log(users);
                for (let user of users) {  //遍历json并打印数据
                    // console.log(user);
                    const tr = document.createElement('tr');
                    // for in json 对象遍历
                    for (let key in user) {
                        const td = document.createElement('td');
                        td.innerText = user[key];  // 设置内容
                        tr.appendChild(td); 
                    }
                    tableBody.appendChild(tr)
                }
            })

可以看到,我们后端的数据已经在前端渲染出来了。

image.png

  • 给表单添加提交事件,能够将用户提出的问题传到后端,后端交给AI处理。 JSON.stringify()用于将JS对象转换为json字符串
  const oForm = document.forms['aiForm'];  // 特定的获取表单对象  [name]
  oForm.addEventListener('submit', e => {
            // 事件对象
            // console.log(e);
            e.preventDefault(); // 阻止表单默认行为,表单提交后将不再会转到 action 属性的页面
            // fetch 在页面不刷新的时候向ai server 发出请求
            // const question = oForm.question.value;
            // 发送数据到后端
            // web 2.0  动态页面开发 js fetch 可以主动拉取数据
            // 获取用户提出的问题,去掉空格
            const question = oForm.question.value.trim();
            // 如果没有输入问题,提示
            if (!question) {
                alert('请输入问题');
                return;
            }
            console.log(question)
            // 再次请求本地服务器的1314端口(本例中用来使用AI),并向服务器传入数据 question 和 data
            fetch(`http://localhost:1314/api?question=${question}&data=${JSON.stringify(usersData)}`)
                // 二进制 -> json
                // 后端返回回答
                .then(data => data.json())
                .then(data => {
                    console.log(data)
                    // 将回答打印在页面上
                    document.getElementById('message').innerText =
                        data.result;
                })
        })

这里我们完成了后端得到回答后将回答传给前端,并打印到页面上。 下面,我们将处理LLM的部分,让后端向LLM提问,并获得回答。

LLM 大模型

现在在LLM文件下

npm init -y   // 初始化
npm i openai  // 下载openai 
npm i dotenv  // 下载dotenv,用于个人安全防护

(有关AI使用和dotenv的部分可以查看我的文章零基础LLM+Prompt:原来Prompt要这么用

创建main.js文件

//  node 内置的 http 模块  用于创建 HTTP 服务器和客户端
// 相当于es6的import from (因为在这里使用的js文件,不能使用import 模块化)
const http = require('http');
const OpenAi = require('openai');
const url = require('url');// node 内置 url  用于解析、格式化和编码 URL
const dotenv=require('dotenv');
dotenv.config();
const client = new OpenAi({
    apiKey: process.env.OPENAI_API_KEY,  // 你自己的密钥
    baseURL: process.env.OPENAI_API_BASE_URL   // 代理转发地址
});
const getCompletion = async (prompt, model = "gpt-3.5-turbo") => {
    // 用户提的问题
    const messages = [{
        role: 'user',
        content: prompt
    }];
    // AIGC chat 接口
    const response = await client.chat.completions.create({
        model: model,
        messages: messages,
        // LLM 生成内容的随机性
        temperature: 0.1
    })
    return response.choices[0].message.content
}

接下来,创建一个http服务。

   // 创建http服务器   
   // req 是请求对象,包含了请求的信息 res 是响应对象,用于发送响应给客户端
   const server = http.createServer(async (req, res) => {
    // 设置响应头,解决跨域问题
    res.setHeader('Access-Control-Allow-Origin', '*'); // 允许所有来源访问,也可以指定具体的域名,如'http://example.com'
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); // 允许的请求方法
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // 允许的请求头
    
    // 解析请求的 URL,并将查询字符串解析为对象
    const parsedUrl = url.parse(req.url, true);
    // 获取解析后的查询参数对象
    const queryObj = parsedUrl.query;  // 打印queryObj,可以看到包含了 question 和 data,正是在前端时传入的数据

    console.log(parsedUrl.query);
    // 提出 prompt  使用解析后的对象
    const prompt = `
  ${queryObj.data}
  请根据上面的JSON数据,回答${queryObj.question} 这个问题
  `
    const result = await getCompletion(prompt)
    // 将返回结果存储在对象中
    let info = {
        result
    }
    
    res.statusCode = 200   // 设置响应状态码为 200,表示请求成功
    res.setHeader('Content-Type', 'text/json') // 设置响应的内容类型为 `text/json`
    res.end(JSON.stringify(info)) // 将info对象转换为 JSON 字符串,并发送给客户端,结束响应
})
// 开启监听 1314 端口
server.listen(1314)

打印解析后的查询参数对象,获取到了用户提出的问题和后端数据。

image.png

测试

我们现在来检测一下:

  1. 后端打开服务器:npm run dev
  2. LLM运行main.js接口文件
  3. 前端打开页面
  4. 提问得到结果

image.png

我们已经成功完成了一个AI全栈项目!!!

总结

从后端到前端再到LLM,从项目的构思到最终的实现,每一步都充满了挑战与收获。LLM模型的集成是整个项目的核心,我们确保了AI能够提供准确的响应。此外,我们还特别注重数据的安全性和隐私保护,确保用户信息得到妥善处理。

sdffs.jpg