Non-blocking and Reactive(ノンブロッキングとリアクティブ)
非同期およびリアクティブのサポートは実験的なものです。このページの非同期およびリアクティブ API には
@Experimental アノテーションが付いています。
API や動作は今後のリリースで変更される可能性があります。同期 API と TokenStream API には影響しません。
デフォルトでは、AI Service の呼び出しは、やり取り全体の間、呼び出しスレッドをブロックします。LLM の呼び出し、ツールの 実行、チャットメモリへのアクセス、ガードレールのすべてが、メソッドが戻る前に行われます。この方式はシンプルで、 ほとんどのアプリケーションでうまく機能します。
リアクティブなスタック(Quarkus/Mutiny、Vert.x、Spring WebFlux)上で構築している場合や、1 つのスレッドで 多数の同時のやり取りを処理する必要がある場合、LangChain4j は同じやり取りをブロックせずに実行できます。AI Service のメソッドで 異なる戻り値の型を宣言するだけで選択できます。インターフェースの他の部分は何も変更する必要はありません。
4 つのモード
| 戻り値の型 | 性質 |
|---|---|
String、POJO、Result<T>、… | 同期。呼び出しスレッドをブロックする |
TokenStream | コールバックによるストリーミング |
CompletableFuture<T>、CompletionStage<T> | 単一のレスポンス。ノンブロッキング |
Flow.Publisher<AiServiceStreamingEvent>、Flow.Publisher<String> | イベントのストリーム。ノンブロッキング |
interface Assistant {
// synchronous
String chat(String message);
// one response, without blocking the caller
CompletableFuture<String> chatAsync(String message);
// the answer, streamed token by token
Flow.Publisher<String> chatStreaming(String message);
// everything that happens during the interaction, as events
Flow.Publisher<AiServiceStreamingEvent> chatEvents(String message);
}
ブロックせずに 1 つのレスポンスを取得する
CompletableFuture<String> future = assistant.chatAsync("Tell me a joke");
future.thenAccept(System.out::println);
future をキャンセルする(future.cancel(true))と、呼び出し元が解放され、以降の LLM のラウンドが停止し、
実行中の HTTP 呼び出しの中断をベストエフォートで試みます。
future は、モデル自身のスレッド(HTTP モデルの場合は、レスポンスを読み取る
トランスポートの I/O ワーカー)で完了します。そのため、明示的な executor なしで追加された継続(thenApply、thenAccept など)は、
そのスレッドで実行され、そこでブロックすると、実行中のすべての呼び出しのスループットが低下します。継続はノンブロッキングに保つか、
独自の Executor を渡してください: future.thenApplyAsync(fn, executor)。
回答をストリーミングする
Flow.Publisher<String> を返すメソッドは、回答のテキストをチャンクごとにストリーミングします。
Flow.Publisher<String> publisher = assistant.chatStreaming("Tell me a joke");
publisher は**コールド(cold)**です。サブスクライブするまで何も起こらず、サブスクリプションごとに新しいやり取りが開始されます。
起きることすべてをストリーミングする
Flow.Publisher<AiServiceStreamingEvent> は、回答のテキストだけでなく、やり取り 全体を公開します。これは、コールバックベースの TokenStream が
提供するのと同じ情報です。
| イベント | 意味 |
|---|---|
PartialResponseEvent | 回答のチャンク |
PartialThinkingEvent | モデルの推論のチャンク |
PartialToolCallEvent、CompleteToolCallEvent | 組み立て中のツール呼び出しと、その完了 |
BeforeToolExecutionEvent、AfterToolExecutionEvent | 実行される直前のツールと、その結果 |
IntermediateResponseEvent | ツール呼び出しの 1 ラウンドを終えたレスポンス |
RetrievedContentsEvent | RAG で取得されたコンテンツ |
ToolCompensatedEvent | やり取りがキャンセルまたは失敗した後に、完了済みのツールが補償されたこと |
RawEvent | LangChain4j がマッピングしていない、プロバイダー固有のイベント |
FinalResponseEvent | 最終的な回答 |
assistant.chatEvents("What is the weather in Munich?").subscribe(new Flow.Subscriber<>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(AiServiceStreamingEvent event) {
// the event types are nested in AiServiceStreamingEvent:
// import dev.langchain4j.service.AiServiceStreamingEvent.PartialResponseEvent;
if (event instanceof PartialResponseEvent partial) {
System.out.print(partial.partialResponse().text());
}
}
@Override
public void onError(Throwable error) { }
@Override
public void onComplete() { }
});
新しいイベントタイプが今後追加される可能性があるため、認識できないイベントも適切に処理してください。default 分岐のない 網羅的な型 switch は書かないでください。
onNext の中でブロックしないでください。イベントは、それを生成したスレッドで配信されます。トークンレベルのイベントではモデルのトランスポートの I/O ワーカー、
ツール実行のイベントではツール呼び出しを完了したスレッドです。重い
イベントごとの処理は、独自の Executor にオフロードしてください。イベントは有界バッファを介して中継されるため、大きく遅れた
サブスクライバーは、無制限にバッファリングされる代わりに IllegalStateException で終了します。サイズはデフォルトで 16384 イベントで、
AiServices.builder(...).streamingBufferSize(int) で設定できます。
サードパーティのリアクティブ型
AI Service のメソッドは JDK の型(CompletableFuture と Flow.Publisher)を返すため、この API が
特定のリアクティブプログラミングライブラリに縛られることはありません。
Reactor の型は langchain4j-reactor モジュールを通じてサポートされています。 単一レスポンスのモードには Mono<T>、
リアクティブなモードには Flux<AiServiceStreamingEvent> を使います。依存関係を追加するだけで十分で、アダプターは
ServiceLoader を通じて自動的に登録されます。
interface Assistant {
Mono<String> answer(String userMessage);
Flux<AiServiceStreamingEvent> events(String userMessage);
}
Mutiny の Uni/Multi は、まだサポートされていません。接続点(ServiceLoader で検出される CompletableFutureAdapter と
PublisherAdapter の SPI)は用意されていますが、それらのアダプターは同梱されていないため、そのような
戻り値の型を宣言すると、現状では失敗します。CompletableFuture または Flow.Publisher を使い、呼び出し側で変換してください。
Flux<String> は、同じモジュール内の、より古い TokenStream ベースのアダプターが処理します。
AI Servicesを参照してください。これはこの機能より前から存在し、ここで説明している
ノンブロッキングのパスには含まれず、すべてのプロバイダーで動作します。
ノンブロッキングであるべきもの
ノンブロッキングは、すべてのレイヤーで成り立つ必要があります。どこか 1 か所でもブロックするステップがあると、呼び出し全体が再びブロッキングになります。各レイヤーには、 既存のブロッキングメソッドに加えて、対応する非同期のメソッドがあります。
| レイヤー | ブロッキング | ノンブロッキングの対応メソッド |
|---|---|---|
| チャットモデル | chat(ChatRequest) | chatAsync(ChatRequest)、および StreamingChatModel の Publisher を返す chat(ChatRequest) |
| 埋め込みモデル | embed(...) | embedAsync(...) |
| スコアリングモデル | scoreAll(...) | scoreAsync(...) |
| チャットメモリ | add、messages、set | addAsync、messagesAsync、setAsync |
| チャットメモリストア | getMessages、updateMessages、deleteMessages | getMessagesAsync、updateMessagesAsync、deleteMessagesAsync |
| ガードレール | validate(...) | validateAsync(...) |
| ツール | ToolExecutor.execute(...) | ToolExecutor.executeAsync(...) |
| RAG | RetrievalAugmentor.augment(...)、リトリーバー、ルーター、アグリゲーター、クエリトランスフォーマー | augmentAsync(...)、retrieveAsync(...)、routeAsync(...)、aggregateAsync(...)、transformAsync(...) |
| 埋め込みストア | search(...) | searchAsync(...) |
| Web 検索 | search(...) | searchAsync(...) |
| MCP | McpClient.executeTool(...) | executeToolAsync(...) |
| ガードレールの実行 | ChatExecutor.execute(...) | ChatExecutor.executeAsync(...) |
| HTTP クライアント | execute(...) | executeAsync(...)、stream(...) |
対応するメソッドを実装していないコンポーネントは、黙ってブロックするのではなく、明示的に失敗します。返される
future または publisher は、コンポーネント名と不足しているメソッドを示す AsyncNotSupportedException で失敗します。
AsyncNotSupportedException は UnsupportedFeatureException の一種であるため、1 つの catch 節で、どちらの
「サポートされない」ケースも捕捉でき、再試行されることはありません。
避けられないブロッキングコード
ツールは典型的なケースです。データベースやブロッキングする HTTP API を呼び出すツールを、ノンブロッキングにすることはできません。そのような ツールはオフロードされるため、モデル自身のスレッドをブロックすることはありません。
Java 21 以降では、オフロード用の executor は仮想スレッドを作成するため、ブロッキングするツールは仮想スレッドをパークさせます。
仮想スレッドはキャリアスレッドを占有せず、そこからアンマウントされます。LangChain4j のターゲットは Java 17 であり、Java 17 ~ 20 では同じ
executor は上限のないプラットフォームスレッドのプールになります。これは、負荷時のリソースの使われ方が大きく異なります。これが問題になる場合は、
独自の有界な Executor を指定してください(後述)。
これは @Tool アノテーションが付いたメソッドに適用されます。executeAsync をオーバーライドしていない手書きの ToolExecutor は、
オプトインしていない他のコンポーネントと同様に、代わりに AsyncNotSupportedException で明示的に失敗します。
ツールは、非同期およびリアクティブのモードでは、デフォルトで並行して実行されます。1 つずつ実行するには、 シングルスレッドの executor を渡します。
AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new MyTools())
.executeToolsConcurrently(Executors.newSingleThreadExecutor())
.build();
RAG では、retrieveAsync を実装していないリトリーバーは、黙ってブロックするのではなく、デフォルトで失敗します。代わりに、
DefaultRetrievalAugmentor または
EmbeddingStoreContentRetriever で offloadBlocking(true) を指定して、オフロードを選択できます。
同期モードとは異なるデフォルト
非同期およびリアクティブのモードは、同じやり取りを別のスレッドで実行しているだけではありません。いくつかのデフォルトは 意図的に異なっています。既存のメソッドを移行する場合は、これらを確認してください。
同期 / TokenStream | CompletableFuture / Flow.Publisher | |
|---|---|---|
| 複数のツール呼び出し | 順次実行される | 並行して実行される |
| ツールの実行エラー | LLM に返される | 呼び出しを失敗させる(例外が ToolErrorVisibleToLlm を実装している場合を除く) |
| ツールの引数の解析エラー | 呼び出しを失敗させる | LLM に返される |
@Moderate | サポートされる | AI Service の作成時に拒否される |
ツールのエラー
2 つのツールエラーのデフォルトは、意図的に逆になっています。実行の失敗を LLM に送ると、ツールのバグがあなたから隠され、
モデルがそれを回避して回答をでっち上げることを促してしまいます。そのため、非同期モードでは、例外自体が LLM に伝えてよい内容を示している場合を除き、
代わりに呼び出しを失敗させます
(ToolErrorVisibleToLlmを参照)。一方、不正な形式の引数文字列は、モデルが生成したものであり、
通常は指摘されれば修正できるため、
呼び出しを失敗させるのではなく、返送されます。
どちらも引き続き設定可能で、明示的に設定したハンドラーはすべてのモード で使用されます。
AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new MyTools())
.toolExecutionErrorHandler(myExecutionErrorHandler)
.toolArgumentsErrorHandler(myArgumentsErrorHandler)
.build();
これらのハンドラーでできることについては、エラー処理を参照してください。
ツールの並行性
これらのモードでは、ツールは常に Executor 上で実行されるため、1 つの LLM レスポンスに含まれる複数のツール呼び出しは、同時に
実行されます。ツールが並行して実行しても安全でない場合は、シングルスレッドの executor を渡してください。
executeToolsConcurrently(Executors.newSingleThreadExecutor())。
イベントの順序
リアクティブなパスでは、ツールは CompleteToolCallEvent が発行されるとすぐに開始されるため、ラウンドの残りの部分と
重なります。そのため、あるラウンドの BeforeToolExecutionEvent / AfterToolExecutionEvent が、そのラウンドの
IntermediateResponseEvent より前に届くことがあります。IntermediateResponseEvent はラウンドごとのマーカーとして扱い、
そのラウンドのすべてのツールイベントがそれより前に来ることを保証するバリアとは考えないでください。コールバックベースの TokenStream は、常に
中間レスポンスを最初に報告します。