当前位置: 首页 > Web前端 > HTML

花1块钱让你的网站支持ChatGPT

时间:2023-03-29 12:44:51 HTML

最近ChatGPT在技术圈太火了,投票圈也刷屏了。我也决定凑热闹,在我的博客中加入ChatGPT对话功能。先附上体验链接,源码也可以在最下方找到。感谢您的支持。我的OpenAI个人账户的免费额度已经用完了。我很抱歉。请按照文章和源码自行搭建体验,或注册账号后台体验。体验ChatGPTChatGPT是OpenAI训练的AI对话模型,可以支持各种场景下的智能对话。想要体验ChatGPT,首先要注册一个账号,但该产品不能直接在国内网络上使用,需要自行解决网络问题。解决网络问题后,注册时会要求提供邮箱验证,然后要验证手机号,可惜国内手机号不能用。你也可以选择用谷歌账号登录,但最后还是要验证你的电话号码。所以我们需要找到一个可以接收短信验证码的国外手机号,然后我们就可以登录SMS-ACTIVATE了。这是一个在这个星球上数以百万计的服务中注册帐户的网站。我们为世界上大多数国家提供虚拟号码,以便您可以在线接收带有确认码的短信。我们的服务还包括虚拟号码的长期租赁、转发连接、电话验证等等。SMS-ACTIVATE的价格以卢布为单位。我们需要使用手机号码进行短信验证。查了一下,最便宜的是印度的一个手机号,零售价10.5卢布。按照汇率计算,大概1元多。SMS-ACTIVATE支持某宝充值。我买了一个印度账号,可以收到OpenAI的验证码。注意这个号码是租用的,是有时间限制的,所以大家要抓紧时间完成注册流程。20分钟后,此号码将不是您的号码。注册OpenAI账号后,可以到ChatGPT的WebWorkbench体验AI对话。在体验了ChatGPT通过API接入OpenAI能力之后,对于从事技术的我们来说,可能会思考如何将这种能力融入到自己的产品中。快速入门ChatGPT是OpenAI训练的模型。OpenAI也提供了API供开发者调用,文档和案例比较全面。机器学习中很重要的一个步骤就是参数调优,但是对于前端开发者来说,大部分人肯定不知道该怎么做,所以我们可以参考官方最适合自己的案例。这款Chat案例非常适合我们的场景需求。官网提供了一个nodejsstarter,我们可以基于此快速开始测试。gitclonehttps://github.com/openai/openai-quickstart-node.git其核心代码就是这么一部分,其中使用的openai是官方封装好的NodeJS库。constcompletion=awaitopenai.createCompletion({model:"text-davinci-003",prompt:'questioncontent',temperature:0.9,max_tokens:150,top_p:1,frequency_penalty:0,presence_penalty:0.6,});在调用API之前,您需要在您的OpenAI账户中生成一个APIKey。目前官方免费配额为18刀,超出部分需要付费。计费是根据Token计算的。至于什么是Token,可以参考Keyconcepts。我们拿了上面Chat案例的参数直接使用。基本上有一个AI回答问题七八分。大家可以自己试试效果,并不复杂。接下来就是研究如何把这个starter的关键代码集成到自己的产品中了。产品分析之前在我的博客做过一个简单的WebSocket聊天功能,在AI对话需求中,前端UI部分基本可以参考WebSocket聊天功能,工作量不是很大,主要工作量吧还是在前后端的逻辑和对接上。ChatGPT的产品模式并不是常规的WebSocket全双工对话,而是像我们平时的界面调整一样,发生用户输入后,客户端向服务器发送请求,等待服务器响应,最后反馈给用户.只是从界面上看是聊天,其实并不是标准的聊天流程。因此前后端交互主要依赖HTTP接口。核心元素Prompt在调用openai.createCompletion时有一个很重要的参数prompt,就是对话的上下文信息。只有这些信息足够完整,AI才能给出正确的反馈。例如,假设对话中有2个回合。//第一轮你:爱因斯坦是谁?AI:爱因斯坦(AlbertEinstein)是20世纪最重要的物理学家,他被誉为“时空之父”。他发现了相对论并获得了诺贝尔物理学奖。第一轮,参数提示是谁是爱因斯坦?,机器人通俗易懂,可以立即给出真实的答复。//Round2你:他做了什么贡献?AI:他为社会做出了很多贡献,比如改善公共卫生、建设教育基础设施、提高农业生产能力、促进经济发展等,第二轮参数提示他做了什么贡献?,看到机器人的回复,你可能会觉得有点离谱,因为这根本就是不对的。但是仔细想想,这是因为机器人不知道上下文信息,所以机器人无法理解他代表的是什么,只能通过他做出什么贡献?整个句子都是推测出来的,所以从结果来看,符合语言的逻辑,但是不符合我们给的上下文。如果我们把第二轮的参数提示改成你:谁是爱因斯坦?\nAI:阿尔伯特·爱因斯坦(AlbertEinstein)是20世纪最重要的物理学家,他被誉为“时空之父”。他发现了相对论并获得了诺贝尔物理学奖。\n你:他有什么贡献?\nAI:,机器人将能够理解上下文信息并给出下一个合乎逻辑的答案。//改进第2轮你:他做了什么贡献?AI:爱因斯坦对科学做出了重大贡献。他发明了相对论,改变了人们对世界、物理定律和宇宙的认识,奠定了量子力学的基础。他也发现了……所以,我们的初步结论是:提示参数应该包含更完整的对话话题内容,从而保证AI接下来给出的答案符合我们的基本要求认识。前后端交互对于前端,我们通常关注的是我发什么数据给后端,后端反馈给我什么数据。因此,前端关注点之一是用户输入。用上面的例子,爱因斯坦是谁?他有什么贡献?这两个内容应该分别作为两个前端请求的参数。而且,对于前端来说,我们不需要考虑后端传递给OpenAI的提示是否完整,只要合理地将用户输入的内容传递给后端即可。对于后端,我们需要注意session问题。每个用户都应该有自己和AI的私密对话空间,不能和其他用户交流字符串数据。这可以基于会话来实现。前端每次传来的信息只是简单的用户输入,而后端要注意与OpenAI的对接过程,将用户输入和会话中保留的一些信息结合起来,组合成一个完整的提示并发送它打开AI,从而获得正常的对话过程。所以基本流程应该是这样的:我们按照这个流程输出第一版代码。后端V1版本代码router.get('/chat-v1',asyncfunction(req,res,next){//获取用户输入constwd=req.query.wd;//构造提示参数if(!req.session.chatgptSessionPrompt){req.session.chatgptSessionPrompt=''}constprompt=req.session.chatgptSessionPrompt+`\nquestion:`+wd+`\nAI:`try{constcompletion=awaitopenai.createCompletion({model:"text-davinci-003",prompt,temperature:0.9,max_tokens:150,top_p:1,frequency_penalty:0,presence_penalty:0.6,stop:["\nquestion:","\nAI:"],});//之后成功调用OpenAI,更新会话req.session.chatgptSessionPrompt=prompt+completion.data//返回结果res.status(200).json({code:'0',result:completion.data.choices[0].text});}catch(error){console.error(error)res.status(500).json({message:"开启AI调用异常"});}});前端V1版本关键码constsendChatContentV1=async()=>{//先显示你说的msgList.value.push({time:format(newDate(),"HH:mm:ss"),user:“我说”,内容:chatForm.chatContent,类型:“我的”,customClass:“我的”,});loading.value=true;try{//调用chat-v1接口,等待结果const{result}=awaitchatgptService.chatV1({wd:chatForm.chatContent});//显示AI的回复msgList.value.push({time:format(newDate(),"HH:mm:ss"),user:"ChatAI",content:result,type:"others",customClass:"其他的”,});}最后{loading.value=false;}};基本的对话能力已经可以了,但是最明显的缺点就是等一个回合的时间太长了。我们希望它会更快,至少在交互方面。流式输出(服务端推送+EventSource)好在OpenAI也支持流式输出,可以在前端配合EventSource使用。您还可以将stream参数设置为true,以便API流回文本(作为仅数据服务器发送的事件)。基本数据流如下所示:后端修改如下:router.get('/chat-v2',asyncfunction(req,res,next){//...省略一些代码try{constcompletion=awaitopenai.createCompletion({//...省略一些代码//添加流参数stream:true},{responseType:'stream'});//将响应的内容类型设置为text/event-streamres.setHeader("content-type","text/event-stream")//completion.data是一个ReadableStream,res是一个WritableStream,可以通过pipe连接到pipe,流式传输到前端completion.data.pipe(res)}//...省略部分代码});前端放弃使用axios发起HTTP请求,而是使用EventSource。constsendChatContent=async()=>{//...省略部分代码//先显示你说的msgList.value.push({time:format(newDate(),"HH:mm:ss"),用户:“我说”,内容:chatForm.chatContent,类型:“我的”,customClass:“我的”,});//通过EventSource获取数据constes=newEventSource(`/api/chatgpt/chat?wd=${chatForm.chatContent}`);//记录AI回复的内容letcontent="";//...省略部分代码es.onmessage=(e)=>{if(e.data==="[DONE]"){//[DONE]标记数据结束,调用反馈到向服务器发送反馈chatgptService.feedback(content);es.close();loading.value=false;updateScrollTop();返回;}//从数据中检索Textconsttext=JSON.parse(e.data).choices[0].text;if(text){if(!content){//第一条数据来了,先显示msgList.value.push({ti我:format(newDate(),"HH:mm:ss"),user:"ChatAI",content:text,type:"others",customClass:"others",});//拼接内容+=文本;}else{//先连接内容+=text;//然后更新内容,实现打字机效果msgList.value[msgList.value.length-1].content=content;}}};};从代码中可以发现,前端在收到EventSource消息的时候,也会调用一个反馈接口进行反馈。这是因为后台在使用Pipe输出时,并没有记录AI回复的文字。考虑到前端已经对文本进行了处理,这里前端就做一次。反馈,将本次AI回复的完整内容回传给后台,后台更新保存在session中的对话信息,保证对话上下文的完整性。反馈接口的实现比较简单:router.post('/feedback',function(req,res,next){if(req.body.result){req.session.chatgptSessionPrompt+=req.body.resultres.status(200).json({code:'0',msg:"更新成功"});}else{res.status(400).json({msg:"参数错误"});}});我只是给出了一个简单的做法,在实际产品中可能会有更多的考虑,或者session内容应该在后端进行处理,而不是依赖于前端的反馈。最终效果大致是这样的:限制访问频率由于OpenAI也有免费额度,所以也应该对调用频率和次数进行限制,防止恶意调用。这也可以通过会话来处理。我这里也提供了一个粗略的处理方法,具体请看下文。实际产品可能会写Redis,写库,加定时任务等,我这方面不够专业,就不多说了。关于访问频率,我暂定3秒内最多调用一次,我们可以在调用OpenAI成功后在session中记录时间戳。req.session.chatgptRequestTime=Date.now()当有新的请求到来时,可以用当前时间减去上一次记录的chatgptRequestTime,判断是否在3秒以内,如果是,则返回HTTP状态码429;如果3秒内没有,可以继续下面的逻辑。if(req.session.chatgptRequestTime&&Date.now()-req.session.chatgptRequestTime<=3000){//不允许在3s内重复调用returnres.status(429).json({msg:"请降低requestFrequency"});}请求次数也是如此。我这里也写的很简单。其实应该有跨日清扫这样的逻辑。这里我偷懒了,暂时没有做这个。if(req.session.chatgptTimes&&req.session.chatgptTimes>=50){//其实需要跨天清零,这里偷懒了。returnres.status(403).json({msg:"达到调用限制,欢迎明天再来"});}同一个话题不要讲太多,否则传给OpenAI的提示参数会很大,也就是可能会消耗很多Token,也可能会超过OpenAI参数的限制。if(req.session.chatgptTopicCount&&req.session.chatgptTopicCount>=10){//当某个话题的聊天次数超过限制时,需要强制重置chatgptSessionPrompt并改变话题。req.session.chatgptSessionPrompt=''req.session.chatgptTopicCount=0returnres.status(403).json({msg:"这个话题有点深,还是改一下吧"});}switchtopicclient还应该具备切换主题的能力,否则session中记录的信息可能包含多个主题的内容,可能与用户的预期不符。然后我们就可以做一个界面了。router.post('/changeTopic',function(req,res,next){req.session.chatgptSessionPrompt=''req.session.chatgptTopicCount=0res.status(200).json({code:'0',msg:"你可以尝试新话题"});});结语总体而言,OpenAI开放的智能对话能力可以满足基本需求,但还有很大的提升空间。本文中我给出的代码仅供参考,不保证功能完美。附上源码地址,能不能点个星,是个球[认真脸]。