关于后端:ChatGPT来了让我们快速做个AI应用

你好,我是徐文浩。

过来的两讲,我带着你通过OpenAI提供的Embedding接口,实现了文本分类的性能。那么,这一讲里,咱们从新回到Completion接口。而且这一讲里,咱们还会疾速搭建出一个有界面的聊天机器人来给你用。在这个过程里,你也会第一次应用 HuggingFace 这个平台。

HuggingFace 是当初最风行的深度模型的社区,你能够在外面下载到最新开源的模型,以及看到他人提供的示例代码。

ChatGPT来了,更快的速度更低的价格

我在第03讲里,曾经给你看了如何通过Completion的接口,实现一个聊天机器人的性能。在那个时候,咱们采纳的是本人将整个对话拼接起来,将整个上下文都发送给OpenAI的Completion API的形式。不过,在3月2日,因为ChatGPT的炽热,OpenAI放出了一个间接能够进行对话聊天的接口。这个接口叫做 ChatCompletion,对应的模型叫做gpt-3.5-turbo,岂但用起来更容易了,速度还快,而且价格也是咱们之前应用的 text-davinci-003 的十分之一,堪称是物美价廉了。

import openai
openai.ChatCompletion.create(
  model="gpt-3.5-turbo",
  messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Who won the world series in 2020?"},
        {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
        {"role": "user", "content": "Where was it played?"}
    ]
)

[reference_begin]注:点击在这个链接你能够看到接口调用示例。[reference_end]

在OpenAI的官网文档里,能够看到这个接口也非常简单。你须要传入的参数,从一段Prompt变成了一个数组,数组的每个元素都有role和content两个字段。

  1. role这个字段一共有三个角色能够抉择,别离是 system 代表零碎,user代表用户,而assistant则代表AI的答复。
  2. 当role是system的时候,content外面的内容代表咱们给AI的一个指令,也就是通知AI应该怎么答复用户的问题。比方咱们心愿AI都通过中文答复,咱们就能够在content外面写“你是一个只会用中文答复问题的助理”,这样即便用户问的问题都是英文的,AI的回复也都会是中文的。
  3. 而当role是user或者assistant的时候,content外面的内容就代表用户和AI对话的内容。和咱们在第03讲里做的聊天机器人一样,你须要把历史上的对话一起发送给OpenAI的接口,它才有可能了解整个对话的上下文的能力。

有了这个接口,咱们就很容易去封装一个聊天机器人了,我把代码放在了上面,咱们一起来看一看。

import openai
import os

openai.api_key = os.environ.get("OPENAI_API_KEY")

class Conversation:
    def __init__(self, prompt, num_of_round):
        self.prompt = prompt
        self.num_of_round = num_of_round
        self.messages = []
        self.messages.append({"role": "system", "content": self.prompt})

    def ask(self, question):
        try:
            self.messages.append({"role": "user", "content": question})
            response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo",
                messages=self.messages,
                temperature=0.5,
                max_tokens=2048,
                top_p=1,
            )
        except Exception as e:
            print(e)
            return e

        message = response["choices"][0]["message"]["content"]
        self.messages.append({"role": "assistant", "content": message})

        if len(self.messages) > self.num_of_round*2 + 1:
            del self.messages[1:3] //Remove the first round conversation left.
        return message
  1. 咱们封装了一个Conversation类,它的构造函数 init 会承受两个参数,prompt 作为 system的content,代表咱们对这个聊天机器人的指令,num_of_round 代表每次向ChatGPT发动申请的时候,保留过来几轮会话。
  2. Conversation类自身只有一个ask函数,输出是一个string类型的question,返回后果也是string类型的一条message。
  3. 每次调用ask函数,都会向ChatGPT发动一个申请。在这个申请里,咱们都会把最新的问题拼接到整个对话数组的最初,而在失去ChatGPT的答复之后也会把答复拼接下来。如果答复完之后,发现会话的轮数超过咱们设置的num_of_round,咱们就去掉最后面的一轮会话。

