Spring AI と Docker Model Runner を使用した Java での GenAI アプリの構築

ジェネレーティブ AI (GenAI) プロジェクトの開始を考えるとき、この新しい分野で開始するには Python が必要だと思うかもしれません。ただし、すでにJava開発者である場合は、新しい言語を学ぶ必要はありません。Javaエコシステムは、GenAIアプリケーションの構築をアクセスしやすく、生産的にするための堅牢なツールとライブラリを提供します。

このブログでは、Javaを使用してGenAIアプリを構築する方法を学びます。ここでは、Spring AIとDockerのツールを使用して、RAGがモデルの応答をどのように強化するかを示すために、ステップバイステップのデモを行います。Spring AI は、多くのモデルプロバイダー (チャットと埋め込みの両方)、ベクターデータベースなどと統合されます。この例では、Spring AI プロジェクトが提供する OpenAI モジュールと Qdrant モジュールを使用して、これらの統合の組み込みサポートを利用します。さらに、OpenAI 互換の API を提供する Docker Model Runner (クラウドでホストされる OpenAI モデルの代わりに) を使用して、 AI モデルをローカルで簡単に実行できるようにします。TestcontainersとSpring AIのツールを使用してテストプロセスを自動化し、LLMの回答が提供されたドキュメントに文脈に基づいていることを確認します。最後に、Grafana を使用してオブザーバビリティを実現し、アプリが設計どおりに動作することを確認する方法を示します。 

はじめ 

Spring Initializr に移動し、Web、OpenAI、Qdrant Vector Database、Testcontainers の依存関係を選択して、サンプル アプリケーションの構築を始めましょう。

これには、モデルと直接対話する「/chat」エンドポイントと、ベクトルデータベースに格納されたドキュメントからモデルに追加のコンテキストを提供する「/rag」エンドポイントの2つのエンドポイントがあります。

Docker Model Runner の設定

公式ドキュメントで説明されているように、Docker Desktop または Docker Engine で Docker Model Runner を有効にします。

次に、次の2つのモデルをプルします。

docker model pull ai/llama3.1
docker model pull ai/mxbai-embed-large
  • AI/ラマ3.1– チャットモデル
  • ai/mxbai-embed-large – 埋め込みモデル

どちらのモデルも、 DockerHubのai 名前空間でホストされています。また、モデルの特定のタグを選択することもできますが、通常はモデルの異なる量子化が提供されます。どのタグを選択すればよいかわからない場合は、デフォルトのタグから始めるのが良いでしょう。

GenAI アプリの構築

/src/main/java/com/example の下に ChatController を作成し、これがチャットモデルと対話するためのエントリポイントになります。

@RestController
public class ChatController {

	private final ChatClient chatClient;

	public ChatController(ChatModel chatModel) {
		this.chatClient = ChatClient.builder(chatModel).build();
	}

	@GetMapping("/chat")
	public String generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
		return this.chatClient.prompt().user(message).call().content();
	}

}


  • ChatClient は、モデルと対話するための使用可能な操作を提供するインターフェイスです。実際のモデル値 (使用するモデル) は、構成プロパティを介して挿入されます。
  • message query param が指定されていない場合は、モデルにジョークを言うように依頼します (defaultValue に見られるように)。

Docker Model Runner を指すようにアプリケーションを構成し、"ai/llama3を使用します。1"次のプロパティを /src/test/resources/application.properties に追加してモデル化します。

spring.ai.openai.base-url=http://localhost:12434/engines
spring.ai.openai.api-key=test
spring.ai.openai.chat.options.model=ai/llama3.1

spring.ai.openai.api-keyはフレームワークに必要ですが、Docker Model Runnerには必要ないため、ここでは任意の値を使用できます。

./mvnw spring-boot:test-run または ./gradlew bootTestRun を実行してアプリケーションを起動し、Testcontainers について尋ねてみましょう。

http :8080/chat message=="What’s testcontainers?"

以下に、LLM(ai / llama3.1)が提供する答えを見つけることができます

Testcontainers is a fantastic and increasingly popular library for **local testing with containers**. Let's break down what it is, why it's useful, and how it works:

**What is Testcontainers?**

Testcontainers provides a way to run real, fully functional containerized services (like databases, message queues, web servers, etc.) directly within your tests. Instead of relying on mocked or stubbed versions of these services, you're using the *actual* services, which leads to much more realistic and reliable test results.

**Why Use Testcontainers?**

