Java中OpenAI API客户端源码教程

随着生成式人工智能和 ChatGPT 的广泛使用,许多语言已经开始提供与其OpenAI API交互的库。Java 也不例外。

在本教程中,我们将讨论openai-java。这是一个允许更方便地与 OpenAI API 通信的客户端。但是,在一篇文章中回顾整个库是不可能的。因此,我们将使用一个实际示例并构建一个连接到 ChatGPT 的简单控制台工具。

依赖项
首先,我们必须导入项目所需的依赖项。我们可以在Maven 存储库中找到这些库。这三个模块专用于交互的不同方面:

<dependency>
    <groupId>com.theokanning.openai-gpt3-java</groupId>
    <artifactId>service</artifactId>
    <version>0.18.2</version>
</dependency>
<dependency>
    <groupId>com.theokanning.openai-gpt3-java</groupId>
    <artifactId>api</artifactId>
    <version>0.18.2</version>
</dependency>
<dependency>
    <groupId>com.theokanning.openai-gpt3-java</groupId>
    <artifactId>client</artifactId>
    <version>0.18.2</version>
</dependency>

请注意,名称明确提到了 GPT3,但它也适用于 GPT4 。


在本教程中,我们将构建一个工具,帮助我们根据我们最喜欢的学习平台的文章和教程创建课程,或者至少尝试这样做。虽然互联网为我们提供了无限的资源,我们几乎可以在网上找到任何东西,但整理信息却变得更加困难。

尝试学习新事物变得越来越令人不知所措,因为很难确定最佳的学习路径并过滤掉对我们没有好处的东西。为了解决这个问题,我们将构建一个简单的客户端来与 ChatGPT 交互,并要求它在浩瀚的 jdon文章海洋中为我们指引方向。

OpenAI API 代币
第一步是将我们的应用程序连接到 OpenAI API。为此,我们需要提供一个 OpenAI 令牌,该令牌可以在网站上生成。


但是,我们应该小心使用token,避免暴露它。openai -java示例为此使用了环境变量。这可能不是生产的最佳解决方案,但它对于小型实验很有效。

在运行过程中,我们不一定需要识别整个机器的环境变量;我们可以使用 IDE 中的配置。例如,IntelliJ IDEA 提供了一种简单的方法。

我们可以生成两种类型的代币:个人代币和服务账户。个人代币的含义不言自明。服务账户代币用于可以连接到 OpenAI 项目的机器人或应用程序。虽然两者都可以,但个人代币足以满足我们的目的。

OpenAiService
OpenAI API 的入口点是名为OpenAiService 的类。此类的实例允许我们与 API 交互并接收来自 ChatGPT 的响应。要创建它,我们应该传递上一步中生成的令牌:

String token = System.getenv("OPENAI_TOKEN");
OpenAiService service = new OpenAiService(token);

这是我们旅程的第一步;我们需要识别信息并填充请求。

聊天完成请求
我们使用ChatCompletionRequest创建请求。最小设置仅要求我们提供消息和模型:

ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest
  .builder()
  .model(GPT_3_5_TURBO_0301.getName())
  .messages(messages)
  .build();

让我们逐步回顾这些参数。

model
选择适合我们要求的模型至关重要,而且它也会影响成本。因此,我们需要做出合理的选择。例如,通常不需要使用最先进的模型来清理文本或基于一些简单的格式对其进行解析。同时,更复杂或更重要的任务需要更先进的模型来实现我们的目标。

虽然我们可以直接传递模型名称,但最好使用 ModelEnum :

@Getter
@AllArgsConstructor
public enum ModelEnum {         
    GPT_3_5_TURBO("gpt-3.5-turbo"),
    GPT_3_5_TURBO_0301(
"gpt-3.5-turbo-0301"),
    GPT_4(
"gpt-4"),
    GPT_4_0314(
"gpt-4-0314"),
    GPT_4_32K(
"gpt-4-32k"),
    GPT_4_32K_0314(
"gpt-4-32k-0314"),
    GPT_4_1106_preview(
"gpt-4-1106-preview");
    private String name;
}

它不包含所有模型,但对于我们来说,这已经足够了。如果我们想使用不同的模型,我们可以将其名称作为字符串提供。

message
接下来是我们创建的消息。我们使用ChatMessage类来实现它。在我们的例子中,我们只传递角色和消息本身:

List<ChatMessage> messages = new ArrayList<>();
ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), PROMPT);
messages.add(systemMessage);

有趣的是,我们发送的是一组消息。虽然在通常的聊天中,我们是通过逐条发送消息进行交流的,但在这种情况下,它更类似于电子邮件线程。

系统在完成后工作并将下一条消息附加到链中。这样,我们就可以维护对话的上下文。我们可以将其视为无状态服务。但是,这意味着我们必须传递消息以保留上下文。

同时,我们可以换一种方式,创建一个助手。通过这种方法,我们可以将消息存储在线程中,而不需要来回发送整个历史记录。

