ChatGPT对话
首先,ChatGPT对话功能分为流式与非流式,一般来说,流式更为常见。流式就是接收ChatGPT回答的信息时,是一段一段的,有体验过ChatGPT的同学应该深有体会,流式对话响应到前端的对话框时,吐字是一个个吐。
非流式对话,则是一次性回答,直接返回它的文本。
两者中,流式对话的响应及体验更佳,当然,这也取决于你的应用场景,若是交互式的,选择流式,若是类似于机器人业务的,选择非流式即可。
这两种对话模式分别对应了两种不同的OpenAI客户端。OpenAiClient和OpenAiStreamClient,顾名思义,分别对应非流式和流式的。
我们之前的准备工作中,创建的是非流式对话的客户端,流式对话的客户端不支持绘图和语音转文字的功能。因此,我们先以非流式的为例。
非流式对话
我们使用的SDK对话模式支持复杂的与简单的提问方式,复杂的就是可以对问题描述进行调参,这里面就涉及到问题类Completion和ChatCompletion。两者相似,相关常用属性和方法如下:
- model:问题模型,text-davinci-003、text-davinci-002等
- prompt:问题描述,一般是用户输入的内容
- tokens():获取当前问题描述所需的tokens值,这个tokens值是OpenAI收费的依据。
当我们需要调参进行对话时,就需要创建一个问题类的对象,例如:
// 问题内容
String prompt = "你好";
// 转换为Message对象
Message message = Message.builder().role(Message.Role.USER).content(prompt).build();
// 创建问题类
ChatCompletion chatCompletion = ChatCompletion.builder().messages(Arrays.asList(message)).model(ChatCompletion.Model.GPT_3_5_TURBO.getName()).build();
发起对话时,代码如下:
// 创建对话并获取答案类对象
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion);
// 获取消息
List<ChatChoice> choices = chatCompletionResponse.getChoices();
依据上述,我们就能写出发起非流式对话与接收返回的消息的代码了,这里编写一个测试方法进行测试
@Test
public void chatTest(){
// 问题内容
String prompt = "你好";
// 转换为Message对象
Message message = Message.builder().role(Message.Role.USER).content(prompt).build();
// 创建问题类
ChatCompletion chatCompletion = ChatCompletion.builder().messages(Arrays.asList(message)).model(ChatCompletion.Model.GPT_3_5_TURBO.getName()).build();
// 创建对话并获取答案类对象
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion);
// 获取会话内容
List<ChatChoice> choices = chatCompletionResponse.getChoices();
System.out.println(choices.toString());
}
测试结果如下:
若出现超时错误,请检查是否设置了代理等相关信息。使用我提供的接口,无需代理;访问openai的接口,需要代理。当本地开启了代理,需要设置Proxy对象。
上述实现非流式对话的过程可能较为复杂,我们使用的SDK为我们提供了更为便捷的实现方式,传入Messages对象即可。
@Test
public void chatTest1(){
// 创建消息列表
List<Message> messages = new ArrayList<>();
Message currentMessage = Message.builder().content("你好").role(Message.Role.USER).build();
messages.add(currentMessage);
// 直接将消息列表发送过去
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(messages);
// 获取响应
List<ChatChoice> choices = chatCompletionResponse.getChoices();
System.out.println(choices.toString());
}
上述代码中直接使用Message列表对象作为参数,至于为什么使用列表而不是使用单独的Message对象,这涉及到ChatGPT上下文的实现。
ChatGPT上下文的实现是通过将用户与ChatGPT的对话记录发送给ChatGPT,让它明白上下文从而给出接下来的回答。因此,Message列表里的Message对象会被划分为ChatGPT的message和用户的message,当然,还有一个系统角色设定的message。对于每个角色,我们使用Message.Role枚举类的参数进行选择,上文的Message.Role.USER就是表示当前message是用户的。
流式对话
再介绍完非流式对话的实现后,就是流式对话了。首先,我们所使用的SDK向我们提供了两种OpenAI客户端,分别对应流式与非流式:
- OpenAiClient: 非流式
- OpenAiStreamClient: 流式
除了客户端有所差异外,调用方式大多是一致的。示例代码如下:
- 获取OpenAiStreamClient实例对象:
// 创建openAi客户端
openAiStreamClient = OpenAiStreamClient.builder()
// 获取key
.apiKey(ChatGPTConfig.getApiKey())
// 获取接口
.apiHost(ChatGPTConfig.getApiHost())
.okHttpClient(okHttpClient)
.build();
编写流式输出代码
@Test
public void chatStreamTest(){
List<Message> messages = new ArrayList<>();
Message currentMessage = Message.builder().content("Hello").role(Message.Role.USER).build();
messages.add(currentMessage);
// 接收服务器端发送的事件流的内置对象
ConsoleEventSourceListener consoleEventSourceListener = new ConsoleEventSourceListener();
openAiStreamClient.streamChatCompletion(messages,consoleEventSourceListener);
// 协调多个线程之间的同步操作
CountDownLatch countDownLatch = new CountDownLatch(1);
try{
countDownLatch.await();
}catch (Exception e){
e.printStackTrace();
}
}
其中,在创建对话连接时,必须传入监听回调函数接口ConsoleEventSourceListener的示例对象。
运行结果如下:
实现上下文对话
ChatGPT的上下文对话并不是我们想象得那么简单,我们需要传入之前的对话记录给ChatGPT,然后ChatGPT会依据对话记录进行回复,这就是ChatGPT的上下文记忆功能的实现逻辑。
实际应用中,这种上下文对话是我们主要的应用场景,若不实现上下文对话,可能就会出现这种情况:
示例代码:
@Test
public void contextTest(){
ArrayList<Message> messages = new ArrayList<>();
// 用户需要问的内容
String[] context = new String[]{
"你好!","请问我第一句是什么?","请问你第一句是什么?"};
for(int i=0;i<3;i++){
String content = context[i];
Message message = Message.builder().content(content).role(Message.Role.USER).build();
// 将用户的问题放入messages对象里
messages.add(message);
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(messages);
List<ChatChoice> choices = chatCompletionResponse.getChoices();
for(ChatChoice e:choices){
System.out.println(e.getMessage().getContent());
}
// 将刚刚放进去的内容移出来 使得每次提问都不保存上下文信息
messages.remove(0);
}
}
我在这里设定了用户三次发起的对话,分别是:“你好”,“请问我第一句是什么?”,“请问你第一句是什么”。在不存储对话内容的情况下,ChatGPT的回答如下
运行结果:
可以很明显的感受到,ChatGPT是不知道之前的对话内容的,因此,实现上下文对话,就需要将之前的对话对话内容也加进去,让ChatGPT依据上下文内容进行回复。
注意:之前的对话需要传入用户的对话和ChatGPT的对话,不能单独只传用户或ChatGPT的对话,否则,ChatGPT依据上下文进行的回复仍然会有很大的缺陷和漏洞。
实现上下文对话的示例代码:
@Test
public void contextTest(){
ArrayList<Message> messages = new ArrayList<>();
String[] context = new String[]{
"你好!","请问我第一句是什么?","请问你对我第一句的回复是什么?"};
for(int i=0;i<3;i++){
String content = context[i];
Message message = Message.builder().content(content).role(Message.Role.USER).build();
// 将用户的问题放入messages对象里
messages.add(message);
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(messages);
List<ChatChoice> choices = chatCompletionResponse.getChoices();
for(ChatChoice e:choices){
System.out.println(e.getMessage().getContent());
}
System.out.println(choices.toString());
// 取出ChatGPT对话内容
String assistant = choices.get(0).getMessage().getContent();
// 将这条消息设置为ChatGPT的 --- Message.Role.ASSISTANT
Message build = Message.builder().content(assistant).role(Message.Role.ASSISTANT).build();
messages.add(build);
}
}
运行结果如下:
可以看到,ChatGPT已经具有根据上下文进行回复的功能了。但是,我们向messages序列中不断放消息记录,依据ChatGPT的收费规则,我们的费用是依据传入给ChatGPT和ChatGPT回复给我们的总的字数进行计算的。倘若不进行限制,那么会随着我们询问的增多,每次询问所需费用也会增多。因此,我们需要对上下文的记录条数进行限制,一般是10条。
示例代码如下:
@Test
public void contextTest(){
List<Message> messages = new ArrayList<>();
String[] context = new String[]{
"你好!","请问我第一句是什么?","请问你对我第一句的回复是什么?"};
for(int i=0;i<3;i++){
String content = context[i];
Message message = Message.builder().content(content).role(Message.Role.USER).build();
// 将用户的问题放入messages对象里
messages.add(message);
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(messages);
List<ChatChoice> choices = chatCompletionResponse.getChoices();
for(ChatChoice e:choices){
System.out.println(e.getMessage().getContent());
}
System.out.println(choices.toString());
// 取出ChatGPT对话内容
String assistant = choices.get(0).getMessage().getContent();
Message build = Message.builder().content(assistant).role(Message.Role.ASSISTANT).build();
messages.add(build);
// 当消息列表记录10条以上时,我们截选最后十条
if (messages.size() > 10){
messages = messages.subList(messages.size()-10,messages.size());
}
}
}
实操案例
上述就是一个上下文逻辑的实现,我们可以据此写一个交互型的ChatGPT小程序。
准备工作
首先创建一个App类,写一个start方法,该方法用来初始化一些必要的信息:
public static void start() throws Exception {
// 设置本地代理:如果你需要通过代理进行访问,就需要通过相关参数创建这个对象
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 10809));
// 设置日志级别:对于控制台输出的日志信息,我们有必要通过设置级别进行过滤,这里推荐设置HEADERS
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.NONE);
// 创建http客户端
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(new OpenAiResponseInterceptor())
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
// 创建openAi客户端
openAiClient = OpenAiClient.builder()
// 获取key
.apiKey(ChatGPTConfig.getApiKey())
// 获取接口
.apiHost(ChatGPTConfig.getApiHost())
.okHttpClient(okHttpClient)
.build();
}
具体实现
我们需要实现的ChatGPT小程序的需求是:用户通过控制台输入问题,然后将ChatGPT回复的内容输出出来,并且要保证ChatGPT具有上下文对话的功能。
代码如下:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try{
start();
List<Message> messages = new ArrayList<>();
while (true){
String next = scanner.next();
// 用户输入 exit ,就退出,否则一直进行对话
if(next.equals("exit")){
break;
}
// 将用户输入的字符串信息转化为Message对象并添加到messages序列中。
Message message_user = Message.builder().content(next).role(Message.Role.USER).build();
messages.add(message_user);
// 发起ChatGPT对话
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(messages);
List<ChatChoice> choices = chatCompletionResponse.getChoices();
// 获取ChatGPT的回复
String message_assistant = choices.get(0).getMessage().getContent();
// 输出
System.out.println(message_assistant);
Message assistant = Message.builder().content(message_assistant).role(Message.Role.ASSISTANT).build();
messages.add(assistant);
// 记录进行限制 最多记录5条信息
if(messages.size() > 5){
messages = messages.subList(messages.size()-5,messages.size());
// 输出存储的messages序列中的内容
printInfoOfList(messages);
}
}
}catch (Exception e){
e.printStackTrace();
}
}
输出messages序列中的内容的方法如下:
public static void printInfoOfList(List<Message> messages){
for(Message e:messages){
System.out.println(e.getRole() + "-->" + e.getContent());
}
}
启动,运行,结果如下:
至于流式输出,并不建议通过上述方法来实现用户交互,通常是通过WebSocket协议建立双向通信通道使得数据能够实时的进行交互,这一点后续会带着读者一起实现。
至此,利用Java实现ChatGPT对话的大部分内容已经介绍完毕,后面的内容将介绍调用ChatGPT绘图功能和音频转文字功能。