思路:自己用Flask写一个有前端有后端的聊天机器人Web应用,然后使用OpenAI的API来生成回复。之后,将程序用Docker虚拟化,从而方便地部署在服务器上。

使用Flask编写应用

首先,使用Flask编写一个简单的聊天机器人Web应用。具体的Python代码如下所示。

from flask import Flask, render_template, redirect, url_for, request, jsonify
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user

app = Flask(__name__)
app.secret_key = 'your_secret_key'  # 用于保护会话

# 设置Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)

# 设置未登录用户访问受保护页面时跳转到登录页面
login_manager.login_view = "login"

# 模拟数据库中的用户数据
users = {'user1': {'password': '1'}}


# User类需要继承UserMixin
class User(UserMixin):
   def __init__(self, id):
       self.id = id


# 通过用户ID加载用户对象
@login_manager.user_loader
def load_user(user_id):
   return User(user_id)


# 模拟处理用户输入的函数
def process_message(message):
   # Todo 换成API处理信息
   return f"你说的是: {message}"


@app.route('/')
@login_required
def home():
   return render_template('chat.html')
   # return render_template('home.html')


@app.route('/login', methods=['GET', 'POST'])
def login():
   if request.method == 'POST':
       username = request.form['username']
       password = request.form['password']

       # 验证用户
       if username in users and users[username]['password'] == password:
           user = User(username)
           login_user(user)
           return redirect(url_for('home'))
       else:
           return "Invalid username or password", 401  # 登录失败

   return render_template('login.html')


@app.route('/logout')
def logout():
   logout_user()
   return redirect(url_for('login'))


@app.route('/chat', methods=['POST'])
@login_required
def chat():
   user_message = request.form['message']  # 获取用户输入
   response = process_message(user_message)  # 调用处理函数生成回复
   return jsonify({'response': response})


if __name__ == '__main__':
   app.run(debug=True)

之后,将process_message函数替换成用API生成回复的代码。这部分可以先不做,待其他部分测试通过后再进行修改。

import openai

client = openai.OpenAI(api_key='sk-proj-xxxxxxxxxxxx')

def process_message(message):
   completion = client.chat.completions.create(
       model="gpt-4o-mini",
       messages=[
          {"role": "system", "content": "You are a helpful assistant."},
          {
               "role": "user",
               "content": message
          }
      ]
  )
   print(completion.choices[0].message)
   return completion.choices[0].message.content

注:这个函数只能将一条信息发送给API处理。如果需要之前的Prompt,则需要为每个session引入一个list来记录历史的消息内容,并在构造messages时加入这些信息。

