AI Agentとは?詳細な解説とJavaの例で理解する

この記事には短い動画があります:YouTubeTikTok

1. 概要

「AI Agent」はどこにでも現れるキーワードですが、定義は不明確なことが多いです。すべてのチャットボットをエージェントと呼ぶ人もいますし、複雑なマルチステップシステムだけを真のエージェントと考える人もいます。この記事では、AI Agentが何であるか、必須コンポーネントは何か、そしてその背後にあるループメカニズムについて明確に説明します。読み終わった後、JavaでMinimalなエージェントを自分で構築して、その内部動作を理解し、その後、Spring AIを使ってリアルなLLMバージョンにアップグレードできるようになります。

2. AI Agentとは?

簡潔な定義:AI Agentは、単なる質問に答えるのではなく、目標を達成するために自ら行動を決定する、LLMを脳として使用するシステムです。

通常のチャットボットとの違い:

特徴 通常のチャットボット AI Agent
処理の回数 1回:質問→回答 複数ステップ:観察→推論→行動→繰り返し
Tool/APIの呼び出し 通常はしない はい、どのツールを呼び出すかは自分で決定
長期的な目標の有無 いいえ はい、エージェント自身がタスクを細分化して目標を達成
例 「ハノイの天気は?」 → 説明文を返答 「明日のハノイ~ホーチミン間の最安チケットを予約する」 → 自動でチケットを検索、比較、予約、確認

言い換えれば、LLMは単なる「言語脳」で、テキストを生成することを知っています。Agentはその脳を、実世界で行動する(API呼び出し、ファイル読み込み、データベースクエリ)ことができる実体に変え、タスク完了まで推論プロセスを自動で繰り返すことができます。

3. AI Agentのコアコンポーネント

最小限のエージェントには4つのコンポーネントが必要です。これらのいずれかが欠落すると、システムはエージェントの「衣装を被った」チャットボットに過ぎません。

3.1. LLM – 推論脳

LLMはプロンプト(目標、履歴、利用可能なツールのリストを含む)を受け取り、次の決定を生成します:直接回答するか、どのツールを呼び出すか、どのパラメータを使うか。

3.2. Tool(function calling) – エージェントの手足

Toolは、エージェントが呼び出すことができる具体的な関数です。注文の追跡、天気APIの呼び出し、データベースクエリなど。これは、エージェントを「話す」から「実行する」に変える部分です。

public interface Tool {
    String name();
    boolean canHandle(String input);
    String execute(String input);
}

canHandleは、このツールが現在の入力を処理できるかどうかをチェックします。executeはアクションを実行し、結果(observation)を返します。

3.3. Memory – メモリ

エージェントは、不要な繰り返しを避けるために、前のステップで何をしたかを覚えておく必要があります(会話履歴、呼び出したツールの結果など)。メモリは単純なList<String>でステップのログを保存するか、またはより複雑なベクトルストアで関連情報を検索できます。

3.4. Control loop – 制御ループ

これがプロセス全体を駆動する「エンジン」です。Thought → Action → Observationステップを繰り返します。目標に達するか、許可された最大ステップに達するまで継続します。次のセクションでこのメカニズムについて詳しく説明します。

4. ReAct ループ:エージェントが「考えてから行動する」方法

ReAct(Reasoning + Acting)は、エージェントの最も一般的なループモデルです:

  1. Thought:LLMが「次に何をすべきか」を推論します。
  2. Action:ツールを選択し、適切なパラメータで呼び出します。
  3. Observation:ツールから結果を受け取り、コンテキストに戻します。
  4. ステップ1から繰り返します。LLMが最終回答に十分な情報があると判断するか、maxStepsに達するまで。

このメカニズムを明確に理解するために、実際のLLMを呼び出す必要がなく(APIキーの費用がかかる)、純粋なJavaでミニエージェントを構築し、実際のLLMの代わりにif-elseを使用します。

public class SimpleAgent {

    private final List<Tool> tools;
    private final int maxSteps;

    public SimpleAgent(List<Tool> tools, int maxSteps) {
        this.tools = tools;
        this.maxSteps = maxSteps;
    }