* **Realistic Testing:** This is the biggest benefit.  Mocking databases or message queues can be brittle and difficult to maintain.  Testcontainers provides a service that behaves exactly like the real thing, leading to tests that more accurately reflect how your application will perform in production.
* **Simplified Test Setup:**  Forget about manually setting up and configuring databases or other services on your test machine. Testcontainers automatically handles the container creation, configuration, and cleanup for you.
* **Faster Tests:** Because the services are running locally, there’s no network latency involved, resulting in significantly faster test execution times.
* **Consistent Environments:**  You eliminate the "it works on my machine" problem. Everyone running the tests will be using the same, pre-configured environment.
* **Supports Many Services:** Testcontainers supports a huge range of services, including:
    * **Databases:** PostgreSQL, MySQL, MongoDB, Redis, Cassandra, MariaDB
    * **Message Queues:** RabbitMQ, Kafka, ActiveMQ
    * **Web Servers:**  Tomcat, Jetty, H2 (for in-memory databases)
    * **And many more!**  The list is constantly growing.


**How Does It Work?**

1. **Client Library:** Testcontainers provides client libraries for various programming languages (Java, Python, JavaScript, Ruby, Go, .NET, and more).
2. **Container Run:** When you use the Testcontainers client library in your test, it automatically starts the specified container (e.g., a PostgreSQL database) in the background.
3. **Connection:** Your test code then connects to the running container using standard protocols (e.g., JDBC for PostgreSQL, HTTP for a web server).
4. **Test Execution:**  You execute your tests as usual.
5. **Cleanup:**  When the tests are finished, Testcontainers automatically shuts down the container, ensuring a clean state for the next test run.

**Example (Conceptual - Python):**

```python
from testcontainers.postgresql import PostgreSQLEnvironment

# Create a PostgreSQL environment
env = PostgreSQLEnvironment()

# Start the container
env.start()

# Connect to the database
db = env.db()  #  This creates a connection object to the running PostgreSQL container

# Perform database operations in your test
# ...

# Stop the container (cleanup)
env.shutdown()
```

**Key Concepts:**

* **Environment:**  A Testcontainers environment is a configuration that defines which containers to run and how they should be configured.
* **Container:**  A running containerized service (e.g., a database instance).
* **Connection:** An object that represents a connection to a specific container.

**Resources to Learn More:**

