MENU

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

2025 年 07 月 30 日 •

前言

本次项目会同步更新在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,这个文件用来呈现主界面:

<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标签的部分:

<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标签展示内容。

后端

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

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的子类:

class HTTPServerWithModel(HTTPServer):
    def __init__(self, server_address, handler, model, tokenizer):
        super().__init__(server_address, handler)
        self.model = model
        self.tokenizer = tokenizer

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

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这一重要结构就好。

后续的优化:

  • 界面搭建
  • 模型返回速度优化
  • 异步问题