使用Django-Channels实现websocket通信+大模型对话
前言
最近一直在做这个大模型项目,我选了 Django 作为框架(现在很多大模型应用都用的 FastAPI,不过我已经用习惯 Django 了)
之前使用 AspNetCore 作为后端的时候,我先后尝试了 Blazor Server,WebAPI SSE(Server Sent Event)等方案来实现大模型对话,目前好像 SSE 是用得比较多的,ChatGPT 也是用的这个。
自从进入 3.x 时代,Django 开始支持异步编程了,所以也能实现 SSE (不过我在测试中发现有点折腾),最终还是决定使用 WebSocket 来实现,Django 生态里的这套东西就是 channels 了。(话说 FastAPI 好像可以直接支持 SSE 和 WebSocket )
先看效果
放一张聊天界面
我还做了个对话历史页面
关于 django-channels
搬运一下官网的介绍,https://channels.readthedocs.io/en/latest/introduction.html
Channels wraps Django’s native asynchronous view support, allowing Django projects to handle not only HTTP, but protocols that require long-running connections too - WebSockets, MQTT, chatbots, amateur radio, and more.
OK,Channels 将 Django 从一个纯同步的 HTTP 框架扩展到可以处理异步协议如 WebSocket。原本 Django 是基于 WSGI 的,channels 使用基于 ASGI 的 daphne 服务器,而且不止能使用 WebSocket ,还能支持 HTTP/2 等新的技术。
几个相关的概念:
- Channels: 持久的连接,如 WebSocket,可用于实时数据传输。
- Consumers: 处理输入事件的异步功能,类似于 Django 的视图,但专为异步操作设计。
- Routing: 类似于 Django 的 URL 路由系统,Channels 使用 routing 来决定如何分发给定的 WebSocket 连接或消息到相应的 Consumer。
与传统的 Django 请求处理相比,Django Channels 允许开发者使用异步编程模式,这对于处理长时间运行的连接或需要大量并发连接的应用尤其有利。这种架构上的变化带来了更高的性能和更好的用户体验,使 Django 能够更好地适应现代互联网应用的需求。
通过引入 Channels, Django 不再只是一个请求/响应式的 Web 框架,而是变成了一个真正意义上能够处理多种网络协议和长时间连接的全功能框架。这使得 Django 开发者可以在不离开熟悉的环境的情况下,开发出更加丰富和动态的应用。
使用场景
先介绍下使用场景
这个 demo 项目的后端使用 StarAI 和 LangChain 调用 LLM 获取回答,然后通过 WebSocket 与前端通信,前端我选了 React + Tailwind
安装
以 DjangoStarter 项目为例(使用 pdm 作为包管理器)
pdm add channels[daphne]
然后修改 src/config/settings/components/common.py
把 daphne 添加到注册Apps里,注意要放在最前面
# 应用定义
INSTALLED_APPS: Tuple[str, ...] = (
'daphne',
)
之后使用 runserver
时,daphne 会代替 Django 内置的服务器运行
接着,channels 还需要一个 channel layer,这可以让多个消费者实例相互通信以及与 Django 的其他部分通信,这个 layer 可以选 Redis
pdm add channels_redis
配置
ASGI
OK,接下来得修改一下 src/config/asgi.py
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django_asgi_app = get_asgi_application()
from apps.chat.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
"http": django_asgi_app,
# Just HTTP for now. (We can add other protocols later.)
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
),
})
除了官网文档说的配置之外,我这里已经把聊天应用的路由加进来了
因为这个 demo 项目只有这一个 app 使用了 WebSocket ,所以这里直接把 chat 里的 routing 作为 root URLRouter
如果有多个 WebSocket app 可以按需修改
channel layer
修改 src/config/settings/components/channels.py
文件
from config.settings.components.common import DOCKER
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("redis" if DOCKER else "127.0.0.1", 6379)],
},
},
}
其他的配置就不赘述了,DjangoStarter 里都配置好了
编写后端代码
channels 的使用很简单,正如前面的介绍所说,我们只需要完成 consumer 的逻辑代码,然后配置一下 routing 就行。
那么开始吧
consumer
创建 src/apps/chat/consumers.py
文件
身份认证、使用大模型生成回复、聊天记录等功能都在这了
先上代码,等下介绍
import asyncio
import json
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import WebsocketConsumer, AsyncWebsocketConsumer
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from .models import Conversation, Message
class LlmConsumer(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
self.chat_id = None
self.conversation = None
@database_sync_to_async
def get_conversation(self, pk, user):
obj, _ = Conversation.objects.get_or_create(id=pk, user=user)
return obj
@database_sync_to_async
def add_message(self, role: str, content: str):
return Message.objects.create(
conversation=self.conversation,
role=role,
content=content,
)
async def connect(self):
self.chat_id = self.scope["url_route"]["kwargs"]["chat_id"]
await self.accept()
# 检查用户是否已登录
user = self.scope["user"]
if not user.is_authenticated:
await self.close(code=4001, reason='Authentication required. Please log in again.')
return
else:
self.conversation = await self.get_conversation(self.chat_id, user)
async def disconnect(self, close_code):
...
async def receive(self, text_data=None, bytes_data=None):
text_data_json: dict = json.loads(text_data)
history: list = text_data_json.get("history")
user = self.scope["user"]
if not user.is_authenticated:
reason = 'Authentication required. Please log in again.'
await self.send(text_data=json.dumps({"message": reason}))
await asyncio.sleep(0.1)
await self.close(code=4001, reason=reason)
return
await self.add_message(**history[-1])
llm = ChatOpenAI(model="gpt4")
resp = llm.stream(
[
*[
HumanMessage(content=e['content']) if e['role'] == 'user' else AIMessage(content=e['content'])
for e in history
],
]
)
message_chunks = []
for chunk in resp:
message_chunks.append(chunk.content)
await self.send(text_data=json.dumps({"message": ''.join(message_chunks)}))
await asyncio.sleep(0.1)
await self.add_message('ai', ''.join(message_chunks))
工作流程
- 前端发起ws连接之后,执行
connect
方法的代码,身份认证没问题的话就调用self.accept()
方法接受连接 - 接收到前端发来的信息,会自动执行
receive
方法,处理之后调用self.send
可以发送信息
身份认证
虽然在 asgi.py
里已经配置了 AuthMiddlewareStack
但还是需要在代码里自行处理认证逻辑
这个中间件的作用是把 header 里的 authorization 信息变成 consumer 里的 self.scope["user"]
要点
- consumer 有同步版本和异步版本,这里我使用了异步版本
AsyncWebsocketConsumer
- async consumer 里访问 Django ORM 需要使用
database_sync_to_async
装饰器 - 在
connect
里面,身份验证失败后调用self.close
关闭连接,里面的参数 code 不能和 HTTP Status Code 冲突,我一开始用的 401 ,结果前端接收时变成其他 code ,换成自定义的 4001 才可以接收到。但reason
参数是一直接收不到的,不知道为啥,可能跟浏览器的 WebSocket 实现有关? - receive 方法里,将大模型生成的内容使用流式输出发送给客户端时,一定要在 for 循环里加上
await asyncio.sleep
等待一段时间,这是为了留出时间让 WebSocket 发送消息给客户端,不然会变成全部生成完再一次性发给客户端,没有流式输出的效果。 - receive 方法里,接收到消息后先判断当前是否有登录(或者登录是否过期,表现和未登录一致),如果未登录则发送信息 "Authentication required. Please log in again." 给客户端,然后再关闭连接。
routing
写完了 consumer ,配置一下路由
编辑 src/apps/chat/routing.py
文件
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"ws/chat/demo/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
re_path(r"ws/chat/llm/(?P<chat_id>\w+)/$", consumers.LlmConsumer.as_asgi()),
]
这里注意只能使用 re_path
不能 path
(官方文档说的)
客户端开发
OK,后端部分到这就搞定了,接下来写一下客户端的代码
我选择了 React 来实现客户端
无关代码就不放了,直接把关键的代码贴上来
function App() {
const [prompt, setPrompt] = useState('')
const [messages, setMessages] = useState([])
const [connectState, setConnectState] = useState(3)
const [conversation, setConversation] = useState()
const chatSocket = useRef(null)
const messagesRef = useRef(null)
const reLoginModalRef = useRef(null)
React.useEffect(() => {
openWebSocket()
getConversation()
return () => {
chatSocket.current.close();
}
}, [])
React.useEffect(() => {
// 自动滚动到消息容器底部
messagesRef.current.scrollIntoView({behavior: 'smooth'})
}, [messages]);
const getConversation = () => {
// ... 省略获取聊天记录代码
}
const openWebSocket = () => {
setConnectState(0)
chatSocket.current = new WebSocket(`ws://${window.location.host}/ws/chat/llm/${ConversationId}/`)
chatSocket.current.onopen = function (e) {
console.log('WebSocket连接建立', e)
setConnectState(chatSocket.current.readyState)
};
chatSocket.current.onmessage = function (e) {
const data = JSON.parse(e.data)
setMessages(prevMessages => {
if (prevMessages[prevMessages.length - 1].role === 'ai')
return [
...prevMessages.slice(0, -1), {role: 'ai', content: data.message}
]
else
return [
...prevMessages, {role: 'ai', content: data.message}
]
})
};
chatSocket.current.onclose = function (e) {
console.error('WebSocket 链接断开。Chat socket closed unexpectedly.', e)
setConnectState(chatSocket.current.readyState)
if (e.code === 4001) {
// 显示重新登录对话框
new Modal(reLoginModalRef.current, options).show()
}
};
}
const sendMessage = () => {
if (prompt.length === 0) return
const newMessages = [...messages, {
role: 'user', content: prompt
}]
setMessages(newMessages)
setPrompt('')
chatSocket.current.send(JSON.stringify({
'history': newMessages
}))
}
}
前端代码没啥好说的,简简单单
我用了 messagesRef.current.scrollIntoView({behavior: 'smooth'})
来实现消息自动滑动到底部,之前用 Blazor 开发 AIHub 的时候好像是用了其他实现,我记得不是这个
不过这个方法挺丝滑的
WebSocket 地址直接写在这里面感觉没那么优雅,而且后面部署后得改成 wss://
也麻烦,得研究一下有没有更优雅的实现。
小结
这个只是简单的demo,实际上生产还得考虑很多问题,本文就是为 channels 的应用开了个头,后续有新的研究成果会持续更新博客~
使用Django-Channels实现websocket通信+大模型对话的更多相关文章
- Django使用channels实现Websocket连接
简述: 需求:消息实时推送消息以及通知功能,采用django-channels来实现websocket进行实时通讯.并使用docker.daphne启动通道,保持websocket后台运行 介绍Dja ...
- 实时 Django 终于来了 —— Django Channels 入门指南
Reference: http://www.oschina.net/translate/in_deep_with_django_channels_the_future_of_real_time_app ...
- Django Channels 入门指南
http://www.oschina.NET/translate/in_deep_with_django_channels_the_future_of_real_time_apps_in_django ...
- Django Channels 学习笔记
一.为什么要使用Channels 在Django中,默认使用的是HTTP通信,不过这种通信方式有个很大的缺陷,就是不能很好的支持实时通信.如果硬是要使用HTTP做实时通信的话只能在客户端进行轮询了,不 ...
- 通过Django Channels设计聊天机器人WEB框架
这两个月都在忙着设计针对银联客服业务的智能聊天机器人,上一周已经交完设计报告,这一周还和部门同事一起分享了系统设计及运行效果.因为时间的关系,系统原型我使用了Flask+jQuery的组合,感觉用以原 ...
- 【翻译】Django Channels 官方文档 -- Tutorial
Django Channels 官方文档 https://channels.readthedocs.io/en/latest/index.html 前言: 最近课程设计需要用到 WebSocket,而 ...
- 把酒言欢话聊天,基于Vue3.0+Tornado6.1+Redis发布订阅(pubsub)模式打造异步非阻塞(aioredis)实时(websocket)通信聊天系统
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_202 "表达欲"是人类成长史上的强大"源动力",恩格斯早就直截了当地指出,处在蒙昧时代即低 ...
- django + nginx + uwsgi + websocket
最近使用django框架做了一个简单的聊天机器人demo, 开发的过程中使用了django自带的websocket模块,当使用django框架自带的wsgi服务去启动的话,没有什么问题.如果要使用uw ...
- PowerDesigner 学习:十大模型及五大分类
个人认为PowerDesigner 最大的特点和优势就是1)提供了一整套的解决方案,面向了不同的人员提供不同的模型工具,比如有针对企业架构师的模型,有针对需求分析师的模型,有针对系统分析师和软件架构师 ...
- Django 1.10中文文档-模型参考
模型字段 本文档包含了Django提供的全部模型 Field 包括 字段选项 和 字段类型 的API参考. 参见 如果内建的字段不能满足你的需求, 你可以蚕食 django-localflavor ( ...
随机推荐
- 11-DNS域名解析服务
背景 我们都知道,用ip可以唯一标识互联网上的主机. 从前,互联网的主机非常的少.我们都可以记住每台Server的ip. 就像是大哥大时期,电话非常少,电话号码也就非常少,我们都能记住某个人的电话. ...
- FFmpeg开发笔记(三十二)利用RTMP协议构建电脑与手机的直播Demo
不管是传统互联网还是移动互联网,实时数据传输都是刚需,比如以QQ.微信为代表的即时通信工具,能够实时传输文本和图片.其中一对一的图文通信叫做私聊,多对多的图文通信叫做群聊. 除了常见的图文即时通信,还 ...
- nginx 如何利用gzip压缩配置来优化网站访问速度
前言: 最近公司设计的网站前端是基于nuxt架构的,部署到nginx上后,首页的访问以及二级页面的访问极慢,f12观察后发现主要是一些js页面加载极慢拉低了网站的访问速度,于是便想到利用nginx里的 ...
- 【基础推导】MPC控制器及其车辆模型详细推导 (附代码链接及详细推导说明)
0. 参考与前言 Python 代码:github AtsushiSakai/PythonRobotics C++ 代码:github jchengai/gpir/mpc_controller 相关参 ...
- 面试官:你了解git cherry-pick吗?
事情要从一次不规范的代码开发开始说起 背景故事 时间 2024年某个风平浪静的周五晚上 地点 中国,北京,西二旗,某互联网大厂会议室 人物 小杰,小A,小B,老K 对话 老K:昨天提交的代码被测试打回 ...
- vba--分拆工作薄
Sub 分拆工作薄() '分拆工作薄到当前文件夹 Dim sht As Worksheet Dim MyBook As Workbook Application.DisplayAlerts = Fal ...
- MyBatis学习篇
什么是MyBatis (1)Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动.创建连接.创建statement等繁杂 ...
- SQLServer 的Distinct
distinct去除重复的数据(distinct是对整个结果集进行数据重复处理,不是针对某一列) -> 检查返回不重复的数据(对于整条记录不重复才会去除,如ID不一样) 用法:select di ...
- FFmpeg开发笔记(三十九)给Visual Studio的C++工程集成FFmpeg
<FFmpeg开发实战:从零基础到短视频上线>一书的"第11章 FFmpeg的桌面开发"介绍了如何在Windows环境对Qt结合FFmpeg实现桌面程序,那么Win ...
- 使用.NET6实现动态API
ApiLite是基于.NET6直接将Service层生成动态api路由,可以不用添加Controller,支持模块插件化,在项目开发中能够提高工作效率,降低代码量. 开发环境 .NET SDK 6.0 ...