使用WebSocket技术实现ChatGPT应用

在之前的文章当中介绍了利用Server-sent events技术实现了ChatGPT消息流回复,也就是ChatGPT在回复的时候的打字效果。本文介绍如何使用WebSocket技术来实现类似的效果,同时还实现在实时聊天的过程当中可以停止生成的功能。 WebSocket简介 WebSocket是一种网络通信协议,通常在web应用当中作用他。看这名字就知道他跟Socket有一些相似之处,但又没有太多的联系。 什么是Socket Socket翻译为套接字,就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。 Socket 通信示例 主机 A 的应用程序必须通过 Socket 建立连接才能与主机B的应用程序通信,而建立 Socket 连接需要底层 TCP/IP 协议来建立 TCP 连接。而建立 TCP 连接需要底层 IP 协议来寻址网络中的主机。 上图直接画出来了编程模型,如果用C语言或者Java语言编写过Socket应用,对上面的一些函数调用就非常的熟悉了。后面介绍的WebSocket编程模型与上面的有非常相似的地方,熟悉Socket编程模型对实现WebSocket是非常有帮助的。 什么WebSocket WebSocket是一种与HTTP不同的协议。两者都位于OSI模型的应用层,并且都依赖于传输层的TCP协议。 虽然它们不同,但是RFC 6455中规定:it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries(WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介),从而使其与HTTP协议兼容。 为了实现兼容性,WebSocket握手使用HTTP Upgrade头从HTTP协议更改为WebSocket协议。 WebSocket协议支持Web浏览器(或其他客户端应用程序)与Web服务器之间的交互,具有较低的开销,便于实现客户端与服务器的实时数据传输。 服务器可以通过标准化的方式来实现,而无需客户端首先请求内容,并允许消息在保持连接打开的同时来回传递。通过这种方式,可以在客户端和服务器之间进行双向持续对话。 通信通过TCP端口80或443完成,这在防火墙阻止非Web网络连接的环境下是有益的。另外,Comet之类的技术以非标准化的方式实现了类似的双向通信。 大多数浏览器都支持该协议,包括Google Chrome、Firefox、Safari、Microsoft Edge、Internet Explorer和Opera。 与HTTP不同,WebSocket提供全双工通信。此外,WebSocket还可以在TCP之上实现消息流。TCP单独处理字节流,没有固有的消息概念。 在WebSocket之前,使用Comet可以实现全双工通信。但是Comet存在TCP握手和HTTP头的开销,因此对于小消息来说效率很低。WebSocket协议旨在解决这些问题。 早期,很多网站为了实现推送技术,所用的技术都是轮询。轮询是指由浏览器每隔一段时间(如每秒)向服务器发出HTTP请求,然后服务器返回最新的数据给客户端。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求与回复可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。 比较新的轮询技术是Comet。这种技术虽然可以实现双向通信,但仍然需要反复发出请求。而且在Comet中普遍采用的HTTP长连接也会消耗服务器资源。 在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。 Websocket使用ws或wss的统一资源标志符(URI)。其中wss表示使用了TLS的Websocket。如: ws://example.com/wsapi wss://secure.example.com/wsapi Websocket与HTTP和HTTPS使用相同的TCP端口,可以绕过大多数防火墙的限制。默认情况下,Websocket协议使用80端口;运行在TLS之上时,默认使用443端口。 优点 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。 WebSocket的基本教程推荐阮一峰的博客https://www.ruanyifeng.com/blog/2017/05/websocket.html ...

June 25, 2023

聊聊ChatGPT消息流回复原理

