django传统全栈开发一个ChatGPT应用

news2025/1/20 22:48:12

根据客户需求,开发一个能多人使用的ChatGPT平台,背后使用的是ChatGPTapi_key

需求

1、可多轮对话

2、可删除对话

3、流式显示对话

4、可多人使用

5、多个api_key均衡使用

在这里插入图片描述

技术分析

第一次接触openai的二次开发,看文档、看文章,技术点如下:

1、不同等级的api_key使用不同的model即模型,普通账号能使用text-davinci-003gpt-3.5-turbo模型,都是ChatGPT 3.5的;

2、api_key有限流,普通账号限流挺严的,每分钟3次请求每分钟40000的tokens,意味着需要搭建一个api_key池,维护多个账号,自己写算法动态调节避免被限流。不然少数的几个账号分分钟就能触碰每分钟3次请求的限制;

3、openai是官方提供的sdk,有同步接口,也有异步接口,由于时间短任务中,异步就不考虑了,直接上同步;

4、前端没写过vue,虽然有点跃跃欲试,最后还是选择了熟悉的layui,前端结构化的就不谈了,把功能写出来就完事了;

5、关于api_key,其实还有点,即key的状态,sdk里也没找到什么可用的接口来获取key的剩余额度、有效期等信息,暂时先放一放,让客户自行充值就好了,后面有办法了再解决。

api_key维护

简单来说写了三个类,算法也很简单,使用的数据结构如下:

[
  # API实现在下方
  {'key': <API object xxxxxx>, 'counter': 0}, 
  {'key': <API object xxxxxx>, 'counter': 0},
  ...
]

类实现分别为:

1、Singleton 单例的抽象基类

2、API主题类

3、ApiPool代理类

主要由ApiPool对外提供服务,继承抽象基类实现单例,确保全局数据的唯一性。

抽象基类
class Singleton(type):

    _instance = None

    def __call__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance

本想上redis维护api_key池的,又得多写代码,考虑也就十几号人同时用,要啥自行车,直接写单例模式来维护,上面的抽象基类就是为这个事服务的。

API主题类

class API:
    # 使用时间间隔为20秒 避免触发限流
    rqtl = 20 

    def __init__(self, key):
        self.key = key
        self.__time = time.time() # 初始化时记录时间戳

    @property
    def last_time(self):
        return self.__time

    @last_time.setter
    def last_time(self, value: float):
        self.__time = value
    
    def __repr__(self):
        return f'<{self.key} - {self.last_time}>'
    
    @property
    def can_use(self):
        return self.__bool__()

    def __bool__(self):
        """调用时时间差大于20秒可用 反之不可用"""
        return bool(
            (time.time() - self.last_time) >= API.rqtl
        )

    def __call__(self):
        return self.key

该类主要实现的是api_key是否可用,所有的api_key都保存在数据库,系统启动或重启时,从数据库加载所有的api_key,逐个使用API初始化,并保存时间戳,对外暴露can_use,当调用这个方法时,会使用当前时间戳和记录的时间戳做比,大于等于20秒就使用,在使用时就更新时间戳,所以也暴露了last_time.setter

