前言
本次项目会同步更新在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这一重要结构就好。
后续的优化: