コンテンツにスキップ

02. 最初の ReAct エージェント

ゴール

  • StateGraph の基本構造を理解する
  • ツール呼び出しループ(ReAct)を 自分の手で 組む
  • create_react_agent ヘルパーを使うのは後回し。中身を理解してから使う

LangGraph の世界観

LangGraph は agent = 有限状態機械(FSM) という見方をする。

  • State: 共有メモリ。messages、scratchpad、ユーザー入力など
  • Node: state を受け取り、新しい state を返す関数
  • Edge: node 間の遷移。条件付き edge で分岐可能
  • Graph: node と edge をまとめたもの。コンパイルすると Runnable になる
       ┌──────────┐
   →   │ agent    │ ───→ tool call あり ──→ ┌─────┐
       │ (LLM)    │                          │tools│
       └──────────┘ ←── tool result ─────── └─────┘
              └── tool call なし → END

ハンズオン1: 最小 ReAct

src/agent.py:

from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import BaseMessage
from langchain_core.tools import tool


class State(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]


@tool
def get_weather(city: str) -> str:
    """指定された都市の天気を返す(モック)"""
    return f"{city}の天気: 晴れ、気温22度"


@tool
def calculator(expression: str) -> str:
    """Python の eval で式を評価する(学習用、本番では使わない)"""
    try:
        return str(eval(expression, {"__builtins__": {}}, {}))
    except Exception as e:
        return f"error: {e}"


tools = [get_weather, calculator]
llm = ChatAnthropic(model="claude-sonnet-4-5", temperature=0).bind_tools(tools)


def agent_node(state: State) -> dict:
    """LLM を呼んで次のアクションを決める"""
    response = llm.invoke(state["messages"])
    return {"messages": [response]}


def tool_node(state: State) -> dict:
    """直前の AIMessage に含まれる tool_call をすべて実行"""
    last_msg = state["messages"][-1]
    results = []
    for call in last_msg.tool_calls:
        tool_fn = {t.name: t for t in tools}[call["name"]]
        result = tool_fn.invoke(call["args"])
        from langchain_core.messages import ToolMessage
        results.append(ToolMessage(content=str(result), tool_call_id=call["id"]))
    return {"messages": results}


def should_continue(state: State) -> str:
    """tool_call があれば tools へ、なければ END へ"""
    last_msg = state["messages"][-1]
    if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
        return "tools"
    return END


builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
builder.add_edge("tools", "agent")

graph = builder.compile()

実行:

# src/run.py
from agent import graph
from dotenv import load_dotenv

load_dotenv()

result = graph.invoke({
    "messages": [("user", "東京の天気を教えて、それから 23 * 17 を計算して")]
})

for msg in result["messages"]:
    msg.pretty_print()
uv run python src/run.py

何が起きているか

  1. user message → agent node
  2. LLM が get_weather(city="東京") を tool_call として返す
  3. should_continuetools を返す → tool_node で実行
  4. ToolMessage が messages に追加され、また agent node へ
  5. LLM が今度は calculator(expression="23 * 17") を要求
  6. 結果を見た LLM が tool_call なしの応答を返す → END

これが ReAct ループの正体。while llm.has_tool_call(): execute() を state machine で表現しているだけ。

ハンズオン2: create_react_agent で同じものを作る

from langgraph.prebuilt import create_react_agent

graph = create_react_agent(llm, tools)

これだけ。中身は上で書いたものとほぼ同じ。ヘルパーの中身を理解した上で使う。

グラフ可視化

print(graph.get_graph().draw_mermaid())

Mermaid 形式の図が出る。Obsidian に貼って構造を眼で覚える。

Try(手を動かす)

  1. tool を1つ追加する(例: search_web(query) を DuckDuckGo API で)
  2. agent_node の中で system message を差し込む(「あなたは丁寧な日本語アシスタント」)
  3. graph.stream(...) で途中経過を出力してみる(step ごとに何が起きるか観察)
  4. tool 実行で例外が出たらどうなるか試す(now: 落ちる。次章で対処)

学んだこと

  • StateGraph は state を中心に node を繋ぐ FSM
  • ReAct ループは「LLM → tool → LLM → tool → ... → END」を条件付き edge で表現
  • create_react_agent はこの典型パターンのヘルパー
  • グラフは Mermaid で可視化できる

次は 03_state_and_checkpoint。プロセスを落としても続きから再開できる永続化を扱う。


作成: 2026-05-16 / 最終更新: 2026-05-16