然后,写好对应的前端页面login.html和chat.html。

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Login</title>
</head>
<body>
   <h1>Login</h1>
   <form method="POST">
       <label for="username">Username:</label>
       <input type="text" id="username" name="username" required><br>

       <label for="password">Password:</label>
       <input type="password" id="password" name="password" required><br>

       <button type="submit">Login</button>
   </form>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Chatbot</title>
   <style>
       body {
           font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
           background: linear-gradient(135deg, #6e7dff, #84aaff);
           margin: 0;
           padding: 0;
           display: flex;
           justify-content: center;
           align-items: center;
           height: 100vh;
           overflow: hidden;
      }

       .chat-container {
           width: 100%;
           background-color: #fff;
           padding: 20px;
           border-radius: 15px;
           box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
           display: flex;
           flex-direction: column;
           height: 90%;
           max-height: 800px; /* 限制最大高度 */
      }

       h1 {
           color: #333;
           font-size: 28px;
           margin-bottom: 10px;
           text-align: center;
           font-weight: 600;
      }

       h2 {
           color: #444;
           font-size: 20px;
           margin-bottom: 15px;
           text-align: center;
           font-weight: 400;
      }

       .messages {
           flex-grow: 1;
           width: calc(100% - 30px);
           overflow-y: auto;
           margin-bottom: 20px;
           padding: 15px;
           border-radius: 12px;
           background-color: #f9f9f9;
           box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
           display: flex;
           flex-direction: column;
           gap: 10px;
           scroll-behavior: smooth;
      }

       .message {
           padding: 12px 16px;
           border-radius: 20px;
           max-width: 80%;
           margin-bottom: 10px;
           font-size: 16px;
           line-height: 1.5;
           word-wrap: break-word;
           transition: all 0.3s ease;
      }

       .user-message {
           background-color: #4CAF50;
           color: white;
           align-self: flex-end;
      }

       .bot-message {
           background-color: #e0e0e0;
           color: #333;
           align-self: flex-start;
      }

       input[type="text"] {
           width: 80%;
           padding: 12px;
           margin-right: 15px;
           border-radius: 30px;
           border: 1px solid #ccc;
           outline: none;
           font-size: 16px;
           transition: all 0.3s ease;
      }

       input[type="text"]:focus {
           border-color: #007bff;
           box-shadow: 0 0 8px rgba(0, 123, 255, 0.6);
      }

       button {
           padding: 12px 20px;
           background-color: #007bff;
           color: white;
           border: none;
           border-radius: 30px;
           cursor: pointer;
           font-size: 16px;
           transition: all 0.3s ease;
      }

       button:hover {
           background-color: #0056b3;
           transform: translateY(-2px);
      }

       a {
           text-decoration: none;
           font-size: 14px;
           color: #007bff;
           margin-bottom: 20px;
           display: block;
           text-align: center;
           transition: all 0.3s ease;
      }

       a:hover {
           text-decoration: underline;
           transform: translateY(-2px);
      }

       /* 响应式布局:小屏幕优化 */
       @media (max-width: 768px) {
           .chat-container {
               padding: 15px;
          }
           input[type="text"] {
               width: 60%;
          }
           button {
               padding: 10px 18px;
          }
      }
   </style>
</head>
<body>
   <div class="chat-container">
       <h1>Welcome, {{ current_user.id }}!</h1>
       <a href="{{ url_for('logout') }}">Logout</a>
       <h2>Chat with Bot</h2>
       <div class="messages" id="messages">
           <!-- 消息会显示在这里 -->
       </div>
       <form id="chatForm" style="width: 100%; display: flex; justify-content: space-between; align-items: center;">
           <input type="text" id="message" placeholder="Type a message..." required>
           <button type="submit">Send</button>
       </form>
   </div>

   <script>
       const chatForm = document.getElementById("chatForm");
       const messagesContainer = document.getElementById("messages");

       chatForm.onsubmit = async function (event) {
           event.preventDefault();

           const userMessage = document.getElementById("message").value;

           // 添加用户消息到页面
           const userMessageElement = document.createElement("div");
           userMessageElement.classList.add("message", "user-message");
           userMessageElement.textContent = "You: " + userMessage;
           messagesContainer.appendChild(userMessageElement);

           // 清空输入框
           document.getElementById("message").value = "";

           // 发送请求到后端获取机器人的回复
           const response = await fetch("/chat", {
               method: "POST",
               headers: {
                   "Content-Type": "application/x-www-form-urlencoded",
              },
               body: `message=${encodeURIComponent(userMessage)}`
          });

           const data = await response.json();

           // 添加机器人消息到页面
           const botMessageElement = document.createElement("div");
           botMessageElement.classList.add("message", "bot-message");
           botMessageElement.textContent = "Bot: " + data.response;
           messagesContainer.appendChild(botMessageElement);

           // 滚动到最新消息
           messagesContainer.scrollTop = messagesContainer.scrollHeight;
      };
   </script>
</body>
</html>

之后,访问127.0.0.1:5000,就可以使用这个聊天机器人了。

Dockerized

将服务封装到Docker容器中,可以保持环境的一致性、隔离性,提升可移植性,同时在大多数场景下几乎没有性能损失。

首先,导出pip对应的依赖。

pip freeze > requirements.txt

之后,创建一个名为Dockerfile的纯文本文件。此时,文件目录看上去应该像这样。

HelloFlask
|-templates
|-chat.html
|-login.html
|-app.py
|-Dockerfile
|-requirements.txt

dockerfile示例

# 使用官方的 Python 镜像
FROM python:3.9-slim

# 设置工作目录
WORKDIR /app

# 将应用代码复制到容器的工作目录
COPY . /app

# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt

# 暴露容器端口(Flask 默认是 5000)
EXPOSE 5000

# 启动应用
CMD ["python", "app.py"]

之后,开始打包Docker镜像。首先cd到HelloFlask目录,然后

docker build -t helloflask .

如果执行成功,就可以通过如下命令查看到镜像了。

docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED              SIZE
helloflask   latest   123456789999   About a minute ago   177MB

在本地,已经可以用这个image跑起来container了

docker run helloflask

之后,将镜像打包,并将tar文件复制到服务器上,进行导入

docker save -o helloflask.tar helloflask
docker load -i helloflask.tar

需要注意的是,打包时候用的机器的环境要与目标服务器的环境兼容。如果打包时用的arm64架构的处理器,而目标服务器是amd64架构的处理器,就会报错。

之后,就可以在服务器上使用这个镜像创建容器了。运行这个容器后,访问<your_ip>:5000即可。

docker run -itd helloflask

其中参数i表示使容器保持交互式;t表示为容器分配一个伪终端;d表示使容器在后台运行,不占用当前终端。如果需要进入这个容器的shell,可以使用如下命令。

docker exec -it <container_id> bash

在这个终端中尽情玩耍吧。

root@<container_id>:/app# python -V
Python 3.9.20
root@<container_id>:/app# exit()