simple-openai 簡介
1. 概述
simple-openai程式庫是一個統一的、由社群驅動的 Java HTTP 用戶端,用於 OpenAI API 和相容的 API 提供者。它將底層 REST 呼叫封裝在一個一致且類型安全的 API 中,涵蓋聊天補全、工具、視覺、結構化輸出、嵌入以及一些較新的 OpenAI 功能。
使用此程式庫而非特定供應商的 SDK,可確保幾乎所有應用程式程式碼都獨立於特定模型供應商。該程式庫支援標準 OpenAI API,以及 Gemini Google、Gemini Vertex、Mistral、DeepSeek、Azure OpenAI 和 Anyscale。但是,並非所有供應商都透過simple-openai公開相同的功能集。
在本教程中,我們將建立一系列小型控制台應用程序,這些應用程式透過simple-openai與即時 LLM 進行通訊。在範例中,我們將使用 Google Gemini,透過免費的 API 金鑰進行操作,同時仍遵循適用於 OpenAI 的模式。
值得注意的是,我們將使用 Java 21。但是,最低支援版本是 Java 11。
2. 專案設定與庫配置
為了準備一個最小的 Java 入門項目,我們將新增使用simple-openai和 Gemini 所需的依賴項。
2.1. Maven 依賴項
讓我們從一個簡單的 Maven 專案開始。唯一需要的函式庫是simple-openai :
<dependencies>
<dependency>
<groupId>io.github.sashirestela</groupId>
<artifactId>simple-openai</artifactId>
<version>3.22.2</version>
</dependency>
</dependencies>
截至撰寫本文時,此版本為最新版本。但是,在將依賴項新增至新專案之前,**我們始終應該檢查Maven 倉庫中的最新版本**。事實上,這一點對於確保與最新的 API 變更相容尤為重要。
2.2. Gemini API 金鑰和curl測試
要使用 Gemini 運行範例,我們需要一個來自 Google AI Studio 的 API 金鑰。我們可以輕鬆免費取得一個。但是,免費套餐每天對程式碼範例中使用的 AI 模型的請求次數限制為 20 次。
建立金鑰後,我們將其儲存在名為GEMINI_API_KEY的環境變數中。我們可以將其定義在 IDE 的運行配置中,也可以定義在啟動 IDE 的 shell 腳本中,以便所有範例在執行時間都能讀取它。
在繼續之前,我們先快速檢查變數是否對 JVM 可見:
Logger logger = System.getLogger("simpleopenai");
logger.log(Level.INFO,
"GEMINI_API_KEY configured: {0}",
System.getenv("GEMINI_API_KEY") != null);
下一步的關鍵是驗證金鑰和本機環境能否成功存取 Gemini 端點。為此,我們創建了gemini-curl-tests.txt文件,其中包含一組curl測試,涵蓋了 Gemini 所有與 OpenAI 相容的功能。
2.3. 配置客戶端
simple-openai函式庫為每個 API 提供者提供了一個主客戶端類別。例如, SimpleOpenAI面向標準的 OpenAI API,而SimpleOpenAIGeminiGoogle則是針對 Gemini Google API 。每個用戶端都知道如何與相應的 HTTP 端點通信,並提供相同的高級入口點,例如聊天自動完成服務,具體取決於 API 提供者的支援情況。
為了避免在每個範例中重複客戶端配置,我們將其集中在一個小型輔助類別中,該類別還保存模型名稱和一個共用日誌記錄器:
public final class Client {
public static final Logger LOGGER = System.getLogger("simpleopenai");
public static final String CHAT_MODEL = "gemini-2.5-flash";
private Client() {
}
public static SimpleOpenAIGeminiGoogle getClient() {
String apiKey = System.getenv("GEMINI_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("GEMINI_API_KEY is not set");
}
return SimpleOpenAIGeminiGoogle.builder()
.apiKey(apiKey)
.build();
}
}
接下來的所有範例中,我們都假設客戶端使用var聲明,以防止程式碼依賴特定的提供者類別:
var client = Client.getClient();
這是合理的,因為不同的提供者類別對於它們共同擁有的服務(例如chatCompletions()共享相同的公共方法簽章。
如果以後我們決定從 Gemini 切換到 OpenAI 或其他提供相同服務的供應商,我們只需要更新此類中的getClient()實作和模型常數,並調整環境變數名稱即可。
3. 使用simple-openai控制台聊天用戶端
為了確認Client助理、環境和 Gemini 聊天端點都能正確協同工作,我們從一個基本範例開始。
3.1 單回合聊天完成
我們來創建一個小型控制台應用程序,該程式會向模型發送一個用戶問題並等待回應。在這種情況下,程式碼非常簡單簡潔:
ChatRequest chatRequest = ChatRequest.builder()
.model(Client.CHAT_MODEL)
.message(UserMessage.of(
"Suggest a weekend trip in Japan, no more than 60 words."
))
.build();
CompletableFuture<Chat> chatFuture =
client.chatCompletions().create(chatRequest);
Chat chat = chatFuture.join();
Client.LOGGER.log(Level.INFO, "Model reply: {0}", chat.firstContent());
我們來看一個範例回覆:
Model reply: Escape Tokyo to **Hakone** for a rejuvenating weekend! [...]
該模型可能會傳回 Markdown 格式的文本,因此我們可以預期控制台輸出將包含諸如**bold**或列表之類的標記。
現在,我們準備好創建一個真正的聊天機器人。
3.2. Java 中會話狀態的保持
要將單回合範例轉換為基本的控制台聊天機器人,我們只需將對話歷史記錄儲存在 Java 中,並將其與每個請求一起發送。
為了便於理解其工作原理,我們暫時用註解替換獲取和列印助手回應的程式碼:
List<ChatMessage> history = new ArrayList<>();
history.add(SystemMessage.of(
"You are a helpful travel assistant. Answer briefly."
));
try (Scanner scanner = new Scanner(System.in)) {
while (true) {
System.out.print("You: ");
String input = scanner.nextLine();
if (input == null || input.isBlank()) {
continue;
}
if ("exit".equalsIgnoreCase(input.trim())) {
break;
}
history.add(UserMessage.of(input));
ChatRequest.ChatRequestBuilder chatRequestBuilder =
ChatRequest.builder().model(Client.CHAT_MODEL);
for (ChatMessage message : history) {
chatRequestBuilder.message(message);
}
ChatRequest chatRequest = chatRequestBuilder.build();
// the next snippet goes here: it shows how to obtain the String "reply"
history.add(AssistantMessage.of(reply));
}
}
雖然while循環之前的初始SystemMessage是可選的,但它在控制響應風格、助手角色及其細節和個性方面起著重要作用。
我們也來看看取得和列印助手回應的程式碼:
CompletableFuture<Chat> chatFuture =
client.chatCompletions().create(chatRequest);
Chat chat = chatFuture.join();
String reply = chat.firstContent().toString();
Client.LOGGER.log(Level.INFO, "Assistant: {0}", reply);
關鍵是,這部分內容單獨成段,因為只有當我們討論串流媒體時,這部分內容才會改變。
交換幾個訊息後,可能會得到類似這樣的結果:
You: How can I get from New York to Tokyo?
Assistant: Fly.
You: Where can I stay overnight?
Assistant: Hotels, hostels, motels, or Airbnbs.
You: exit
由於我們在SystemMessage中要求的是簡短回答,所以回覆非常簡短。但是,如果我們要求的是更詳細的回复,那麼採用串流回復方式會更合適。
3.3. 切換到串流響應
對於較長的答案,通常最好是隨著訊息的到達逐步顯示輸出。使用simple-openai ,我們可以將create()替換為createStream() ,後者會傳回一個包含增量資料塊的Stream<Chat> 。
讓我們來看看獲取和列印助手回應的新程式碼:
CompletableFuture<Stream<Chat>> chatStreamFuture =
client.chatCompletions().createStream(chatRequest);
Stream<Chat> chatStream = chatStreamFuture.join();
StringBuilder replyBuilder = new StringBuilder();
chatStream.forEach(chunk -> {
String content = chunk.firstContent();
replyBuilder.append(content);
System.out.print(content);
});
String reply = replyBuilder.toString();
在進行測試之前,讓我們修改初始SystemMessage訊息,以便請求更詳細的回應:
history.add(SystemMessage.of(
"You are a helpful travel assistant. Answer in at least 150 words."
));
這些改變使我們能夠觀察到與最知名的聊天機器人(例如 ChatGPT)非常相似的行為。我們放慢了視訊播放速度,以便清晰地顯示控制台輸出中的資料流:
這種對話方式對於玩具聊天機器人來說尚可接受,但對於真正的旅行公司幾乎毫無用處。實際應用需要人工智慧模型與內部系統交互,以便返回旅行社提供的具體選項、價格和空房情況,而不是泛泛而談的建議。因此,我們將致力於彌補這一差距。
4. 呼叫功能:多國語言飯店預訂助手
工具呼叫是指 AI 模型存取內部系統、向自訂 Java 程式碼請求結構化操作並使用傳回的資料繼續對話的方式。
這也正是多語言支援在商業場景中至關重要的原因所在。即使系統指令和內部資料使用英語,只要底層模型支持,使用者通常也可以使用自己的語言進行交互,並收到該語言的回應。
為了使這種行為更加明確,並且更易於在不同模型之間移植,我們可以向初始SystemMessage中添加一行,例如Reply in the same language as the user 。
至此,我們可以建立一個飯店預訂助理。完整的實作過程比較繁瑣,所需的技能超出了simple-openai的範圍:
-
HotelService.java包含一個小型記憶體資料庫和兩個方法,一個用於搜索,一個用於預訂。 -
HotelFunctions.java透過FunctionExecutor將這些方法作為工具公開。 -
HandlingToolCallsInTheChatLoop.java實作了聊天循環,該循環偵測工具呼叫、執行工具呼叫並將結果回饋給模型。
也就是說,讓我們繼續來看simple-openai特有的程式碼部分。
4.1 虛假庫存和定價
為了方便驗證行為, HotelService從固定的飯店清單開始:
this.inventory = new ArrayList<>(List.of(
new Hotel("HTL-001", "Sakura Central Hotel", "Tokyo", 170, 2),
new Hotel("HTL-002", "Asakusa Riverside Inn", "Tokyo", 130, 3),
new Hotel("HTL-003", "Shinjuku Business Stay", "Tokyo", 110, 2),
new Hotel("HTL-004", "Gion Garden Hotel", "Kyoto", 160, 2),
new Hotel("HTL-005", "Kyoto Station Plaza", "Kyoto", 120, 3),
new Hotel("HTL-006", "Dotonbori Lights Hotel", "Osaka", 140, 2)
));
searchOffers()回報的報價採用簡單的定價規則:基本價格為每晚價格,每增加一位客人加收少量費用。這樣,控制台輸出的每晚價格很容易與庫存進行核對。
4.2. 將 Java 方法公開為工具
在simple-openai中,我們透過在FunctionExecutor中註冊FunctionDef條目來定義工具。每個工具都指向一個實作了Functional類,該類別的欄位就成為了 JSON 參數模式。
讓我們來看一個最簡化的註冊流程:
executor.enrollFunction(FunctionDef.builder()
.name("search_hotels")
.description("Search for available hotels given a city, check-in date, nights, and guests")
.functionalClass(SearchHotels.class)
.strict(Boolean.TRUE)
.build());
executor.enrollFunction(FunctionDef.builder()
.name("create_booking")
.description("Create a booking given a hotel id, check-in date, nights, guests, and guest name")
.functionalClass(CreateBooking.class)
.strict(Boolean.TRUE)
.build());
在執行時,模型會決定呼叫search_hotels還是create_booking 。代碼仍然是酒店可用性和價格的權威來源。
4.3. 處理聊天循環中的工具調用
我們將工具附加到請求中,發送請求,然後檢查模型回應中是否存在工具呼叫。如果存在工具調用,則執行這些調用並將ToolMessage條目新增至歷史記錄中,然後再次調用模型。
本質上,這個循環只有幾行程式碼:
for (ToolCall toolCall : toolCalls) {
Object result = functionExecutor.execute(toolCall.getFunction());
history.add(ToolMessage.of(toJson(result), toolCall.getId()));
}
某些與 OpenAI 相容的介面對工具呼叫有效負載要求嚴格,可能會拒絕包含意外null字段或缺少工具呼叫標識符的請求。為了提高工具循環在不同提供者之間的相容性,程式碼庫會在將工具呼叫傳送回模型之前對其進行清理。
4.4 範例運行
所有組件連接完畢後,當資料缺失時,助手可以詢問後續問題。它還可以重複使用先前訊息中的上下文資訊來填充隱含參數,而無需再次詢問。
讓我們記錄輸出中的每個toolCall ,以了解幕後發生了什麼:
You: Please find a hotel in Tokyo, check-in 2026-01-10, 7 nights, 2 guests.
[...] Tool call: search_hotels with args:
{"checkIn":"2026-01-10","nights":7,"guests":2,"city":"Tokyo"} [...]
Assistant: I found 3 hotels for you:
* Shinjuku Business Stay: $135 per night, total price $945
* Asakusa Riverside Inn: $155 per night, total price $1085
* Sakura Central Hotel: $195 per night, total price $1365
Which one would you like to book?
You: I would like to book a room at the Sakura Central Hotel.
Assistant: What is your full name?
You: Francesco Galgani
[...] Tool call: create_booking with args:
{"hotelId":"HTL-001","guests":2,"checkIn":"2026-01-10","nights":7,"guestName":"Francesco Galgani"} [...]
Assistant: Thank you, your booking for Sakura Central Hotel is confirmed. Booking ID: BK-1DE60AFC.
You: I also need a room in Kyoto for the same guests for two nights immediately following our stay in Tokyo.
[...] Tool call: search_hotels with args:
{"nights":2,"checkIn":"2026-01-17","guests":2,"city":"Kyoto"} [...]
Assistant: I found two hotels for you in Kyoto:
* Kyoto Station Plaza: $145 per night, total price $290
* Gion Garden Hotel: $185 per night, total price $370
Which one would you like to book?
You: The second one is fine.
[...] Tool call: create_booking with args:
{"checkIn":"2026-01-17","hotelId":"HTL-004","nights":2,"guests":2,"guestName":"Francesco Galgani"} [...]
Assistant: Thank you, your booking for Gion Garden Hotel is confirmed. Booking ID: BK-2BD025D3.
我們用不同的語言重複了測試,以驗證多語言支援是否按預期工作,同時保持工具呼叫邏輯不變。
5. 結論
在本文中,我們建立了一組基於控制台的 Java 範例,這些範例透過simple-openai庫呼叫 OpenAI 相容的 API 。我們首先實作了一個簡單的單回合聊天自動完成功能,然後保留了對話狀態以實現一個基本的聊天機器人,最後切換到串流以逐步顯示更長的答案。
我們還整合了工具呼叫功能,以突破通用回應的限制,讓助手能夠使用 Java 執行結構化操作,並以簡單的飯店預訂場景為例進行說明。這種模式是實際應用的基礎,因為它將業務邏輯和資料保留在系統內部,而模型則專注於語言理解和對話。
與往常一樣,本文的完整程式碼可在 GitHub 上找到。