上面,咱们就来试一试这个Conversation类好不好使。

prompt = """你是一个中国厨师,用中文答复做菜的问题。你的答复须要满足以下要求:
1. 你的答复必须是中文
2. 答复限度在100个字以内"""
conv1 = Conversation(prompt, 2)
question1 = "你是谁?"
print("User : %s" % question1)
print("Assistant : %s\n" % conv1.ask(question1))

question2 = "请问鱼香肉丝怎么做?"
print("User : %s" % question2)
print("Assistant : %s\n" % conv1.ask(question2))

question3 = "那蚝油牛肉呢?"
print("User : %s" % question3)
print("Assistant : %s\n" % conv1.ask(question3))
  1. 咱们给到了ChatGPT一个指令,通知它它是一个中国厨子,用中文答复问题,而且答复在100个字以内,并且咱们设定了AI只记住过来3轮的对话。
  2. 而后,咱们依照程序问了他,“你是谁”,“鱼香肉丝怎么做”以及“那蚝油牛肉呢”这三个问题。
  3. 能够看到,在答复里它说本人能够答复做菜的问题,而且答复都在100个字以内。
  4. 并且,咱们问他“那蚝油牛肉呢”的时候,它也确实记住了上下文,晓得咱们问的是菜的做法,而不是价格或者其余信息。

    User : 你是谁?
    Assistant : 我是一个AI语言模型,能够答复你的做菜问题。
    User : 请问鱼香肉丝怎么做?
  5. 瘦猪肉切丝,用盐、淀粉、料酒腌制;
  6. 香葱、姜、蒜切末备用;
  7. 青红椒切丝备用;
  8. 热锅凉油,放入肉丝煸炒至变色;
  9. 放入葱姜蒜末和青红椒丝煸炒;
  10. 加入适量盐、糖、醋、酱油、料酒、水淀粉翻炒平均;
  11. 淋上少许香油即可。
    User : 那蚝油牛肉呢?
    Assistant : 蚝油牛肉的做法:
  12. 牛肉切薄片,退出盐、生抽、淀粉腌制20分钟;
  13. 青红椒切丝,姜蒜切末备用;
  14. 热锅冷油,下姜蒜末煸炒出香味;
  15. 退出牛肉片疾速翻炒至变色;
  16. 退出青红椒丝翻炒平均;
  17. 倒入蚝油、生抽、糖、水淀粉调味;
  18. 翻炒平均,收汁后淋上香油即可。

在问完了3个问题之后,咱们又问了它第四个问题,也就是咱们问它的第一个问题是什么。这个时候,它因为记录了过来第1-3轮的对话,所以还能正确地答复进去,咱们问的是“你是谁”。

question4 = "我问你的第一个问题是什么?"
print("User : %s" % question4)
print("Assistant : %s\n" % conv1.ask(question4))

输入后果:

User : 我问你的第一个问题是什么?
Assistant : 你问我:“你是谁?”

而这个时候,如果咱们从新再问一遍“我问你的第一个问题是什么”,你会发现答复变了。因为啊,上一轮曾经是第四轮了,而咱们设置记住的num_of_round是3。在上一轮的问题答复完了之后,第一轮的对于“你是谁”的问答,被咱们从ChatGPT的对话历史里去掉了。所以这个时候,它会通知咱们,第一个问题是“鱼香肉丝怎么做”。

question5 = "我问你的第一个问题是什么?"
print("User : %s" % question5)
print("Assistant : %s\n" % conv1.ask(question5))

输入后果:

User : 我问你的第一个问题是什么?
Assistant : 你问我:“请问鱼香肉丝怎么做?”

计算聊天机器人的老本

无论是在第03讲里,还是这一讲里,咱们每次都要发送一大段之前的聊天记录给到OpenAI。这是由OpenAI的GPT-3系列的大语言模型的原理所决定的。GPT-3系列的模型可能实现的性能非常简单,它就是依据你给他的一大段文字去续写前面的内容。而为了可能不便地为所有人提供服务,OpenAI也没有在服务器端保护整个对话过程本人去拼接,所以就不得不由你来拼接了。

