思路:自己用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()