技术栈: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 消费者及其路径:
python
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/', consumers.ChatConsumer.as_asgi()),
]
python
import json
import asyncio
import httpx
from channels.generic.websocket import AsyncWebsocketConsumer
import re
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
print("AI消费者已连接")
await self.accept()
async def disconnect(self, close_code):
print("AI消费者已断开")
pass
async 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': '问题不能为空',
}))
return
try:
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:
# 使用正则表达式提取 content
match = 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用于存储对话数据:
python
// 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 ,**并定义一些变量:
python
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连接:
python
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动态更新消息。
发送消息:
python
async function sendQuestion() {
// csrfToken验证
if (!csrfToken) {
console.error("CSRF Token is not available");
return;
}
if (!question.value.trim()) {
alert("请输入有效的问题!");
return;
}
try {
// 如果有上一轮对话,将其存入 Ai_store
if (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 构造历史对话数据和当前对话数据,即当前对话中所有对话数据,然后传给后端,后端解析后,传给 讯飞星火,如此形成循环。
组件生命周期:
javascript
onMounted(() => {
fetchCsrfToken();
initWebSocket();
});
onUnmounted(() => {
if (socket) {
socket.close();
}
});
组件挂载则初始化,卸载则断开 socket 连接。
新建对话:
javascript
// 新建对话函数
function newConversation() {
try {
// 清空数据
Ai_store.clearMessages();
messages.value = [];
currentAiResponse = '';
// 清空用户输入框
question.value = '';
console.log("新建对话:所有数据已清空");
} catch (error) {
console.error("Error creating new conversation:", error.message);
}
}
用户点击按钮调用此函数,所有数据清空,重新开始对话。
计算 html :
javascript
// 计算属性:实时拼接并格式化对话记录
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>');
});
合并所有对话数据,制造格式化消息,返回给页面,用于展示。
页面:
javascript
<el-drawer
v-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
感谢您的观看!!!