Java 中的 OpenAI API 客户端

1、概览

随着生成式 AI 和 ChatGPT 的广泛应用,许多语言都开始提供与 OpenAI API 交互的库。Java 也不例外。

本文将带你了解 openai-java 库,它是一个开源的 OpenAI API 客户端,可以很方便地与 OpenAI API 通信。

2、依赖

首先,导入项目所需的 依赖,以下这三个模块专门用于交互的不同方面:

<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

3、Baeldung 辅导员

接下来,我们要构建一个工具,尝试根据我们最喜欢的学习平台上的文章和教程来创建课程表。虽然互联网为我们提供了无限的资源,我们几乎可以在线找到任何东西,但筛选信息却很困难。

4. OpenAI API Token

第一步是将应用连接到 OpenAI API。为此,需要提供一个 OpenAI Token,该 Token 可在 OpenAI 网站 上生成:

OpenAI API Token

注意,要小心保存你的 Token,避免暴露它。为此,openai-java 示例使用了环境变量。这可能不是用于生产的最佳解决方案,但在小型实验中完全可行。

在运行时,不一定需要为机器配置环境变量;大多数 IDE 都支持在运行时为应用单独设置环境变量,例如 IntelliJ IDEA。

我们可以生成两种 Token:个人(Personal)和服务账户(Service Account)。个人 Token 不言自明。服务账户 Token 用于连接到 OpenAI 项目的机器人或应用。虽然两者都可以使用,但对于我们的目的来说,个人 Token 已经足够了。

5、OpenAiService

OpenAI API 的入口是名为 OpenAiService 的类,非常方便。通过该类的实例,我们可以与 API 进行交互,并接收来自 ChatGPT 的响应。要创建该类,需要传递在上一步中生成的 Token:

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

5.1、ChatCompletionRequest

使用 ChatCompletionRequest 创建一个请求。最低限度的 设置 要求我们只提供消息和一个模型:

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

让我们一步步回顾一下这些参数。

5.2、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;
}

它并不包含所有的模型,但一般情况下,这已经足够了。如果想使用不同的模型,可以将其名称作为 String 提供。

5.3、Message

接下来是创建的消息。使用的是 ChatMessage 类。在本例中,只传递 role(角色)和 message(消息)本身:

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

这里发送的是一组消息,尽管在通常的聊天中,我们是一条一条地发送信息,但在这种情况下,它更类似于电子邮件 Threads

系统在完成后继续追加下一条消息到消息链中。这样,我们可以保持对话的上下文。我们可以将其视为一个无状态的服务。然而,这意味着我们必须传递消息以保持上下文。

同时,我们还可以另辟蹊径,创建一个 assistant(助手)。通过这种方法,我们可以将消息存储在 Threads 中,而且不需要来回发送整个历史记录。

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

5.4、Role

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

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 角色指的是初始上下文或提示。USER 代表 ChatGPT 的用户,而 ASSISTANT 本身就是一个 ChatGPT。这意味着,从技术上讲,我们也可以从 ASSISTANT 的角度编写消息。顾名思义,FUNCTION 角色确定了助手可以使用的功能。

5.5、Token

在模型和消息的上下文中的 Token 和上文所述的 API 的访问 Token 含义不同。可以把 Token 看作是我们可以处理的消息量和我们希望得到的响应量。

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

ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest
  .builder()
  .model(MODEL)
  .maxTokens(MAX_TOKENS)  // 限制 Token
  .messages(messages)
  .build();

由于每个模型对单词和 Token 的处理略有不同,它们之间没有直接的映射关系。这个参数限制了回答的 Token 数量。使用默认值可能会导致过度回复,并增加使用费用。因此,明确配置这个参数是一个好的实践。

在每个响应后获取已使用 Token 的信息:

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

5.6、Tokenization

上一个示例中输出了响应中使用的 Token 数量。虽然这一信息很有价值,但我们往往还需要估算请求的大小。为此,可以使用 OpenAI 提供的 Tokenizers

为了以更自动化的方式完成这项工作,openai-java 为我们提供了 TikTokensUtil,我们可以将模型名称和消息传递给它,然后得到 Token 的数量。

5.7、配置

还可以使用一个名为 n() 的神秘方法来配置请求。它可以控制每个请求得到多少个响应。简而言之,可以为同一个请求提供两个不同的响应。默认情况下,只有一个。

有时,它对机器人和网站助手也很有用。不过,响应是根据所有选项的 Token 计费的。

5.8、偏差和随机性

我们可以使用一些附加选项来控制 ChatGPT 答案的随机性和偏差。例如,logitBias() 可以提高看到或看不到特定 Token 的概率。注意,这里说的是 Token 而不是特定的单词。但是,这并不意味着这个 Token 不会 100% 出现。

此外,还可以使用 topP()temperature() 来随机化响应。虽然这在某些情况下很有用,但本文用不着。

6、课程

现在,检查一下工具的运行情况。整体代码如下

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.

和预期的一样,该工具会提供一些我们可以用来了解相关主题的文章:

Great! Here's a suggested order for Baeldung's articles on binary trees:

1. Introduction to Binary Trees: https://www.baeldung.com/java-binary-tree-intro
2. Implementing a Binary Tree in Java: https://www.baeldung.com/java-binary-tree
3. Depth First Traversal of Binary Tree: https://www.baeldung.com/java-depth-first-binary-tree-traversal
4. Breadth First Traversal of Binary Tree: https://www.baeldung.com/java-breadth-first-binary-tree-traversal
5. Finding the Maximum Element in a Binary Tree: https://www.baeldung.com/java-binary-tree-maximum-element
6. Binary Search Trees in Java: https://www.baeldung.com/java-binary-search-tree
7. Deleting from a Binary Search Tree: https://www.baeldung.com/java-binary-search-tree-delete

I hope this helps you out!
Total tokens used: 288
Anything else?

这样,我们就通过创建课程和学习新知识来解决问题。然而,并非一切都那么美好,问题在于只有一篇文章是真实的。在大多数情况下,ChatGPT 列出了不存在的文章,并附有相应的链接。虽然这些名称和链接听起来很合理,但它们不会给我们带来任何帮助。

这是任何 AI 工具的一个重要方面。生成模型很难检查信息的有效性。由于生成模型的基础是预测和挑选最合适的下一个词,因此可能很难验证信息的正确性。我们不能百分之百地依赖于生成模型的信息

7、总结

从处理电子邮件、创建购物清单到优化教育,AI 工具都能帮助我们改进应用和自动化日常琐事。Java 提供了几种与 OpenAI API 交互的方法,如 openai-java 客户端库。

不过,重要的是要记住,生成模型尽管很有说服力,却很难验证信息是否真实。因此,我们有责任重新检查关键信息,或者为模型提供足够的信息,使其能够给出有效的答案。


Ref:https://www.baeldung.com/java-openai-api-client