チャットクライアント API

ChatClient は、AI モデルと通信するためのスムーズな API を提供します。同期プログラミングモデルとストリーミングプログラミングモデルの両方をサポートします。

Fluent API には、AI モデルに入力として渡されるプロンプトの構成要素を構築するためのメソッドがあります。Prompt には、AI モデルの出力と動作をガイドする指示テキストが含まれています。API の観点から見ると、プロンプトはメッセージのコレクションで構成されます。

AI モデルは、ユーザーからの直接入力であるユーザーメッセージと、会話をガイドするためにシステムによって生成されるシステムメッセージという 2 種類の主なメッセージを処理します。

これらのメッセージには、多くの場合、ユーザー入力に基づいて実行時に置換され、ユーザー入力に対する AI モデルのレスポンスをカスタマイズするプレースホルダーが含まれます。

使用する AI モデルの名前や、生成される出力のランダム性や創造性を制御する温度設定など、指定できるプロンプトオプションもあります。

ChatClient の作成

ChatClient は、ChatClient.Builder オブジェクトを使用して作成されます。任意の ChatModel Spring Boot 自動構成に対して自動構成された ChatClient.Builder インスタンスを取得するか、プログラムで作成することができます。

自動構成された ChatClient.Builder を使用する

最も単純な使用例では、Spring AI は Spring Boot の自動構成を提供し、クラスに挿入するためのプロトタイプ ChatClient.Builder Bean を作成します。以下は、単純なユーザーリクエストに対する String レスポンスを取得する簡単な例です。

@RestController
class MyController {

    private final ChatClient chatClient;

    public MyController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/ai")
    String generation(String userInput) {
        return this.chatClient.prompt()
            .user(userInput)
            .call()
            .content();
    }
}

この単純な例では、ユーザー入力によってユーザーメッセージの内容が設定されます。call() メソッドは AI モデルにリクエストを送信し、content() メソッドは AI モデルのレスポンスを String として返します。

プログラムで ChatClient を作成する

プロパティ spring.ai.chat.client.enabled=false を設定することで、ChatClient.Builder の自動構成を無効にすることができます。これは、複数のチャットモデルを一緒に使用する場合に便利です。次に、必要な ChatModel ごとにプログラムで ChatClient.Builder インスタンスを作成します。

ChatModel myChatModel = ... // usually autowired

ChatClient.Builder builder = ChatClient.builder(this.myChatModel);

// or create a ChatClient with the default builder settings:

ChatClient chatClient = ChatClient.create(this.myChatModel);

ChatClient 流れるような API

ChatClient Fluent API を使用すると、オーバーロードされた prompt メソッドを使用して Fluent API を開始し、3 つの異なる方法でプロンプトを作成できます。

  • prompt(): 引数のないこのメソッドを使用すると、Fluent API の使用を開始して、プロンプトのユーザー、システム、その他の部分を構築できます。

  • prompt(Prompt prompt): このメソッドは Prompt 引数を受け入れ、Prompt の非 Fluent API を使用して作成した Prompt インスタンスを渡すことができます。

  • prompt(String content): これは、前のオーバーロードに似た便利なメソッドです。ユーザーのテキストコンテンツを取得します。

ChatClient のレスポンス

ChatClient API は、Fluent API を使用して AI モデルからのレスポンスをフォーマットするいくつかの方法を提供します。

ChatResponse の返却

AI モデルからのレスポンスは、型 ChatResponse で定義されるリッチ構造です。レスポンスの生成方法に関するメタデータが含まれており、それぞれ独自のメタデータを持つ複数のレスポンス ( 世代と呼ばれる) を含めることもできます。メタデータには、レスポンスの作成に使用されたトークンの数 (各トークンは 1 ワードの約 3/4 です) が含まれます。ホスト型 AI モデルは、リクエストごとに使用されるトークンの数に基づいて課金されるため、この情報は重要です。

call() メソッドの後に chatResponse() を呼び出して、メタデータを含む ChatResponse オブジェクトを返す例を以下に示します。

ChatResponse chatResponse = chatClient.prompt()
    .user("Tell me a joke")
    .call()
    .chatResponse();

エンティティを返す

返された String からマップされたエンティティクラスを返す必要がある場合がよくあります。entity() メソッドはこの機能を提供します。

例: Java レコードが与えられた場合:

record ActorFilms(String actor, List<String> movies) {}

以下に示すように、entity() メソッドを使用して AI モデルの出力をこのレコードに簡単にマッピングできます。

ActorFilms actorFilms = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorFilms.class);

また、シグネチャー entity(ParameterizedTypeReference<T> type) を持つオーバーロードされた entity メソッドもあり、これを使用すると、ジェネリクスリストなどの型を指定できます。

List<ActorFilms> actorFilms = chatClient.prompt()
    .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorFilms>>() {});

ストリーミングレスポンス

stream() メソッドを使用すると、次に示すように非同期レスポンスを取得できます。

Flux<String> output = chatClient.prompt()
    .user("Tell me a joke")
    .stream()
    .content();

Flux<ChatResponse> chatResponse() メソッドを使用して ChatResponse をストリーミングすることもできます。

将来的には、リアクティブ stream() メソッドを使用して Java エンティティを返すことができる便利なメソッドを提供する予定です。それまでの間は、次に示すように、構造化出力コンバーターを使用して集約されたレスポンスを明示的に変換する必要があります。これは、ドキュメントの後のセクションで詳しく説明する Fluent API でのパラメーターの使用箇所も示しています。

var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<ActorsFilms>>() {});

Flux<String> flux = this.chatClient.prompt()
    .user(u -> u.text("""
                        Generate the filmography for a random actor.
                        {format}
                      """)
            .param("format", this.converter.getFormat()))
    .stream()
    .content();

String content = this.flux.collectList().block().stream().collect(Collectors.joining());

List<ActorFilms> actorFilms = this.converter.convert(this.content);

call() の戻り値

ChatClient で call() メソッドを指定した後、レスポンス型にはいくつかの異なるオプションがあります。

  • String content(): レスポンスの文字列コンテンツを返します

  • ChatResponse chatResponse(): 複数の世代と、レスポンスの作成に使用されたトークンの数など、レスポンスに関するメタデータを含む ChatResponse オブジェクトを返します。

  • entity() は Java 型を返す

    • entity(ParameterizedTypeReference<T> type): エンティティ型の Collection を返すために使用されます。

    • entity(Class<T> type): 特定のエンティティ型を返すために使用されます。

    • entity(StructuredOutputConverter<T> structuredOutputConverter)String をエンティティ型に変換するために StructuredOutputConverter のインスタンスを指定するために使用されます。

call() の代わりに stream() メソッドを呼び出すこともできます。

stream() の戻り値

ChatClient で stream() メソッドを指定した後、レスポンス型にはいくつかのオプションがあります。

  • Flux<String> content(): AI モデルによって生成される文字列の Flux を返します。

  • Flux<ChatResponse> chatResponse(): レスポンスに関する追加のメタデータを含む ChatResponse オブジェクトの Flux を返します。

デフォルトの使用

@Configuration クラスでデフォルトのシステムテキストを使用して ChatClient を作成すると、ランタイムコードが簡素化されます。デフォルトを設定すると、ChatClient を呼び出すときにユーザーテキストを指定するだけで済み、ランタイムコードパスで各リクエストに対してシステムテキストを設定する必要がなくなります。

デフォルトのシステムテキスト

次の例では、常に海賊の声で応答するようにシステムテキストを構成します。ランタイムコードでシステムテキストが繰り返されないようにするには、@Configuration クラスに ChatClient インスタンスを作成します。

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a Pirate")
                .build();
    }

}

そしてそれを呼び出すための @RestController です:

@RestController
class AIController {

	private final ChatClient chatClient;

	AIController(ChatClient chatClient) {
		this.chatClient = chatClient;
	}

	@GetMapping("/ai/simple")
	public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
		return Map.of("completion", this.chatClient.prompt().user(message).call().content());
	}
}

curl 経由でアプリケーションエンドポイントを呼び出すと、結果は次のようになります。

❯ curl localhost:8080/ai/simple
{"completion":"Why did the pirate go to the comedy club? To hear some arrr-rated jokes! Arrr, matey!"}

パラメーター付きのデフォルトのシステムテキスト

次の例では、システムテキスト内のプレースホルダーを使用して、設計時ではなく実行時に補完の音声を指定します。

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
                .build();
    }

}
@RestController
class AIController {
	private final ChatClient chatClient;

	AIController(ChatClient chatClient) {
		this.chatClient = chatClient;
	}

	@GetMapping("/ai")
	Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message, String voice) {
		return Map.of("completion",
				this.chatClient.prompt()
						.system(sp -> sp.param("voice", voice))
						.user(message)
						.call()
						.content());
	}

}

httpie 経由でアプリケーションエンドポイントを呼び出すと、結果は次のようになります。

http localhost:8080/ai voice=='Robert DeNiro'
{
    "completion": "You talkin' to me? Okay, here's a joke for ya: Why couldn't the bicycle stand up by itself? Because it was two tired! Classic, right?"
}

その他のデフォルト

ChatClient.Builder レベルでは、デフォルトのプロンプト構成を指定できます。

  • defaultOptions(ChatOptions chatOptions)ChatOptions クラスで定義されたポータブルオプション、または OpenAiChatOptions などのモデル固有のオプションを渡します。モデル固有の ChatOptions 実装の詳細については、JavaDocs を参照してください。

  • defaultFunction(String name, String description, java.util.function.Function<I, O> function)name は、ユーザーテキスト内の関数を参照するために使用されます。description は関数の目的を説明し、AI モデルが正確なレスポンスのために正しい関数を選択できます。function 引数は、必要に応じてモデルが実行する Java 関数インスタンスです。

  • defaultFunctions(String…​ functionNames): アプリケーションコンテキストで定義された `java.util.Function` の Bean 名。

  • defaultUser(String text)defaultUser(Resource text)defaultUser(Consumer<UserSpec> userSpecConsumer): これらのメソッドを使用すると、ユーザーテキストを定義できます。Consumer<UserSpec> では、ラムダを使用してユーザーテキストと既定のパラメーターを指定できます。

  • defaultAdvisors(Advisor…​ advisor): アドバイザーを使用すると、Prompt の作成に使用されるデータを変更することができます。QuestionAnswerAdvisor 実装では、プロンプトにユーザーテキストに関連するコンテキスト情報を追加することで、Retrieval Augmented Generation のパターンが有効になります。

  • defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer): このメソッドを使用すると、AdvisorSpec を使用して複数のアドバイザーを構成するための Consumer を定義できます。アドバイザーは、最終的な Prompt を作成するために使用されるデータを変更できます。Consumer<AdvisorSpec> を使用すると、ユーザーテキストに基づいて関連するコンテキスト情報をプロンプトに追加することで Retrieval Augmented Generation をサポートする QuestionAnswerAdvisor などのアドバイザーを追加するラムダを指定できます。

実行時に、default プレフィックスなしの対応するメソッドを使用して、これらのデフォルトをオーバーライドできます。

  • options(ChatOptions chatOptions)

  • function(String name, String description, java.util.function.Function<I, O> function)

  • functions(String…​ functionNames)

  • user(String text)user(Resource text)user(Consumer<UserSpec> userSpecConsumer)

  • advisors(Advisor…​ advisor)

  • advisors(Consumer<AdvisorSpec> advisorSpecConsumer)

アドバイザー

アドバイザー API は、Spring アプリケーションで AI 駆動型のインタラクションをインターセプト、変更、強化するための柔軟かつ強力な方法を提供します。

ユーザーテキストを使用して AI モデルを呼び出す場合の一般的なパターンは、プロンプトにコンテキストデータを追加または拡張することです。

このコンテキストデータにはさまざまな種類があります。一般的な種類は次のとおりです。

  • あなた自身のデータ : これは、AI モデルがトレーニングされていないデータです。モデルが同様のデータを見たことがある場合でも、レスポンスの生成では追加されたコンテキストデータが優先されます。

  • 会話履歴 : チャットモデルの API はステートレスです。AI モデルに名前を伝えても、その後のやり取りではその名前は記憶されません。レスポンスを生成する際に以前のやり取りが考慮されるようにするには、各リクエストで会話履歴を送信する必要があります。

ChatClient のアドバイザー構成

ChatClient Fluent API は、アドバイザーを構成するための AdvisorSpec インターフェースを提供します。このインターフェースは、パラメーターを追加したり、複数のパラメーターを一度に設定したり、1 つ以上のアドバイザーを チェーンに追加したりするためのメソッドを提供します。

interface AdvisorSpec {
    AdvisorSpec param(String k, Object v);
    AdvisorSpec params(Map<String, Object> p);
    AdvisorSpec advisors(Advisor... advisors);
    AdvisorSpec advisors(List<Advisor> advisors);
}
アドバイザーが チェーンに追加される順序は、アドバイザーの実行順序を決定するため重要です。各アドバイザーはプロンプトまたはコンテキストを何らかの方法で変更し、1 つのアドバイザーによって行われた変更は チェーンの次のアドバイザーに渡されます。
ChatClient.builder(chatModel)
    .build()
    .prompt()
    .advisors(
        new MessageChatMemoryAdvisor(chatMemory),
        new QuestionAnswerAdvisor(vectorStore)
    )
    .user(userText)
    .call()
    .content();

この構成では、最初に MessageChatMemoryAdvisor が実行され、プロンプトに会話履歴が追加されます。次に、QuestionAnswerAdvisor がユーザーの質問と追加された会話履歴に基づいて検索を実行し、より関連性の高い結果が提供される可能性が高くなります。

検索拡張生成

