技術棧:vue3 + element-plus + axios + pinia + router + Django5 + websocket + 訊飛星火API
本文將實現一個 AI 聊天對話功能,將前端用戶輸入問題以及之前對話發送給后端,通過 api 訪問大模型,返回前端實時對話數據。
調用?訊飛星火API 大家可以看這篇(文A):創作中心-CSDN
前端 vue3 +后端 Django5 連接可以看這篇(文B):【玩轉全棧】—— Django 連接 vue3 保姆級教程,前后端分離式項目2025年4月最新!!!_django vue3 前后端分離-CSDN博客
Django5 配置 websocket(文C):【全棧開發】---- 一文掌握 Websocket 原理,并用 Django 框架實現_django websocket-CSDN博客
【玩轉全棧】---- Django 基于 Websocket 實現群聊(解決channel連接不了)_django websocket聊天室-CSDN博客
目錄
效果預覽:
前期準備
代碼實現
后端
前端
資源獲取
效果預覽:
Django連接vue3,接入ai
前期準備
文A 已講解如何在 Django 調用免費的訊飛星火API 。
文B 已講解如何連接前端 vue3 、后端 Django5,配置 vite.config 文件代理,后端解決跨域等等。還有如何在前端獲取 token ,并在前端發送 Post 請求時以攜帶該 token 以越過安全驗證,使得后端 Django 能接收到數據,這里不過多贅述,結尾也有相關資源可以下載。
文C 以講解如何在 Django 中配置 websocket 環境,以及如何實現聊天室功能。
沒實現的可以先回去實現。
代碼實現
后端
Django 配置好 websocket? ,定義 AI 消費者及其路徑:
routings.py:
from django.urls import re_path
from . import consumerswebsocket_urlpatterns = [re_path(r'ws/chat/', consumers.ChatConsumer.as_asgi()),
]
consumers.py:
import json
import asyncio
import httpx
from channels.generic.websocket import AsyncWebsocketConsumer
import reclass ChatConsumer(AsyncWebsocketConsumer):async def connect(self):print("AI消費者已連接")await self.accept()async def disconnect(self, close_code):print("AI消費者已斷開")passasync def receive(self, text_data):print("獲取到的text_data:", text_data)try:text_data_json = json.loads(text_data)print("text_data_json:",text_data_json)if not text_data_json:await self.send(text_data=json.dumps({'error': '問題不能為空',}))returntry:async for chunk in self.call_spark_ai(text_data_json):if chunk == '[DONE]':await self.send(text_data=json.dumps({'done': True,}))else:# 將 AI 的答復推送給前端await self.send(text_data=json.dumps({'message': chunk,}))await asyncio.sleep(0.1) # 增加延遲,降低推送頻率except Exception as e:await self.send(text_data=json.dumps({'error': f'調用 AI 接口失敗: {str(e)}',}))except json.JSONDecodeError:await self.send(text_data=json.dumps({'error': '無效的 JSON 數據',}))async def call_spark_ai(self, question):print("已調用 Spark_ai函數")# 訊飛星火 AI API 的 URL 和認證信息url = "https://spark-api-open.xf-yun.com/v1/chat/completions"headers = {"Authorization": "Bearer 你的密鑰","Content-Type": "application/json",}data = {"max_tokens": 4096,"top_k": 4,"temperature": 0.5,"messages": question,"model": "4.0Ultra","stream": True,}async with httpx.AsyncClient() as client:async with client.stream("POST", url, headers=headers, json=data) as response:if response.status_code != 200:raise Exception(f"AI 接口返回錯誤: {response.status_code} {await response.text()}")# 定義正則表達式pattern = r'"content":"(.*?)"'async for line in response.aiter_lines():print(line.strip())if line.strip():try:# 使用正則表達式提取 contentmatch = re.search(pattern, line)if match:content = match.group(1)print("content:", content)if content:yield content # 只推送 content 部分except Exception as e:print(f"處理消息時出錯: {e}")continue
消費者涉及到的內容比較多,下面我將一一解釋:
首先注意!由于連的 websocket ,需要頻繁地接收客戶端發送的消息、向客戶端發送消息并保持連接狀態。這些操作本質上是 I/O 密集型任務,涉及到網絡請求和響應。如果使用同步代碼來處理這些任務,線程會阻塞,導致性能瓶頸。而異步代碼可以高效地處理大量并發連接,避免線程阻塞。
text_data_json 獲取到前端的對話數據,并添加空數據判斷。
定義?call_spark_ai() 函數,傳入參數是對話列表,通過調用訊飛星火 API ,得到流式數據,通過正則獲取到 content 數據,通過 yield 并發式返回。
然后在消費者中異步使用該函數,將返回值返回給前端。
記得在?headers 中添加自己的密鑰。
前端
新建一個 Ai_store 用于存儲對話數據:
// stores/Ai_store.js
import { defineStore } from 'pinia';export const useAiStore = defineStore('ai', {state: () => ({messages: [],}),actions: {// 添加對話addMessage(role, content) {this.messages.push({ "role":role, "content":content });},// 清空對話列表clearMessages() {this.messages = [];},},
});
定義了一個 messages 用于存儲對話,addMessage()?添加對話對話和內容,clearMessage() 使messages 清空,即新建對話。
導入、初始化 pinia ,并定義一些變量:
import { useAiStore } from '../stores/Ai_store';// 初始化 pinia store
const Ai_store = useAiStore();// 定義消息類型
type Message = {role: string;content: string;
};
const messages = ref<Message[]>([]); // 存儲當前一輪對話(用戶提問和 AI 回答)
const question = ref(''); // 用戶輸入的問題
const aiResponse = ref<string[]>([]); // AI 的響應數據
let csrfToken: string | null = null; // CSRF Token
let socket: WebSocket | null = null; // WebSocket 連接(全局變量)
let currentAiResponse = ''; // 當前問題的實時回復內容
初始化 websocket 連接:
function initWebSocket() {if (socket) {socket.close(); // 關閉之前的連接}socket = new WebSocket(`ws://localhost:8080/ws/chat/`);// 監聽 WebSocket 打開事件socket.onopen = () => {console.log("WebSocket connection opened");};// 監聽 WebSocket 消息事件socket.onmessage = (event: MessageEvent) => {console.log("到達websocket消息事件");const data = JSON.parse(event.data);console.log("data:", data);if (data.message) {// 將消息添加到當前響應中currentAiResponse += data.message;// 更新AI回復內容if (messages.value.length > 0) {messages.value[messages.value.length - 1].content = currentAiResponse;}} else if (data.error) {console.error("Error from backend:", data.error);}};// 監聽 WebSocket 關閉事件socket.onclose = () => {console.log("WebSocket connection closed");setTimeout(initWebSocket, 5000); // 自動重連,間隔 5 秒};}
socket 路徑?ws://localhost:8080/ws/chat/ 要和后端對應起來,保證連接順利。
onmessage 接受后端返回的消息流,將 message.data 動態加入到?currentAiResponse ,currentAiResponse 動態更新消息。
發送消息:
async function sendQuestion() {// csrfToken驗證if (!csrfToken) {console.error("CSRF Token is not available");return;}if (!question.value.trim()) {alert("請輸入有效的問題!");return;}try {// 如果有上一輪對話,將其存入 Ai_storeif (messages.value.length > 0) {console.log("messages:", messages);messages.value.forEach(msg => {Ai_store.addMessage(msg.role, msg.content);});}// 清空 messages 并存儲新的用戶問題messages.value = [];messages.value.push({ role: 'user', content: question.value });// 清空 AI 的響應數據和完整字符串currentAiResponse = "";// 通過 WebSocket 發送問題const join_messages = ref<Message[]>([]);join_messages.value = [...Ai_store.messages];join_messages.value.push({ role: "user", content: question.value })console.log("join_messages:", join_messages)const message = JSON.stringify(join_messages.value);if (socket) {socket.send(message);}console.log("Sent question to WebSocket:", message);// 清空問題輸入框question.value = '';// 初始化 AI 回復占位符messages.value.push({ role: 'system', content: '' });} catch (error) {console.error("Error sending question:", error.response?.data || error.message);}}
如何 messages 中有對話數據,則添加至 pinia 中,當作歷史對話數據,以在頁面上展示之前對話數據,通過?join_messages 構造歷史對話數據和當前對話數據,即當前對話中所有對話數據,然后傳給后端,后端解析后,傳給 訊飛星火,如此形成循環。
組件生命周期:
onMounted(() => {fetchCsrfToken();initWebSocket();});onUnmounted(() => {if (socket) {socket.close();}});
組件掛載則初始化,卸載則斷開 socket 連接。
新建對話:
// 新建對話函數function newConversation() {try {// 清空數據Ai_store.clearMessages();messages.value = [];currentAiResponse = '';// 清空用戶輸入框question.value = '';console.log("新建對話:所有數據已清空");} catch (error) {console.error("Error creating new conversation:", error.message);}}
用戶點擊按鈕調用此函數,所有數據清空,重新開始對話。
計算 html :
// 計算屬性:實時拼接并格式化對話記錄const formattedResponse = computed(() => {// 合并歷史記錄和當前問題的實時回復const allMessages = [...Ai_store.messages, ...messages.value];// 格式化消息const formattedMessages = allMessages.map(msg => {return `<strong style="color: ${msg.role === 'user' ? 'blue' : 'green'};">${msg.role === 'user' ? '您:' : 'AI:'}</strong><br>${String(msg.content || '').replace(/(\\n)+/g, '<br>').replace(/\t+/g, ' ')}`;});// 拼接最終的HTML字符串return formattedMessages.join('<br><br>');});
合并所有對話數據,制造格式化消息,返回給頁面,用于展示。
頁面:
<el-drawerv-model="drawerVisible"direction="ltr":modal="true":close-on-click-modal="true"custom-class="custom-drawer":with-header="false"><div class="drawer-content"><div class="header-not"><h1>AI 對話界面</h1><button @click="newConversation">新建對話</button></div><!-- 輸入框 --><div class="fixed-container"><textarea v-model="question" placeholder="請輸入問題"></textarea><button @click="sendQuestion" class="send_button">發送問題</button></div><!-- 對話記錄 --><div class="response" v-html="formattedResponse"></div></div></el-drawer>
資源獲取
本次分享結束,源碼也已放入資源:
https://download.csdn.net/download/2403_83182682/90626683
感謝您的觀看!!!