AIエージェントの5つのパターン - 詳細な解説とJavaの例

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

1. 概要

LLMとコード数行を組み合わせて「自動で動作する」ようにしたときのこと。基本的なAIエージェントができたことになります。しかし、そのようなエージェントは通常、聞かれたことにそのまま答えるだけで、自分の間違いを直す方法を知らず、ツールを呼び出す方法を知らず、タスクを分割する方法も知りません。この記事は、エージェントを「より知的に」する5つのデザインパターンについて説明しています:Reflection、Tool Use、Planning、Multi-Agent、ReActです。各パターンには、短く動作するJavaコード例(Spring AI)があり、いつ使うべき、いつ使うべきでないかの説明が付いています。読み終わったら、自分の問題に合った適切なパターンを選ぶことができます。すべてを1つの巨大なプロンプトに組み合わせるのではなく。

詳細に入る前に、以下の例についてはSpring AIの依存関係が必要です:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini

注意: この記事のコードはパターンの構造を示すものであり、本番環境用の製品ではありません。実際にデプロイするときは、リトライ、タイムアウト、ロギングを追加してください。

2. Reflectionパターン:回答を自己批判し修正する

Reflectionは最もシンプルなパターンです。エージェントが回答を生成し、同じLLM(または別のLLM)を使ってその回答をスコアリング/批評し、その後修正します。このループは、ドラフトを書いて、送信する前に自分で読み直すようなものです。

2.1. 動作フロー

  1. 生成:エージェントが最初の回答を生成します。
  2. 批評:エージェント(または別のエージェント)が「レビュアー」の役割を果たし、エラーを指摘します。
  3. 修正:エージェントが批評に基づいて修正し、質に達するまでステップ2~3を繰り返す可能性があります。
@Service
public class ReflectionAgent {

    private final ChatClient chatClient;

    public ReflectionAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String answerWithReflection(String question, int maxRounds) {
        String draft = chatClient.prompt()
                .user("次の質問に簡潔に答えてください:" + question)
                .call()
                .content();

        for (int round = 0; round < maxRounds; round++) {
            String critique = chatClient.prompt()
                    .user("""
                        あなたは厳しいレビュアーです。以下の回答の誤りと不足を指摘してください。
                        不足がないなら「OK」を返してください。
                        質問:%s
                        回答:%s
                        """.formatted(question, draft))
                    .call()
                    .content();

            if (critique.strip().equalsIgnoreCase("OK")) {
                break;
            }

            draft = chatClient.prompt()
                    .user("フィードバックに基づいて回答を修正してください:" + critique
                            + "\n前の回答:" + draft)
                    .call()
                    .content();
        }
        return draft;
    }
}

answerWithReflectionは最初のドラフトを生成し、その後最大maxRounds回繰り返します。LLMをレビュアーの役割で呼び出してスコアリングし、レビュアーが「OK」を返すと停止、そうでない場合は修正版を生成します。結果として、通常のLLM呼び出しと比較して、回答がより正確になる傾向があります。

注意: Reflectionは通常と比べてLLM呼び出し数が2~3倍になります。高い精度が必要なタスク(コード審査、計算、契約書作成)のみに使用してください。通常のチャットには使用しないでください。遅く、費用がかかるためです。

3. Tool Useパターン:エージェントに外部ツールの呼び出しをさせる

LLMは現在の時刻を知らず、データベースにアクセスできず、大きな数を正確に計算できません。Tool Useパターンでは、エージェントが必要なときに「関数を呼び出す」ことができます。LLMはどのツールを呼び出すかを決定し、コードがそのツールを実行してから、結果をLLMに返して推論を続けさせます。

@Component
public class OrderTools {

    private final OrderRepository orderRepository;

    public OrderTools(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Tool(description = "注文コードで注文ステータスを検索します")
    public String getOrderStatus(String orderId) {
        return orderRepository.findById(orderId)
                .map(Order::getStatus)
                .orElse("注文 " + orderId + " が見つかりません");
    }

    @Tool(description = "10%の税金を含む注文の合計金額を計算します")
    public double calculateTotal(double subtotal) {
        return subtotal * 1.1;
    }
}
@Service
public class CustomerSupportAgent {

    private final ChatClient chatClient;

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

    public String handle(String userMessage) {
        return chatClient.prompt()
                .user(userMessage)
                .call()
                .content();
    }
}

@Toolアノテーションは、LLMが呼び出せるメソッドであることを示します。descriptionはモデルがそれをいつ使うべきかを理解するのに役立ちます。defaultTools(orderTools)を登録すると、Spring AIが自動的に:ツールのスキーマをLLMに送信し、ツール呼び出しリクエスト(JSON形式)を受け取り、対応するJavaメソッドを実行し、結果をLLMに返して回答を完成させます。

ユーザーが「ORD-001注文はどこにありますか?」と質問すると、エージェントは自動的にgetOrderStatus("ORD-001")を呼び出すことを決定します。意図解析のロジックを手動で書く必要はありません。

ユーザー:ORD-001注文はどこにありますか?
エージェント:注文ORD-001は現在「配送中」の状態です。

JUnit 5 + Mockitoでシンプルなテストを実施して、実際のLLMを呼び出さずにツールが正しく動作することを確認します:

class OrderToolsTest {

