使用一个大型语言模型的一个令人兴奋的事情是,我们可以用它来构建一个定制的聊天机器人 (Chatbot) ,只需要很少的工作量。在这一节中,我们将探索如何利用聊天的方式,与个性化(或专门针对特定任务或行为的)聊天机器人进行扩展对话。
像 ChatGPT 这样的聊天模型实际上是组装成以一系列消息作为输入,并返回一个模型生成的消息作为输出的。这种聊天格式原本的设计目标是简便多轮对话,但我们通过之前的学习可以知道,它对于不会涉及任何对话的单轮任务也同样有用。
参考第二章的 环境配置小节内容即可。
接下来,我们将定义两个辅助函数。
第一个方法已经陪伴了您一整个教程,即 getCompletion
,其适用于单轮对话。我们将 Prompt 放入某种类似用户消息的对话框中。
另一个称为 getCompletionFromMessage
,传入一个消息列表。这些消息可以来自大量不同的角色 (roles) ,我们会描述一下这些角色。
第一条消息中,我们以系统身份发送系统消息 (system message) ,它提供了一个总体的指示。系统消息则有助于设置助手的行为和角色,并作为对话的高级指示。你可以想象它在助手的耳边低语,引导它的回应,而用户不会注意到系统消息。因此,作为用户,如果你曾经使用过 ChatGPT,您可能从来不知道 ChatGPT 的系统消息是什么,这是有意为之的。系统消息的好处是为开发者提供了一种方法,在不让请求本身成为对话的一部分的情况下,引导助手并指导其回应。
在 ChatGPT 网页界面中,您的消息称为用户消息,而 ChatGPT 的消息称为助手消息。但在构建聊天机器人时,在发送了系统消息之后,您的角色可以仅作为用户 (user) ;也可以在用户和助手 (assistant) 之间交替,从而提供对话上下文。
现在让我们尝试在对话中使用这些消息。我们将使用上面的函数来获取从这些消息中得到的回答,同时,使用更高的温度 (temperature)(越高生成的越多样,更多内容见第七章)。
系统消息说,你是一个说话像莎士比亚的助手。这是我们向助手描述它应该如何表现的方式。然后,第一个用户消息是给我讲个笑话。接下来以助手身份给出回复是,为什么鸡会过马路? 最后发送用户消息是我不知道。
List<ChatMessage> chatMessages = new ArrayList<>();
ChatMessage message = new ChatMessage();
message.setRole("system");
message.setContent("你是一个像莎士比亚一样说话的助手。");
chatMessages.add(message);
ChatMessage message1 = new ChatMessage();
message1.setRole("user");
message1.setContent("给我讲个笑话");
chatMessages.add(message1);
ChatMessage message2 = new ChatMessage();
message2.setRole("assistant");
message2.setContent("鸡为什么过马路");
chatMessages.add(message2);
ChatMessage message3 = new ChatMessage();
message3.setRole("user");
message3.setContent("我不知道");
chatMessages.add(message3);
String result = this.getCompletionFromMessage(chatMessages, 1.5d);
log.info("iterative1:\n{}", result);
因为它想躲过汤锅! 这个笑话有些“愚蠢”,与幽默玩了一个常见的危险情境,引起一些意外,以尽力让人们笑一笑。这种类型的笑话强调了荒诞和意想不到的情节。
让我们看另一个例子。助手的消息是你是一个友好的聊天机器人,第一个用户消息是嗨,我叫Isa。我们想要得到第一个用户消息。
List<ChatMessage> chatMessages = new ArrayList<>();
ChatMessage message = new ChatMessage();
message.setRole("system");
message.setContent("你是个友好的聊天机器人。");
chatMessages.add(message);
ChatMessage message1 = new ChatMessage();
message1.setRole("user");
message1.setContent("Hi, 我是Isa。");
chatMessages.add(message1);
String result = this.getCompletionFromMessage(chatMessages, 1.5d);
log.info("iterative2:\n{}", result);
嗨Isa,很高兴认识你!有什么我可以帮助你的吗?
让我们再试一个例子。系统消息是,你是一个友好的聊天机器人,第一个用户消息是,是的,你能提醒我我的名字是什么吗?
List<ChatMessage> chatMessages = new ArrayList<>();
ChatMessage message = new ChatMessage();
message.setRole("system");
message.setContent("你是个友好的聊天机器人。");
chatMessages.add(message);
ChatMessage message1 = new ChatMessage();
message1.setRole("user");
message1.setContent("好,你能提醒我,我的名字是什么吗?");
chatMessages.add(message1);
String result = this.getCompletionFromMessage(chatMessages, 1.5d);
log.info("iterative3:\n{}", result);
当然可以!请告诉我你的名字,我将会一直记得。
如上所见,模型实际上并不知道我的名字。
因此,每次与语言模型的交互都互相独立,这意味着我们必须提供所有相关的消息,以便模型在当前对话中进行引用。如果想让模型引用或 “记住” 对话的早期部分,则必须在模型的输入中提供早期的交流。我们将其称为上下文 (context) 。尝试以下示例。
List<ChatMessage> chatMessages = new ArrayList<>();
ChatMessage message = new ChatMessage();
message.setRole("system");
message.setContent("你是个友好的聊天机器人。");
chatMessages.add(message);
ChatMessage message1 = new ChatMessage();
message1.setRole("user");
message1.setContent("Hi, 我是Isa");
chatMessages.add(message1);
ChatMessage message2 = new ChatMessage();
message2.setRole("assistant");
message2.setContent("Hi Isa! 很高兴认识你。今天有什么可以帮到你的吗?");
chatMessages.add(message2);
ChatMessage message3 = new ChatMessage();
message3.setRole("user");
message3.setContent("是的,你可以提醒我, 我的名字是什么?");
chatMessages.add(message3);
String result = this.getCompletionFromMessage(chatMessages, 1.5d);
log.info("iterative4:\n{}", result);
当然,Isa,你的名字是Isa。请问有什么具体的提醒需要我帮助你吗?
现在我们已经给模型提供了上下文,也就是之前的对话中提到的我的名字,然后我们会问同样的问题,也就是我的名字是什么。因为模型有了需要的全部上下文,所以它能够做出回应,就像我们在输入的消息列表中看到的一样。
现在,我们构建一个 “订餐机器人”,我们需要它自动收集用户信息,接受比萨饼店的订单。
下面这个函数将收集我们的用户消息,以便我们可以避免像刚才一样手动输入。这个函数将从我们下面构建的用户界面中收集 Prompt ,然后将其附加到一个名为上下文( context )的列表中,并在每次调用模型时使用该上下文。
模型的响应也会添加到上下文中,所以用户消息和模型消息都被添加到上下文中,上下文逐渐变长。这样,模型就有了需要的信息来确定下一步要做什么。
现在,我们将设置并运行这个 UI 来显示订单机器人。初始的上下文包含了包含菜单的系统消息,在每次调用时都会使用。此后随着对话进行,上下文也会不断增长。