目次
- はじめに
- 前提知識と環境設定
- ReActパターンの理解
- カスタムツールの実装
- ReActエージェントの推論コンポーネント構築
- LangGraphにおけるカスタムステート設計
- グラフノードの実装
- グラフの組み立て
- エージェントのテストとデバッグ
- まとめと発展
- 付録:完全なコード
はじめに
大規模言語モデル(LLM)を活用したAIエージェントの開発において、効率的なワークフロー設計は成功の鍵を握ります。特に「考えて行動する」というプロセスを繰り返すReAct(Reasoning and Acting)パターンは、複雑なタスクを解決するエージェントにとって重要な手法です。
しかし、従来のReActエージェントの実装は複雑で理解しづらいことがありました。LangGraph(LangChainのグラフベースフレームワーク)を使用すると、このパターンをより直感的に実装できます。
この記事では、LangGraphを使用してReActエージェントを実装する方法を、初心者にも理解しやすいように段階的に解説します。完成すると、以下のような機能を持つエージェントが構築できます:
- ユーザーの質問に対する推論
- 必要に応じたウェブ検索ツールの利用
- 計算ツールの使用(例:数値の3倍計算)
- 複数のステップにわたる推論と行動のループ処理
完成したエージェントは「サンフランシスコの天気は何度ですか?それを3倍にしてください」といった質問に答えられるようになります。
前提知識と環境設定
必要なパッケージとAPIキー
このプロジェクトを進めるには、以下のパッケージとAPIキーが必要です:
-
パッケージ:
- langchain
- langgraph
- langchain-openai
- langchain-hub
- python-dotenv
- black, isort(コード整形用)
-
APIキー:
- OpenAI API キー(GPT-3.5/4へのアクセス用)
- Tavily API キー(ウェブ検索機能用)
- LangChain API キー(トレース機能用、任意)
Poetry による環境設定
環境を設定するために、Poetryを使用します:
# プロジェクトディレクトリを作成
mkdir langgraph-react
cd langgraph-react
# Poetry環境を初期化
poetry init
# 依存関係をインストール
poetry add langchain langgraph langchain-openai langchain-hub black isort python-dotenv
環境変数の設定:
# .envファイルを作成
touch .env
.env
ファイルに以下の内容を追加します:
OPENAI_API_KEY=your_openai_api_key
TAVILY_API_KEY=your_tavily_api_key
LANGCHAIN_API_KEY=your_langchain_api_key
LANGCHAIN_PROJECT=your_langchain_project
ReActパターンの理解
ReActアルゴリズムの概要
ReAct(Reasoning and Acting)は、LLMベースのエージェントが問題を解決するための効果的なパターンです。以下の2つの主要ステップを繰り返します:
- 推論(Reasoning): エージェントは現在の状況を分析し、次に何をすべきかを決定します
- 行動(Acting): 決定に基づいて行動(ツールの実行など)を行います
この反復が、エージェントの自律的な問題解決を可能にします。
従来の実装と比較したLangGraphの利点
従来のLangChainでのReActエージェント実装では、複雑なロジックを理解するのが難しいことがありました。LangGraphを使用すると以下の利点があります:
- 視覚的な明確さ: エージェントのフローをグラフとして視覚化できる
- モジュール性: 各ノードが明確な責任を持ち、デバッグや拡張が容易
- 状態管理: グラフ全体で一貫した状態管理が可能
- 柔軟性: 条件付きエッジを使って複雑な決定ロジックを実装できる
カスタムツールの実装
エージェントが使用するツールを実装します。今回は2つのツールを作成します:
Tavilyサーチツール
Tavilyサーチツールは、ウェブ検索を実行するための組み込みツールです:
from langchain_community.tools.tavily_search import TavilySearchResults
# 検索結果を1件に制限したTavilyサーチツールを作成
search_tool = TavilySearchResults(max_results=1)
カスタム「triple」関数の作成
数値を3倍にする単純なカスタムツールを作成します:
from langchain.tools import tool
@tool
def triple(number: float) -> float:
"""指定された数値を3倍にします。
Args:
number: 3倍にする数値
Returns:
入力値の3倍の数値
"""
# 文字列として渡される可能性があるためfloatに変換
return float(number) * 3
両方のツールをリストにまとめます:
tools = [search_tool, triple]
ReActエージェントの推論コンポーネント構築
ReActエージェントRunnableの作成
推論エンジンとして機能するReActエージェントRunnableを作成します:
from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent
from langchain_hub import pull
# LLMの初期化(GPT-3.5 Turbo)
llm = ChatOpenAI(model="gpt-3.5-turbo")
# ReActプロンプトをLangChain Hubからプル
react_prompt = pull("hwchase17/react")
# ReActエージェントRunnableを作成
react_agent = create_react_agent(llm, tools, react_prompt)
ReActプロンプトの理解
ReActプロンプトは、エージェントの思考プロセスを「Thought(思考)」、「Action(行動)」、「Action Input(行動の入力)」、「Observation(観察)」という形式に導きます。
一般的なReActプロンプトの構造:
- システムメッセージ: タスクと利用可能なツールの説明
- フォーマット指示: 思考プロセスの記述方法
- サンプル: 正しい使用法を示す例
- 現在の状態: 現在のやり取りとスクラッチパッド(中間ステップの履歴)
Runnableが返すのは次の2種類のオブジェクトのいずれかです:
- AgentAction: 実行すべきツールとその入力パラメータ
- AgentFinish: ユーザーに返すべき最終的な答え
LangGraphにおけるカスタムステート設計
カスタムステートの必要性
LangGraphでは、グラフのノード間で情報を共有するためのステート(状態)が必要です。ReActエージェントの場合、特定の情報を追跡する必要があります:
- ユーザーの入力
- 現在のエージェントの結果(行動または終了)
- これまでの中間ステップの履歴
TypedDictによるステート実装
Python 3.8以降で導入されたTypedDict
を使用して、型付きの状態クラスを作成します:
from typing import Annotated, TypedDict, Union, List, Tuple
from operator import add
from langchain.agents import AgentAction, AgentFinish
class AgentState(TypedDict):
"""エージェントの状態を表すクラス"""
input: str # ユーザーの入力
agent_outcome: Union[AgentAction, AgentFinish, None] # エージェントの現在の結果
intermediate_steps: Annotated[List[Tuple[AgentAction, str]], add] # 中間ステップのリスト
operator.addアノテーションの使用
Annotated[..., add]
構文は、LangGraphに特別な意味を持ちます:
-
add
演算子は、ノードが返す新しい値を既存のリストに追加するように指示します - アノテーションがない場合は、値が置き換えられます
この機能により、intermediate_steps
に中間ステップを蓄積できます。これはエージェントのスクラッチパッドとして機能し、推論履歴を保持します。
グラフノードの実装
LangGraphのグラフには、少なくとも2つのノードが必要です:
agent_reasonノード
このノードは、ReActエージェントの推論部分を実行します:
def run_agent_reasoning_engine(state: AgentState):
"""エージェントの推論エンジンを実行するノード"""
# ReActエージェントRunnableを実行
result = react_agent.invoke(state)
# 状態を更新して返す
return {"agent_outcome": result}
actノード
このノードは、エージェントが選択したツールを実行します:
from langchain.tools import ToolExecutor
# ツール実行のためのツールエグゼキューターを初期化
tool_executor = ToolExecutor(tools)
def execute_tools(state: AgentState):
"""ツールを実行するノード"""
# 実行すべきツールの情報を取得
agent_action = state["agent_outcome"]
# ツールを実行
output = tool_executor.invoke(agent_action)
# 中間ステップとして結果を記録
return {"intermediate_steps": [(agent_action, str(output))]}
グラフの組み立て
StateGraphの作成
LangGraphのStateGraph
クラスを使用して、ノードとエッジを持つグラフを作成します:
from langgraph.graph import StateGraph, END
# カスタムステートを持つStateGraphを作成
flow = StateGraph(AgentState)
# ノードを追加
flow.add_node("agent_reason", run_agent_reasoning_engine)
flow.add_node("act", execute_tools)
# エントリーポイントを設定
flow.set_entry_point("agent_reason")
条件付きエッジの実装
条件付きエッジを使用して、推論ノードの結果に基づいてフローを制御します:
def should_continue(state: AgentState):
"""次のノードを決定する条件関数"""
# エージェントが終了を示していれば、グラフを終了
if isinstance(state["agent_outcome"], AgentFinish):
return END
# そうでなければ行動ノードへ
return "act"
# 条件付きエッジを追加
flow.add_conditional_edges("agent_reason", should_continue)
# 行動ノードから常に推論ノードへ戻るエッジを追加
flow.add_edge("act", "agent_reason")
# グラフをコンパイル
app = flow.compile()
完成したグラフの構造は以下のようになります:
エージェントのテストとデバッグ
サンプルクエリでのテスト
実装したエージェントをテストしてみましょう:
question = "サンフランシスコの天気は何度ですか?それを3倍にしてください。"
result = app.invoke({"input": question})
print(result["agent_outcome"].return_values["output"])
期待される出力例:
サンフランシスコの現在の天気は12.8°Cです。これを3倍にすると38.4°Cになります。
ステートの変化とデバッグ
エージェントの実行中に状態がどのように変化するかを確認すると、動作の理解が深まります:
-
初期状態:
-
input
: ユーザーの質問 -
agent_outcome
: None -
intermediate_steps
: []
-
-
最初の推論後:
-
agent_outcome
: AgentAction(天気検索ツールを使用)
-
-
ツール実行後:
-
intermediate_steps
: [(検索アクション, 天気情報)]
-
-
2回目の推論後:
-
agent_outcome
: AgentAction(tripleツールを使用)
-
-
2回目のツール実行後:
-
intermediate_steps
: [(検索アクション, 天気情報), (tripleアクション, 計算結果)]
-
-
最終推論後:
-
agent_outcome
: AgentFinish(最終回答)
-
このデバッグ過程では、ツールの出力や状態の変化を注意深く観察することが重要です。特にtripleツールでは、文字列から浮動小数点数への適切な変換が必要です。
まとめと発展
構築したものの概要
このチュートリアルでは、LangGraphを使用してReActエージェントを構築しました:
- カスタム状態クラスを設計して情報の流れを管理
- 推論と行動のノードを実装
- 条件付きエッジを使用してフローを制御
- 複数のツールを統合(検索と計算)
従来のLangChainでのReAct実装と比較すると、LangGraphアプローチの明確さと拡張性が際立ちます。
潜在的な改善点
このエージェントは以下のように拡張できます:
-
より多くのツールの追加:
- 計算ツール
- データベースアクセス
- APIインテグレーション
-
エラー処理の改善:
- ツール実行の失敗を処理するノード
- 入力検証と前処理
-
メモリの追加:
- 会話履歴の保持
- 長期記憶の統合
-
モニタリングと分析:
- LangSmithを使用したより詳細なトレースの追加
- パフォーマンスメトリクスの収集
-
ヒューマンインザループ機能:
- 人間の介入が必要な場合に支援を求めるノード
付録:完全なコード
最終的な完全なコードは以下の通りです:
# react.py - ReActエージェントの実装
from dotenv import load_dotenv
load_dotenv()
from langchain_hub import pull
from langchain.agents import create_react_agent
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain.prompts import PromptTemplate
from langchain.tools import tool
from langchain_openai import ChatOpenAI
@tool
def triple(number: float) -> float:
"""指定された数値を3倍にします。
Args:
number: 3倍にする数値
Returns:
入力値の3倍の数値
"""
return float(number) * 3
# ツールの作成
tools = [
TavilySearchResults(max_results=1),
triple
]
# LLMの初期化
llm = ChatOpenAI(model="gpt-3.5-turbo")
# ReActプロンプトをロード
react_prompt = pull("hwchase17/react")
# ReActエージェントRunnableを作成
react_agent = create_react_agent(llm, tools, react_prompt)
# state.py - カスタムステート実装
from typing import Annotated, TypedDict, Union, List, Tuple
from operator import add
from langchain.agents import AgentAction, AgentFinish
class AgentState(TypedDict):
"""エージェントの状態を表すクラス"""
input: str # ユーザーの入力
agent_outcome: Union[AgentAction, AgentFinish, None] # エージェントの現在の結果
intermediate_steps: Annotated[List[Tuple[AgentAction, str]], add] # 中間ステップのリスト
# nodes.py - グラフノード実装
from dotenv import load_dotenv
load_dotenv()
from langchain.tools import ToolExecutor
from react import react_agent, tools
from state import AgentState
# ツールエグゼキューターの初期化
tool_executor = ToolExecutor(tools)
def run_agent_reasoning_engine(state: AgentState):
"""エージェントの推論エンジンを実行するノード"""
# ReActエージェントRunnableを実行
result = react_agent.invoke(state)
# 状態を更新して返す
return {"agent_outcome": result}
def execute_tools(state: AgentState):
"""ツールを実行するノード"""
# 実行すべきツールの情報を取得
agent_action = state["agent_outcome"]
# ツールを実行
output = tool_executor.invoke(agent_action)
# 中間ステップとして結果を記録
return {"intermediate_steps": [(agent_action, str(output))]}
# main.py - グラフ作成と実行
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import AgentFinish
from langgraph.graph import StateGraph, END
from state import AgentState
from nodes import run_agent_reasoning_engine, execute_tools
# ノード名の定数
AGENT_REASON = "agent_reason"
ACT = "act"
def should_continue(state: AgentState):
"""次のノードを決定する条件関数"""
# エージェントが終了を示していれば、グラフを終了
if isinstance(state["agent_outcome"], AgentFinish):
return END
# そうでなければ行動ノードへ
return ACT
# カスタムステートを持つStateGraphを作成
flow = StateGraph(AgentState)
# ノードを追加
flow.add_node(AGENT_REASON, run_agent_reasoning_engine)
flow.add_node(ACT, execute_tools)
# エントリーポイントを設定
flow.set_entry_point(AGENT_REASON)
# 条件付きエッジを追加
flow.add_conditional_edges(AGENT_REASON, should_continue)
# 行動ノードから常に推論ノードへ戻るエッジを追加
flow.add_edge(ACT, AGENT_REASON)
# グラフをコンパイル
app = flow.compile()
# グラフを実行する
if __name__ == "__main__":
# グラフをメルメイド図として保存
app.get_graph().draw_mermaid_png("graph.png")
# エージェントを実行
question = "サンフランシスコの天気は何度ですか?それを3倍にしてください。"
result = app.invoke({"input": question})
# 結果を表示
print(result["agent_outcome"].return_values["output"])
このチュートリアルを通じて、LangGraphを使った効率的なエージェント設計の基礎を理解し、より複雑なAIアプリケーションへと発展させる準備が整いました。
理解度チェッククイズ
-
ReActパターンの2つの主要ステップは何ですか?
- 計画と実行
- 分析と合成
- 推論と行動
- 観察と応答
-
LangGraphでカスタムステートに
Annotated[..., add]
を使用する目的は?- パフォーマンスを向上させるため
- 値を上書きせずに追加するため
- 型チェックを強化するため
- デバッグを容易にするため
-
ReActエージェントのグラフで条件付きエッジが必要な理由は?
- パフォーマンスの最適化のため
- より多くのツールを追加するため
- エージェントが行動するか終了するかを決定するため
- エラー処理を実装するため
-
AgentStateクラスに含まれる3つの主要な属性は?
- input, agent_outcome, intermediate_steps
- input, output, memory
- query, result, history
- state, action, observation
-
ReActエージェントが返す可能性のある2種類の結果は?
- Success, Failure
- Continue, Stop
- AgentAction, AgentFinish
- Query, Response
このエージェント実装を出発点として、より複雑で強力なAIシステムを開発してください。LangGraphの強みを活かし、視覚的に明確で保守しやすいエージェントフローを設計しましょう!