    private final OrderRepository repository = mock(OrderRepository.class);
    private final OrderTools tools = new OrderTools(repository);

    @Test
    void shouldReturnStatusWhenOrderExists() {
        when(repository.findById("ORD-001"))
                .thenReturn(Optional.of(new Order("ORD-001", "配送中")));

        String result = tools.getOrderStatus("ORD-001");

        assertThat(result).isEqualTo("配送中");
    }
}

注意: ツールが実際のデータベースやAPIに触れる前に、入力を常に検証してください。LLMがプロンプトが不明確である場合、パラメータを誤った形式で渡す可能性があります(例えば、orderId が空)。

4. Planningパターン:大きなタスクを小さなステップに分割する

複雑な要求(「3つのサプライヤーを比較するレポートを書き、価格表と推奨事項を含める」)では、LLMに一度に実行させるとステップを見落としやすいです。Planningパターンでは2つの役割に分割します:Plannerがステップのリストを作成し、Executorが各ステップを順序に沿って実行します。

public record PlanStep(int order, String description) {}

@Service
public class PlannerAgent {

    private final ChatClient chatClient;

    public PlannerAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public List<PlanStep> createPlan(String goal) {
        String json = chatClient.prompt()
                .user("""
                    以下の目標を最大5つの実行ステップに分割してください。
                    [{"order":1,"description":"..."}]の形式でJSONArrayを返してください:
                    %s
                    """.formatted(goal))
                .call()
                .content();

        return parseSteps(json);
    }
}
@Service
public class ExecutorAgent {

    private final ChatClient chatClient;

    public ExecutorAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String runPlan(String goal, List<PlanStep> steps) {
        StringBuilder context = new StringBuilder();
        for (PlanStep step : steps) {
            String result = chatClient.prompt()
                    .user("目標:" + goal
                            + "\n既に実行した内容:" + context
                            + "\n実行するステップ:" + step.description())
                    .call()
                    .content();
            context.append("ステップ").append(step.order())
                    .append(":").append(result).append("\n");
        }
        return context.toString();
    }
}

PlannerAgentはステップのリストをJSON形式で生成するだけで、ExecutorAgentは各ステップを実行して、次のステップが前のステップで何をしたかを知るようにcontextを蓄積します。plannerとexecutorを分離することで、デバッグが簡単になります。結果が間違っている場合、計画が合理的かを確認してから、個別のステップ実行を確認します。

「3つのサプライヤーを比較するレポートを書く」のための計画:
1. 各サプライヤーの価格と機能情報を収集する
2. 比較表を作成する
3. 長所と短所を分析する
4. 推奨事項を提供する
5. エグゼクティブサマリー部分を書く

注意: Planningパターンは、複数のステップが相互に依存するタスクで有効です。単一ステップの質問(例:「このテキストを要約してください」)では、plannerを追加すると、不要なトークンと時間が浪費されるだけです。

5. Multi-Agentパターン:複数の専門化したエージェントの協力

「すべてを知っている」エージェント1つではなく、Multi-Agentパターンでは、複数の専門化したエージェントにタスクを分割します:研究エージェント、執筆エージェント、査読エージェント…そして、Orchestratorがそれらの間のフローを調整します。チームの各メンバーが1つのことに優れているシンプルなチームモデルのようなものです。

@Service
public class ResearchAgent {
    private final ChatClient chatClient;

    public ResearchAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String research(String topic) {
        return chatClient.prompt()
                .user("次のトピックについて5つの最も重要な情報を列挙してください:" + topic)
                .call()
                .content();
    }
}

@Service
public class WriterAgent {
    private final ChatClient chatClient;

    public WriterAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String write(String topic, String researchNotes) {
        return chatClient.prompt()
                .user("以下のデータに基づいて「" + topic
                        + "」について200語の記事を書いてください:" + researchNotes)
                .call()
                .content();
    }
}
@Service
public class OrchestratorAgent {

    private final ResearchAgent researchAgent;
    private final WriterAgent writerAgent;

    public OrchestratorAgent(ResearchAgent researchAgent, WriterAgent writerAgent) {
        this.researchAgent = researchAgent;
        this.writerAgent = writerAgent;
    }

    public String produceArticle(String topic) {
        String notes = researchAgent.research(topic);
        return writerAgent.write(topic, notes);
    }
}

OrchestratorAgentはLLMを自分で呼び出さず、ただ調整するだけです:ResearchAgentを呼び出して素のデータを取得し、WriterAgentに与えて完全な記事を書かせます。各エージェントは独自のプロンプトを持ち、特定のタスクに最適化されているため、1つの長いプロンプトにすべてを詰め込むよりも、各部分の品質が通常より高くなります。

mockを使ってテストして、LLMに依存せずに調整フローが正しく機能することを確認します:

class OrchestratorAgentTest {

