5. エージェントプレビュー — 「なるほど!」の瞬間
これまで、あなたがフローを制御するシステムを構築してきました。プロンプトを書き、LLMを呼び出し、レスポンスを処理します。LLMは強力ですが、それでもあなたの指示に従っているだけです。
この章では、根本的な転換を紹介します:LLMが次に何が起こるかを決定したらどうなるでしょうか?
この章が存在する理由
問題: このプレビューなしに第11章までチャットボットを構築し続けると、「エージェントはより優れたチャットボットに過ぎない」と考えてしまう可能性が高いです。
真実: エージェント(agent)は根本的に異なります。エージェントは単にテキストを生成するのではなく、プログラムを制御する決定を行います。
解決策: この章では、これから向かう方向を示します。そうすることで、次の章でパイプラインと状態管理を構築する際に、各要素がなぜ重要なのかを理解できます。
学ぶこと(そして学ばないこと)
この章(第5章):
- エージェント的思考の15分間の最小限のプレビュー
- LLMの出力が異なるコードパスをトリガーする方法
- ルーティングとエージェントループの概念的な違い
後の章(第12章以降):
- ツール、ループ、自己修正を備えた実際のエージェントシステム
- 本番環境対応の実装
- マルチエージェント協調などの高度なパターン
なぜギャップがあるのか? エージェントを構築する前に、基礎が必要です:プロンプトエンジニアリング(第4章)、実行パイプライン(第6章)、構造化出力(第7章)、状態管理(第8章)、知識統合(第9-11章)。
学習アプローチに関する注意: この章は概念に焦点を当てており、実装の詳細には焦点を当てていません。実際の設計と開発技術は第12章以降で扱います。ここで見るすべてを実装する方法を知らなくても心配しないでください—それは意図的です。エージェントが何であるかを概念的に理解することで、今は十分です。
それでは始めましょう。
5.1) チャットボットから意思決定者へ
概念: 質問に答える代わりに、LLMがどのツールを使用するかを決定する
第3章と第4章では、LLMがテキストレスポンスを生成するチャットシステムを構築しました。フローは単純でした:
- ユーザーが質問する
- LLMが回答を生成する
- 回答を表示する
これは静的チェーン(chain)です:フローは事前に決定されています。LLMの唯一の仕事はテキストを生成することです。
では、リクエストのタイプを変更してみましょう。ユーザーが「847 × 923は何ですか?」と尋ねます。
第3章のチャットボットはこれに答えようとしますが、LLMは実際には計算しません—もっともらしく見えるトークンを予測します。「2 + 2」の場合、答え「4」がトレーニングデータに非常に頻繁に現れるため、LLMは正しく答えます。しかし「847 × 923」—LLMが見たことのない計算—の場合、数字のように見える何かを生成しますが、おそらく間違っています。
本当に欲しいのは:
- LLMが認識する: 「これは数学の問題です」
- LLMが決定する: 「トークン予測ではなく、計算機ツールを使用します」
- Pythonが実行する: 847 × 923 = 781,781
- システムが返す: 正しい答え
LLMの仕事は計算することではなく、計算できるツールにルーティングすることです。
ここに重要な洞察があります:LLMは数学を解く必要はありません—計算機を使用することを決定する必要があります。
これがチャットボットから意思決定者への転換です。LLMはリクエストを調べ、適切なツールにルーティングします。プログラムフローについての決定を行っており、単にテキストを生成しているだけではありません。
この違いを視覚化してみましょう:
静的チェーンでは、LLMがすべてに直接答えようとします。動的ルーティングシステムでは、LLMがリクエストを適切なツールにルーティングします。
ビジュアル: 「線形チェーン」(静的)と「ルーター」(動的)の比較
静的チェーンがユーザーリクエストを処理する方法は次のとおりです:
LLMはすべてに直接答えようとします。「フランスの首都は何ですか?」や「240の15%は何ですか?」と尋ねても、LLMはテキストレスポンスを生成します。首都は正しく答えられるかもしれません(トレーニングデータで「パリ」を何度も見ているため)が、パーセンテージの計算は間違える可能性が高いです。
問題: LLMはすべての質問に対して同じアプローチ—トークン予測—を使用します。より良いツールが存在する場合でもです。
では、異なるタイプのリクエストを異なる方法で処理できるシステムを導入しましょう:
- 事実に関する質問: 「フランスの首都は何ですか?」→ 検索ツールを使用
- 数学の問題: 「240の15%は何ですか?」→ 計算機を使用
- 会話的: 「お元気ですか?」→ LLMが直接応答
これを可能にする動的ルーターは次のとおりです:
LLMは入力を調べ、リクエストのタイプに基づいてパスを選択します。これがルーティングロジックであり、LLMがルーターとして機能します。
重要な違い: 常にテキストを生成する代わりに、LLMは各リクエストをどのように処理するかを決定します—適切なツールにルーティングすることによって。
重要なポイント: 回答の生成からアクションの選択へ
これがエージェント的システムにおける根本的な転換です:
従来のLLM使用法: 質問をすると、LLMが回答を書きます。
エージェント的LLM使用法: 質問をすると、LLMが何をすべきかを決定し、コードがその決定を実行します。
LLMの出力はもはやユーザー向けのテキストだけではありません—プログラムへの指示です。
このように考えてください:従来のチャットボットでは、LLMは顧客の質問に答える従業員です。エージェント的システムでは、LLMは各リクエストをどの部門が処理すべきかを決定するマネージャーです。
この意思決定能力がエージェント的システムの本質です。LLMは単に応答するだけでなく、プログラムが次に何をするかを制御します。入力に基づいて、計算機をトリガーしたり、検索APIを呼び出したり、直接応答したりできます。プログラムの動作はLLMの決定に基づいて変化します。
しかし、これを実際にどのように実装するのでしょうか?これを機能させる最小限のコードを見てみましょう。
5.2) 最小限の実行
トリガー: 構造化出力を強制するシステムプロンプト
LLMをルーターとして機能させるには、出力を制約する必要があります。完全な回答を生成する代わりに、構造化された決定—何をすべきかとどのデータを使用するかの両方をコードに伝えるJSONオブジェクト—を出力してほしいのです。
これを実現するシステムプロンプトは次のとおりです:
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
import json
llm = ChatOpenAI(model="gpt-4o-mini")
system_prompt = """あなたはルーティングアシスタントです。ユーザーのリクエストを分析し、JSONで応答してください。
出力形式:
{
"action": "CALC" | "SEARCH" | "CHAT",
"input": "ツール用のクリーンな入力"
}
例:
ユーザー: "15かける7を計算して"
出力: {"action": "CALC", "input": "15 * 7"}
ユーザー: "日本の首都は何ですか?"
出力: {"action": "SEARCH", "input": "日本の首都"}
ユーザー: "こんにちは!"
出力: {"action": "CHAT", "input": "こんにちは!"}
重要な情報を抽出し、適切なツール用にフォーマットしてください。
有効なJSONのみを出力し、それ以外は何も出力しないでください。"""
def get_routing_decision(user_input: str) -> dict:
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=user_input)
]
response = llm.invoke(messages)
# JSONレスポンスをパース
try:
decision = json.loads(response.content.strip())
return decision
except json.JSONDecodeError:
# LLMが有効なJSONを出力しない場合のフォールバック
return {"action": "CHAT", "input": user_input}
# 例とは異なる入力でテスト
print(get_routing_decision("25かける48は何ですか?"))
# 出力: {'action': 'CALC', 'input': '25 * 48'}
print(get_routing_decision("ハムレットを書いたのは誰ですか?"))
# 出力: {'action': 'SEARCH', 'input': 'ハムレットの著者'}
print(get_routing_decision("今日はお元気ですか?"))
# 出力: {'action': 'CHAT', 'input': '今日はお元気ですか?'}このプロンプトは意図的に制限的です。LLMに創造的であることを求めているのではなく、入力を分類し、関連情報を抽出して構造化された形式で出力することを求めています。
ここで何が起こっているか注目してください:
- LLMがユーザーの質問を読む
- 意図を判断する(計算、事実検索、会話)
- 重要な情報を抽出してクリーンにする
- アクションとクリーンな入力の両方を含むJSONオブジェクトを出力する
- コードがこの構造化データを受け取り、それに応じてルーティングする
LLMは質問に答えているのではなく、何が質問に答えるべきかを決定しています。
ブリッジ: LLMの決定をツール実行に接続する
次に、LLMの決定を実際のツール実行に接続する必要があります。最小限のブリッジは次のとおりです:
def safe_calculator(expression: str) -> float:
try:
# 安全性のため制限された名前空間でevalを使用
# 注意: eval()にはセキュリティリスクがあります。
# 本番環境では、sympyやastベースの評価などの数学パースライブラリを使用してください。
result = eval(expression, {"__builtins__": {}}, {})
return float(result)
except:
raise ValueError(f"計算できませんでした: {expression}")
def search(query: str) -> str:
"""プレースホルダー検索関数"""
# 実際には、これは検索APIを呼び出します
# 今は「ハムレットの著者」のようなクリーンなクエリを受け取ります
return f"検索結果: {query}"
def route_and_execute(user_input: str) -> str:
"""LLMの決定を取得し、適切なツールを実行"""
decision = get_routing_decision(user_input)
action = decision["action"]
tool_input = decision["input"] # LLMがクリーンにした入力
if action == "CALC":
try:
result = safe_calculator(tool_input)
return f"計算結果: {result}"
except ValueError as e:
return f"計算を実行できませんでした: {e}"
elif action == "SEARCH":
result = search(tool_input)
return result
else: # CHAT
# 会話的なクエリの場合、LLMに直接応答させる
response = llm.invoke([HumanMessage(content=tool_input)])
return response.content
# 完全なフローをテスト
print(route_and_execute("25かける48は何ですか?"))
# LLMが「25 * 48」を抽出 → 計算機がクリーンな入力を受け取る
# 出力: 計算結果: 1200.0
print(route_and_execute("ハムレットを書いたのは誰ですか?"))
# LLMが「ハムレットの著者」に再フォーマット → より良い検索クエリ
# 出力: 検索結果: ハムレットの著者
print(route_and_execute("今日はお元気ですか?"))
# 出力: ありがとうございます、元気です!これが最もシンプルな形式の最小限のルーティングパターンです:
- LLMの決定: LLMからルーティング決定を取得
- コード実行: Pythonコードが決定をチェック
- ツール呼び出し: 適切な関数が呼び出される
- 結果返却: 出力がフォーマットされて返される
重要な洞察: LLMの出力はもはやユーザー向けのテキストではありません—プログラムの動作を制御する構造化データです。actionフィールドはコードにどのパスを取るべきかを伝え、inputフィールドはそのツール用のクリーンなデータを提供します。このJSONレスポンスは実行可能なロジックになります。
これはチャットボットとは根本的に異なります。チャットボットでは、LLMの出力がユーザーに直接送られます。ここでは、LLMの出力がコードに送られ、コードが何を実行するかを決定します。
このセクションで構築したものはルーターです—完全なエージェントの前段階です。基本原理(LLMがフローを制御)を示していますが、真のエージェントループを定義する反復と自己修正が欠けています。
このシンプルなルーティングシステムには大きな制限があります:自分の間違いを修正できません。なぜそれが重要で、次に何が来るのかを探ってみましょう。
5.3) 用語の理解: ルーター vs エージェント
今構築したものはルーターです。しかし、完全なエージェントと比較するとどうでしょうか?明確な定義を確立しましょう:
ルーター(今構築したもの)
- リクエストごとに1つの分類決定を行う
- どのツール/パスを実行するかを選択
- 一度実行して返す
- 反復なし、状態なし、自己修正なし
- 例: メール分類器、意図検出器、ツールセレクター
ツール呼び出しアシスタント(第13章)
- LLMが関数呼び出しAPIを介して直接ツールを呼び出せる
- 通常は単一ターン(1つのリクエスト → 1つのレスポンス)
- ルーティングよりも洗練されているが、必ずしも反復的ではない
- 例: 「天気を検索して要約する」を1回の呼び出しで実行
エージェント(完全ループ)(第14-17章)
- 考える → 行動する → 観察するサイクルを反復
- 反復間で状態を保持
- 結果に基づいて決定を修正できる
- 自己修正を実装
- 例: コードが動作するまで修正を試みるデバッグアシスタント
エージェント的システム(包括的な用語)
- LLMの出力が制御フローに影響を与えるあらゆるシステム
- ルーター、ツール呼び出しアシスタント、完全なエージェントを含む
- 「エージェント的」は特性を表し、「エージェント」は特定のアーキテクチャを表す
- 例: 上記のすべてがエージェント的動作を示す
5.2で構築したものはルーターです—1つの決定を行い、それを実行します。これは単純な分類タスクにはうまく機能しますが、複数のステップ、検証、または軌道修正を必要とする状況には対応できません。初期の決定が最適でない場合、またはタスクが予想よりも複雑であることが判明した場合、ルーターには適応するメカニズムがありません。
エージェントはこの制限を解決します。反復を導入することで、アクションの結果を観察し、アプローチを再考し、再試行できます。これにより、解決への道筋が最初から明確でない複雑な複数ステップのタスクに適しています。
第14-17章では、これらの反復的なエージェントループを構築する方法を学びます。
注意: 最近、o1やo3のような推論モデルは、一部の複数ステップタスクを内部で処理でき、特定のシナリオで明示的なループの必要性を減らします。実際のエージェントをパートIVで構築する際に、ループと推論モデルをいつ使用するかを探ります。
今理解すべきこと:
- エージェント的システムは制御フローから始まる: LLMの出力がコードが次に何をするかを決定する
- ルーティングは最もシンプルな形式: 1つの決定、1つのツール、1つの結果—5.2で構築したもの
- 実際のエージェントにはループが必要: 複数ステップのタスク、検証、エラー回復を処理するため(第14-17章)
- 自己修正が重要な違い: エージェントは結果を観察し、アプローチを調整できる
まだカバーしていないこと(後の章まで扱いません):
- 実行パイプラインの構成方法(第6章)
- 構造化出力の処理方法(第7章)
- 会話メモリの管理方法(第8章)
- ツールの適切な定義方法(第12章)
- 自己修正を伴うエージェントループの実装方法(第14-17章)
- エージェントを本番環境対応にする方法(第18-26章)
この章は概念的な転換についてでした—システムがエージェント的動作を示すものを理解することです。実装の詳細は後で扱います。
次の章では、実用的な基礎の構築を続けます:LangChain Expression Language(LCEL)を使用した実行パイプラインの構成です。これらは、パートIVで実際のエージェントを実装する際に使用する構成要素です。
「なるほど!」の瞬間は完了しました。エージェント的システムとは、LLMが決定し、コードが実行するシステムであることを理解しました。それ以外のすべて—ツール、ループ、状態管理—は、この決定-実行サイクルをより堅牢で有能にすることです。
基礎の構築を続けましょう。