ApiPool代理类
class ApiPool(metaclass=Singleton):
    """
    1、从数据库里取出api
    2、每个api都是API类的实例 每个实例会记录上次使用的时间
    3、取api使用时 先判断是否can_use 能就取 反之取使用次数最少的
    """
    
    def __init__(self, query):
        # django启动或重启时从数据库中加载api_key
        self.__lst = self.init(query)

    def init(self, query):
        lst = []
        for api in query:
            lst.append(
                {'key': API(api.api_key), 'counter': 0}
            )
        return lst

    @property
    def lst(self):
        return self.__lst
    
    # 取一个可用的api_key
    def get(self):
        _api = None
        for api in self.__lst:
            if api.get('key').can_use:
                _api = api['key']
                # 使用一次就+1
                api['counter'] += 1
                # 更新时间戳
                api['key'].last_time = time.time()
                break
        
        # 如果所有的key的时间间隔都未超过20秒
        # 则使用第一个 因为它的使用次数最少
        if not _api:
            api = self.__lst[0]
            _api = api['key']
            # 使用一次就+1
            api['counter'] += 1
            # 更新时间戳
            api['key'].last_time = time.time()

        # 提取后重新排序 counter 升序
        self.__lst.sort(
            key=lambda api: api['counter']
        )

        return _api
    
    # django后台增加api_key或设置为可用时调用
    def add(self, key):
        s = False
        # 存在时不操作
        for api in self.__lst:
            _key = api.get('key').key
            if key == _key:
                return s
        # 不存在时才增加    
        if isinstance(key, str):
            self.__lst.append({'key': API(key), 'counter': 0})
            s = True

        return s
    
    # django后台删除api_key设置为不可用时调用
    def remove(self, key: str):
        k = None
        for api in self.__lst:
            if api.get('key').key == key:
                k = api
                break
        if k:
            self.__lst.remove(k)
            return True
        return False

    def __repr__(self):
        return f'<ApiPool {len(self.__lst)}>'
    
    # 应对某些情况时使用
    @property
    def available(self):
        lst = []
        for api in self.__lst:
            if api.get('key').can_use:
                lst.append(api)
        return lst

ApiPool对外提供服务,在django启动时就得实例化,在settings.py中初始化不可行,因为那时django的app都未完成初始化,所以最后在某个views.py中实例化,前端请求达到views.py调用openai接口前,先调用get方法拿到一个api_key。演示如下:

# 实例化ApiPool
from . apikey import ApiPool
api_pool = ApiPool(ApiKey.objects.filter(status=True))

@login_required
@require_POST
def conversation(request):
    """省略其他代码"""
    key = api_pool.get()
    if key is None:
        return JsonResponse({'code': 400, 'msg': '暂无可用的key'})
    ret = sync_stream_ChatCompletion(messages, uuid, q, key())
    return StreamingHttpResponse(ret, content_type='application/octet-stream')

前端技术点

前端没使用古老的XMLHttpRequest也没使用jquery.ajax,使用了浏览器原生的fetch(fetch不好的地方就是要两次then才能拿到数据)和后端交互,因为它用来接收steam数据流相对方便些,大概的结构如下:


fetch(url, {options})
.then(response=>{
  // 判断下响应是否为'application/octet-stream'
  // 因为后端也写了json的响应再无api_key可用的情况下
  
  // 1、'application/octet-stream'时,直接闭包处理
  let reader = response.body.getReader();
  function read(){
    return reader.read().then(//拿到流式数据写到页面)
    // 因为是流式,所以需要递归调用
  };
  return read()
  
  // 2、'application/json'时
  let ret = response.json()
  function bad(){
    return ret.then(//友好提示无key可用)
  };
  return bad;
})

有待完善的地方

1、上下文维护不容易,目前是简单粗暴地采用前三轮对话和当前提问一起提交给openai,对于tokens的消耗其实是个问题;但暂时也没有很好的解决方案,值得关注;

2、并没有真正维护到api_key的状态,因为不清楚api_key还有多少额度,只能让客户自己关注并及时充值了;后面时机合适可以完善好这方面;

3、全部基于同步。openai提供了异步接口,其实也写了一部分,但时间有限,如果写异步,那么还需要配套的异步视图uvicorn部署,如果时机合适,值得再改造一番。

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

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

相关文章

软件测试-金融银行项目怎么测?系统业务测试总结分析...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 银行测试人员的组…

《计算机组成原理》期末考试手写笔记——模块三:运算方法与运算器(变形补码计算+原码补码一位乘法计算+浮点运算)

本课程的期末考试复习笔记与你分享&#xff0c;祝你考试成功~ 目录 &#xff08;一&#xff09;知识点总结 知识点1&#xff1a;计算机中的运算&#xff08;不考&#xff09; 知识点2&#xff1a;定点加减法运算&#xff08;很简单&#xff09; 知识点3&#xff1a;定点乘法…

IDE/在VS2015中集成Qt开发环境

文章目录 概述安装VS2015安装Qt VS Tools 插件Qt相关配置应用到所有项目配置增加配置删除 VS项目属性下Qt工程配置&#xff08;Qt Project Settings&#xff09;Qt VersionsQt Modules编辑器找不到Qt的类 QtDesigner在VS下使用无法打开UI/打开失败找不到UI编译中间文件UI添加的…

package.json与package-lock.json区别需不需要被.gitignore文件忽略

前言 先要搞清楚package.json与package-lock.json文件分别是什么作用 .gitignore文件是干什么的-主页文章有 介绍 package.json 1.项目名称版本号&#xff0c;描述&#xff0c;以及运行命令和一些配置的node环境命令 2.记录那些包会在开发环境中用到&#xff0c;哪些包会…

【项目】从零实现一个高并发内存池

目录 一、项目介绍 1、该项目的原型 2、该项目所涉及到的技术及博主往期参考文章 3、池化技术 4、内存池的内碎片和外碎片 二、先来看一个定长内存池设计 三、高并发内存池的三层框架设计 1、thread cache的实现 1.1thread cache整体框架 1.2哈希桶映射对齐规则 1.3…

【送书福利-第九期】Web安全攻防从入门到精通

大家好&#xff0c;我是洲洲&#xff0c;欢迎关注&#xff0c;一个爱听周杰伦的程序员。关注公众号【程序员洲洲】即可获得10G学习资料、面试笔记、大厂独家学习体系路线等…还可以加入技术交流群欢迎大家在CSDN后台私信我&#xff01; 本文目录 一、前言二、内容介绍三、作者介…

【LeetCode】768. 最多能完成排序的块 II

768. 最多能完成排序的块 II&#xff08;困难&#xff09; 思路 对于已经分好块的数组&#xff0c;若块数大于 1&#xff0c;则可以得到以下结论&#xff1a;「 右边的块的所有数字均大于或等于左边的块的所有数字」。考虑这个问题&#xff1a;对于已经分好块的数组&#xff0…

Linux——多线程互斥

多线程互斥 抢票问题互斥锁锁的接口 理解锁锁的背景概念如何看待锁&#xff1a;加锁和解锁的原理锁的封装 可重入与线程安全死锁死锁的概念与条件 抢票问题 这里用上一篇&#xff1a; https://blog.csdn.net/qq_63580639/article/details/131054847?spm1001.2014.3001.5501 的…

LVGL lv_color_t 像素定义详解

更多源码分析请访问&#xff1a;LVGL 源码分析大全 目录 1、概述2、颜色格式详解2.1、LV_IMG_CF_RAW_X2.2、LV_IMG_CF_TRUE_COLOR_X2.3、LV_IMG_CF_INDEXED_XBIT2.4、LV_IMG_CF_ALPHA_XBIT2.5、LV_IMG_CF_RGBX 3、送显函数&#xff08;flush_cb&#xff09;中的 lv_color_t附录…

【P54】JMeter 生成概要结果(Generate Summary Results)

文章目录 一、生成概要结果&#xff08;Generate Summary Results&#xff09;参数说明二、准备工作三、测试计划设计 一、生成概要结果&#xff08;Generate Summary Results&#xff09;参数说明 可以将测试结果在客户端模式下输出&#xff0c;同时能美化压测输出的结果 使…

申请Let‘s Encrypt免费SSL证书、自动化续签证书

一、环境 安装证书的环境为Centos Nginx&#xff0c;如果没有安装Nginx则需要先安装。 二、申请流程 1、开放80和443端口 firewall-cmd --permanent --add-port80/tcp firewall-cmd --permanent --add-port443/tcp firewall-cmd --reload2、安装 certbot 使用certbot工具能…

Doris动态表使用快速入门实战

1. 动态表构功能概述 半结构化数据&#xff0c;是介于结构化和非结构化之间的数据。和普通纯文本相比&#xff0c;半结构化数据具有一定的结构性。和结构化数据相比&#xff0c;其结构变化复杂&#xff0c;我们又不能方便的使用结构化的方式去描述它。 半结构的数据中通常即包…

Java基础知识总结归纳

0. 入门常识 0.1 Java 特点 0.2 Java 和 C 0.3 JRE 和 JDK 0.4 Java 程序编译过程 1. 数据类型 1.1 基本数据类型 1.2 引用类型 1.3 封装类 1.4 缓存池 2. 字符串 String 2.1 定义 2.2 不可变性的优点 2.3 String vs StringBuffer vs StringBuffer 2.4 字符串常量池…

【网络协议详解】——BGP协议(学习笔记)

目录 &#x1f552; 1. 概述&#x1f552; 2. BGP 发言人&#x1f552; 3. 工作原理&#x1f552; 4. 报文格式&#x1f558; 4.1 报文首部&#x1f558; 4.2 打开报文&#x1f558; 4.3 更新报文&#x1f558; 4.4 保活报文&#x1f558; 4.5 通知报文 &#x1f552; 5. BGP 的…

接口实战一“篇”入魂!你真正地了解接口测试么?

目录 前言&#xff1a; 需求描述&#xff1a; 计划和目标&#xff1a; 需求分析&#xff1a; 1.功能点划分 2.接口测试用例设计 3.测试用例评审 4.测试准备 5.测试执行 前言&#xff1a; 接口测试是软件测试的一种类型&#xff0c;它主要关注软件系统中的接口或 API …

STM32CubeIDE 入门教程

1.安装教程 1.1 去ST 官网下载软件安装包&#xff1a;https://www.st.com/content/st_com/en.html 1.2软件安装&#xff1a; 2.创建工程 2.1 双击打开软件&#xff0c;选择工作区路径&#xff0c;不要出现中文名称 2.2 选择直接创建STM32 工程&#xff0c;我们这里以STM32F103…

详细讲解!Selenium真正绕过webdriver检测

目录 前言&#xff1a; 一、什么是真正绕过浏览器检测&#xff1f; 2、 普通的启动webdriver 3、Js注入真正绕过webdriver的检测属性 js注入的文件[stealth.min.js] 前言&#xff1a; Selenium是一个流行的开源测试工具&#xff0c;用于网络应用程序测试。它使测试人员能够…

运用短信案例举例!Appium自动化测试该如何进行PO模式

目录 前言&#xff1a; 案例一&#xff1a;自动发送短信 前言&#xff1a; Appium是一种广泛用于移动应用程序自动化测试的工具&#xff0c;可以支持iOS和Android系统。在测试App时&#xff0c;使用Page Object&#xff08;PO&#xff09;模式可以提高测试用例的可重用性和可…

如何短时间内上手公众号的运营?选择工具很重要

现在网上都在讲如何运营微信公众号&#xff0c;讲有什么微信公众号技巧&#xff0c;这些都是需要用户话花较多的时间去尝试和理解&#xff0c;今天小编分享一个更简单高效运营公众号的方法&#xff01;下面跟着小编的教程一起学习如何使用乔拓云公众号助手去运营自己的公众号吧…

Vue3 ElementPlus Dialog封装

引言 多个页面中需要录入用户数据&#xff08;弹窗内容相同&#xff09;&#xff0c;重复写弹窗代码比较繁琐。因此封装一下组件&#xff0c;使用效果如下&#xff1a; 本例中模型较简单&#xff0c;记录下使用方法和原理 实现原理 参考VUE官方两个例子&#xff0c;基本父子件…