去年底以来ChatGPT已经火出天际,似乎奇点已经到来。前不久GPT-4发布的新闻更是把这人工智能推向了一个新的高潮。我们注意到,ChatGPT的回复是具有打字的效果,时不时还停顿一下,看起来你就像在跟一个具有智慧的人在聊天,时不时思索一下。 那么ChatGPT的回复效果是如何实现的呢,今天就来聊一聊其中的一种实现方式:Server-sent events。这是一种服务器向客户端主动推送消息的技术。 服务器向浏览器推送信息,最先想到的就是WebSocket了。除了 WebSocket,还有一种方法:Server-Sent Events(以下简称 SSE)。本文介绍它的用法以及一个可以运行的示例。 除了SSE和WebSocket以外,还有一种经常使用到的方法是轮询,客户端不断的向服务器端发起查询请求以获得实时数据信息。 下面是 WebSocket、轮询和 SSE 的功能对比: SSE 和轮询使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议 SSE 相比 WebSocket 更加轻量,使用简单;WebSocket 使用相对复杂;轮询使用简单 SSE 默认支持断线重连,WebSocket 需要自己实现断线重连 SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据 SSE 支持自定义发送的消息类型 WebSocket 支持双向推送消息,SSE 是单向的 轮询性能开销大、轮询时间久导致客户端及时更新数据 什么是SSE SSE是HTML 5规范的一个组成部分,是一种服务器推送技术,使客户端可以通过HTTP连接从服务器自动接收更新。使用server-sent events,服务器可以在任何时刻向我们的Web页面推送数据和信息。这些被推送进来的信息可以在这个页面上作为Events + data的形式来处理。 严格地说,HTTP 协议无法做到服务器主动推送信息,但是,服务器会向客户端发送响应的内容里面会声明,接下来会发送流信息(streaming),然后服务端就会不断的向客户发送数据流,直到结束。客户端在收到响应后会保持连接,持续的接收服务器发送过来的数据。 SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE,其他浏览器都支持。 各种浏览器支持情况可以查看Can I Use 举个栗子 下面我们使用Python及FastAPI框架,编写服务端代码。 服务端代码 import time import random from fastapi import FastAPI, Response from fastapi.responses import StreamingResponse app = FastAPI() async def fake_number_streamer(): for i in range(random.randint(10, 20)): yield "{}".format(i+1) time.sleep(0.1 * random.randint(1, 10)) @app.get("/") async def index(): # 读取html内容输出给客户端 return Response(open("index.html", encoding="utf-8").read()) @app.get("/sse") async def sse(): return StreamingResponse(fake_number_streamer(), media_type="text/event-stream") 服务端代码比较简单,首先需要安装fastapi (pip install fastapi)。然后需要定义一下生成器函数,用来模拟持续输出信息的逻辑,这里直接用一个随机数,输出从1开始,到10,20之间的随机数列。 ...

April 8, 2023

手把手教你用Python调用gpt-3.5-turbo复现ChatGPT

大家好,我是凯文。今天为大家介绍如何使用Python复刻ChatGPT的思路,以及核心代码。 预备知识 首先,Python的版本推荐是3.9。我们需要安装OpenAI的API,这可以通过使用pip install openai命令进行完成。接下来,我们需要在OpenAI的网站上注册并申请API密钥,这样我们才能开始使用它们的API(关于如何申请API密钥请另行搜索,或者联系我) 一旦我们有了API密钥,我们就可以从Python中使用OpenAI的API。具体来说,我们需要使用openai.ChatCompletion.create函数来生成对话响应。我们还需要使用GPT-3.5模型,这可以通过设置model参数为“gpt-3.5-turbo”来完成。 下面我们以一个命令行程序来演示整个核心逻辑。我们先来看流程图 流程图 flowchart LR CommandLine(接收命令行输入) --> ConstructMessages(组装消息) ConstructMessages --> CallOpenAIAPI(调用OpenAI的API接口) CallOpenAIAPI --> 输出对话内容 代码 我们先从如何调用OpenAI的API接口开始,使用自底向上的方法来开发我们的ChatGPT。 from dataclasses import dataclass from typing import List import openai MODEL = "gpt-3.5-turbo" TEMPERATURE = 0.5 openai.api_key = "YOUR API KEY" openai.proxy = "socks5h://127.0.0.1:1080" @dataclass class Prior: question: str answer: str 上面的代码首先引入三个模块,然后定义模型名称MODEL以及回答内容的创意性Temperature(说人话就是随机性,值的范围是0到2,值越大随机性越大). 接下来就是设置API Key了,如果你运行程序的电脑不能直接连接到openai的服务器,请设置proxy。 然后定义一下dataclass Prior,这个相当于保存历史记录用的,记录问题及答案文本内容。 下面来看chat函数: def chat(priors: List[Prior], question: str): messages = [] for prior in priors: messages.append({"role": "user", "content": prior.question}) messages.append({"role": "assistant", "content": prior.answer}) messages.append({"role": "user", "content": question}) resp = openai.ChatCompletion.create( model=MODEL, messages=messages, temperature=TEMPERATURE, stream=True ) return resp 接收一个priors列表,以及一个提问的文本question。函数前半部分组装messages参数,根据历史记录组装一个类似下面列表的列表: ...

April 2, 2023

使用ChatGPT在指定数据集上进行对话

