ChatGLM-6B之SSE通信(Server-sent Events)

news2024/11/28 4:42:35

写这篇博客还是很激动开心的,因为是我经过两周的时间,查阅各个地方的资料,经过不断的代码修改,不断的上传到有显卡的服务器运行才得出的可行的接口调用解决方案,在这里记录并分享一下。

研究历程(只是感受,这段可以跳过,直接看下边的正题,找“正题”二字)

起初领导让我写一个接口——前端传递用户问题,后端返回ChatGLM模型生成的问题的答案。这个工作太简单了,因为GitHub上ChatGLM-6B根目录的api.py已经实现了,我只需改一个模型路径、端口号启动即可,我默默地更新了代码然后修改后启动运行了,然后摸了三天鱼,三天后和领导说完成了,深藏功与名。领导高兴地拿着我的接口文档就给其他部门的同事用了,结果没几天,同事就反馈说,这接口是http请求啊,前端一请求,后端带着问题去送入模型,这模型生成还需要时间,等完全生成了,服务端再返给前端,这期间用户一直等待,还没等返回结果,用户早生气的买套壳ChatGPT公司的服务了,谁还用你的ChatGLM?我当然知道接口慢了,而且返回时间和生成的文本长度成正比,这怎么办?用websocket?双向通信?这接口是python写的,我再研究一下python的websocket怎么写?当初干java一看websocket的代码就劝退——又臭又长,导致我现在都不会ws,所以我现学一下吗?不,不可能,我对ws过敏,我查了查ChatGPT是如何实现的,网上说是用SSE(Server-sent Events)实现的,我还问了一下ChatGPT,结果他嘴硬,说没有。。。
在这里插入图片描述
无语~,我用postman调了一下ChatGPT的api,发现返回的数据德行如下:

data: {"id":"cmpl-7Jy6ml72P6prTUU5mmtMGUsWBOgNY","object":"text_completion","created":1684993644,"choices":[{"text":"新","index":0,"logprobs":null,"finish_reason":null}],"model":"text-davinci-003"}
data: {"id":"cmpl-7Jy6ml72P6prTUU5mmtMGUsWBOgNY","object":"text_completion","created":1684993644,"choices":[{"text":"能","index":0,"logprobs":null,"finish_reason":null}],"model":"text-davinci-003"}
data: {"id":"cmpl-7Jy6ml72P6prTUU5mmtMGUsWBOgNY","object":"text_completion","created":1684993644,"choices":[{"text":"源","index":0,"logprobs":null,"finish_reason":null}],"model":"text-davinci-003"}
...<省略若干数据>
data: {"id":"cmpl-7Jy6ml72P6prTUU5mmtMGUsWBOgNY","object":"text_completion","created":1684993644,"choices":[{"text":"节","index":0,"logprobs":null,"finish_reason":null}],"model":"text-davinci-003"}
data: {"id":"cmpl-7Jy6ml72P6prTUU5mmtMGUsWBOgNY","object":"text_completion","created":1684993644,"choices":[{"text":"能","index":0,"logprobs":null,"finish_reason":null}],"model":"text-davinci-003"}
data: {"id":"cmpl-7Jy6ml72P6prTUU5mmtMGUsWBOgNY","object":"text_completion","created":1684993644,"choices":[{"text":"材","index":0,"logprobs":null,"finish_reason":null}],"model":"text-davinci-003"}
data: {"id":"cmpl-7Jy6ml72P6prTUU5mmtMGUsWBOgNY","object":"text_completion","created":1684993644,"choices":[{"text":"料","index":0,"logprobs":null,"finish_reason":null}],"model":"text-davinci-003"}
data: {"id":"cmpl-7Jy6ml72P6prTUU5mmtMGUsWBOgNY","object":"text_completion","created":1684993644,"choices":[{"text":"等","index":0,"logprobs":null,"finish_reason":"length"}],"model":"text-davinci-003"}
data: [DONE]

不用问,有用的字段就是text,猜也能猜出是前端拼接的这个字段的数据,组成一句话然后渲染。这里除了text字段,大家还要注意一下最后一行,data: [DONE],这个应该是要告诉前端,后端已经生成完毕,至于怎么用,前端小姐姐可能清楚。

