使用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

在Spacemacs中使用conda环境启用lsp-pyright

如果你是Emacs的用户,那么肯定听说过Spacemacs,以开箱即用而出名,界面也非常的漂亮。放弃了一段时间的Emacs,之前都是自己写的配置,没有时间维护,便尝试了一下Spacemacs。 在使用Spacemacs开发Python的时候,启动lsp-pyright对Python的支持,lsp-pyright可以识别virtualenv虚拟环境,暂时还没有对conda env的支持。好在还是有办法设置python程序的路径。 实现方式用到了几个关键词方法: dotspacemacs-configuration-layers列表添加python、lsp以及conda directory local variable with-eval-after-load 下面解释每个点的配置 Spacemacs layers 启动python和lsp,这两个layer不需要做过多的设置。 启动conda的时候需要设置conda-anaconda-home变量,如下: (conda :variables conda-anaconda-home "D:/path/to/miniconda3") python解释器我使用的是miniconda,可以修改成自己对应的路径。 Directory local variable Emacs支持在项目目录创建一个 .dir-locals.el 文件,来创建针对项目文件的本地变量,而不会覆盖全局的变量。这样多个项目之间就不会相互影响了。这块可以参考官方文档。 在我们的Python项目根目录下创建一个 .dir-locals.el 文件,写入以下内容: ((python-mode . ((eval . (with-eval-after-load 'lsp-pyright (progn (lsp-register-custom-settings `(("python.pythonPath" "D:/path/to/miniconda3/envs/stringle_pro/python.exe")))) ))))) 首先python-mode 指定只有在python模式下后面的代码才会执行; (eval . func_call)是一个cons pair,eval函数相当于是标记后面的fanc_call是一个可以执行的S表达式; 如果要设置指定模式下的变量值,则使用下面的代码: ((python-mode . ((variable-name variable-value)))) with-eval-after-load with-eval-after-load 是Emacs内置的一个宏,定义在 subr.el。 这个地方是很需要注意代码运行的顺序与时机的。在Emacs打开python代码的时候会调用 lsp 启动lsp-mode,lsp会询问使用的lsp后端,python的lsp后端有非常的多,pyls, mspy等。我选择的是lsp-pyright。然后lsp根据设置的python lsp后端,加载lsp-pyright的代码。 这时上面的with-eval-after-load就会起作用了,在加载完lsp-pyright.el的代码后就会执行with-eval-after-load里面的代码。把 python.pythonPath 这个lsp变量设置完成后。 lsp就会调用lsp-pyright里面lsp相关函数,启动pyright后端。 总结 本文说明了如何在spacemacs下启用lsp来支持在conda env虚拟环境下开发python的配置工作,需要对Emacs和lsp的工作原理有一定的认识与了解。

June 19, 2023

Python Docker FastAPI快速开发部署RESTful API

本文介绍如何快速使用Python, Docker, FastAPI快速开发RESTful API,以及如何编译为Docker镜像,使用Docker容器运行程序。 预备知识 在开始开发之前,需要先安装Python和Docker。 Python是一种流行的编程语言,因此在许多操作系统上都可以很容易地安装。可以使用Miniconda,也可以使用官方提供的标准安装包进行安装,个人喜欢Miniconda,不会像Anaconda那么大,同时又提供了非常多必要的依赖模块。 而Docker则是一种容器化平台,可帮助我们轻松地将应用程序封装在容器中,便于部署和管理。目前Windows、Linx、macOS对Docker的支持也非常的完善了。 FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 3.6+ 并基于标准的 Python 类型提示。 初始化项目及运行环境 Python没有像Java那样极其成熟的一站式的Maven项目管理工具,但是Python有其自身的简洁与规范。像pyproject.toml,以及最近用Rust写的rye,都是很出色的工具。本文使用的例子,完全使用手动管理即可😄 创建一个项目目录my-project,创建文件requirements.txt,main.py,Dockerfile,后面逐一填写里面的内容。 上面我们安装了Miniconda,建议使用conda来创建一个独立的运行环境,这样即不会影响其他项目的运行环境,还可以保持本项目运行环境的整洁。 conda create -n my-project python=3.9 pip 这个命令创建一个名为my-project的运行环境,同时安装3.9版本的Python,和最新的pip工具。 然后激活这个运行环境 conda activate my-project 添加依赖模块 我们使用requirements.txt来管理程序依赖的模块,requirements.txt文件内容如下 -i https://mirrors.aliyun.com/pypi/simple/ fastapi uvicorn[standard] 本文只需要用到这两个模块。第一行的-i指定pip使用阿里pypi镜像地址,如果在国内这会大大加快依赖安装的速度,可以换成阿里或者其他的镜像源。 在本地开发电脑上可以使用下面的命令安装依赖 pip install -r requirements.txt 编写代码 Python代码 FastAPI非常容易使用,也非常适合快速开发RESTful API。以下是我们快速入门的示例代码: from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return "Hello World" @app.get("/items/{item_id}") def read_item(item_id: int, q: str = None): return {"item_id": item_id, "q": q} 在这个简单的示例中,我们创建了一个FastAPI应用程序,它包括两个路由。第一个路由处理根路径并返回一个简单的“Hello World”字符串。第二个路由处理传递给它的路径参数,并返回这些参数的值。第二个路由会以json格式响应内容给调用者(FastAPI框架会自动帮我们字典转换为json数据)。 ...

June 6, 2023

Python函数式编程之自定义函数get_in

Python函数式编程之 get_in 编程语言支持通过以下几种方式来解构具体问题: 大多数的编程语言都是 过程式 的,所谓程序就是一连串告诉计算机怎样处理程序输入的指令。C、Pascal 甚至 Unix shells 都是过程式语言。 在 声明式 语言中,你编写一个用来描述待解决问题的说明,并且这个语言的具体实现会指明怎样高效的进行计算。 SQL 可能是你最熟悉的声明式语言了。 一个 SQL 查询语句描述了你想要检索的数据集,并且 SQL 引擎会决定是扫描整张表还是使用索引,应该先执行哪些子句等等。 面向对象 程序会操作一组对象。 对象拥有内部状态,并能够以某种方式支持请求和修改这个内部状态的方法。Smalltalk 和 Java 都是面向对象的语言。 C++ 和 Python 支持面向对象编程,但并不强制使用面向对象特性。 函数式 编程则将一个问题分解成一系列函数。 理想情况下,函数只接受输入并输出结果,对一个给定的输入也不会有影响输出的内部状态。 著名的函数式语言有 ML 家族(Standard ML,Ocaml 以及其他变种)和 Haskell。 一些语言的设计者选择强调一种特定的编程方式。 这通常会让以不同的方式来编写程序变得困难。其他多范式语言则支持几种不同的编程方式。Lisp,C++ 和 Python 都是多范式语言;使用这些语言,你可以编写主要为过程式,面向对象或者函数式的程序和函数库。在大型程序中,不同的部分可能会采用不同的方式编写;比如 GUI 可能是面向对象的而处理逻辑则是过程式或者函数式。 在函数式程序里,输入会流经一系列函数。每个函数接受输入并输出结果。函数式风格反对使用带有副作用的函数,这些副作用会修改内部状态,或者引起一些无法体现在函数的返回值中的变化。完全不产生副作用的函数被称作“纯函数”。消除副作用意味着不能使用随程序运行而更新的数据结构;每个函数的输出必须只依赖于输入。 真实案例 上面是一些基本的理论,在实际的工作当中,经常会看到从一个多层嵌套的字典当中取一个值,会使用连续的下标,但是如果中间某个值并非一个字典,就会导致程序的运行时错误。而这种错误经常得等到代码上线之后才会被发现,开发的时候经常都是测试的正常数据。 例如: # 查询Elasticsearch,获得检索结果 resp = es_request("_search", dsl) # 从检索结果当中取出结果总数 total = resp["hits"]["total"]["value"] 上面的代码通常不会有问题,因为Elasticsearch的检索结果总是会包含 hits/total/value 这个值,如果没有结果,这个值就为0。但是,看看下面的动态取值: value = resp["data"]["aggs"][aggs_key]["value"] 这是在封装了Elasticsearch查询之后的结果,假设是一个叫search_service的微服务,如果aggs下的值为空,那么再取动态的 aggs_key 的值就会报 Key Error 的错误。 ...

May 2, 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

Python变量可变性

Python变量的可变与不可变 Mutable and Immutable 可变是一种奇特的说法,即对象的内部状态已更改/突变。 所以,最简单的定义是:内部状态可以改变的对象是可变的。 另一方面,不可变对象一旦创建就不允许对其进行任何更改。这两种状态都是 Python 数据结构的组成部分。 可变定义 可变是指某些东西是可变的或有能力改变的。 在 Python 中“可变”是对象改变其值的能力。这些通常是存储数据集合的对象。 不可变定义 不可变是指随着时间的推移不可能发生变化。 在 Python 中,如果一个对象的值不能随时间改变,那么它就是不可变的。一旦创建,这些对象的值是永久固定的,直到被回收。 可变和不可变对象列表 内置可变类型: 列表(List) 集合(Set) 字典(Dictionarie) 自定义类(User-Defined Classes 取决于用户的定义) 内置不可变类型: 数字 (Integer, Rational, Float, Decimal, Complex & Booleans) 字符串(String) 元组(Tuple) 冻结的集合(Frozen Set) 自定义类(User-Defined Classes 取决于用户的定义) 对象可变性是使 Python 成为动态类型语言的特征之一。尽管 Python 中的 Mutable 和 Immutable 是一个非常基本的概念,但由于不变性的不传递性,它有时会让人有些困惑。 Python 中的对象 在 Python 中,一切都被视为对象。 每个对象都具有以下三个属性: Identity — 这是指对象在计算机内存中引用的地址,使用id函数可以得到唯一标识。 Type — 这是指创建的对象的类型。 例如:整数、列表、字符串等。 Value — 这是指对象存储的值。 例如:List=[1,2,3] 将保存数字 1,2 和 3 虽然 ID 和 Type 一经创建就无法更改,但 Mutable 对象的值可以更改。 ...

November 13, 2022

Python项目结构实践

相比于Java和Rust,Python的项目结构没有一个明确的规范,最近在做Python机器学习项目的时候碰到很多的情况是执行文件非常多,有些同学并没有把一些公用的方法和功能进行抽取,而是每个文件都含有读取/写入文件的重复代码。在对setuptools进行一些学习研究,其实Python项目的结构也可以做得非常好,只是大学在赶功能的时候并没有进行一定的规划。下面做一些总结,有了这个结构,可以把随手写的一些项目分享给其他的项目使用。 Setuptools Setuptools是用到构建和分发Python包(Packages或者叫项目)的一个工具,可以: 自动查找/下载/安装/升级项目的依赖包 项目内部多个模块之间的import更加方便 可以把项目用到的数据文件打包到一个压缩文件内 自动查找项目内的包/模块 自动生成可执行程序 以及其他一些好用的功能… 最主要的功能还是管理依赖,进行源代码的分发(可以把代码分发到Pypi上面,或者分发给其他项目使用) 首先需要确保你已经安装了这个工具: pip install -U setuptools 基本用法 在你的项目根目录下面创建一个setup.py文件,写入下面的内容: from setuptools import setup, find_packages setup( name="HelloWorld", version="0.1", packages=find_packages(), ) 这是一个最基本的配置脚本,定义了包名name,版本号version,以及这个项目包含的包列表packages,包列表使用setuptools提供的find_packages自动在目录里面进行查找。 如果你是在开发阶段可以使用下面的命令来安装你的包,以开发模式安装的包不会安装在site-packages目录,而是在site-packages目录下面会建立一个链接(快捷方式)指向到你的开发目录,这样你所做的改动会马上体现出来。 pip install -e . 两种目录结构 假设我们的包名为hello,下面以两种结构分别介绍如何来进行配置 结构一 hello (1) |- hello (2) | |- __init__.py | `- world.py |- main.py `- setup.py 这种目录结构是pythonic的风格,以包名为源代码的根目录名称,然后把所有的模块都放在这个目录下面(即hello(2)目录)。 这种结果的setup.py如下: from setuptools import setup, find_packages setup( name='hello', version='0.1.0', description='' packages=find_packages(), install_requires=[] ) 结构二 hello |- src | `- hello | |- __init__.py | `- world.py |- main.py `- setup.py 这种目录结构把所有的源码文件都放在src子目录里面,这样的习惯可能是Java程序员比较喜欢的。 ...

June 26, 2019

Python的日志模块

Python3的日志模块(logging)简单而又强大,通过简单的配置可以实现日志写入不同的地方,对于开发和线上环境的debug有很大的帮助。 超简单例子 一个简单的例子 import logging logging.warning('Watch out!') # 打印输出到控制台 logging.info('I told you so') # 不会打印输出 如果保存为python文件(example.py),然后在控制台运行(python example.py)将看到下面的输出: WARNING:root:Watch out! 默认情况logging模块只会输出WARNING级别以及往上的ERROR, CRITICAL级别的消息。 写入到文件 import logging logging.basicConfig(filename='example.log',level=logging.DEBUG) logging.debug('This message should go to the log file') logging.info('So should this') logging.warning('And this, too') 保存为example2.py,运行python example2.py后,打开example.log文件,内容如下: DEBUG:root:This message should go to the log file INFO:root:So should this WARNING:root:And this, too 这个例子设置了日志级别为logging.DEBUG,日志文件也会记录相应级别的日志消息。 需要注意logging.basicConfig需要放在最前面调用,之后才可以调用logging.debug, logging.info。logging.basicConfig是一次性调用的函数,调用过一次,其他的调用将不再生效。 多次运行上面的例子,日志文件只会记录一次日志消息,如果要记录每次运行的消息,需要指定filemode参数: logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG) 从多个模块记录日志 假设你的项目由多个模块缓存,应该在程序入口处(或者在其他的调用info, debug函数之前)对日志模块进行配置。 # myapp.py import logging import mylib def main(): logging.basicConfig(filename='myapp.log', level=logging.INFO) logging.info('Started') mylib.do_something() logging.info('Finished') if __name__ == '__main__': main() # mylib.py import logging def do_something(): logging.info('Doing something') 运行myapp.py(python myapp.py)文件将会在myapp.log日志文件看到如下的输出信息: ...

March 27, 2018

Emacs下Python开发配置

Python开发环境配置其实比较简单,而我一直没有配置好是参照了网上各种各样的配置,导致了非常混乱的配置,这次自己亲自测试安装自己需要的Emacs包来配置。 需要的Emacs包 Elpy company-mode anaconda-mode 就这三个包就可以了。Elpy是一个很强大的Python集成开发环境了,后面两个其实就是做自动补全用的。如果启动Emacs后,执行M-x elpy-config没有办法出来配置的界面,可能是Emacs没有识别到python, pip, conda等命令,可以安装下面的包来解决: exec-path-from-shell 需要的Python包 Elpy的github页面已经列出来了: # Either of these pip install rope pip install jedi # flake8 for code checks pip install flake8 # and autopep8 for automatic PEP8 formatting pip install autopep8 # and yapf for code formatting pip install yapf 另外anaconda-mode需要安装setuptools pip install setuptools Emacs配置 下面是最简单的配置,~/.emacs.d/init.el (require 'package) (add-to-list 'package-archives '("melpa-stable" . "https://stable.melpa.org/packages/")) (package-initialize) (package-refresh-contents) (defvar my-packages '(elpy company anaconda-mode exec-path-from-shell)) (mapc #'(lambda (package) (unless (package-installed-p package) (package-install package))) my-packages) (exec-path-from-shell-initialize) (elpy-enable) (add-hook 'after-init-hook 'global-company-mode) (add-hook 'python-mode-hook 'anaconda-mode) (add-hook 'python-mode-hook 'anaconda-eldoc-mode) 常用按键 Keybinding Function Description M-. anaconda-mode-find-definitions 跳转到定义处。如果不使用anaconda-mode,则是绑定到elpy的elpy-goto-definition,elpy有时候工作得不是很好 M-? anaconda-mode-show-doc 在另外一个window中显示光标当前所在位置符号的文档 M-, anaconda-mode-find-assignments 跳转到变量赋值位置 M-r anaconda-mode-find-references 在另外一个window中显示光标当前所在位置变量的所有引用 M-* anaconda-mode-go-back 返回上一个位置 show一下我的.emacs.d配置,欢迎交流。

March 20, 2018