在传递过程中,消息的内容是合理的,但角色的目的却不合理。因为我们一次发送所有消息,所以我们需要提供某种方法来根据角色识别消息与用户之间的关系。

角色
如前所述,角色对于 ChatGPT 理解对话背景至关重要。我们可以用它们来识别消息背后的参与者。这样,我们可以帮助 ChatGPT 正确解释消息。ChatMessages支持四个角色:聊天、系统、助手和功能:

public enum ChatMessageRole {
    SYSTEM("system"),
    USER(
"user"),
    ASSISTANT(
"assistant"),
    FUNCTION(
"function");
    private final String value;
    ChatMessageRole(final String value) {
        this.value = value;
    }
    public String value() {
        return value;
    }
}

通常,SYSTEM 角色指的是初始上下文或提示。用户代表 ChatGPT 的用户,而助手本身就是一个 ChatGPT。这意味着,从技术上讲,我们也可以从助手的角度编写消息。顾名思义,功能角色标识了助手可以使用的功能。

访问令牌token
虽然我们之前讨论过 API 的访问令牌,但在模型和消息的上下文中,其含义有所不同。我们可以将令牌视为我们可以处理的信息量以及我们想要获得的响应量。

我们可以通过限制响应中的标记数量来限制模型生成巨大的响应:

ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest
  .builder()
  .model(MODEL)
  .maxTokens(MAX_TOKENS)
  .messages(messages)
  .build();

单词和标记之间没有直接映射,因为每个模型对它们的处理略有不同。此参数将答案限制为特定数量的标记。使用默认值可能会允许过多的响应并增加使用费用。因此,明确配置它是一种很好的做法。

我们可以在每个响应后添加有关已用令牌的信息:

long usedTokens = result.getUsage().getTotalTokens();
System.out.println("Total tokens used: " + usedTokens);

标记化
在上一个示例中,我们显示了响应中使用的令牌数量。虽然这些信息很有价值,但我们通常也需要估计请求的大小。为此,我们可以使用OpenAI 提供的标记器。

为了以更自动化的方式做到这一点,openai-java 为我们提供了TikTokensUtil,我们可以向其传递模型名称和消息并获取令牌的数量。

我们可以使用另一种方法来配置我们的请求,这个方法的名字很神秘,叫做n()。它控制我们想要为每个请求获取多少个响应。简而言之,我们可以对同一个请求有两个不同的答案。默认情况下,我们只有一个。

有时,它可能对机器人和网站助手有用。但是,响应是根据所有选项中的令牌计费的。

偏见和随机化
我们可以使用一些附加选项来控制 ChatGPT 答案的随机性和偏差。例如,logitBias()可以使看到或看不到特定标记的可能性更大。请注意,我们在这里谈论的是标记,而不是特定的单词。然而,这并不意味着这个标记不会 100% 出现。

此外,我们可以使用topP() 和temperature() 来随机化响应。虽然在某些情况下这很有用,但我们不会更改学习工具的默认值。

代码类
现在,让我们检查一下我们的工具是否有效。我们将获得以下总体代码:

public static void main(String[] args) {
    String token = System.getenv("OPENAI_TOKEN");
    OpenAiService service = new OpenAiService(token);
    List<ChatMessage> messages = new ArrayList<>();
    ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), PROMPT);
    messages.add(systemMessage);
    System.out.print(GREETING);
    Scanner scanner = new Scanner(System.in);
    ChatMessage firstMsg = new ChatMessage(ChatMessageRole.USER.value(), scanner.nextLine());
    messages.add(firstMsg);
    while (true) {
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest
          .builder()
          .model(GPT_3_5_TURBO_0301.getName())
          .messages(messages)
          .build();
        ChatCompletionResult result = service.createChatCompletion(chatCompletionRequest);
        long usedTokens = result.getUsage().getTotalTokens();
        ChatMessage response = result.getChoices().get(0).getMessage();
        messages.add(response);
        System.out.println(response.getContent());
        System.out.println(
"Total tokens used: " + usedTokens);
        System.out.print(
"Anything else?\n");
        String nextLine = scanner.nextLine();
        if (nextLine.equalsIgnoreCase(
"exit")) {
            System.exit(0);
        }
        messages.add(new ChatMessage(ChatMessageRole.USER.value(), nextLine));
    }
}

如果我们运行它,我们可以通过控制台与它交互:

Hello!
What do you want to learn?

作为回应,我们可以写下我们感兴趣的主题:

$ I would like to learn about binary trees.

正如预期的那样,该工具会为我们提供一些可用于了解主题的文章

这样,我们通过创建课程和学习新知识解决了这个问题。然而,事情并非如此光明;问题是只有一篇文章是真实的。在大多数情况下,ChatGPT 列出了不存在的文章并附有适当的链接。虽然名称和链接听起来很合理,但它们不会带我们去任何地方。

这是任何 AI 工具的关键方面。生成模型很难检查信息的有效性。由于它们基于预测和挑选最合适的下一个单词,因此它们可能很难验证信息。我们不能 100% 依赖生成模型提供的信息。