    public String run(String goal) {
        String observation = goal;
        for (int step = 1; step <= maxSteps; step++) {
            Tool chosen = tools.stream()
                    .filter(t -> t.canHandle(observation))
                    .findFirst()
                    .orElse(null);

            if (chosen == null) {
                return "処理できるツールがありません:" + observation;
            }

            System.out.printf("ステップ%d - Thought:ツール'%s'を使う必要があります%n", step, chosen.name());
            String result = chosen.execute(observation);
            System.out.printf("ステップ%d - Observation:%s%n", step, result);

            return result; // シンプルな例:1ステップ後に完了
        }
        return "許可されたステップ数(" + maxSteps + ")を超えても完了できませんでした。";
    }
}

maxStepsはループステップの「予算」です — エージェントが目標に決して到達しない場合、無限に実行されないように必須です。forループはThought(ツール選択)、Action(execute)、Observation(コンソール出力結果)をシミュレートします。

3つの説明用ツール:

public class WeatherTool implements Tool {
    @Override
    public String name() {
        return "WeatherTool";
    }

    @Override
    public boolean canHandle(String input) {
        return input.toLowerCase().contains("天気");
    }

    @Override
    public String execute(String input) {
        return "ハノイは今日28°C、くもりです。";
    }
}
public class OrderStatusTool implements Tool {
    @Override
    public String name() {
        return "OrderStatusTool";
    }

    @Override
    public boolean canHandle(String input) {
        return input.toLowerCase().contains("注文");
    }

    @Override
    public String execute(String input) {
        return "注文#1023は配送中で、2日以内に配達予定です。";
    }
}
public class CalculatorTool implements Tool {
    @Override
    public String name() {
        return "CalculatorTool";
    }

    @Override
    public boolean canHandle(String input) {
        return input.matches(".*\\d+\\s*\\+\\s*\\d+.*");
    }

    @Override
    public String execute(String input) {
        String digits = input.replaceAll("[^0-9+]", "");
        String[] parts = digits.split("\\+");
        int sum = Integer.parseInt(parts[0]) + Integer.parseInt(parts[1]);
        return "結果:" + sum;
    }
}

実行してみます:

SimpleAgent agent = new SimpleAgent(
        List.of(new CalculatorTool(), new WeatherTool(), new OrderStatusTool()),
        3
);

String result = agent.run("ハノイの今日の天気を教えてください");
System.out.println(result);

結果:

ステップ1 - Thought:ツール'WeatherTool'を使う必要があります
ステップ1 - Observation:ハノイは今日28°C、くもりです。
ハノイは今日28°C、くもりです。

正しいツール選択の動作と、適切なツールがない場合をテストします:

import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;

class SimpleAgentTest {

    @Test
    void shouldPickWeatherToolWhenGoalMentionsWeather() {
        SimpleAgent agent = new SimpleAgent(
                List.of(new CalculatorTool(), new WeatherTool(), new OrderStatusTool()),
                3
        );

        String result = agent.run("ハノイの今日の天気を教えてください");

        assertThat(result).contains("ハノイ");
    }

    @Test
    void shouldReturnErrorWhenNoToolMatches() {
        SimpleAgent agent = new SimpleAgent(List.of(new CalculatorTool()), 3);

        String result = agent.run("私のために歌を歌ってください");

        assertThat(result).startsWith("処理できるツールがありません");
    }
}

この例ではif-elseをLLMの代わりに使用するため、「推論」は非常に厳密です。本当のエージェントが異なる点:ハードコードされたcanHandleの代わりに、LLM自身がツール説明を読んで、どのツールを呼び出すかを決定し、パラメータはLLM自身が自然言語質問から抽出します。

5. Spring AIとtool callingで実際のエージェントを構築

LLMが実際に決定を下すのを見るために、Spring AI — Spring BootにAIモデルを統合するフレームワークを使用します。これはtool calling機能を備えています。

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
    <version>1.0.0</version>
</dependency>

APIキーとモデルを設定:

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o-mini

@Toolアノテーションを使用してツールを宣言します。前の例のOrderStatusToolの役割と同じですが、今度は実際のLLMが呼び出します:

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

@Component
public class OrderTools {

    @Tool(description = "注文IDで注文ステータスを照会")
    public String getOrderStatus(String orderId) {
        // 実際にはここでOrderService/データベースを呼び出します
        return "注文" + orderId + "は配送中で、2日以内に配達予定です。";
    }
}

descriptionは非常に重要です:LLMはこの説明に基づいてこのツールを呼び出すべきかどうかとどのパラメータを渡すかを決定します。