那这个是不是SSE通信呢?不急,我们来小小写点SSE通信接口代码玩玩。

  • 服务端sse_test.py
import asyncio
import uvicorn, json, datetime
import uvicorn
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from sse_starlette.sse import ServerSentEvent, EventSourceResponse


app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=False,
    allow_methods=["*"],
    allow_headers=["*"]
)


@app.get('/stream')
async def stream():
    def generator():
        for char in '李总是个大帅逼':
            yield char

    async def event_generator():
        for i in generator():
            now = datetime.datetime.now()
            time = now.strftime("%Y-%m-%d %H:%M:%S")
            yield {"data": {"data": i, "history": [], "finished": False, "time": time}}
            await asyncio.sleep(1)
        yield {"data": '[DONE]'}

    return EventSourceResponse(event_generator())


if __name__ == '__main__':
    uvicorn.run('sse_test:app', reload=True)

上述服务启动后,是可以直接用postman调用的,访问[get]http://localhost:8000/stream即可,结果如下:

data: {'data': '李', 'history': [], 'finished': False, 'time': '2023-05-26 15:35:14'}
data: {'data': '总', 'history': [], 'finished': False, 'time': '2023-05-26 15:35:15'}
data: {'data': '是', 'history': [], 'finished': False, 'time': '2023-05-26 15:35:16'}
data: {'data': '个', 'history': [], 'finished': False, 'time': '2023-05-26 15:35:17'}
data: {'data': '大', 'history': [], 'finished': False, 'time': '2023-05-26 15:35:18'}
data: {'data': '帅', 'history': [], 'finished': False, 'time': '2023-05-26 15:35:19'}
data: {'data': '逼', 'history': [], 'finished': False, 'time': '2023-05-26 15:35:20'}
data: [DONE]

看见没?“李总是个大帅逼!”不对没有叹号,不对李总不是大帅逼,不对,这不是重点,重点是看返回结构,是不是和ChatGPT返回的很像?我在代码里贴心的写下了yield {"data": '[DONE]'},返回结果还把[DONE]的引号去了。(哈哈,ChatGPT就是嘴硬,之前问他有没有用到知识图谱,他说用到了,过两三个月再问,他说没用到。。。再看ChatGLM代码,模型和接口突出一个清晰明了,哪有什么知识图谱?开箱即用)。

这里大家可能有疑问,postman请求接口后好像不是及时返回,还是后端一句话生成好返回的,postman还是等待了,没错,你没错,这个是postman的问题,有诗为证,不,有前端代码为证,我写了一点小前端来验证一下,如下:

  • 客户端SSE Client.html
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<title>SSE Client</title>
	</head>
	<body>
		<h1>Receive: <span id="sse"></span></h1>
		<script>
			const numberElement = document.getElementById("sse");
			const source = new EventSource('http://localhost:8000/stream');

			source.onmessage = (event) => {
				numberElement.innerText = event.data;
			};

			source.onerror = (error) => {
				console.error("SSE error:", error);
			};
		</script>
	</body>
</html>

直接双击Chrome打开,自己看看屏幕上写着什么?“李总是个大帅逼!”不对没有叹号,不对李总不是大帅逼,不对,这不是重点,是不是像屏闪动画一样[doge]?
在这里插入图片描述

哈哈哈,闲话不多说,我们进入正题~

正题

1.SSE(Server-sent Events)

在这里插入图片描述
SSE的概念网上一大堆,不想复制粘贴,把ChatGPT的回答放在这里,重点标好了,我用大白话挑重点再说一遍:

  1. SSE是基于HTTP的,所以我们可以用http的方式去和服务端建立通信,这样少了一些学习成本(点名websocket java端代码又臭又长!)。
  2. 它是单向通信:即客户端向服务器建立连接后,服务器持续向客户端疯狂输出,(类似:李雷:“我爱你”,韩梅梅:“我爱你我爱你我爱你。。。[DONE]-_-!!!”);
    这个和websorket不同,websorket是双向通信,(类似:李雷:“我爱你”,韩梅梅:“我爱你”,李雷:“我爱你”,韩梅梅:“我爱你”,[forever~]”)
  3. SSE返回的是事件流类型,事件流中包含标识符、类型、数据、注释,这些都是可选字段,上述案例中的事件流中只有数据,即data,完整的事件流示例如下:
id: 12345 											# 标识符
event: update 										# 类型(值可以随便定义,想写什么写什么)
data: {"message": "Hello, SSE!"} 					# 数据(数据建议为json格式)

: This is a comment									#注释(就是冒号开头)

2.ChatGLM的流式方法

(只描述探讨过程,查阅代码请移步ChatGLM-6B)
大家如果看过ChatGLM的api.py文件,会发现这个http接口中调用的是model.chat(),然后直接将生成的数据组成json返回给前端了。

这明显不是流式输出(起码和我刚才写的那段代码结构不像)。

然后我们再看下web_demo.py文件,这个用过ChatGLM的同学应该熟悉,官方提供的前端交互页面就是这个模块中的,其中用到的技术是Gradio(Gradio是什么东西我没细研究过,我个人认为是个和JSP差不多的视图层技术),重点可以看下Gradio在调用什么——predict()方法,在predict()方法中可以看到model.stream_chat(),不用问,见名知意,这个就是流式方法,而且是for循环迭代,最后yield产出每次迭代的结果,这和刚才我写的案例不谋而合。

好的,我们就用model.stream_chat()做文章,下面直接上代码。

3.ChatGLM之SSE通信

讲解请重点看代码中的注释

from fastapi import FastAPI, Request, Response
from transformers import AutoTokenizer, AutoModel
import uvicorn, json, datetime
import torch
from fastapi.middleware.cors import CORSMiddleware
from sse_starlette.sse import ServerSentEvent, EventSourceResponse

DEVICE = "cuda"
DEVICE_ID = "0"
CUDA_DEVICE = f"{DEVICE}:{DEVICE_ID}" if DEVICE_ID else DEVICE


def torch_gc():
    if torch.cuda.is_available():
        with torch.cuda.device(CUDA_DEVICE):
            torch.cuda.empty_cache()
            torch.cuda.ipc_collect()


app = FastAPI()
# 解决跨域问题
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=False,
    allow_methods=["*"],
    allow_headers=["*"]
)


@app.post("/stream")
async def stream(arg_dict: dict):
    global model, tokenizer
    async def generate():
        prompt = arg_dict["prompt"]
        history = arg_dict["history"]
        # 仅向模型传入最近五组对话作为上下文,用于多轮对话语境。
        # (若不想限制,可直接删去这行)
        history = history[-5:]
        # 记录上一次迭代后模型输出的文本长度,用于截断下次模型输出的文本,以便事件流逐字逐词输出
        size = 0
        # for循环调用流式方法
        # 每次迭代response都比上一次多一个字或一个词
        for response, history in model.stream_chat(tokenizer, prompt, history=history):
        	# 所以用上次记录的size去截取当前的response
            word = response[size:]
            # 更新当前response文本长度,用于下次迭代截断
            size = len(response)
            # 记录时间,不是重点
            now = datetime.datetime.now()
            time = now.strftime("%Y-%m-%d %H:%M:%S")
            # 构造返回体
            answer = {
                "id": 0,
                "time": time,
                "text": word
            }
            # log = "[" + time + "] " + '", prompt:"' + prompt + '", response:"' + repr(word) + '"'
            # print(log)
            torch_gc()
            # 这里注意,如果只是像ChatGPT一样只返回数据,只需返回一个键为"data",值为字典的字典即可;
            #如果还想输出id、event、注释等,请使用ServerSentEvent类来封装,ServerSentEvent类使用有坑,后续补充或者评论区提问。
            yield {"data": answer}
        # 迭代结束,返回结束标识,用于前端处理
        yield {"data": "[DONE]"}
    return EventSourceResponse(generate())


if __name__ == '__main__':
    tokenizer = AutoTokenizer.from_pretrained("<这里写模型存放目录>", trust_remote_code=True)
    model = AutoModel.from_pretrained("<这里写模型存放目录>", trust_remote_code=True).half().cuda()
    model.eval()
    # 端口号自行修改
    uvicorn.run(app, host='0.0.0.0', port=8011, workers=1)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/572999.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

