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}
文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。