赞
踩
根据客户需求,开发一个能多人使用的ChatGPT
平台,背后使用的是ChatGPT
的api_key
。
1、可多轮对话
2、可删除对话
3、流式显示对话
4、可多人使用
5、多个api_key均衡使用
第一次接触openai
的二次开发,看文档、看文章,技术点如下:
1、不同等级的api_key
使用不同的model
即模型,普通账号能使用text-davinci-003
和gpt-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 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
池的,又得多写代码,考虑也就十几号人同时用,要啥自行车,直接写单例模式
来维护,上面的抽象基类就是为这个事服务的。
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
。
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
部署,如果时机合适,值得再改造一番。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。