更适合iPhone的手柄,按键手感真不赖,LEADJOY M1B上手

很多朋友平时玩手游的时候&#xff0c;操作体验往往不是很好&#xff0c;特别是到了夏天&#xff0c;手机玩久了总是热气腾腾的&#xff0c;对此&#xff0c;只需要配上一副手游手柄&#xff0c;就可以获得媲美掌机的游戏体验。最近我就在用一款LEADJOY M1B游戏手柄&#xff0c…

如何选择语音芯片?主流语音方案如何选,九芯电子来推荐

市场分析 近年来&#xff0c;随着我国半导体的不断发展和技术领域的不断突破&#xff0c;语音芯片实现了越来越多的国产化。其中涌现出的像NVD系列、NRK330X系列等不乏国产优秀产品。凭借其优秀的性能、设计&#xff0c;赢得了市场上的好评如潮。 对比分析 OTP语音芯片&#…

webAPI学习笔记3——BOM浏览器对象模型

目录 1、BOM概述 1.1 什么是 BOM 1.2 BOM 的构成 2. window 对象的常见事件 2.1 窗口加载事件 2.2 调整窗口大小事件 3. 定时器 3.1 两种定时器 3.2 setTimeout() 定时器 案例&#xff1a; 5秒后自动关闭的广告 3.3 停止 setTimeout() 定时器 3.4 setInterval() 定时…

解决不联网环境pip安装librosa、numba、llvmlite报错和版本兼容问题

项目场景&#xff1a; 项目是需要在内网不联网环境部署GitHub上一个有关音频、视频处理的深度学习Python工程&#xff0c;因此许多包需要下载好wheel包或tar包后在内网环境安装。 这个过程遇到了许多兼容性问题引起的报错。Python版本与librosa、numba、llvmlite版本兼容问题…

小狗避障-第14届蓝桥杯省赛Scratch中级组真题第4题

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第139讲。 小狗避障&#xff0c;本题是2023年5月7日举行的第14届蓝桥杯省赛Scratch图形化编程中级组编程第4题&#xf…

从代码审计的角度分析 Ruoyi v4.7.6 的任意文件下载漏洞

前言 Ruoyi 的 v4.7.6 是 2022 年 12 月 16 日发布的一个版本&#xff0c;而任意文件下载漏洞实际上 12 月底的时候就已经爆出了&#xff0c;也陆续有一些文章在写这个漏洞&#xff0c;但是 Ruoyi 一直没有更新修复。 上月中旬&#xff08;2023 年 5 月&#xff09;&#xff0c…

内网渗透(八十四)之ADCS配置启用基于SSL的LDAP(LDAPS)

ADCS配置启用基于SSL的LDAP(LDAPS) 打开AD CS,选择证书颁发机构 选择证书模板,右键管理 选择Kerberos身份验证,右键 复制模板 然后会有一个Kerberos身份验证的副本,右键更改名称,更改为LDAPS 选择LDAPS,右键属性 设置模板属性,请求处理——>允许导出私钥(O) 创建证书…

最快实现一个自己的扫地机

​ 作者&#xff1a;良知犹存 转载授权以及围观&#xff1a;欢迎关注微信公众号&#xff1a;羽林君 或者添加作者个人微信&#xff1a;become_me 扫地机介绍 扫地机器人行业本质是技术驱动型行业&#xff0c;产品围绕导航系统的升级成为行业发展的主旋律。按功能划分&a…

【武汉万象奥科】瑞芯微RK3568芯片

▎产品展示 RK3568核心板是基于Rockchip的RK3568设计的一款高性能核心板。该处理器集成了最新的高性能CPU、GPU&#xff0c;并拥有丰富的接口&#xff0c;非常适用于工业自动化控制、人机界面、中小型医疗分析器、电力等多种行业应用。 ▎RK3568产品特点 ▎高性能处理器 ○ 采用…

linuxOPS基础_vmware虚拟机安装及介绍

