広告

第1部 - なぜ私は Spring AI ではなく LangChain4j を選んだのか

Dev.to / 2026/4/1

💬 オピニオンDeveloper Stack & InfrastructureIdeas & Deep AnalysisTools & Practical Usage

要点

  • この記事では、AI はサーガ(saga)ベースの分散アーキテクチャにおいて、失敗を自動診断し、失敗データを用いてサーガの手順を動的に並べ替え、さらにシステムを自然言語で照会できるようにすることで価値を付加できると主張している。
  • 著者が Java のプロダクション用途で Python の LangChain や Spring AI ではなく LangChain4j を選んだ理由として、インターフェースとアノテーションによるエージェント定義のアプローチと、1.0 以降でも API の安定性が保たれている点を挙げている。
  • 比較表では、エコシステムとの適合性、エージェント定義の使い勝手、API 安定性、そして MCP のサポート状況を、Python LangChain、Spring AI、LangChain4j の間で示している。
  • 第1回目では、エージェントを動かすために必要な中核となるメンタルモデルを紹介することで土台を築く。具体的には、Models(ChatModel 抽象によるステートレスなトークン予測)、Agents(ツール呼び出しループ)、および関連概念である。

AIがあるからこそ分散サーガは難しい。すでに補償トランザクション、Kafkaトピック、ステートマシン、5つのマイクロサービスにまたがるロールバックチェーンに対処しているというのに、その上にAIレイヤーを追加するのは、複雑さをさらに増やすレシピに聞こえます。
しかし、このシリーズがまさに扱うのはそこです。サーガベースのアーキテクチャでAIが実際にどこで役立つのか、そしてシステムをより脆くしない形でどう組み込むか。AIレイヤーは失敗を自動で診断し、実際の失敗データに基づいてサーガのステップ順を動的に並べ替え、開発者が自然言語でシステム全体に問い合わせできるようにします。
本記事(最初の投稿)では基礎を扱います。なぜJava SDKとしてLangChain4jを選んだのか、必要な中核概念、そして動作するエージェントをどう立ち上げるかを説明します。

Why LangChain4j

JavaでAI搭載アプリケーションを作るなら、選択肢は3つです。PythonのLangChain(別スタック)、Spring AI(ネイティブSpring)、それともLangChain4j(スタンドアロンのJavaライブラリ)。生産で重要になる点で比較するとこうなります:

LangChain4jは私を驚かせました。エージェントをJavaのインターフェースとして定義し、そこに@SystemMessageを貼り付けるだけで終わりです。実装クラスは不要。フレームワークが実行時にプロキシを生成します。あまりに簡単で「落とし穴があるのでは」と探し続けましたが、実運用でも問題なく動きました。

実際にメモに書き留めた比較は以下のとおりです:

Python LangChain Spring AI LangChain4j
エコシステムとの適合 Javaアプリに加えて新しいスタック ネイティブSpring あらゆるJavaプロジェクトで摩擦ゼロ
エージェント定義 明示的なチェーン構築 動くが追加の配線が必要 インターフェース + アノテーション = 完了
APIの安定性 バージョン間で破壊的変更 0.x→1.xは書き直しに感じた 1.0以降は安定。SemVerが守られる
MCPサポート 完全(Python SDK) 1.1 GA以降は完全 完全(クライアント + サーバーが標準で同梱)

Spring AI 1.1はMCPサポートで追いついてきており、それは素晴らしいです。ただ、私がLangChain4jのエージェント定義モデルとAPI安定性に惹かれたのは、その点です。

Core Concepts

コードに入る前に、まずは頭の中のイメージです。理解するべきことは実際には4つだけです:

Models:AIエンジン。テキストを送ると、トークンの予測が返ってきます。呼び出し間での記憶はありません。LangChain4jはこれをChatModelインターフェースの背後に隠しているので、1行でGemini、Ollama、OpenAI、Claudeの間を切り替えられます。

Agents:ループです。モデルがタスクを受け取り、どのツールを呼ぶかを判断し、ツールを呼び、結果を見て、完了するまで繰り返します。LangChain4jでは、これをJavaのインターフェースとして定義します。

Tools:モデルが呼び出せるJavaメソッド。メソッドに@Toolを付けて注釈し、モデルはそのシグネチャと説明を見ます。モデルはいつそれを呼ぶかを判断します。if/elseのルーティングロジックを書く必要はありません。LLMがそれを自動で理解します。

RAG:Retrieval Augmented Generation(検索拡張生成)。モデルに質問する前に、自分たちのデータベースから関連するコンテキストを検索してプロンプトに注入します。これにより、モデルを再学習せずに自分たちのデータに基づく回答が得られます。

Setting Up Your First Chat Model

まずは基本から始めましょう。必要なのはChatModel、つまりLLMへの接続です。

Option 1: Gemini (Cloud)

依存関係を追加します:

implementation "dev.langchain4j:langchain4j-google-ai-gemini:1.11.0"

モデルを構築します:

GoogleAiGeminiChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_API_KEY"))
    .modelName("gemini-2.5-flash")
    .temperature(0.0)
    .maxOutputTokens(1024)
    .build();

Option 2: Ollama (Local, Free)

Ollamaは、LLMをローカルのマシン上で動かします。Llama、Mistral、Gemma、Qwen——100種類以上のモデルがあり、ollamaは1つ引っ張ってくるだけです。APIキー不要、クラウドアカウント不要、データはローカルに留まります。
このプロジェクトでは、Ollamaに2つの用途で頼っています。1つ目は開発中のチャットモデルとしてです。システムプロンプトを少し変えるたびにGeminiのクォータを消費したくありません。2つ目、そしてより重要なのは埋め込み(embeddings)です。nomic-embed-textモデル(約274MB)が、RAGパイプライン全体を支えます。つまり、サーアイベントをベクトル化し、過去の類似した失敗を検索し、診断エージェントにコンテキストを渡します。これはmill

