乐趣区

关于后端: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 尽可能多地记住上下文么?

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

退出移动版