ChatClientを使用してツールをエージェントに接続するサービス:

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class OrderAssistantService {

    private final ChatClient chatClient;

    public OrderAssistantService(ChatClient.Builder builder, OrderTools orderTools) {
        this.chatClient = builder
                .defaultTools(orderTools)
                .build();
    }

    public String ask(String question) {
        return chatClient.prompt(question).call().content();
    }
}

ask("注文ORD-1023はどこにありますか?")を呼び出すと、Spring AIは内部的に以下を実行します:

  1. クエリとツールgetOrderStatusの説明をモデルに送信します。
  2. モデルは「getOrderStatusをorderId="ORD-1023"で呼び出す」という決定を返すか、すぐに答えるのではなく。
  3. Spring AIは対応するJavaメソッドを自動的に呼び出し、結果を取得します。
  4. 結果をモデルに送り返して、モデルが最終的な自然な回答を作成します。
注文ORD-1023は現在配送中で、あと2日以内に配達される予定です。

上記の4つのステップ全体がReAct ループの完全な実装です。違いは、Spring AIがforループを隠しているため、セクション4の純粋なJavaの例のようにcontrol loopを自分で書く必要がないことです。

注意: バージョンspring-ai-starter-model-openaiとアーティファクト名は時間とともに変わる可能性があります。依存関係を追加する前に、使用しているSpring Bootバージョンに対応するBOMを常に確認してください。

6. 一般的なエージェントの種類

  • Single-agent ReAct:単一のエージェントがThought-Action-Observationをループします。上記の2つの例のような中程度のタスクに適しています。
  • Plan-and-Execute:エージェントは最初に詳細な計画(ステップのリスト)を立てて、ReActのように継続的に推論し直さずに各ステップを実行します。構造化された複数ステップのタスク(例:「5つのセクションからなるレポートを作成」)に適しています。
  • Multi-agent:複数の専門的なエージェントが協力します。通常、「Supervisor」エージェントが複数の「Worker」エージェント(例:検索エージェント、テキスト作成エージェント、データ検証エージェント)を調整し、結果が統合されます。

どのタイプを選択するかはタスクの複雑さに依存します:シンプルなタスクはsingle-agentを使用、複数のドメイン(検索+作成+検証)を含むタスクはmulti-agentに分割して、各部分を制御しやすく、デバッグしやすくします。

7. よくあるエラー&エージェントを使用すべきではない場合

  • 無限ループ、トークンの消費:maxStepsまたはトークン予算を設定しない場合、LLMが「まだ完了していない」と考えるため、エージェントは永遠にループする可能性があります。セクション4のSimpleAgentのように、常にステップ数を明確に制限します。
  • ツール説明が曖昧:ツールのdescriptionが明確でない場合、LLMが間違ったツールを呼び出したり、パラメータを間違った型で渡したりする可能性があります。具体的な説明を書き、期待されるパラメータ形式を明確に示します。
  • 危険なアクションの保護策の欠落:エージェントがデータを削除したり、お金を転送したりするツールを呼び出す権限がある場合、確認ステップがないことは大きなリスクです。取り消せないアクションを実行する前に、常に確認レイヤー(人間の参入またはルールベースのチェック)を追加します。
  • 単純なタスクに対するオーバーエンジニアリング:質問がLLMの1回の呼び出しのみで回答を得られる場合(例:文の翻訳)、tool callingでエージェントループ全体を構築しないでください。余分なレイテンシーが発生し、不要にモデルを複数回呼び出すコストが増えます。

注意: システムが外部アクション(API、データベース)の呼び出しが不要で、テキストを1回生成するだけで済む場合、それは通常のLLMのタスクです — エージェントではありません。不要な複雑性を追加しないでください。

8. 結論

AI Agentは通常のチャットボットとは異なり、自ら行動を決定し、目標を達成するまで推論プロセスを繰り返すことができます。4つのコンポーネント(LLM、tool、memory、control loop)に基づいています。ReAct ループ(Thought-Action-Observation)は、現在のほとんどのエージェントの基盤となるメカニズムであり、本番環境でSpring AIなどのフレームワークに切り替える前に、純粋なJavaでシミュレートしてその性質を理解することができます。最も重要なのは:常にステップ数を制限し、ツール説明を明確に書き、取り消せないアクションに対して保護策を追加することです — エージェントは強力ですが、制御がなければ「やりすぎ」になりやすいのです。

AI AgentLLMSpring AIJavaFunction Calling