即便ChatGPT的接口是把对话分成了一个数组,然而实际上,最终发送给模型的还是拼接到一起的字符串。OpenAI在它的Python库外面提供了一个叫做 ChatML 的格局,其实就是ChatGPT的API的底层实现。OpenAI理论做的,就是依据一个定义好特定分隔符的格局,将你提供的多轮对话的内容拼接在一起,提交给 gpt-3.5-turbo 这个模型。

<|im_start|>system
You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.
Knowledge cutoff: 2021-09-01
Current date: 2023-03-01<|im_end|>
<|im_start|>user
How are you<|im_end|>
<|im_start|>assistant
I am doing well!<|im_end|>
<|im_start|>user
How are you now?<|im_end|>

[reference_begin]注:chatml的文档里,你能够看到你的对话,就是通过 <|im_start|>system|user|assistant、<|im_end|> 这些分隔符宰割拼装的字符串。底层依然是一个内容续写的大语言模型。[reference_end]

ChatGPT的对话模型用起来很不便,然而也有一点须要留神。就是在这个须要传送大量上下文的状况下,这个费用会比你设想的高。OpenAI是通过模型解决的Token数量来免费的,然而要留神,这个免费是“双向收费”。它是依照你发送给它的上下文,加上它返回给你的内容的总Token数来计算破费的Token数量的。

这个从模型的原理上是正当的,因为每一个Token,无论是你发给它的,还是它返回给你的,都须要通过GPU或者CPU运算。所以你发的上下文越长,它耗费的资源也越多。然而在应用中,你可能感觉我来了10轮对话,一共1000个Token,就只会收1000个Token的费用。而实际上,第一轮对话是只耗费了100个Token,然而第二轮因为要把后面的上下文都发送进来,所以须要200个,这样10轮下来,是须要破费5500个Token,比后面说的1000个可多了不少。

所以,如果做了利用要计算破费的老本,你就须要学会计算Token数。上面,我给了你一段示例代码,看看在ChatGPT的对话模型下,怎么计算Token数量。

通过API计算Token数量

第一种计算Token数量的形式,是从API返回的后果外面获取。咱们批改一下方才的Conversation类,从新创立一个Conversation2类。和之前只有一个不同,ask函数除了返回回复的音讯之外,还会返回这次申请耗费的Token数。

class Conversation2:
    def __init__(self, prompt, num_of_round):
        self.prompt = prompt
        self.num_of_round = num_of_round
        self.messages = []
        self.messages.append({"role": "system", "content": self.prompt})

    def ask(self, question):
        try:
            self.messages.append( {"role": "user", "content": question})
            response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo",
                messages=self.messages,
                temperature=0.5,
                max_tokens=2048,
                top_p=1,
            )
        except Exception as e:
            print(e)
            return e

        message = response["choices"][0]["message"]["content"]
        num_of_tokens = response['usage']['total_tokens']
        self.messages.append({"role": "assistant", "content": message})
        
        if len(self.messages) > self.num_of_round*2 + 1:
            del self.messages[1:3]
        return message, num_of_tokens

而后咱们还是问一遍之前的问题,看看每一轮问答耗费的Token数量。

conv2 = Conversation2(prompt, 3)
questions = [question1, question2, question3, question4, question5]
for question in questions:
    answer, num_of_tokens = conv2.ask(question)
    print("询问 {%s} 耗费的token数量是 : %d" % (question, num_of_tokens))输入后果:

输入后果:

询问 {你是谁?} 耗费的token数量是 : 108
询问 {请问鱼香肉丝怎么做?} 耗费的token数量是 : 410
询问 {那蚝油牛肉呢?} 耗费的token数量是 : 733
询问 {我问你的第一个问题是什么?} 耗费的token数量是 : 767
询问 {我问你的第一个问题是什么?} 耗费的token数量是 : 774

能够看到,前几轮的Token耗费数量在逐步增多,然而最初3轮是一样的。这是因为咱们代码里只应用过来3轮的对话内容向ChatGPT发动申请。

通过Tiktoken库计算Token数量

第二种形式,咱们在上一讲用过,就是应用Tiktoken这个Python库,将文本分词,而后数一数Token的数量。

须要留神,应用不同的GPT模型,对应着不同的Tiktoken的编码器模型。对应的文档,能够查问这个链接:https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb

咱们应用的ChatGPT,采纳的是cl100k_base的编码,咱们也能够试着用它计算一下第一轮对话应用的Token数量。

import tiktoken
encoding = tiktoken.get_encoding("cl100k_base")

conv2 = Conversation2(prompt, 3)
question1 = "你是谁?"
answer1, num_of_tokens = conv2.ask(question1)
print("总共耗费的token数量是 : %d" % (num_of_tokens))

prompt_count = len(encoding.encode(prompt))
question1_count = len(encoding.encode(question1))
answer1_count = len(encoding.encode(answer1))
total_count = prompt_count + question1_count + answer1_count
print("Prompt耗费 %d Token, 问题耗费 %d Token,答复耗费 %d Token,总共耗费 %d Token" % (prompt_count, question1_count, answer1_count, total_count))

输入后果:

总共耗费的token数量是 : 104
Prompt耗费 65 Token, 问题耗费 5 Token,答复耗费 20 Token,总共耗费 90 Token

咱们通过API取得了耗费的Token数,而后又通过Tiktoken别离计算了System的批示内容、用户的问题和AI生成的答复,发现了两者还有小小的差别。这个是因为,咱们没有计算OpenAI去拼接它们外部须要的格局的Token数量。很多时候,咱们都须要通过Tiktoken事后计算一下Token数量,防止提交的内容太多,导致API返回报错。

Gradio帮你疾速搭建一个聊天界面

咱们曾经有了一个封装好的聊天机器人了。然而,当初这个机器人,咱们只能本人在Python Notebook外面玩,每次问点问题还要调用代码。那么,接下来咱们就给咱们封装好的Convesation接口开发一个界面。

咱们间接选用Gradio这个Python库来开发这个聊天机器人的界面,因为它有这样几个益处。

  1. 咱们现有的代码都是用Python实现的,你不须要再去学习JavaScript、TypeScript以及相干的前端框架了。
  2. Gradio渲染进去的界面能够间接在Jupyter Notebook外面显示进去,对于不理解技术的同学,也不再须要解决其余环境搭建的问题。
  3. Gradio这个公司,曾经被目前最大的开源机器学习模型社区HuggingFace收买了。你能够收费把Gradio的利用部署到HuggingFace上。我等一下就教你怎么部署,你能够把你本人做进去的聊天机器人部署下来给你的敌人们用。
  4. 在前面的课程里,有些时候咱们也会应用一些开源的模型,这些模型往往也托管在HuggingFace上。所以应用HuggingFace+Gradio的部署形式,特地不便咱们最演示给其他人看。

[reference_begin]注:Gradio官网也有用其余开源预训练模型创立Chatbot的教程https://gradio.app/creating-a-chatbot/[reference_end]

在理论开发之前,还是依照常规咱们先装置一下Python的Gradio的包。

conda install gradio

Gradio利用的代码我也列在了上面,对应的逻辑也非常简单。

  1. 首先,咱们定义好了system这个零碎角色的提醒语,创立了一个Conversation对象。
  2. 而后,咱们定义了一个answer办法,简略封装了一下Conversation的ask办法。次要是通过history保护了整个会话的历史记录。并且通过responses,将用户和AI的对话分组。而后将它们两个作为函数的返回值。这个函数的签名是为了合乎Gradio里Chatbot组件的函数签名的需要。
  3. 最初,咱们通过一段with代码,创立了对应的聊天界面。Gradio提供了一个现成的Chatbot组件,咱们只须要调用它,而后提供一个文本输入框就好了。
import gradio as gr
prompt = """你是一个中国厨师,用中文答复做菜的问题。你的答复须要满足以下要求:
1. 你的答复必须是中文
2. 答复限度在100个字以内"""