検索拡張生成ガイドを参照してください。

チャットメモリ

インターフェース ChatMemory は、チャット会話履歴のストレージを表します。会話にメッセージを追加したり、会話からメッセージを取得したり、会話履歴をクリアしたりするメソッドを提供します。

現在、InMemoryChatMemory と CassandraChatMemory の 2 つの実装があり、それぞれメモリ内および time-to-live で永続的にチャット会話履歴を保存できます。

time-to-live を使用して CassandraChatMemory を作成するには:

CassandraChatMemory.create(CassandraChatMemoryConfig.builder().withTimeToLive(Duration.ofDays(1)).build());

以下のアドバイザー実装は、ChatMemory インターフェースを使用して会話履歴を含むプロンプトをアドバイスしますが、メモリがプロンプトに追加される方法の詳細は異なります。

  • MessageChatMemoryAdvisor : メモリが取得され、メッセージのコレクションとしてプロンプトに追加されます

  • PromptChatMemoryAdvisor : メモリが取得され、プロンプトのシステムテキストに追加されます。

  • VectorStoreChatMemoryAdvisor : コンストラクター VectorStoreChatMemoryAdvisor(VectorStore vectorStore, String defaultConversationId, int chatHistoryWindowSize, int order) このコンストラクターを使用すると、次のことが可能になります。

    1. ドキュメントの管理とクエリに使用する VectorStore インスタンスを指定します。

    2. コンテキストに何も提供されていない場合に使用するデフォルトの会話 ID を設定します。

    3. トークンサイズに基づいて、チャット履歴を取得するためのウィンドウサイズを定義します。

    4. チャットアドバイザーシステムで使用されるシステムテキストアドバイスを提供します。

    5. チェーンでこのアドバイザーの優先順位を設定します。

VectorStoreChatMemoryAdvisor.builder() メソッドを使用すると、デフォルトの会話 ID、チャット履歴ウィンドウのサイズ、取得するチャット履歴の順序を指定できます。

複数のアドバイザーを使用する @Service 実装のサンプルを以下に示します。

import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY;

@Service
public class CustomerSupportAssistant {

    private final ChatClient chatClient;

    public CustomerSupportAssistant(ChatClient.Builder builder, VectorStore vectorStore, ChatMemory chatMemory) {

        this.chatClient = builder
            .defaultSystem("""
                    You are a customer chat support agent of an airline named "Funnair". Respond in a friendly,
                    helpful, and joyful manner.

                    Before providing information about a booking or cancelling a booking, you MUST always
                    get the following information from the user: booking number, customer first name and last name.

                    Before changing a booking you MUST ensure it is permitted by the terms.

                    If there is a charge for the change, you MUST ask the user to consent before proceeding.
                    """)
            .defaultAdvisors(
                    new MessageChatMemoryAdvisor(chatMemory), // CHAT MEMORY
                    new QuestionAnswerAdvisor(vectorStore), // RAG
                    new SimpleLoggerAdvisor())
            .defaultFunctions("getBookingDetails", "changeBooking", "cancelBooking") // FUNCTION CALLING
            .build();
    }

    public Flux<String> chat(String chatId, String userMessageContent) {

        return this.chatClient.prompt()
                .user(userMessageContent)
                .advisors(a -> a
                        .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100))
                .stream().content();
    }

}

ログ

SimpleLoggerAdvisor は、ChatClient の request および response データを記録するアドバイザーです。これは、AI のやり取りのデバッグや監視に役立ちます。

Spring AI は、LLM とベクトルストアのインタラクションの可観測性をサポートしています。詳細については、可観測性ガイドを参照してください。

ログ記録を有効にするには、ChatClient を作成するときに、アドバイザーチェーンに SimpleLoggerAdvisor を追加します。チェーンの末尾に追加することをお勧めします。

ChatResponse response = ChatClient.create(chatModel).prompt()
        .advisors(new SimpleLoggerAdvisor())
        .user("Tell me a joke?")
        .call()
        .chatResponse();

ログを表示するには、アドバイザーパッケージのログレベルを DEBUG に設定します。

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

これを application.properties または application.yaml ファイルに追加します。

次のコンストラクターを使用して、AdvisedRequest および ChatResponse から記録されるデータをカスタマイズできます。

SimpleLoggerAdvisor(
    Function<AdvisedRequest, String> requestToString,
    Function<ChatResponse, String> responseToString
)

使用例:

SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
    request -> "Custom request: " + request.userText,
    response -> "Custom response: " + response.getResult()
);

これにより、ログに記録された情報を特定のニーズに合わせてカスタマイズできます。

運用環境で機密情報を記録する場合は注意してください。