Python Flask项目方式接入阿里云通义AI大模型API 实现一个简单的AI聊天Web项目(流式传输+多轮对话+会话记录+代码高亮)----- noob学生
效果图
流式输出就不展示了
准备工作
前往阿里云大模型服务平台使用自己账号开通大模型服务平台百炼
地址:大模型服务平台_通义大模型_自然语言处理_达摩院-阿里云 (aliyun.com)
1.进入自己的控制台--模型广场--通义千问--找到自己要使用的模型 我这里使用通义千问Max
一般是有免费额度可以使用的 后期可以自己买额度 很便宜
然后到我的应用 创建好自己的应用 选择相应的模型 这时我们就哟APIkey和 自己的appid了
2.在自己电脑安装好Redis (用于存储聊天缓存) 如果使用服务器就在服务器安装好并运行就行
3.安装好mysql数据库 推荐使用5.7版本
实施
创建一个Python Flask项目并创建好虚拟环境解释器使用python3.8 项目结构如下图所示
key.py及其下方文件为我个人测试部署时使用 可以忽略
app.py
运行项目的文件 也就是后端
# -*- coding: utf-8 -*-
from flask import Flask, request, jsonify, Response, render_template, session
from flask_session import Session
from dashscope import Generation
from http import HTTPStatus
from flask_cors import CORS
import redis
import json
from database import save_conversation
app = Flask(__name__)
CORS(app)
#可以自己生成一串
app.secret_key = '3ddd8f0da764cb34850e1da48d03da24'
# 配置 Redis
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.Redis(host='localhost', port=6379)
app.config['SESSION_PERMANENT'] = False
app.config['SESSION_USE_SIGNER'] = True
Session(app)
@app.route('/ChatView')
def index():
return render_template('index.html')
@app.route('/chat', methods=['POST'])
def chat():
user_message = request.json.get('message', '')
if not user_message:
return jsonify({'error': '未提供消息'}), HTTPStatus.BAD_REQUEST
# 从会话中加载消息或初始化一个新列表 可以自己自定义角色 修改'你是一个ai助手'内容就行
messages = session.get('messages', [{'role': 'system', 'content': '你是一个ai助手'}])
# 将用户的消息添加到列表中
messages.append({'role': 'user', 'content': user_message})
session['messages'] = messages # 在添加用户消息后立即保存
def generate():
responses = Generation.call(
"qwen-max",
app_id='自己的应用id',
api_key='自己的api key',
messages=messages,
result_format='message',
stream=True,
incremental_output=True
)
buffer = ''
for response in responses:
if response.status_code == HTTPStatus.OK:
content = response.output.choices[0]['message']['content'].strip()
print(content)
buffer += content
yield f"{content}"
else:
yield f"Error: {response.message}\n\n"
break
return Response(generate(), mimetype='text/event-stream')
@app.route('/update', methods=['POST'])
def update():
try:
data = request.json
bot_message = data.get('bot_message', '')
user_message = data.get('user_message', '')
conversation_id = data.get('conversation_id', '')
if not bot_message or not user_message or not conversation_id:
app.logger.error('Missing bot_message, user_message, or conversation_id')
return jsonify({'error': '未提供消息或对话ID'}), HTTPStatus.BAD_REQUEST
messages = session.get('messages', [])
messages.append({'role': 'assistant', 'content': bot_message})
session['messages'] = messages
save_conversation(conversation_id, user_message, bot_message) # 保存对话
return jsonify({'status': 'updated'}), HTTPStatus.OK
except Exception as e:
app.logger.error(f"Error in /update: {str(e)}")
return jsonify({'error': str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
@app.route('/clear', methods=['POST'])
def clear():
session.pop('messages', None)
return jsonify({'status': 'cleared'}), HTTPStatus.OK
if __name__ == '__main__':
app.run(debug=True)
# 这里是自定义本地运行的端口号和ip地址
# app.run(host='0.0.0.0', port=8080, debug=True)
database.py
指向数据库操作 保存记录的后端文件 需要修改为自己的数据库账户和密码
# -*- coding: utf-8 -*-
import mysql.connector
def get_db_connection():
return mysql.connector.connect(
host="localhost",
user="用户名",
password="密码",
database="数据库名"
)
def save_conversation(conversation_id, user_message, bot_message):
try:
connection = get_db_connection()
cursor = connection.cursor()
# 检查对话是否存在
conversation_query = "SELECT 1 FROM conversations WHERE conversation_id = %s"
cursor.execute(conversation_query, (conversation_id,))
conversation_exists = cursor.fetchone()
if not conversation_exists:
# 插入对话记录
conversation_query = """
INSERT INTO conversations (conversation_id)
VALUES (%s)
"""
cursor.execute(conversation_query, (conversation_id,))
# 插入聊天记录
chat_record_query = """
INSERT INTO chat_records (conversation_id, user_message, bot_message)
VALUES (%s, %s, %s)
"""
cursor.execute(chat_record_query, (conversation_id, user_message, bot_message))
connection.commit()
except mysql.connector.Error as err:
print(f"Error: {err}")
raise
finally:
if connection.is_connected():
cursor.close()
connection.close()
数据库结构 sql语句
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for chat_records -- ---------------------------- DROP TABLE IF EXISTS `chat_records`; CREATE TABLE `chat_records` ( `id` int(11) NOT NULL AUTO_INCREMENT, `conversation_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `user_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, `bot_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `conversation_id`(`conversation_id`) USING BTREE, CONSTRAINT `chat_records_ibfk_1` FOREIGN KEY (`conversation_id`) REFERENCES `conversations` (`conversation_id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for conversations -- ---------------------------- DROP TABLE IF EXISTS `conversations`; CREATE TABLE `conversations` ( `id` int(11) NOT NULL AUTO_INCREMENT, `conversation_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `conversation_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) USING BTREE, INDEX `conversation_id`(`conversation_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
前端页面和样式
/templates/index.html
头像图片可以自定义 修改就行
顶顶顶顶顶
Loading...
正在加载...请稍候
👻Tongyi`Ai By Lwh
新对话
你好!有什么可以帮你的吗
/static/css/styles.css
.message {
white-space: pre-wrap;
word-wrap: break-word;
max-width: 100%;
}
.bot-message-container {
display: flex;
align-items: flex-start;
}
.bot-message-container img {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
border: rgb(125, 125, 242) 3px solid;
}
pre {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
code {
font-family: 'Consolas', 'Courier New', monospace;
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
/* 隐藏默认的滚动条样式 */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
}
code::-webkit-scrollbar {
display: none;
/* Chrome, Safari, and Opera */
}
/* for block of numbers */
.hljs-ln-numbers {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
text-align: center;
color: #ccc;
border-right: 2px dashed #8f0feb;
vertical-align: top;
}
.hljs-ln td {
padding-right: 10px;
padding-left: 10px;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 1s ease-out;
}
.copy {
/* button */
--button-bg: #353434;
--button-hover-bg: #464646;
--button-text-color: #CCCCCC;
--button-hover-text-color: #8bb9fe;
--button-border-radius: 10px;
--button-diameter: 36px;
--button-outline-width: 1px;
--button-outline-color: rgb(141, 141, 141);
/* tooltip */
--tooltip-bg: #171212;
--toolptip-border-radius: 4px;
--tooltip-font-size:8px;
--tooltip-transition-duration: 0.3s;
--tootip-text-color: rgb(255, 255, 255);
--tooltip-padding-x: 5px;
--tooltip-padding-y: 5px;
--tooltip-offset: 8px;
font-weight: bold;
font-family: 'YouYuan', sans-serif;
}
.copy {
box-sizing: border-box;
width: var(--button-diameter);
height: var(--button-diameter);
border-radius: var(--button-border-radius);
background-color: var(--button-bg);
color: var(--button-text-color);
border: none;
cursor: pointer;
position: relative;
outline: none;
}
.tooltip {
position: absolute;
opacity: 0;
visibility: 0;
top: 0;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
font: var(--tooltip-font-size) var(--tooltip-font-family);
color: var(--tootip-text-color);
background: var(--tooltip-bg);
padding: var(--tooltip-padding-y) var(--tooltip-padding-x);
border-radius: var(--toolptip-border-radius);
pointer-events: none;
transition: all var(--tooltip-transition-duration) cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.tooltip::before {
content: attr(data-text-initial);
}
.tooltip::after {
content: "";
position: absolute;
bottom: calc(var(--tooltip-padding-y) / 2 * -1);
width: var(--tooltip-padding-y);
height: var(--tooltip-padding-y);
background: inherit;
left: 50%;
transform: translateX(-50%) rotate(45deg);
z-index: -999;
pointer-events: none;
}
.copy svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.checkmark {
display: none;
}
/* actions */
.copy:hover .tooltip,
.copy:focus:not(:focus-visible) .tooltip {
opacity: 1;
visibility: visible;
top: calc((100% + var(--tooltip-offset)) * -1);
}
.copy:focus:not(:focus-visible) .tooltip::before {
content: attr(data-text-end);
}
.copy:focus:not(:focus-visible) .clipboard {
display: none;
}
.copy:focus:not(:focus-visible) .checkmark {
display: block;
}
.copy:hover,
.copy:focus {
background-color: var(--button-hover-bg);
}
.copy:active {
outline: var(--button-outline-width) solid var(--button-outline-color);
}
.copy:hover svg {
color: var(--button-hover-text-color);
}
@media screen and (max-width: 600px) {
pre {
white-space: pre-wrap;
/* 换行 */
word-wrap: break-word;
/* 防止超出屏幕宽度 */
}
.message.bot {
max-width: 100%;
/* 确保在小屏幕设备上不超出屏幕宽度 */
}
}
/* Loader styles */
.loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loader-text {
font-size: 24px;
color: rgb(0, 0, 0);
margin-bottom: 20px;
align-self: center;
}
.loader-bar {
width: 10%;
height: 10px;
border-radius: 5px;
background-color: rgb(0, 0, 0);
animation: loader-bar-animation 2s ease-in-out infinite;
}
@keyframes loader-bar-animation {
0% {
/* transform: translateX(-100%) rotate(270deg); */
transform: translateX(-100%);
}
50% {
/* transform: translateX(100%) rotate(-90deg); */
transform: translateX(100%);
}
100% {
/* transform: translateX(-100%) rotate(270deg); */
transform: translateX(-100%);
}
}
/static/js/lwhapp.js
这部分很重要!!!
可以修改每次对话可以对话几条 建议20以内
document.addEventListener("DOMContentLoaded", function() {
hljs.highlightAll();
hljs.initLineNumbersOnLoad();
const chatBox = document.getElementById("chat-box");
const userInput = document.getElementById("user-input");
const maxMessages = 10; // 定义最大消息数
let messageCount = 0;
userInput.addEventListener("keydown", function (event) {
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
});
document.getElementById("sendButton").addEventListener("click", sendMessage);
document.querySelector("button[onclick='clearMessages()']").addEventListener("click", clearMessages);
function sendMessage() {
if (messageCount >= maxMessages) {
disableInput();
return;
}
const message = userInput.value;
if (message.trim() === "") return;
addMessage("user", message, false);
const conversationId = localStorage.getItem("conversation_id") || generateConversationId();
saveMessage("user", message);
userInput.value = "";
messageCount++;
if (messageCount >= maxMessages) {
disableInput();
}
fetch("/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message }),
})
.then((response) => {
if (!response.ok) {
throw new Error("网络回复未完成");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let botMessageDiv = addMessage("bot", "", true); // 用于显示机器人的回复
function readStream() {
reader.read().then(({ done, value }) => {
if (done) {
const botResponse = buffer.trim();
saveMessage("bot", botResponse);
// 向后端发送完整的回复以更新会话历史
fetch("/update", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ conversation_id: conversationId, user_message: message, bot_message: botResponse }),
}).then(response => {
if (!response.ok) {
console.error('Error updating conversation:', response.statusText);
}
}).catch(error => {
console.error('Fetch error:', error);
});
return;
}
const text = decoder.decode(value, { stream: true }).trim();
buffer += text;
addTypingEffect(botMessageDiv, buffer);
applyStyles(botMessageDiv);
readStream();
});
}
readStream();
})
.catch((error) => {
console.error("Error:", error);
});
}
function generateConversationId() {
const conversationId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
localStorage.setItem("conversation_id", conversationId);
return conversationId;
}
function addMessage(role, content, isTypingEffect = false) {
let messageDiv;
if (role === "bot") {
const containerDiv = document.createElement("div");
containerDiv.classList.add("bot-message-container", "fade-in");
const avatar = document.createElement("img");
avatar.src = "static/images/Lwh.jpeg";
avatar.alt = "Lwh";
containerDiv.appendChild(avatar);
messageDiv = document.createElement("div");
messageDiv.classList.add("message", role, "font-medium", "p-3", "rounded-lg", "max-w-lg", "text-sm", "self-start", "bg-green-500", "dark:bg-zinc-500", "text-white");
containerDiv.appendChild(messageDiv);
chatBox.appendChild(containerDiv);
} else {
messageDiv = document.createElement("div");
messageDiv.classList.add("message", role, "p-3", "rounded-lg", "max-w-lg", "text-sm", "self-end", "bg-blue-500", "text-white");
chatBox.appendChild(messageDiv);
}
if (isTypingEffect) {
return messageDiv;
}
const codeRegex = /```([\s\S]*?)```/g;
if (codeRegex.test(content)) {
const parts = content.split(codeRegex);
parts.forEach((part, index) => {
if (index % 2 === 0) {
const textNode = document.createTextNode(part);
messageDiv.appendChild(textNode);
} else {
const pre = document.createElement("pre");
const code = document.createElement("code");
const lang = part.split("\n")[0].trim();
const codeContent = part.split("\n").slice(1).join("\n");
const codeHeader = document.createElement("div");
codeHeader.classList.add(
"flex",
"justify-between",
"items-center",
"bg-gray-700",
"text-white",
"font-mono",
"px-2",
"py-1",
"text-base",
"rounded-t-lg"
);
codeHeader.innerHTML = `${lang}
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!