* **Official Website:** [https://testcontainers.io/](https://testcontainers.io/) - This is the best place to start.
* **GitHub Repository:** [https://github.com/testcontainers/testcontainers](https://github.com/testcontainers/testcontainers) -  See the source code and contribute.
* **Documentation:** [https://testcontainers.io/docs/](https://testcontainers.io/docs/) - Comprehensive documentation with examples for various languages.

**In short, Testcontainers is a powerful tool that dramatically improves the quality and reliability of your local tests by allowing you to test against real, running containerized services.**

Do you want me to delve deeper into a specific aspect of Testcontainers, such as:

*   A specific language implementation (e.g., Python)?
*   A particular service it supports (e.g., PostgreSQL)?
*   How to integrate it with a specific testing framework (e.g., JUnit, pytest)?

LLMによって提供された回答には、たとえば、PostgreSQLEnvironmentがtestcontainers-pythonに存在しないなど、いくつかの間違いがあることがわかります。もう1つは、存在しないドキュメントへのリンク testcontainers.io です。ですから、答えにはいくつかの幻覚が見られます。

もちろん、LLMの応答は非決定論的であり、各モデルは特定のカットオフ日まで訓練されるため、情報が古くなり、回答が正確でない可能性があります。

この状況を改善するために、Testcontainers に関する厳選されたコンテキストをモデルに提供しましょう。

ベクトル検索データベースからドキュメントを取得する別のコントローラー RagController を作成します。

@RestController
public class RagController {

	private final ChatClient chatClient;

	private final VectorStore vectorStore;

	public RagController(ChatModel chatModel, VectorStore vectorStore) {
		this.chatClient = ChatClient.builder(chatModel).build();
		this.vectorStore = vectorStore;
	}

	@GetMapping("/rag")
	public String generate(@RequestParam(value = "message", defaultValue = "What's Testcontainers?") String message) {
		return callResponseSpec(this.chatClient, this.vectorStore, message).content();
	}

	static ChatClient.CallResponseSpec callResponseSpec(ChatClient chatClient, VectorStore vectorStore,
			String question) {
		QuestionAnswerAdvisor questionAnswerAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
			.searchRequest(SearchRequest.builder().topK(1).build())
			.build();
		return chatClient.prompt().advisors(questionAnswerAdvisor).user(question).call();
	}

}

Spring AIは多くのアドバイザーを提供します。この例では、QuestionAnswerAdvisor を使用して、ベクトル検索データベースに対してクエリを実行します。ベクターデータベースとの個々の統合をすべて処理します。

ベクトルデータベースへのドキュメントの取り込み

まず、関連するドキュメントをベクターデータベースにロードする必要があります。src/test/java/com/exampleの下に、IngestionConfigurationクラスを作成しましょう。

@TestConfiguration(proxyBeanMethods = false)
public class IngestionConfiguration {

	@Value("classpath:/docs/testcontainers.txt")
	private Resource testcontainersDoc;

	@Bean
	ApplicationRunner init(VectorStore vectorStore) {
		return args -> {
			var javaTextReader = new TextReader(this.testcontainersDoc);
			javaTextReader.getCustomMetadata().put("language", "java");

			var tokenTextSplitter = new TokenTextSplitter();
			var testcontainersDocuments = tokenTextSplitter.apply(javaTextReader.get());

			vectorStore.add(testcontainersDocuments);
		};
	}

}


testcontainers.txt /src/test/resources/docs ディレクトリの下には、次のコンテンツ固有の情報があります。実際のユースケースでは、おそらくより広範なドキュメントのコレクションがあるでしょう。

Testcontainers is a library that provides easy and lightweight APIs for bootstrapping local development and test dependencies with real services wrapped in Docker containers. Using Testcontainers, you can write tests that depend on the same services you use in production without mocks or in-memory services.

Testcontainers provides modules for a wide range of commonly used infrastructure dependencies including relational databases, NoSQL datastores, search engines, message brokers, etc. See https://testcontainers.com/modules/ for a complete list.

Technology-specific modules are a higher-level abstraction on top of GenericContainer which help configure and run these technologies without any boilerplate, and make it easy to access their relevant parameters.

Official website: https://testcontainers.com/
Getting Started: https://testcontainers.com/getting-started/
Module Catalog: https://testcontainers.com/modules/

次に、 src/test/resources/application.properties ファイルにプロパティを追加しましょう。

spring.ai.openai.embedding.options.model=ai/mxbai-embed-large
spring.ai.vectorstore.qdrant.initialize-schema=true
spring.ai.vectorstore.qdrant.collection-name=test

ai/mxbai-embed-large は、ドキュメントの埋め込みを作成するために使用される埋め込みモデルです。それらはベクトル検索データベース(この場合はQdrant)に保存されます。Spring AI は Qdrant スキーマを初期化し、test という名前のコレクションを使用します。

TestDemoApplication Java クラスを更新し、IngestionConfiguration.class を追加しましょう

public class TestDemoApplication {

	public static void main(String[] args) {
		SpringApplication.from(DemoApplication::main)
			.with(TestcontainersConfiguration.class, IngestionConfiguration.class)
			.run(args);
	}

}

次に、 ./mvnw spring-boot:test-run または ./gradlew bootTestRun を実行してアプリケーションを起動し、Testcontainers について再度質問します。

http :8080/rag message=="What’s testcontainers?"

今回の回答には、提供したドキュメントからの参照が含まれており、より正確です。

Testcontainers is a library that helps you write tests for your applications by bootstrapping real services in Docker containers, rather than using mocks or in-memory services. This allows you to test your applications as they would run in production, but in a controlled and isolated environment.

It provides modules for commonly used infrastructure dependencies such as relational databases, NoSQL datastores, search engines, and message brokers.

If you have any specific questions about how to use Testcontainers or its features, I'd be happy to help.

統合テスト

テストはソフトウェア開発の重要な部分です。幸いなことに、TestcontainersとSpring AIのユーティリティは、GenAIアプリケーションのテストをサポートしています。これまでは、アプリケーションを手動でテストし、アプリケーションを起動して、指定されたエンドポイントへのリクエストを実行し、応答の正確性を自分で検証してきました。次に、統合テストを作成して、LLMによって提供される回答がよりコンテキストに関連し、ドキュメントで提供される情報によって補強されているかどうかを確認することで、これを自動化します。

@SpringBootTest(classes = { TestcontainersConfiguration.class, IngestionConfiguration.class },
		webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RagControllerTest {

	@LocalServerPort
	private int port;

	@Autowired
	private VectorStore vectorStore;

	@Autowired
	private ChatClient.Builder chatClientBuilder;

	@Test
	void verifyTestcontainersAnswer() {
		var question = "Tell me about Testcontainers";
		var answer = retrieveAnswer(question);

		assertFactCheck(question, answer);
	}

	private String retrieveAnswer(String question) {
		RestClient restClient = RestClient.builder().baseUrl("http://localhost:%d".formatted(this.port)).build();
		return restClient.get().uri("/rag?message={question}", question).retrieve().body(String.class);
	}

	private void assertFactCheck(String question, String answer) {
		FactCheckingEvaluator factCheckingEvaluator = new FactCheckingEvaluator(this.chatClientBuilder);
		EvaluationResponse evaluate = factCheckingEvaluator.evaluate(new EvaluationRequest(docs(question), answer));
		assertThat(evaluate.isPass()).isTrue();
	}

	private List<Document> docs(String question) {
		var response = RagController
			.callResponseSpec(this.chatClientBuilder.build(), this.vectorStore, question)
			.chatResponse();
		return response.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS);
	}

}

  • ContainerConfigurationをインポートすると、Qdrantが提供されます。
  • IngestionConfiguration をインポートすると、ドキュメントがベクター データベースに読み込まれます。
  • FactCheckingEvaluatorを使用して、チャットモデル(ai / llama3.1)を伝えます。LLMから提供された回答を確認し、ベクトルデータベースに保存されているドキュメントで確認します。

注: 統合テストでは、前の手順で宣言したのと同じモデルを使用しています。しかし、私たちは間違いなく別のモデルを使うことができます。

テストを自動化することで、一貫性が確保され、手動実行に伴うエラーのリスクが軽減されます。 

Grafana LGTMスタックによる可観測性

最後に、アプリケーションに可観測性を導入しましょう。メトリクスとトレースを導入することで、アプリケーションが開発中と本番環境で設計どおりに動作しているかどうかを理解できます。

次の依存関係をpom.xmlに追加します

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
	<groupId>io.micrometer</groupId>
	<artifactId>micrometer-registry-otlp</artifactId>
</dependency>
<dependency>
	<groupId>io.micrometer</groupId>
	<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
	<groupId>io.opentelemetry</groupId>
	<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
	<groupId>org.testcontainers</groupId>
	<artifactId>grafana</artifactId>
       <scope>test</scope>
</dependency>

それでは、src/test/java/com/example の下に GrafanaContainerConfiguration を作成しましょう。

@TestConfiguration(proxyBeanMethods = false)
public class GrafanaContainerConfiguration {

	@Bean
	@ServiceConnection
	LgtmStackContainer lgtmContainer() {
		return new LgtmStackContainer("grafana/otel-lgtm:0.11.4");
	}

}

Grafana は grafana/otel-lgtm イメージを提供し、Prometheus、Tempo、OpenTelemetry Collector、およびその他の関連サービスをすべて 1 つの便利な Docker イメージに結合して開始します。

デモのために、リクエストの 100% をサンプリングするために、/src/test/resources/application.properties にいくつかのプロパティを追加しましょう。

spring.application.name=demo
management.tracing.sampling.probability=1

TestDemoApplication クラスを更新して、GrafanaContainerConfiguration.class を含めます。

public class TestDemoApplication {

	public static void main(String[] args) {
		SpringApplication.from(DemoApplication::main)
			.with(TestcontainersConfiguration.class, IngestionConfiguration.class, GrafanaContainerConfiguration.class)
			.run(args);
	}

}

次に、 ./mvnw spring-boot:test-run または ./gradlew bootTestRun をもう一度実行し、リクエストを実行します。

http :8080/rag message=="What’s testcontainers?"

次に、ログで次のテキストを探します。

o.t.grafana.LgtmStackContainer           : Access to the Grafana dashboard: 
http://localhost:64908

ポートはユーザーによって異なる場合がありますが、クリックすると Grafana ダッシュボードが開きます。ここでは、モデル検索またはベクトル検索に関連するメトリクスをクエリしたり、トレースを表示したりできます。

Testcontainers GenAIフィギュア 1

図 1: モデル メトリック、ベクトル検索パフォーマンス、トレースを示す Grafana ダッシュボード

また、チャットエンドポイントで使用されるトークン使用量メトリックを表示することもできます。

Testcontainers GenAIフィギュア 2 1

図 2: チャットエンドポイントのトークン使用状況メトリクスを表示する Grafana ダッシュボードパネル

名前が "demo" のサービスのトレースを一覧表示すると、このトレースの一部として実行された操作の一覧が表示されます。http get /rag という名前のトレース ID を使用して、同じ HTTP 要求内の完全な制御フローを確認できます。

Testcontainers GenAIフィギュア 3

図 3: Java GenAI アプリケーションの /rag エンドポイントのトレース詳細を示す Grafana ダッシュボード

結論

Docker は、Spring AI プロジェクトを補完する強力な機能を提供し、開発者が使い慣れた信頼できる Docker ツールを使用して GenAI アプリケーションを効率的に構築できるようにします。これにより、ローカルモデルを実行するためのOpenAI互換APIを公開するDocker Model Runnerなど、サービスの依存関係の起動が簡素化されます。Testcontainers は、サービスと依存関係に軽量のコンテナーを提供することで、統合テストを迅速にスピンアウトしてアプリを評価するのに役立ちます。開発からテストまで、Docker と Spring AI は、最新の AI 駆動型アプリケーションを構築するための信頼性と生産性の高い組み合わせであることが証明されています。

さらに詳しく

投稿カテゴリ

関連記事