ChatGPT不全面简介 ChatGPT是一种基于预训练的自然语言生成模型,是GPT系列模型的一种。ChatGPT的论文并没有公开发表,最相关的一篇工作就是InstructGPT(Training language models to follow instructions with human feedback)了,发表时间是2022年3月4日。 InstructGPT基于GPT-3.5来训练的,但是GPT-3.5官方并没有释放出来。InstructGPT是结合了RLHF(reinforcement learning from human feedback)(Christiano et al., 2017; Stiennon et al., 2020)—基于人工反馈的强化学习方法 — 训练出来的文本生成模型。 上面的图展示了InstractGPT训练的三个过程: 从prompt数据库当中采样prompt列表,通过人工标注,用于对GPT-3进行微调; 训练强化学习的奖励模型,采用的方法是输入prompt到多个模型,人工标注生成的内容的相关性;通过标注的相关性数据训练奖励模型; 使用奖励模型来优化InstructGPT models (PPO-ptx)。 在指定数据集上进行对话 什么意思呢?举个栗子,给定你一个文本文件,让ChatGPT根据给定的文本内容来回答你提出的问题。下面看一个具体的例子,我在阅读InstructGPT的论文的时候,英文不太好,摘要不想,就让ChatGPT告诉我摘要在讲什么,还得中文回答我: 看这个回答还是太长了,我得让他再精简一点,要求50个字讲清楚: 虽然对字段的精确理解还差了一些,但确实短了不少😄。 现在来讲一下大概的原理。GPT模型本身就是一个In-context learning的过程,可以根据给定的上下文,生成与上下文非常相关的内容。 有了这个原理,我们就有思路了,如果是一个给定的数据集,而不是一段文本呢?另外,还有一个信息非常重要,我们是使用OpenAI的官方API(gpt-3.5-turbo模型)来实现这个demo,OpenAI的API的token数量是有限制的,所以上下文的内容长度是有限制的。那么很明显了,需要把问题和问题相关的文本块要像上面那样去组织,然后丢给OpenAI的API来回答即可。 流程图 我们来画个流程图 分步骤实现 首先把准备的语料切分成文本块,官方给的例子是按句子进行切分,然后合并相邻的两个句子,如果合并后的长度在500个token以内就保留合并的内容,如果超过这个阈值就不合并。 def split_into_many(text, max_tokens=500): sentences = text.split('. ') n_tokens = [len(tokenizer.encode(" " + sentence)) for sentence in sentences] chunks = [] tokens_so_far = 0 chunk = [] for sentence, token in zip(sentences, n_tokens): if tokens_so_far + token > max_tokens: chunks.append(". ".join(chunk) + ".") chunk = [] tokens_so_far = 0 if token > max_tokens: continue chunk.append(sentence) tokens_so_far += token + 1 return chunks 这个split_into_many函数就是对一大段文本进行切分用的,按照. 分割文本,分割后以max_tokens为阈值进行合并。最后返回文本块数组。 ...

March 14, 2023

10行代码调用ChatGPT

OpenAI无疑是人工智能应用领域的领先者之一。该组织致力于研究人工智能的前沿技术,以及探索如何将这些技术应用于实际问题。它的研究和开发项目覆盖了广泛的领域,包括自动化、机器人、语音识别和自然语言处理等。 下面是OpenAI api首页列出的一些功能以及使用指南: 图1 OpenAI API首页 我们可以看到有Chat ,有文本生成,有基于向量的搜索、分类以及文本对比,有语音转文本,有图像生成,有代码生成,还有大模型的精调。其实还有很多很好玩的功能没有列出来,以后可以再写文章介绍更多的玩法。 这次咱们就来演示如何调用 Chat 的api实现类似ChatGPT的魔法。 OpenAI不仅宣布开放,价格还直接打了个骨折:0.002美元/每1000 token,仅为此前GPT-3.5(text-devinci-003)价格的1/10。ChatGPT API 质优价廉,开发者胖友们可以赶快用起来了。 下面就是示例代码,10行代码实现ChatGPT: import openai openai.api_key = "API_KEY" def ask_chatgpt(text): resp = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "user", "content": text} ], temperature=0, ) return resp.choices[0].message 上面的11行Python代码实现了ChatGPT模型的调用,把temperature设置为0,可以让结果更加稳定,temperature最大值为1,值越大结果更不一样,如果想规避查重风险,直接把temperature参数值调到最大。 下面来对比一下gpt-3.5-turbo(ChatGPT/GPT-3.5)与text-devinci-003(GPT-3)的输出结果。以Java代码写快速排序为例: 图2 gpt-3.5-turbo的输出结果 图2 text-devinci-003的输出结果 可以很明显的看到gpt-3.5-turbo(ChatGPT/GPT-3.5)不仅输出了完整的代码(拷贝下来直接运行),还给出了代码的解释。 是不是更加香了,大家赶快用起来吧。

March 9, 2023