第11回: ReAct Agent — create_react_agent で汎用リサーチエージェントを作る¶
Software Design 2024年11月号 連載第11回。LangGraph の prebuilt create_react_agent を使って、Tavily 検索 + レポート生成 + 十分性チェックを自己評価ループで回す ReAct エージェントを実装する。
全体像¶
ユーザーからのリサーチ依頼 (--task) を受けたら、ReAct エージェントが「検索 → レポート生成 → 十分性判定 → 不十分なら再検索」のループを回し、十分と判定されたレポートを返す。
flowchart TD
User["ユーザー: --task '生成AIスタートアップの最新動向'"] --> Agent
subgraph Agent["create_react_agent (ReAct ループ)"]
Reason["Reason: 次に何のツールを呼ぶか<br>LLM (gpt-4o) が思考"]
Act["Act: ツール呼び出し"]
Observe["Observe: ツール結果を Messages に追記"]
Reason --> Act --> Observe --> Reason
end
Agent -.tool_call.-> Search["search ツール"]
Agent -.tool_call.-> Writer["report_writer ツール"]
Agent -.tool_call.-> Check["sufficiency_check ツール"]
Search --> Tavily["Tavily API<br>max_results=5, include_raw_content"]
Tavily --> Summarize["summarize_search_chain<br>gpt-3.5 で各結果を要約 (batch)"]
Summarize -->|"<source>...</source> × N"| Agent
Writer --> WriterLLM["gpt-4o<br>report_writer_system プロンプト"]
WriterLLM -->|レポート文| Agent
Check --> CheckLLM["gpt-4o<br>sufficiency_classifier プロンプト"]
CheckLLM -->|"判定: True / False"| Agent
Agent -->|True 判定後| Output["最終レポートを返す"]
ReAct (Reasoning + Acting) の核は 「LLM がツールを呼ぶか終了するかを毎ステップ判断するループ」。create_react_agent はこの Reason → Act → Observe → Reason → … を LangGraph のグラフとして組み立てるショートカット。
時系列で見るとこう:
sequenceDiagram
autonumber
participant U as ユーザー
participant A as ReAct Agent (gpt-4o)
participant S as search
participant W as report_writer
participant C as sufficiency_check
U->>A: --task "生成AIの最新動向"
A->>S: search("生成AI スタートアップ 2024")
S-->>A: <source>要約1</source>...<source>要約N</source>
A->>W: report_writer(task, sources)
W-->>A: レポートv1
A->>C: sufficiency_check(task, レポートv1)
C-->>A: 判定: False / 理由: 資金調達情報が薄い
A->>S: search("生成AI スタートアップ 資金調達 2024")
S-->>A: 追加 <source>...</source>
A->>W: report_writer(task, sources統合)
W-->>A: レポートv2
A->>C: sufficiency_check(task, レポートv2)
C-->>A: 判定: True
A-->>U: レポートv2 (最終)
使用ライブラリ・原理¶
langgraph.prebuilt.create_react_agent¶
LangGraph が提供する ReAct パターンの組み立て済みグラフ。中身は概ね以下のような有限状態機械:
[agent ノード] ← LLM がツール呼び出しを判断
│
│ tool_calls あり → [tools ノード] → 結果を ToolMessage で追記 → [agent ノード]へ
│ tool_calls なし → END
- agent ノード: 現在の
messagesを入力に LLM を呼び、AIMessageを生成。AIMessage.tool_callsが空なら終了、あれば次へ - tools ノード:
tool_callsに対応する関数を実行し、各結果をToolMessageとしてmessagesに追加 - LangGraph の
MessageState(messagesを持つ State)を使うので、全履歴が messages として蓄積される
旧バージョン (langgraph==0.0.69) では messages_modifier で初期メッセージや system プロンプトを注入する。現代版 (0.2+) では prompt 引数または state_modifier に置き換わっている。
@tool デコレータ (LangChain)¶
from langchain_core.tools import tool で Python 関数を「LLM が呼べるツール」に変換する。仕組み:
- 関数シグネチャと docstring を読んで JSON Schema を自動生成
- その Schema が
tools=[...]経由で LLM のtoolsパラメータに渡され、Function Calling / Tools API として LLM に提示される - LLM が
{"name": "search", "arguments": {"query": "..."}}を返したら、エージェント側が 対応する関数を実行 → 結果をToolMessageでフィードバック
つまり docstring がツール仕様の一部。日本語で書いても効くが、英語の方がモデルの理解が安定する(このサンプルも英語 docstring)。
Tavily¶
LLM RAG 用に最適化された検索 API。通常の Google/Bing 検索と違い、1 リクエストで複数サイトを集約・スコアリング・ランク付けして返す。include_raw_content=True を付けると、本文 HTML をスクレイピング & テキスト抽出したものまで返してくれる(PDF も対応)。
- 無料枠あり (月 1,000 リクエスト)。APIキーは https://tavily.com から取得
- レスポンスは
results: [{title, url, content, raw_content, score}]の配列 raw_contentは時に数万字になるので、サンプルでは 10,000 字でクリップしてから要約
pydantic-settings¶
.env ファイルや環境変数から Pydantic モデルに値をロードするライブラリ(Pydantic v2 で BaseSettings が別パッケージに分離された)。
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
OPENAI_API_KEY: str # 必須
LLM_MODEL_NAME: str = "gpt-4o-2024-05-13" # デフォルト値あり
- 型注釈通りにバリデーション
- 環境変数が無いと インスタンス化時に
ValidationError(fail-fast で安全)
このリポジトリの方針(API キーは 1Password CLI 経由必須)からするとレガシー。現代版では pydantic-settings を外して op read で取得する。
@lru_cache でクライアントをシングルトン化¶
@lru_cache
def tavily_client() -> TavilyClient:
return TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
functools.lru_cache を 引数なし関数に付けると、初回呼び出しの戻り値をキャッシュして以降は同じインスタンスを返す。グローバル変数を避けつつシングルトンを実現する Pythonic な書き方。
ファイル別の役割¶
| ファイル | 役割 |
|---|---|
main.py |
エージェント本体・ツール3種・Settings 定義・エントリポイント。約 160 行で全部入り |
prompts/multi_step_answering_system.prompt |
ReAct エージェント本体への system プロンプト。「検索→レポート→検証→不十分なら再検索」の手順を自然言語で指示 |
prompts/summarize_search_system.prompt |
search ツール内部で各検索結果を要約するチェイン用 |
prompts/report_writer_system.prompt |
report_writer ツール内部でレポート生成するチェイン用。「ジャーナリスティックな語り口」「事実と数字を含む」など細かい指示あり |
prompts/sufficiency_classifier_system.prompt |
sufficiency_check ツール内部で True/False 判定するチェイン用 |
requirements.txt |
langchain==0.2.5, langgraph==0.0.69, tavily-python, pydantic-settings 等。全部古い |
学んだこと(要点)¶
1. ReAct は「LLM がツールを呼ぶか終了するかを毎ステップ判断するループ」¶
create_react_agent を使えば中身を書かずに済むが、何が起きているかは押さえておくべき:
- LLM は毎ターン
messages全体を見てtool_callsを返すか終了するかを決める - ツール結果は
ToolMessageとして messages に追記され、次ターンの LLM 入力に含まれる - ループの終了は LLM 自身が決める(system プロンプトの「十分なら出力して終了」がループ終了条件になっている)
2. 「ツールの中で LLM を呼ぶ」二重構造¶
このサンプルの面白いところは、3 つのツールがすべて内部で LLM を呼んでいること:
| ツール | 内部の LLM 呼び出し |
|---|---|
search |
Tavily 検索結果を summarize_search_chain (gpt-3.5) で各個要約 |
report_writer |
report_writer_system プロンプトで gpt-4o がレポート生成 |
sufficiency_check |
sufficiency_classifier プロンプトで gpt-4o が True/False 判定 |
つまり 外側の ReAct ループ (gpt-4o) が、内側の LCEL チェインたちを「ツールとして」呼ぶ。「LLM の出力を別の LLM にツールとして渡す」というレイヤード設計。
3. sufficiency_check で擬似的な自己評価ループを実装¶
LLM 単体だと「不十分でも自信満々に終了する」ことが多い。それを 明示的に「十分かどうかを判定する別の LLM 呼び出し」を挟むことで、自己評価ループを作れる。
ただしこのサンプルでは判定結果(判定: True/False)はテキストとして返るだけで、構造化出力にはしていない。実運用では with_structured_output() で Pydantic 化した方が安全。
4. プロンプトはファイル分離¶
load_prompt(name) で prompts/<name>.prompt から読む。ハマりどころは:
- プロンプトに
{}が含まれると.format()で衝突するので、Python format 用は 波括弧をエスケープ ({{}})。sufficiency_classifier_system.promptがそれ multi_step_answering_system.promptは{}を 1 つだけ含んでいて、format(datetime.now()...)で日付を埋め込む
5. agent.stream(inputs, stream_mode="values") で中間結果を逐次表示¶
stream_mode="values" は 各ノード実行後の state 全体を yield する(差分ではなく毎回フル)。それを message.pretty_print() で逐次表示している。デモ向けで分かりやすいが、本番では stream_mode="updates" (差分のみ) や stream_mode="messages" (token ストリーミング) の方が効率的。
拡張アイデア¶
- 構造化出力で
sufficiency_checkを安定化 - 戻り値を Pydantic モデル (
Verdict(is_sufficient: bool, reason: str)) にして、テキストパース不要にする -
現状の「判定: True\n理由: …」テキスト解析をエージェントに任せている部分が崩れやすい
-
ループ上限の明示
- ReAct ループは LLM が「十分」と言うまで回り続ける。コスト爆発を防ぐため
RunnableConfig({"recursion_limit": N})でハードリミットを設定する -
現状はプロンプトに「一定回数で打ち切る」と書いているだけで、保証は無い
-
検索クエリの多様化
-
同じ
queryで再検索しても新しい情報が得られない。sufficiency_checkで「足りない情報」を抽出して、それを次のクエリに渡す改修 -
Tavily 以外のソース統合
-
arXiv API、社内 Notion 検索、Wikipedia API などを別ツールとして追加し、LLM に どのソースを使うか選ばせる
-
観測性: LangSmith でトレース
LANGCHAIN_TRACING_V2=true+LANGCHAIN_API_KEYで各ツール呼び出し・トークン消費を可視化- ReAct は内部状態が複雑なので、トレースが無いとデバッグが厳しい
実際にやった移植差分 (連載原典 → 本リポジトリ)¶
連載原典は 2024年6月時点の API (langchain==0.2.5, langgraph==0.0.69, pydantic-settings + .env) で書かれていた。これを LangGraph 0.2.x + LangChain 0.3.x + リポジトリ方針 (op 経由のキー取得) に揃えた。
1. ライブラリの更新¶
| 旧 | 新 |
|---|---|
langchain==0.2.5 |
langchain>=0.3,<1.0 |
langgraph==0.0.69 |
langgraph>=0.2,<1.0 |
gpt-4o-2024-05-13 |
gpt-4o-mini |
gpt-3.5-turbo-0125 |
gpt-4o-mini(FAST 側も統一) |
実行は uv run --no-project --with ... で都度解決(README.md 参照)。旧 requirements.txt は参考のため残置。
2. create_react_agent の引数¶
LangGraph 0.2+ で messages_modifier が廃止されたので prompt に変更:
# 旧 (0.0.x)
create_react_agent(llm, tools=tools, messages_modifier=load_prompt(...).format(...))
# 新 (0.2+)
system_prompt = load_prompt(...).format(...)
create_react_agent(llm, tools=tools, prompt=SystemMessage(content=system_prompt))
3. API キー取得を op 経由に¶
pydantic-settings + .env を撤去し、_ensure_key(env_name, op_ref) ヘルパで:
- CI=true → env var を必須 (GitHub Actions の secrets 経由を許容)
- ローカル → env var に既に key があれば raise (生キー混入を防ぐ)。無ければ op read で取得
OPENAI_API_KEY / TAVILY_API_KEY の両方に適用。Settings クラスごと削除。1Password 参照は op://Personal/openAI_API/credential と op://Personal/Tavily_API_key/credential(10回目と同じ key を再利用)。
4. sufficiency_check を構造化出力に¶
旧: LLM がテキスト "判定: True\n理由: ..." を生成 → ReAct 側がパース。プロンプト改変で崩れやすい。
新: with_structured_output(Verdict) で Pydantic モデルとして受ける:
class Verdict(BaseModel):
is_sufficient: bool
reason: str
llm = ChatOpenAI(...).with_structured_output(Verdict)
verdict = (prompt | llm).invoke({})
return f"判定: {verdict.is_sufficient}\n理由: {verdict.reason}"
戻り値の文字列形式は維持しつつ、内部の LLM 出力解釈を Function Calling で堅牢化。
5. ループ上限を明示¶
LLM の自己判断で終わらない場合の保険。
6. raw_content の None 対策¶
旧: document["raw_content"] 直アクセス → Tavily が None を返すケースで TypeError。
新: document.get("raw_content") or document.get("content") or "" で safe fallback。
既知の不具合・注意点¶
- 旧
requirements.txtは参考のため残置。最新依存は README のuv run --no-project --with ...を参照 - ReAct ループの終了は LLM の自己判断 +
recursion_limitのみ。LLM がプロンプトを読み飛ばして 1 回検索しただけで終了するケースは依然ある(プロンプト強化で改善可) sufficiency_checkの戻り値は文字列のまま (判定: True/False\n理由: ...)。完全に構造を活かすなら戻り値型をVerdictにして外側でも分岐したいが、create_react_agentの枠組みでは文字列 ToolMessage が前提なので妥協
記事参照¶
- Software Design 2024年11月号 連載第11回「ReAct Agent」
- 関連: 09回 Research Agent(タスク分解型)、10回 CRAG(自己評価付き RAG)と組み合わせて読むと「自律エージェントの設計バリエーション」が見えてくる
- LangGraph
create_react_agent公式: https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent
作成: 2026-05-22 / 最終更新: 2026-06-10