conv = Conversation(prompt, 10)

def answer(question, history=[]):
    history.append(question)
    response = conv.ask(question)
    history.append(response)
    responses = [(u,b) for u,b in zip(history[::2], history[1::2])]
    return responses, history

with gr.Blocks(css="#chatbot{height:300px} .overflow-y-auto{height:500px}") as demo:
    chatbot = gr.Chatbot(elem_id="chatbot")
    state = gr.State([])

    with gr.Row():
        txt = gr.Textbox(show_label=False, placeholder="Enter text and press enter").style(container=False)

    txt.submit(answer, [txt, state], [chatbot, state])

demo.launch()

你间接在Colab或者你本地的Jupyter Notebook外面,执行一下这一讲到目前的所有代码,就失去了一个能够和ChatGPT聊天的机器人了。

把机器人部署到HuggingFace下来

有了一个能够聊天的机器人,置信你曾经急不可待地想让你的敌人也能用上它了。那么咱们就把它部署到 HuggingFace 下来。

  1. 首先你须要注册一个HuggingFace的账号,点击左上角的头像,而后点击 “+New Space” 创立一个新的我的项目空间。
  1. 在接下来的界面里,给你的Space取一个名字,而后在Select the Space SDK外面,抉择第二个Gradio。硬件咱们在这里就抉择收费的,我的项目咱们在这里抉择public,让其他人也可能看到。不过要留神,public的space,是连你前面上传的代码也可能看到的。
  1. 创立胜利后,会跳转到HuggingFace的App界面。外面给了你如何Clone以后的space,而后提交代码部署App的形式。咱们只须要通过Git把以后space下载下来,而后提交两个文件就能够了,别离是:
  • app.py 蕴含了咱们的Gradio利用;
  • requirements.txt 蕴含了这个利用依赖的Python包,这里咱们只依赖OpenAI这一个包。

代码提交之后,HuggingFace的页面会主动刷新,你能够间接看到对应的日志和Chatbot的利用。不过这个时候,咱们还差一步工作。

  1. 因为咱们的代码里是通过环境变量获取OpenAI的API Key的,所以咱们还要在这个HuggingFace的Space里设置一下这个环境变量。
  • 你能够点击界面外面的Settings,而后往下找到Repository secret。

在Name这里输出 [strong_begin]OPENAI_API_KEY[strong_end],而后在Secret value外面填入你的OpenAI的密钥。

  • 设置实现之后,你还须要点击一下Restart this space确保这个利用从新加载一遍,以获取到新设置的环境变量。

好啦,这个时候,你能够从新点击App这个Tab页面,试试你的聊天机器人是否能够失常工作啦。

我把明天给你看到的Chatbot利用放到了HuggingFace上,你能够间接复制下来试一试。

地址:https://huggingface.co/spaces/xuwenhao83/simple_chatbot

小结

心愿通过这一讲,你曾经学会了怎么应用ChatGPT的接口来实现一个聊天机器人了。咱们别离实现了只保留固定轮数的对话,并且体验了它的成果。咱们也明确了为什么,咱们总是须要把所有的上下文都发送给OpenAI的接口。而后咱们通过Gradio这个库开发了一个聊天机器人界面。最初,咱们将这个简略的聊天机器人部署到了HuggingFace上,让你能够分享给本人的敌人应用。心愿你玩得快乐!

课后练习

在这一讲里,咱们的Chatbot只能保护过来N轮的对话。这意味着如果对话很长的话,咱们一开始对话的信息就被丢掉了。有一种形式是咱们不设定轮数,只限度传入的上下文的Token数量。

  1. 你能依据这一讲学到的内容,批改一下代码,让这个聊天机器人不限度轮数,只在Token数量要超标的时候再删减最开始的对话么?
  2. 除了“遗记”开始的几轮,你还能想到什么方法,让AI尽可能多地记住上下文么?

期待能在评论区看到你的思考,也欢送你把这节课分享给感兴趣的敌人,咱们下一讲再见。

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据