初试全栈开发之聊天机器人(1)-前后端配置

前言

本次项目会同步更新在Github,请移步查看。这个项目比较粗糙,完全是为了学习使用。

最近一直考虑把JavaScript、HTML和CSS捡一捡,上次系统性地写软件,还是在初中的时候——用Dreamweaver开发网站。时过境迁,现在有各种各样的框架可供使用,页面也越来越好看了。

本次就来开发一个聊天机器人的界面——如ChatGPT、DeepSeek、豆包之类的网站页面。选用的技术栈是Vue+Python。

我对于前端开发一窍不通。我对于全栈的理解是——一个人既写界面,又要写后端的服务器逻辑。按照这个定义,姑且开始这次的全栈开发之旅吧。

整体设计

从后端来说,我选用了http库作为后端服务器,模型选择了Qwen2-0.5B,一个参数量非常小的模型——有基本的输入、输出就够了。

前端使用Vue和纯手写的CSS/JS。我用Vue的原因是——这个架构上手容易,还采用“单文件组件”的类似OOP的模式,一个文件内包含JS的部分、CSS的部分以及HTML的部分,一个文件就是一个组件,深得我心。

前端

通过阅读Vue的手册,很快就能写出来一个简单的前端界面。这个界面只包括一个按钮,用来给后端发送请求,一个p标签,用来显示得到的结果:

具体的代码分为两个文件,第一个文件是App.vue,这个文件用来呈现主界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup>
import Sender from './components/Sender.vue';
</script>

<template>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ToyChatPage</title>
</head>
<body>
<Sender></Sender>
</body>
</html>
</template>

<style scoped>
</style>

另一个是Sender.vue,包含了那个按钮和p标签的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<script setup>
import { ref } from 'vue';

const message = ref('');

function fetch_it() {
fetch('http://127.0.0.1:8000', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: 'Can you tell me what is a dog?' })
}).then(
response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
).then(data => {
message.value = data['data'];
}
);
}
</script>

<template>
<div class="sender">
<button @click="fetch_it">Fetch</button>
<p>{{ message }}</p>
</div>
</template>

<style scoped>
.sender {
background-color: #66CCFF;
}
</style>

写了一个简单的fetch,通过POST请求到后端,将得到的结果赋值给message变量,然后通过p标签展示内容。

后端

后端则需要考虑多一些,首先处理输入的参数,完成主函数的编写:

1
2
3
4
5
6
7
8
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--port", "-p", default=8000, type=int, help="Port to listen on")
parser.add_argument("--addr", "-a", default="127.0.0.1", type=str, help="Address to listen on")
parser.add_argument("--model_path_or_name", "-m", default=os.path.join(".", "Qwen2-0.5B"), type=str)
args = parser.parse_args()

run(args)

由于我们需要往服务器里传参——模型和分词器——所以我们需要写一个HTTPServer的子类:

1
2
3
4
5
class HTTPServerWithModel(HTTPServer):
def __init__(self, server_address, handler, model, tokenizer):
super().__init__(server_address, handler)
self.model = model
self.tokenizer = tokenizer

这样,就可以让handler也可以调用这两个变量,得到输出,句柄的编写要复杂一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class ModelResponseHandler(BaseHTTPRequestHandler):
def get_response(self, input_query):
model = self.server.model
tokenizer = self.server.tokenizer
processed_input_query = tokenizer(input_query, return_tensors="pt").to("cuda:0")
output = model.generate(**processed_input_query, max_length=1024, num_return_sequences=1)
return tokenizer.batch_decode(output, skip_special_tokens=True)

def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'POST')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()

def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length).decode('utf-8')

if "application/json" in self.headers['Content-Type']:
try:
data = json.loads(post_data)
except:
self.send_error(400, "Invalid JSON")
return
elif "application/x-www-form-urlencoded" in self.headers['Content-Type']:
data = urllib.parse.parse_qs(post_data)
else:
self.send_error(400, "Unsupported Content-Type")
return

if "query" not in data:
self.send_error(400, "Missing 'query' parameter")
return
if not isinstance(data['query'], str):
self.send_error(400, "Invalid 'query' parameter")
return

response = self.get_response(data['query'])
print(response[0])
response = {"data": response[0]}
response = json.dumps(response)

self.send_response(200)

self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(response.encode('utf-8'))

get_response就是一个标准的调用transformers库,通过大模型获得结果的函数,而至于do_OPTIONS,则需要好好解释一番。

这里涉及到的知识点是CORS——跨源资源共享。如果只是单纯的POST和返回,浏览器默认会阻止请求,因为其认为请求不安全。比如很多RPG游戏也会推出网页版,但是网页版由于CORS政策,没法访问本地的文件,导致无法运行。CORS的工作流程是这样的,首先,客户端会发送一个OPTIONS预检请求到服务器,以获知服务器是否允许该实际请求,这一步,服务器需要返回允许的值,字段Access-Control-Request-Method,说明了实际请求要用什么方法,字段Access-Control-Request-Headers则声明,包里包含什么字段。

do_POST就是普普通通的处理POST请求的函数——一大堆错误处理、包构造之类。

结果

最后的结果如下:

作为一个demo而言,还是很成功的。

后记

其实前后端之间的交互,只要把握住JSON这一重要结构就好。

后续的优化:

  • 界面搭建
  • 模型返回速度优化
  • 异步问题
文章作者:
文章链接: https://www.coderlock.site/2025/07/30/初试全栈开发之聊天机器人-1-前后端配置/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 寒夜雨