虚拟机概念 什么是虚拟机&#xff1f; 虚拟机&#xff0c;有些时候想模拟出一个真实的电脑环境&#xff0c;碍于使用真机安装代价太大&#xff0c;因此而诞生的一款可以模拟操作系统运行的软件。 虚拟机目前有2 个比较有名的产品&#xff1a;vmware 出品的vmware workstatio…

pix2pixHD---model---辨别器

搭建完生成器后搭建辨别器。 首先看辨别器的输入&#xff1a;分别是标签和生成器输出。 在训练时候&#xff0c;辨别器通道输入等于生成器的输出加上conditional即标签和实例的拼接。通道相加就是图片concat。 如果使用实例图片&#xff0c;那么辨别器输入通道数加1&#xff…

《Spring Guides系列学习》guide21 - guide25

要想全面快速学习Spring的内容&#xff0c;最好的方法肯定是先去Spring官网去查阅文档&#xff0c;在Spring官网中找到了适合新手了解的官网Guides&#xff0c;一共68篇&#xff0c;打算全部过一遍&#xff0c;能尽量全面的了解Spring框架的每个特性和功能。 接着上篇看过的gu…

这款高性价比商用笔记本值得入手

随话说&#xff1a;工欲善其事&#xff0c;必先利其器。 对于打工人的我来说&#xff0c;办公一定要有一款适合的笔记本&#xff0c;否则真的是事倍功半。近日入手了戴尔Latitude 3330这款笔记本&#xff0c;通过使用体验&#xff0c;感觉真是一款高性价比的笔记本了。 接下来…

分布式事务解决方案Seata 整合 Spring Cloud + Nacos

1. 简介 Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案。 2. Docker 安装 Seata 2.1 下载镜像 docker pull seataio/se…

电子合同网页预览盖章效果实现

电子合同在现在应用越来越广&#xff0c;需求也就随之产生。 本篇文章主要记录两种网页盖章效果实现方式&#xff0c;自己记录一下&#xff0c; 也给需要的人提供一点思路和帮助。 效果 JqueryCSS实现 原理 通过定位盖章位置&#xff0c;之后操作图片悬浮到盖章位置 1.设置…

浙江大学计算机考研分析

关注我们的微信公众号 姚哥计算机考研 更多详情欢迎咨询 浙江大学&#xff08;A&#xff09;考研难度&#xff08;☆☆☆☆☆☆&#xff09; 浙江大学计算机科学与技术学院成立于1978年&#xff0c;始终秉承“人为本&#xff0c;和为贵&#xff0c;变则通”的文化理念&#…

SecureCRT日志设置每行时间

SecureCRT日志设置时针对每个会话单独设置的 下图两个串口打印的地方&#xff0c;每个是一个会话。 打开【选项】 按照如下选项进行配置&#xff1a; 每次断开重新链接都会重新存一个日志文件&#xff0c;文件生成时间以秒为最小单位。 并且每行都有时间记录。 一般使用”年…

聊聊得物数据研发优化策略 | 精选

1.前言 在离线数据研发中&#xff0c;随着业务的快速发展以及业务复杂度的不断提高&#xff0c;数据量的不断增长&#xff0c;尤其得物这种业务的高速增长&#xff0c;必然带来数据逻辑复杂度的提升&#xff0c;数据量越大&#xff0c;复杂度越高&#xff0c;对任务的性能的要…

【嵌入式Linux】源码菜单配置 | 编译 | 菜单配置的实现 | 源码编译的实现

源码配置编译 源码配置编译,要把中间各个环节都理清楚 厂商把自己增加的东西专门放了个文件独立&#xff0c;方便开发者发现变化 1.菜单配置 移植的第一步&#xff0c;就是选配&#xff0c;通过make menuconfig图形化界面选配 //载入配置 $ make ARCHarm64 tegra_defconfi…

JVM(HotSpot)

1、 类加载机制&#xff1a; 引导类&#xff08;Bootstrap &#xff09;加载器&#xff1a;负责加载支撑JVM运行的位于JRE的lib目录下的核心类库&#xff0c;比如 rt.jar、charsets.jar等扩展类&#xff08;Extension &#xff09;加载器&#xff1a;负责加载支撑JVM运行的位于…