前言
快过来感受下 Ai 的魅力 , 无论你是 crud 仔还是切图仔 🤡, 在 Ai 时代 我觉得不仅仅是项目,我们开发的思想,都值得被重塑一遍 。而恰逢 ai 盛世的菜鸡大学生 , 也被 ai 的浪潮拍的格外精神 , 于是开始使用基础的知识 ,也来实现一个 Ai 全栈项目 , 也来感受下 ,在 Ai 时代 ,该如何做一个 full-stacker 😀
项目源码地址文末取
需求分析
我有一个需求是
- 有一张用户表 , 储存用户的信息
- 字段有 : ID 姓名 家乡
- 我现在要通过提问的方式 (不通过传统后端的方式)
- 去查询用户的信息
- 分析用户之间的关系
- 统计用户的数量 ...
我还有幻想的需求🤡
- 因为我的一句话 , 比如删除某个用户 , 可以通过 ai 转化为 sql 语句 , 在动态地拼接到后端代码中 ,直接执行数据库操作(当然需要设置鉴权操作🤡)
- 告诉 ai ,我要修改某个信息 , 然后直接让 ai 生成修改的 sql 语句 , 拼接到操作数据的代码 , 实现修改操作
- ...
效果如图 :
技术分析
前端
这个项目针对小白 , 所有采用原生的 api 方式进行页面开发 , 没有使用 vue , react 等框架 , 从而更加可以打好基础
- javaScript DOM 编程
- 使用 fetch 向后端请求
- 使用表格 , 表单, 等显示数据
- Html5 , CSS3(Bootstrap)
后端 (node.js)
- 暂时使用 json-server 快速搭建模拟 REST API 服务器的工具 , 制造 Mock 数据 , 模拟后端对前端请求的响应 (在显示用户数据这块使用)
- 库表设计采用 json 文件作为数据源代替数据库 , , 使得更快跑通前后端联调 !
{
"users" : [
{
"id": 1,
"name": "John",
"hometown": "宜春"
},
{
"id": 2,
"name": "Jane",
"hometown": "宜春"
},
{
"id": 3,
"name": "Bob",
"hometown": "南昌"
}
]
}
- 远程 api 调用
- 导入 openai 模块, 并封装解析 prompt 的函数 ,用于接受前端传来问题 ,并产生答案响应给前端
架构设计
项目结构
- frontend 前端项目
- backend 后端项目
- 数据接口 user.json 数据文件
- 文件性数据
- http 服务 json-server 代替后端响应数据
- llm ai server
后端初始化
所谓兵马未动 , 粮草前行 ,让我们先搞搞后端 , 保证数据的来源 ,先简单搞一下🤡 , 有实际开发经验的可能会使用 apifox 等之类的来造 Mock 数据 ,
今天带来不一样的 ,自己写后端来搞搞 , 挑战自我 ! follow me , 入门 AI 全栈开发
- 初始化项目
npm init -y
- 创建数据源
引入 json-server 模拟 REST API 服务器的工具
“json-server” 是一个用于快速搭建模拟 REST API 服务器的工具,它可以根据一个 JSON 文件生成具有 CRUD(创建、读取、更新、删除)操作的 API 接口,方便前端开发人员在没有真实后端服务器的情况下进行开发和测试。
//
npm i json-server
创建数据源 user.json
{
"users" : [
{
"id": 1,
"name": "John",
"hometown": "Hometown 1"
},
{
"id": 2,
"name": "Jane",
"hometown": "Hometown 2"
},
{
"id": 3,
"name": "Bob",
"hometown": "Hometown 3"
}
]
}
- 启动后端项目
在 script 标签中加入 :"dev": "json-server --watch user.json --port 3001 ", 指定端口 3001
"scripts": {
"dev": "json-server --watch user.json --port 3001 ",
"test": "echo \"Error: no test specified\" && exit 1"
},
在控制台输入以下指令启动项目
npm run dev
控制台输出信息
前端初始化
创建一个 html 页面
- 使用 fetch 前后端联调一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI User Rag chatbot</title>
</head>
<body>
<script>
//js 主动向后端发送接口数据请求
fetch('http://localhost:3001/users')
.then(res =>res.json())
.then(data =>console.log(data))
</script>
</body>
</html>
- 在网络选项检查是否请求成功
点击 user 查看请求信息
- 在浏览器控制台检查数据是否请求到了
到这里 , 前后端初始化成功 !
项目完善
前端
实现如下页面 , 并主动请求数据
源码
<!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>
</tbody>
</table>
<!-- 提问框和提交按钮 -->
<div class="row">
<form name="aiForm">
<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>
// js 主动向后端发送数据接口请求
// 前端向后端拉去数据
const tableBody = document.querySelector('table tbody');
const oForm = document.forms['aiForm'];
let userData ;
fetch('http://localhost:3001/users')
// 数据到达前端 二进制 -》 json
.then(res => res.json())
.then(users => {
// console.log(data);
userData = users;
console.log(userData);
for (let user of users) {
// console.log(user);
//创建一个表格行(<tr>)元素
const tr = document.createElement('tr');
// for in json 对象遍历
for (let key in user) {
//为每个属性值创建一个表格单元格(<td>)元素
const td = document.createElement('td');
td.innerText = user[key];
tr.appendChild(td);
}
// 将表格行添加到表格主体(<tbody>)中
tableBody.appendChild(tr)
}
})
oForm.addEventListener('submit', e => {
e.preventDefault(); // 阻止表单默认行为 , 改用fetch 向 Ai serevr 发送数据 接口请求
// 在fetch 不刷新页面的情况下 向 Ai serevr 发送数据 接口请求
// web 2.0 动态页面开发 ,js fetch 可以主动拉去数据
const question = oForm.question.value.trim();
if (!question) {
alert('请输入问题');
return;
}
fetch(`http://localhost:1314/api?question=${question}&data=${JSON.stringify(userData)}`)
.then(res => res.json())
.then(data => {
//console.log(data);
document.getElementById('message').innerText = data.result ;
})
})
</script>
</body>
</html>
源码解析
以下是对上述HTML和JavaScript代码的详细解析:
整体结构
这段代码是一个简单的网页应用程序,主要实现了以下功能:
- 从后端获取用户数据并在页面上以表格形式展示。
- 提供一个输入框供用户输入问题,点击提交按钮后,将问题以及已获取的用户数据发送到另一个后端接口,然后在页面上展示接口返回的回答。
HTML部分解析
<head>标签内内容:
<!DOCTYPE html>:声明这是一个HTML5文档。<html lang="en">:定义文档的根元素,lang="en"表示文档语言为英语。<meta charset="UTF-8">:设置文档的字符编码为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">:引入了Bootstrap v3.0.3的CSS样式文件,用于快速实现页面的美观布局和样式设计。
<body>标签内内容:
<div class="container">:这是一个Bootstrap的容器类,用于包裹页面内容,使其在页面中居中显示并具有一定的间距。
用户表格部分
<div class="row col-md-6 col-md-offset-3">:定义了一个行元素,并设置在中等屏幕(col-md-)下占6列宽度且偏移3列,用于将用户表格在页面上水平居中显示。<table class="table table-striped" id="user_table">:创建一个带有条纹样式(table-striped)的表格,id="user_table"用于在JavaScript中方便地获取该表格元素。
<thead>:表格的表头部分,包含了三个列标题:ID、姓名、家乡。<tbody>:表格的表体部分,初始为空,后续会通过JavaScript动态添加数据。
提问框和提交按钮部分
<div class="row">:定义一个新的行元素。<form name="aiForm">:创建一个表单,name="aiForm"用于在JavaScript中通过表单名称获取该表单元素。
<div class="from-group">:用于将输入框和标签组合在一起,形成一个输入组的样式(基于Bootstrap样式)。<label for="questionInput">提问</label>:为输入框创建一个标签,for="questionInput"关联到对应的输入框元素。<input type="text" id="questionInput" class="form-control" name="question" placeholder="请输入问题" required>:创建一个文本输入框,设置了id、class、name等属性,并且设置为必填项(required)。<button type="submit" class="btn btn-default" name="btn">查询</button>:创建一个提交按钮,样式为Bootstrap的默认按钮样式(btn btn-default)。
显示回答部分
<div class="row" id = "message"></div>:定义一个新的行元素,id="message"用于在JavaScript中找到该元素,以便后续将从后端获取的回答显示在这个元素内。
JavaScript部分解析
获取并展示用户数据:
const tableBody = document.querySelector('table tbody');:通过querySelector方法获取页面上id为user_table的表格中的<tbody>元素,用于后续添加表格行数据。const oForm = document.forms['aiForm'];:通过表单名称获取到前面定义的名为aiForm的表单元素。let userData ;:声明一个变量userData,用于存储从后端获取的用户数据。fetch('http://localhost:3001/users'):使用fetchAPI向本地的http://localhost:3001/users接口发送一个GET请求,用于获取用户数据。
.then(res => res.json()):当接收到响应后,将响应的二进制数据转换为JSON格式。.then(users => {... }):处理转换后的JSON数据(这里假设数据是一个用户对象数组)。在这个回调函数中:
userData = users;:将获取到的用户数据赋值给userData变量。- 通过循环遍历
users数组中的每个用户对象:
const tr = document.createElement('tr');:为每个用户创建一个<tr>表格行元素。- 使用
for in循环遍历用户对象的每个属性:
const td = document.createElement('td');:为每个属性值创建一个<td>表格单元格元素。td.innerText = user[key];:将用户对象的属性值设置为对应的表格单元格的文本内容。tr.appendChild(td);:将创建好的表格单元格添加到当前的表格行中。tableBody.appendChild(tr):将创建好的包含用户数据的表格行添加到表格的<tbody>元素中。处理用户提问并获取回答:
oForm.addEventListener('submit', e => {... }):为表单的submit事件添加一个监听器,当用户点击提交按钮时触发。
e.preventDefault();:阻止表单的默认提交行为,因为要使用fetchAPI自行处理数据提交,而不是让表单按照默认方式提交(默认提交会导致页面刷新)。const question = oForm.question.value.trim();:获取表单中名为question的输入框的值,并去除两端的空白字符。- 如果
question为空字符串,则弹出提示框并返回,不进行后续操作。fetch(http://localhost:1314/api?question={question}&data={JSON.stringify(userData)})`
使用
fetchAPI向本地的<http://localhost:1314/api接口发送一个GET请求,将用户输入的问题以及已经获取到的用户数据(转换为JSON字符串格式)作为查询参数传递给后端接口。> * 接收到后端响应后,将响应数据转换为JSON格式,并将回答内容设置为id为message的页面元素的文本内容,从而在页面上显示给用户。这段代码实现了一个简单的基于前端JavaScript与后端通过
fetchAPI进行数据交互的应用程序,涉及到从后端获取数据展示以及将用户输入数据发送到后端获取回答并展示的功能。
后端
数据源
在后端项目初始化过程中已经完成 , 使用 json - server 响应如下数据
{
"users" : [
{
"id": 1,
"name": "John",
"hometown": "宜春"
},
{
"id": 2,
"name": "Jane",
"hometown": "宜春"
},
{
"id": 3,
"name": "Bob",
"hometown": "南昌"
}
]
}
对 api 模块的封装
npm init -y
npm i openai
源码
// node 内置的http 模块
const http = require('http');
const OpenAi = require('openai');
const url = require('url');// node 内置
const client = new OpenAi({
apiKey: 'xxx',
baseURL: 'xxx'
});
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
})
return response.choices[0].message.content
}
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'); // 允许的请求头
const parsedUrl = url.parse(req.url, true);
const queryObj = parsedUrl.query;
console.log(queryObj);
console.log(queryObj.data);
const prompt = `
请根据 ${queryObj.data}数据,回答${queryObj.question} 这个问题 ,注意你的回答仅限于提供的数据${queryObj.data}
`
const result = await getCompletion(prompt)
let info = {
result
}
res.statusCode = 200
res.setHeader('Content-Type', 'text/json')
res.end(JSON.stringify(info))
})
server.listen(1314)
源码分析
以下是对上述Node.js代码的详细解析:
整体功能概述
这段代码主要实现了一个简单的HTTP服务器,它接收来自客户端的请求,调用OpenAI的API根据请求中的数据生成相应回复,并将回复返回给客户端。同时,代码还设置了跨域访问的相关响应头,以允许不同源的客户端进行访问。
模块引入
const http = require('http');:引入Node.js内置的http模块,用于创建HTTP服务器并处理网络请求和响应。const OpenAi = require('openai');:引入openai模块,用于与OpenAI的API进行交互,这里应该是通过npm安装的OpenAI官方的Node.js SDK或者是与之兼容的第三方实现。const url = require('url');:引入Node.js内置的url模块,用于解析请求的URL,获取其中的查询参数等信息。
OpenAI客户端配置
const client = new OpenAi({:创建一个新的OpenAI客户端实例。
apiKey: 'xxx',:设置访问OpenAI API所需的API密钥,这里的xxx应该替换为真实有效的API密钥。baseURL: 'xxx':设置OpenAI API的基础URL,可能用于指定特定的服务端点或者自定义的API部署地址,同样这里的xxx需要替换为实际的URL。
生成回复的函数定义
const getCompletion = async (prompt, model="gpt-3.5-turbo") => {:定义一个异步函数getCompletion,用于调用OpenAI的API获取给定提示(prompt)的回复。
const messages = [{:创建一个包含用户消息的数组,用于传递给OpenAI的聊天完成接口。
role: 'user',:指定消息的角色为用户。content: prompt:设置消息的内容为传入的提示文本。const response = await client.chat.completions.create({:调用OpenAI客户端的chat.completions.create方法,向OpenAI的聊天完成接口发送请求以生成回复。
model: model,:指定要使用的模型,默认是gpt-3.5-turbo,也可以传入其他支持的模型名称。messages: messages,:传递前面创建的包含用户消息的数组。temperature: 0:设置生成内容的随机性参数为0,这意味着生成的回复将更具确定性,更倾向于给出最常见或最符合逻辑的回答。return response.choices[0].message.content:从OpenAI API返回的响应中提取第一个选择(通常是最相关的回复)的消息内容,并将其作为函数的返回值。
HTTP服务器创建与请求处理
const server = http.createServer(async (req, res) => {:创建一个HTTP服务器,并定义一个异步的请求处理函数,用于处理每个接收到的请求。
跨域访问设置:
res.setHeader('Access-Control-Allow-Origin', '*');:设置允许所有来源的客户端访问该服务器资源,在实际应用中,可能需要根据具体需求指定具体的域名,以提高安全性。res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');:指定允许客户端使用的请求方法,包括GET、POST和OPTIONS。res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');:明确允许客户端在请求中携带的请求头信息,这里包括Content-Type和Authorization等常见的请求头。
请求参数解析与回复生成:
const parsedUrl = url.parse(req.url, true);:使用url模块的parse方法解析接收到的请求URL,true参数表示将查询参数解析为对象形式。const queryObj = parsedUrl.query;:获取解析后的查询参数对象,其中包含了客户端通过URL传递过来的各种参数。console.log(queryObj);:在控制台打印出整个查询参数对象,方便调试查看客户端传递的所有参数信息。console.log(queryObj.data);:在控制台单独打印出查询参数对象中的data属性值,同样用于调试目的,查看特定的数据信息。const prompt =:根据客户端传递的查询参数data和question构建一个提示文本,用于传递给OpenAI的API请求生成回复。
请根据 ${queryObj.data}数据,回答${queryObj.question} 这个问题,注意你的回答仅限于提供的数据${queryObj.data}:明确要求OpenAI根据特定的数据(queryObj.data)来回答特定的问题(queryObj.question),并且回答要基于所提供的数据范围。const result = await getCompletion(prompt):调用前面定义的getCompletion函数,将构建好的提示文本传递给OpenAI的API,获取生成的回复结果。响应设置与返回:
let info = {:创建一个包含回复结果的对象。
result:将从OpenAI API获取的回复结果作为对象的属性。res.statusCode = 200:设置响应的状态码为200,表示请求成功处理。res.setHeader('Content-Type', 'text/json'):设置响应的内容类型为text/json,表明返回的数据是JSON格式的。res.end(JSON.stringify(info)):将包含回复结果的对象转换为JSON字符串形式,并作为响应的主体内容发送回客户端,结束本次响应。
服务器启动
server.listen(1314):启动创建好的HTTP服务器,使其监听在本地的1314端口上,等待客户端的连接和请求。这段代码搭建了一个简单的基于Node.js的服务端应用程序,它能够接收客户端的请求,结合OpenAI的API根据请求中的数据生成针对性的回复,并将回复以JSON格式返回给客户端,同时通过设置跨域访问相关的响应头,允许不同源的客户端进行访问。在实际应用中,需要确保正确配置OpenAI的API密钥以及根据具体需求调整跨域访问的设置等细节。
项目思考
-
假设我们数据源的信息中 ,含有密码 , 身份证号等敏感信息的时候 ,需要脱敏处理 , 因为 api 是第三应用 ,可能会出现泄露信息 , 尤其是对于银行而言 ,假设说 ,要做一个 ai 应用 , 那么给它的数据 , 就非常不安全 ,所以最好做自己的 LLM
-
用户与 ai 的交互应该是交互式的 , 当返回的文字 ,特别多的时候 , 需要很久才可以响应 ,其实我们可以参考参考现有的大模型 ,它们都是一个字一个字的"蹦"出来 , 使用 sse 技术可以实现 ,后期项目迭代开发可以优化这一点 .
AI 开发与传统开发
虽然项目还是处于雏形阶段 ,但是 , 却大大突显出 AI 全栈开发的趣味性来
我觉得与传统项目的区别在于 :
数据的分析 ,变得简单
比如 : 问题"哪几位是老乡 ?" , 只需要写 prompt
const prompt = `
请根据 ${queryObj.data}数据,回答${queryObj.question} 这个问题 ,注意你的回答仅限于提供的数据${queryObj.data}
`
而传统的后端有两种实现方式
1)数据量较小的时候 ,读取到内存中 ,使用后端代码来筛选 hometwon 相同的用户
2)使用 sql 语句查询 hometwon 相同的用户
灵活性变强 , 用户体验较好
不知道大家有没有,这样的体验 , 有些应用 ,太过于臃肿 , 有的时候使用一个功能 ,都不知道在哪里 ,所以我经常在上面的输入框中查询这个功能 ,之后再点击这个功能
而现在 , 只需要一个对话框 ,想知道什么就直接问 ,不需要去找这个按钮到底在哪里 !!!
但我觉得对于一些敏感性的数据 ,或许还需要鉴权 ,加密 ,等操作 .
可 , 对于一些权限要求不高的功能 ,简直是爽爆了 !!!
用户拥有自主权
比如查询用户信息 , 显示信息可以多样化 , 可以使用 json 格式 ,使用 markdown 模式 ,真的是用户说什么就是什么 , 在某些程度上实现了灵活性 ,告别了后端定制的局限性 !!!