Java中OpenAI API客戶端原始碼教程

banq發表於2024-05-25

隨著生成式人工智慧和 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(<font>"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(<font>"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(<font>"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(<font>"Total tokens used: " + usedTokens);

標記化
在上一個示例中,我們顯示了響應中使用的令牌數量。雖然這些資訊很有價值,但我們通常也需要估計請求的大小。為此,我們可以使用OpenAI 提供的標記器。

為了以更自動化的方式做到這一點,openai-java 為我們提供了TikTokensUtil,我們可以向其傳遞模型名稱和訊息並獲取令牌的數量。

我們可以使用另一種方法來配置我們的請求,這個方法的名字很神秘,叫做n()。它控制我們想要為每個請求獲取多少個響應。簡而言之,我們可以對同一個請求有兩個不同的答案。預設情況下,我們只有一個。

有時,它可能對機器人和網站助手有用。但是,響應是根據所有選項中的令牌計費的。

偏見和隨機化
我們可以使用一些附加選項來控制 ChatGPT 答案的隨機性和偏差。例如,logitBias()可以使看到或看不到特定標記的可能性更大。請注意,我們在這裡談論的是標記,而不是特定的單詞。然而,這並不意味著這個標記不會 100% 出現。

此外,我們可以使用topP() 和temperature() 來隨機化響應。雖然在某些情況下這很有用,但我們不會更改學習工具的預設值。

程式碼類
現在,讓我們檢查一下我們的工具是否有效。我們將獲得以下總體程式碼:

public static void main(String[] args) {
    String token = System.getenv(<font>"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% 依賴生成模型提供的資訊。

相關文章