brew install ollama
ollama pull llama3
ollama pull nomic-embed-text   # embeddings later
ollama serve

依存関係を追加します:

implementation "dev.langchain4j:langchain4jollama:1.11.0"

LangChain4jの統合 — チャットと埋め込み:

// チャットモデル — 本番ではGeminiに差し替え可能
OllamaChatModel ollama = OllamaChatModel.builder()
    .baseUrl("http://localhost:11434")
    .modelName("llama3")
    .temperature(0.0)
    .build();

// 埋め込みモデル — RAGに使用(サーアイベントをベクトル化)
OllamaEmbeddingModel embeddingModel = OllamaEmbeddingModel.builder()
    .baseUrl("http://localhost:11434")
    .modelName("nomic-embed-text")
    .build();

実際の運用では:Ollamaがすべての埋め込みを処理します(しかも本番でも、あれだけ確実です)。私は、タスクにより重い推論が必要なときだけGeminiに切り替えます。利用可能なものを確認するにはモデルライブラリを見てください。

Beautiful Part: ワンラインでスワップ

どちらも同じ ChatModel インターフェースを実装しています:

ChatModel model = isProduction ? gemini : ollama;

あなたのエージェントコードは変わりません。私はローカルではOllamaを使い、ステージング/本番ではGeminiを使います。

Your First Agent

ここでLangChain4jが光ります。インターフェースを定義します:

public interface DataAnalystAgent {

    @SystemMessage("""
        You are a data analyst for distributed sagas.
        Answer operational questions using the available tools.
        Never invent data.
        """)
    String analyze(@UserMessage String question);
}

作ります:

DataAnalystAgent agent = AiServices.builder(DataAnalystAgent.class)
    .chatModel(gemini)
    .build();

String answer = agent.analyze("What's the current refund rate?");

以上です。実装クラスは不要です。LangChain4jは、会話ループ、ツール呼び出し、レスポンスのパースを扱うプロキシを作成します。

Adding Tools

ツールなしでは、エージェントは学習データからテキストを生成することしかできません。ツールを使うことで、実データにアクセスできます。

public class OrderTools {

    @Tool("Returns stock for a product. Use to check availability.")
    public int getStock(@P("Product code") String code) {
        return inventoryRepo.findByCode(code).getAvailable();
    }

    @Tool("Returns the fraud risk score for an order.")
    public String getFraudScore(double amount, String type, int hour) {
        return fraudService.calculate(amount, type, hour);
    }
}

それらを登録します:

DataAnalystAgent agent = AiServices.builder(DataAnalystAgent.class)
    .chatModel(gemini)
    .tools(new OrderTools())
    .build();

これで、「"COMIC_BOOKS" は在庫がありますか?」と尋ねると、モデルは getStock ツールを認識し、"COMIC_BOOKS" を渡して呼び出すことを決めます。結果を取得して、応答を組み立てます。あなたはルーティングロジックを一切書いていません。

内部では、次のようなことが起きます:

  1. LangChain4jがメソッドシグネチャをGeminiに functionDeclaration として送信します
  2. Geminiは functionCall で応答します:「getStockcode=COMIC_BOOKS で呼び出したい」
  3. LangChain4jがそれをインターセプトし、あなたのJavaメソッドを実行して結果を取得します
  4. その結果を functionResponse としてGeminiに返します
  5. Geminiが実データを使って最終回答を生成します

トークンとコストに関する簡単な注意

送受信するすべての単語がトークンを消費します。Gemini Flashでは、入力トークンは1,000,000トークンあたり約$0.075、出力は約$0.30です。かなり安価です。しかし、考えるトークン(内部の推論)は1,000,000トークンあたり$3.50になることがあります。

コストを予測可能に保つために私が使っている設定はいくつかあります:

OllamaChatModel.builder()
    .numPredict(512)        // 出力トークン数を上限にする
    .numCtx(32768)          // コンテキストウィンドウサイズ
    .temperature(0.0)       // 決定論的、無駄なサンプリングなし
    .repeatPenalty(1.2)     // ループを避ける、短い応答にする
    .think(true)            // ローカルでは無料、クラウドでは高価
    .listeners(List.of(new TokenUsageListener()))
    .build();

この TokenUsageListener は、呼び出しごとに入力/出力トークンをログに記録します。Ollamaで think(true) はローカルでは無料なのに、クラウドAPIではそれらの「思考トークン」が INPUT としてカウントされることを、痛い目を見て学びました。その結果、1回あたりのコストが10倍になることがあります。

次は

次回の投稿では、これらをすべて使って、分散サーガシステムの上にAIレイヤーを構築する方法を紹介します。Kafkaで調整する5つのマイクロサービスで、各サービスは、そのビジネスロジックをMCPツールとして公開し、任意のエージェントがそれを発見してリモートから呼び出せるようにします。

ここで(そして次回の投稿で)扱った内容は、すでに完全で動作するプロジェクトとして実装済みです。このリポジトリには、5つのマイクロサービス、Kafkaベースのサーガオーケストレーション、MCPツール層、AIエージェントが含まれており、すべてが結線済みで、そのまま実行できる状態になっています: github.com/pedrop3/sagaorchestration

これは、LangChain4jでAI対応マイクロサービスを作る3部構成シリーズの第1回です:

  1. Spring AIよりLangChain4jを選んだ理由
  2. MCPでマイクロサービスにAIエージェントを接続する
  3. 分散サーガを診断・計画・問い合わせする3つのエージェント

広告