    @Test
    void shouldPassResearchNotesToWriter() {
        ResearchAgent research = mock(ResearchAgent.class);
        WriterAgent writer = mock(WriterAgent.class);
        when(research.research("Spring AI")).thenReturn("note-A、note-B");
        when(writer.write("Spring AI", "note-A、note-B")).thenReturn("完全な記事");

        OrchestratorAgent orchestrator = new OrchestratorAgent(research, writer);
        String article = orchestrator.produceArticle("Spring AI");

        assertThat(article).isEqualTo("完全な記事");
        verify(writer).write("Spring AI", "note-A、note-B");
    }
}

注意: Multi-Agentはレイテンシ(複数のLLM呼び出しが順序に沿って行われる)とシステム複雑度を増加させます。各エージェントが実際に明確に異なるプロンプト/コンテキストを持つ場合にのみエージェントを分割してください。あまりに細かく分割する(各エージェント1文)と、オーバーヘッドが不要に増加するだけです。

6. ReActパターン:推論と行動の組み合わせ

ReAct(Reasoning + Acting)はTool Useと推論ループを組み合わせたパターンです:エージェントが考え(Thought)、行動(Action、ツール呼び出しなど)し、結果を観察(Observation)し、十分な情報を得られるまで繰り返します。単純なTool Use(1つのツールを呼び出して回答)とは異なり、ReActではエージェントが複数のツールを柔軟に呼び出し、前の観察に基づいて次のステップを自動的に決定できます。

@Service
public class ReActAgent {

    private final ChatClient chatClient;
    private final List<Object> tools;

    public ReActAgent(ChatClient.Builder builder, WeatherTool weatherTool, SearchTool searchTool) {
        this.tools = List.of(weatherTool, searchTool);
        this.chatClient = builder.defaultTools(tools.toArray()).build();
    }

    public String solve(String question) {
        return chatClient.prompt()
                .system("""
                    実データが必要な場合は、回答する前に適切なツールを呼び出してください。
                    ステップごとに推論してください:Thought -> Action -> Observation -> Thought...
                    十分な情報を得たときだけ最終的な回答を提供してください。
                    """)
                .user(question)
                .call()
                .content();
    }
}

「週末にダラットに行くときレインコートを持って行くべきですか?」という質問で、エージェントは自動的にweatherTool.forecast("ダラット")を呼び出して天気予報を取得し、観察結果「雨の確率70%」に基づいて回答することを決定できます。Thought/Action/Observationの全ステップはフレームワーク内で行われ、手動でループを書く必要はなく、Spring AIが複数回のツール呼び出しループを自動的に処理します。

Thought:ダラットの天気を知る必要があります。
Action:forecast("ダラット")
Observation:雨の確率70%、18-22度
Thought:レインコートを持って行くべき。
Answer:レインコートを持って行ってください。週末のダラットの天気予報では雨の確率が70%です。

注意: ReActは、エージェントがツールを呼び出し続けても収束しない場合「無限ループ」に陥りやすいです。ツール呼び出しの最大回数(maxToolCallsまたはタイムアウト)を常に設定して、API費用が制御不能に増加することを避けてください。

7. 5つのパターンの簡単な比較

パターン 何を解決するか コスト(LLM呼び出し数) いつ使うべきか
Reflection 低い回答品質 高い(2~3倍) 高い精度が必要で、自己チェック可能なタスク
Tool Use LLMがデータ/システムにアクセスできない 低~中程度 検索、計算、API呼び出しが必要
Planning 複数ステップタスクでステップを見落とす 中程度 複雑で構造がはっきりしたタスク
Multi-Agent 1つのエージェントが全タスクに十分でない 高い パイプラインに明確な役割がある(研究/執筆/査読)
ReAct ツールを使うが、ステップ数が事前に不明 変動的 情報を柔軟に探索する必要があるタスク

実際には、これらのパターンは相互に排他的ではありません。顧客サポートエージェントは、Tool Useを使って注文を検索し、Planningを使って複雑なリクエストを処理し、送信前に機密情報を確認するためにReflectionを使用する可能性があります。

8. まとめ

5つのパターン(Reflection、Tool Use、Planning、Multi-Agent、ReAct)は、自分の間違いを修正でき、実際のシステムと対話でき、複雑なタスクを処理でき、情報を柔軟に探索できるAIエージェントを構築するための基本的なブロックです。すべてのタスクに5つすべてを使う必要はありません。Tool Useから始め(ほぼすべてのエージェントに必要)、タスクが複雑になったらPlanningやReActを追加し、Reflection/Multi-Agentはコスト増加に見合う価値がある場合のみ使用してください。すべてを1つのエージェントに詰め込もうとするのではなく、問題に応じて正しいパターンを選ぶことが重要です。

ai-agentspring-aijavallmdesign-pattern