我们要做什么
本文的目标是从零搭建一个带Web界面的AI对话应用——你在浏览器里和AI聊天,对话历史实时保存,可以一键部署到服务器供自己使用。
技术栈:Python 3 + Flask(后端)+ OpenAI API + 原生HTML/JS(前端)。不需要React,不需要复杂框架,完成后约150行代码。
前置:Python 3.8+,已有OpenAI API Key(参考《零基础接入AI API》一文)。
项目结构
my-ai-chat/
├── app.py # Flask 后端
├── requirements.txt # 依赖列表
└── templates/
└── index.html # 前端页面
后端:app.py
from flask import Flask, request, jsonify, render_template, session
from openai import OpenAI
import os, uuid
app = Flask(__name__)
app.secret_key = os.urandom(24) # session 加密密钥
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
# 每个用户的对话历史(生产环境应存数据库,这里简化为内存)
conversations = {}
@app.route("/")
def index():
# 为每个浏览器会话分配唯一ID
if "session_id" not in session:
session["session_id"] = str(uuid.uuid4())
return render_template("index.html")
@app.route("/chat", methods=["POST"])
def chat():
data = request.json
user_message = data.get("message", "").strip()
session_id = session.get("session_id", "default")
if not user_message:
return jsonify({"error": "消息不能为空"}), 400
# 初始化或获取该用户的对话历史
if session_id not in conversations:
conversations[session_id] = [
{"role": "system", "content": "你是一个友好、专业的AI助手,用简洁清晰的中文回答问题。"}
]
# 添加用户消息
conversations[session_id].append({"role": "user", "content": user_message})
# 保留最近20条消息,防止超出Token限制
history = conversations[session_id][-21:]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=history,
max_tokens=1000,
temperature=0.7,
stream=False,
)
ai_reply = response.choices[0].message.content
# 保存AI回复到历史
conversations[session_id].append({"role": "assistant", "content": ai_reply})
return jsonify({
"reply": ai_reply,
"tokens_used": response.usage.total_tokens
})
@app.route("/clear", methods=["POST"])
def clear():
"""清空对话历史"""
session_id = session.get("session_id", "default")
if session_id in conversations:
del conversations[session_id]
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(debug=True, port=5000)
前端:templates/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的AI助手</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family: system-ui, sans-serif; background:#1a1a2e; color:#e0e0e0; height:100vh; display:flex; flex-direction:column; }
#chat-box { flex:1; overflow-y:auto; padding:20px; display:flex; flex-direction:column; gap:12px; }
.msg { max-width:75%; padding:12px 16px; border-radius:12px; line-height:1.6; font-size:0.95rem; }
.user { background:#4f46e5; color:#fff; align-self:flex-end; border-radius:12px 12px 2px 12px; }
.ai { background:#1f2937; border:1px solid #374151; align-self:flex-start; border-radius:12px 12px 12px 2px; }
.ai.loading { color:#6b7280; font-style:italic; }
#input-row { display:flex; gap:8px; padding:16px; border-top:1px solid #2d2d44; background:#16213e; }
#input-row textarea { flex:1; background:#0f172a; border:1px solid #374151; color:#e0e0e0; padding:10px 14px; border-radius:10px; resize:none; font-size:0.95rem; font-family:inherit; outline:none; }
#input-row textarea:focus { border-color:#4f46e5; }
button { background:#4f46e5; color:#fff; border:none; padding:10px 20px; border-radius:10px; cursor:pointer; font-size:0.95rem; transition:background .2s; }
button:hover { background:#4338ca; }
#clear-btn { background:#374151; }
#clear-btn:hover { background:#4b5563; }
#token-info { font-size:0.75rem; color:#6b7280; padding:4px 16px; text-align:right; background:#16213e; }
</style>
</head>
<body>
<div id="chat-box">
<div class="msg ai">你好!我是你的AI助手,有什么可以帮你的?😊</div>
</div>
<div id="token-info"></div>
<div id="input-row">
<textarea id="user-input" rows="2" placeholder="输入消息…(Enter发送,Shift+Enter换行)"></textarea>
<button onclick="sendMsg()">发送</button>
<button id="clear-btn" onclick="clearChat()">清空</button>
</div>
<script>
const box = document.getElementById('chat-box');
const input = document.getElementById('user-input');
const tokenInfo = document.getElementById('token-info');
input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); }
});
function addMsg(text, role) {
const div = document.createElement('div');
div.className = `msg ${role}`;
div.textContent = text;
box.appendChild(div);
box.scrollTop = box.scrollHeight;
return div;
}
async function sendMsg() {
const text = input.value.trim();
if (!text) return;
input.value = '';
addMsg(text, 'user');
const loading = addMsg('AI正在思考…', 'ai loading');
try {
const res = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text })
});
const data = await res.json();
loading.className = 'msg ai';
loading.textContent = data.reply || data.error;
if (data.tokens_used) tokenInfo.textContent = `本次消耗 ${data.tokens_used} tokens`;
} catch(err) {
loading.textContent = '请求失败,请检查网络或API Key';
}
}
async function clearChat() {
await fetch('/clear', { method: 'POST' });
box.innerHTML = '<div class="msg ai">对话已清空,重新开始吧!</div>';
tokenInfo.textContent = '';
}
</script>
</body>
</html>
运行与部署
本地运行
# 安装依赖
pip install flask openai
# 设置环境变量
export OPENAI_API_KEY="sk-你的密钥"
# 启动服务
python app.py
# 浏览器打开 http://localhost:5000
部署到服务器
# requirements.txt
flask==3.0.0
openai==1.12.0
gunicorn==21.2.0
# 用 gunicorn 生产级启动
gunicorn -w 4 -b 0.0.0.0:8000 app:app
扩展方向
- 流式输出:设置
stream=True,用Server-Sent Events把文字逐字推送给前端,体验更好 - 持久化存储:用SQLite/PostgreSQL保存对话历史,实现跨会话记忆
- 多角色预设:提供下拉框切换"翻译助手""代码助手""写作助手"等System Prompt
- 接入RAG:结合上一篇RAG教程,让AI能回答你私有文档中的问题