AIエージェントの5つのパターン - 詳細な解説とJavaの例
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. 動作フロー
- 生成:エージェントが最初の回答を生成します。
- 批評:エージェント(または別のエージェント)が「レビュアー」の役割を果たし、エラーを指摘します。
- 修正:エージェントが批評に基づいて修正し、質に達するまでステップ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つのエージェントに詰め込もうとするのではなく、問題に応じて正しいパターンを選ぶことが重要です。