これは、なにをしたくて書いたもの?
これまでよくOpenAI API互換のサーバーにOpenAI Python APIライブラリーからアクセスして試していたのですが、1度Javaからも
アクセスしてみようかなと思いまして。
アクセス先としては、llama-cpp-pythonを使うことにします。
OpenAI API向けのJavaクライアント
OpenAI APIのJavaクライアントにアクセスするためのライブラリーの話に…入る前に、ちょっと自分がここをやろうとしている理由について
少し書いておきます。
自分はよくOpenAI API互換のサーバーをローカルで動かしていますが、OpenAIに限らずローカルLLMを使いたい場合はPythonからの
アクセスが基本になると思います。直接実行する場合もあると思いますが。
それはそれでよいのですが、テキスト埋め込みはベクトル検索でも使うのでこれ単体で勉強しようと思った時にはベクトル化の手段を
持っておく必要があります。で、Javaでやる場合はどうしようかな?と。
ここで、著名なJavaライブラリーがどのような方法を取っているか確認してみます。
Spring AIはOpenAI、Ollama、Azure OpenAI、PostgresML、Google VertexAI PaLM2、Amazon Bedrock、Transformers(ONNX)、
Mistral AIといった感じで外部サービスに頼る形態になっています。
Embeddings API :: Spring AI Reference
LangChain4jではどうでしょうか。
こちらはインプロセス(ONNX)、Amazon Bedrock、Azure OpenAI、DashScope、Google Vertex AI、HuggingFace、LocalAI、Mistral AI、
Nomic、Ollama、OpenAI、Qianfan、ZhipuAIといった感じでやっぱり外部サービスに頼る感じになっていますね。
Embedding Models | LangChain4j
やっぱりこの分野はJava内では完結しませんよね。
というわけで、なにか別のプロセスやAPIを使う手段を押さえておくとよさそうです。
となると、OpenAI API互換のサーバーにアクセスするのが自分としては今後もやりやすいのかなと。
では、OpenAIのドキュメントで紹介されているJavaクライアントを見ておきましょう。
オフィシャルクライアントというものはありません。コミュニティが開発しているOpenAI-Javaというライブラリーが挙げられています。
GitHub - TheoKanning/openai-java: OpenAI Api Client in Java
なお、2024年になってからは更新されていないみたいです…。リポジトリーの状態もちょっと微妙です…。
What is the status of this library❓❓ · Issue #491 · TheoKanning/openai-java · GitHub
他のライブラリーもこちらを使っているのでしょうか?
Spring AIは自分で実装しているようです。
LangChain4jでは別のライブラリーを使っているようです。
https://github.com/langchain4j/langchain4j/blob/0.30.0/langchain4j-open-ai/pom.xml#L19-L22
こちらのJava client library for OpenAI APIですね。リポジトリー名を見るとOpenAI4jと呼んでもよさそうですが。
GitHub - ai-for-java/openai4j: Java client library for OpenAI API
こちらもそんなに開発が活発というわけではなさそうです…。
なお、どちらのライブラリーもRetrofitを使っています(OpenAI-Javaのapiモジュールはまた別ですが)。
Spring AIが自前な理由がなんとなくわからないでもないですね。
自分でOpenAI APIのOpenAPIから自動生成してもいいのではないかという気もしますが。
GitHub - openai/openai-openapi: OpenAPI specification for the OpenAI API
せっかくならOpenAI APIを使えるようにと調べてみましたが、テキスト埋め込みをしたいだけだったらちょっと別の方法を考えた方が
いいかもですね。
どうするか迷いましたが、今回はいったんこのままOpenAI API路線で突き進むことにして、Java client library for OpenAI APIを
使うことにします。
GitHub - ai-for-java/openai4j: Java client library for OpenAI API
README.md
に使い方も書かれていますし。
OpenAI API互換のサーバーはllama-cpp-pythonとして、Java client library for OpenAI APIからチャットモデルにアクセスしてみることに
します。
環境
今回の環境はこちら。
$ java --version openjdk 21.0.2 2024-01-16 OpenJDK Runtime Environment (build 21.0.2+13-Ubuntu-122.04.1) OpenJDK 64-Bit Server VM (build 21.0.2+13-Ubuntu-122.04.1, mixed mode, sharing) $ mvn --version Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-105-generic", arch: "amd64", family: "unix"
llama-cpp-pythonを動作させる環境はこちら。
$ python3 --version Python 3.10.12 $ pip3 --version pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)
llama-cpp-pythonでOpenAI API互換のサーバーを起動する
まずはアクセス先のOpenAI API互換のサーバーを起動します。
llama-cpp-pythonのインストール。
$ pip3 install llama-cpp-python[server]
依存関係など。
$ pip3 list Package Version ----------------- ------- annotated-types 0.6.0 anyio 4.3.0 click 8.1.7 diskcache 5.6.3 exceptiongroup 1.2.1 fastapi 0.110.2 h11 0.14.0 idna 3.7 Jinja2 3.1.3 llama_cpp_python 0.2.65 MarkupSafe 2.1.5 numpy 1.26.4 pip 22.0.2 pydantic 2.7.1 pydantic_core 2.18.2 pydantic-settings 2.2.1 python-dotenv 1.0.1 PyYAML 6.0.1 setuptools 59.6.0 sniffio 1.3.1 sse-starlette 2.1.0 starlette 0.37.2 starlette-context 0.3.6 typing_extensions 4.11.0 uvicorn 0.29.0
モデルはGemmaの量子化済み2Bのものにします。
mmnga/gemma-2b-it-gguf · Hugging Face
ダウンロード。
$ curl -L https://huggingface.co/mmnga/gemma-2b-it-gguf/resolve/main/gemma-2b-it-q4_K_M.gguf?download=true -o gemma-2b-it-q4_K_M.gguf
起動。
$ python3 -m llama_cpp.server --model gemma-2b-it-q4_K_M.gguf --chat_format gemma
これでllama-cpp-pythonの準備は完了です。
Java client library for OpenAI API(OpenAI4j)からOpenAI API互換のサーバーへアクセスする
それでは、Java client library for OpenAI API(OpenAI4j)からOpenAI API互換のサーバーへアクセスしてみます。
まずはMaven依存関係など。
<properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>dev.ai4j</groupId> <artifactId>openai4j</artifactId> <version>0.17.0</version> </dependency> </dependencies>
現時点の最新版は0.17.0のようなのですが、リポジトリーにタグがありません…。
ソースコードの雛形はこんな感じで用意。
src/main/java/org/littlewings/openai/openai4j/OpenAi4jExample.java
package org.littlewings.openai.openai4j; import dev.ai4j.openai4j.OpenAiClient; import dev.ai4j.openai4j.chat.ChatCompletionModel; import dev.ai4j.openai4j.chat.ChatCompletionRequest; import dev.ai4j.openai4j.chat.ChatCompletionResponse; public class OpenAi4jExample { public static void main(String... args) { // ここにOpenAI APIを使うコードを書く } }
まずはOpenAIへアクセスするためのクライアントのインスタンスを作成します。
OpenAiClient client = OpenAiClient.builder() .openAiApiKey("dummy-api-key") .baseUrl("http://localhost:8000/v1") .build();
baseUrl
には、llama-cpp-pythonのエンドポイントを指定するのですが。/v1
まで指定しないとNot Foundになりました…。
Java client library for OpenAI API / Code examples / Create an OpenAI Client
Chat Completions APIへのアクセス。
Java client library for OpenAI API / Code examples / Chat Completions
どのAPIもそうなのですが、同期、非同期(コールバック)、ストリーミングの3種類の呼び出し方があります。
今回は同期で使います。
ChatCompletionRequest request = ChatCompletionRequest.builder() .model(ChatCompletionModel.GPT_3_5_TURBO) .addUserMessage("Could you introduce yourself?") .temperature(0.0) .build(); ChatCompletionResponse response = client.chatCompletion(request).execute(); System.out.printf("request -> response, message = %n%s%n", response.choices().getFirst().message().content());
Chat Completions APIでの例ですが、OpenAiClient#chatCompletion
の戻り値はSyncOrAsyncOrStreaming
という型になっており、
この後にexecute
を呼び出すと同期、onResponse
を呼び出すと非同期、onPartialResponse
を呼び出すとストリーミングというように
分岐します。
今回はexecute
を呼び出しているので同期ですね。
リクエストとレスポンスの型は、OpenAI API OpenAPIの定義が元になっているので他の言語のクライアントライブラリーと内容に
大差はありません。
ちなみに、OpenAiClient#chatCompletion
でユーザーのメッセージを渡すだけの簡単な使い方もあるみたいです。この時の戻り値は
メッセージのみになるようです。
System.out.printf( "simple request -> simple response, message = %n%s%n", client.chatCompletion("Could you introduce yourself?").execute() );
実装を見るとこんな感じになっていました。
@Override public SyncOrAsyncOrStreaming<String> chatCompletion(String userMessage) { ChatCompletionRequest request = ChatCompletionRequest.builder().addUserMessage(userMessage).build(); ChatCompletionRequest syncRequest = ChatCompletionRequest.builder().from(request).stream(null).build(); return new RequestExecutor<>( openAiApi.chatCompletions(syncRequest, apiVersion), ChatCompletionResponse::content, okHttpClient, formatUrl("chat/completions"), () -> ChatCompletionRequest.builder().from(request).stream(true).build(), ChatCompletionResponse.class, r -> r.choices().get(0).delta().content(), logStreamingResponses ); }
最後にシャットダウン。
client.shutdown();
では実行してみましょう。
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.openai.openai4j.OpenAi4jExample
結果。
request -> response, message = Hello! I am a large language model, trained by Google. I am a conversational AI that can assist you with a wide range of tasks, including answering questions, generating text, and translating languages. Is there anything specific I can help you with today? simple request -> simple response, message = Greetings! I'm a large language model, trained by Google. I'm here to assist you with a wide range of text and language tasks. How can I help you today?
OKそうですね。
簡単ですが、今回はこんな感じでおわりにしましょう。
おわりに
OpenAIのJavaライブラリーから、OpenAI API互換のサーバーへアクセスしてみるということでJava client library for OpenAI API(OpenAI4j)を
試してみました。
今回はこのライブラリーの使い方というより、JavaでのOpenAI周辺のライブラリーの状況がなんとなくわかったことの方が大きいかもですね。
自分は基本的にローカルLLMを使うので、アクセス方法や使うLLM自体ももう少し考えた方がよさそうだな、と思いました。