Java on Azure Day 2024

このエントリは2024/06/05現在の情報に基づいています。将来の機能追加や変更に伴い、記載内容からの乖離が発生する可能性があります。

2024/06/05のオンラインイベント、Java on Azure Day 2024で、LangChain4j (LangChain for Java、LC4J) をAzure OpenAI Service (AOAI) と組み合わせて利用するテーマでスピーカーを務めた。当日利用したスライドのベースとなったものは以下にある。

基本的な使い方は上記スライドにまとめたので、このエントリではLC4JをAOAIと組み合わせて使う場合に、2024/06/05時点でハマりそうなところや、個別にお問い合わせいただいたことをメモとして残しておく。この時点でのLC4Jのバージョンは0.31.0。将来LC4JならびにAOAI SDKの改良により、改善される可能性に期待したい(自分でコントリビュートしてもいいかも)。

アプリケーションをGraalVMでNative Imageにできるか?

ビルド時初期化を抑止する必要のあるクラスが一部存在するので、--initialize-at-run-time= を使って明示的にビルド時の初期化を抑止すれば、理論上は可能。ただ、AOAI SDKに含まれる依存関係が複雑…。

Specify Class Initialization Explicitly
https://www.graalvm.org/latest/reference-manual/native-image/guides/specify-class-initialization/

LC4JをMicronaut + Netty Serverで使うときの注意点

プレゼンテーションにおいても、LC4Jは特定の開発フレームワークに依存するものではない、というコメントをしたが、1点、Micronaut+Netty ServerでLC4J + AOAI の構成で使う場合には注意点がある。

Micronautの場合、デフォルトの挙動は、エンドユーザーの操作はリクエストを実行するスレッドと同じスレッドで実行する。ただ、Netty Serverを使っていて、以下のようなエラー&例外(BlockingがNetty Server上でサポートされていない)が出ている場合、ブロッキングにするか、I/OタスクをスケジュールするExecutorServiceにタスク実行を移しておく必要がある。

09:58:00.188 [default-nioEventLoopGroup-1-2] ERROR c.azure.core.http.policy.RetryPolicy - block()/blockFirst()/blockLast() are blocking, which is not supported in thread default-nioEventLoopGroup-1-2
...
09:58:00.194 [default-nioEventLoopGroup-1-2] ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: block()/blockFirst()/blockLast() are blocking, which is not supported in thread default-nioEventLoopGroup-1-2
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread default-nioEventLoopGroup-1-2
	at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:86)
	at reactor.core.publisher.Mono.block(Mono.java:1712)
	at com.azure.core.http.netty.NettyAsyncHttpClient.sendSync(NettyAsyncHttpClient.java:199)
	at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:51)
	at com.azure.core.http.policy.HttpLoggingPolicy.processSync(HttpLoggingPolicy.java:186)
	at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
	at com.azure.core.implementation.http.policy.InstrumentationPolicy.processSync(InstrumentationPolicy.java:95)
	at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
	at com.azure.core.http.policy.KeyCredentialPolicy.processSync(KeyCredentialPolicy.java:115)
	at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
	at com.azure.core.http.policy.CookiePolicy.processSync(CookiePolicy.java:73)
	at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
...
10:02:22.901 [default-nioEventLoopGroup-1-4] ERROR i.m.h.s.netty.RoutingInBoundHandler - Micronaut Server Error - No request state present. Cause: An established connection was aborted by the software in your host machine
java.io.IOException: An established connection was aborted by the software in your host machine
	at java.base/sun.nio.ch.SocketDispatcher.read0(Native Method)
	at java.base/sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:46)
	at java.base/sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:340)
	at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:294)
	at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:269)
	at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:425)
	at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:255)
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132)
	at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:356)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:994)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:1583)

具体的には、@ExecuteOn(TaskExecutors.IO)@ExecuteOn(TaskExecutors.BLOCKING) を指定する。以下はMicronautでDALL-E3にイメージを作成指示している例。

@Get("/dall-e")
@Produces(MediaType.APPLICATION_JSON)
@ExecuteOn(TaskExecutors.BLOCKING)
List<Map<String, String>> createImage(
        @QueryValue(value = "u",
                    defaultValue = """
                    夏の終わりの夕暮れ時に、海辺をドライブする様子を
                    アメリカンポップアート風に描いてください。
                    """) String question) {
    Response<Image> image = imageModel.generate(question);
    return List.of(Map.of("question", question),
                         Map.of("image", image.content().url().toString()));
}

なお、StreamingChatLanguageModelを使っている場合には、この対策は不要。もし、

同期処理だけれども、ChatLanguageModelを使わずにStreamingChatLanguageModelを使い、結果を onComplete()メソッドで取得すればいいじゃない

というのであれば、無視してもよい。その他、Jetty、Tomcat、Undertowでは発生しない。

デモアプリケーションをSpring Bootで作らなかった理由

後からよく尋ねられた質問の一つがこれ。

「Spring Bootのほうが利用例が多いのに…」
「ふだんSpring Bootで開発しているから、違うフレームワークでの実装を見せられても…」

とのコメントを社内外からもらった。実際のところ、Spring Bootでもよかったのだが、以下の理由でQuarkusを使った。

  • 修正とビルドを繰り返すので、QuarkusのDevモードが圧倒的に楽(これが最大の理由)
  • LC4Jの使い方を説明することが目的であり、特定開発フレームワーク固有の使い方の紹介ではない
    • 統合機能を使わなかったのもこれが理由で、隠蔽されすぎてLC4Jの使い方が余計にわかりづらくなる
    • 設定や構成が隠蔽されるよりは、明示的に表現できるほうが理解しやすい

その他、ビルドしたUber Jarが大きくなりすぎるとか、起動時間が遅いとか、小さな理由はいろいろあるけれども、そのあたりは大した話ではない。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください