Python & AI Tutorials Logo
LangChain & LangGraph

16. 事前構築コンポーネントとマルチブランチルーティング

第15章では、エージェントグラフを手作業で組み立てました — モデルノード、ツールノード、そしてループを続けるか停止するかを決める条件付きエッジです。これは本質的にツール呼び出しエージェントの標準的な構造なので、LangChain と LangGraph は、毎回同じ足場をゼロから書く代わりに使える事前構築コンポーネント(prebuilt components)として提供しています。

この章の前半では、事前構築コンポーネントを使って第15章のエージェントを作り直します。ツール実行ノードとルーティング関数を ToolNodetools_condition に置き換え、最終的にはグラフ組み立て全体を create_agent の単一呼び出しに置き換えます。第15章と挙動が同一のまま、コードが大幅に短くなることがわかるでしょう。

後半では、事前構築コンポーネントと第15章の手動でのグラフ組み立てのアプローチを組み合わせて、より複雑なエージェントを構築します。ここで構築するエージェントは、各リクエストを異なるハンドラーへとルーティングします — 複雑な相談は高性能モデルへ送られ、単純な質問はより安価で小さいモデルが回答します。これは、リクエストの種類に基づいて処理経路が分岐するマルチブランチ構造です。

16.1) 事前構築コンポーネントと create_agent

このセクションでは、第15章のグラフにあった tool_node 関数と should_continue 関数を、事前構築コンポーネントの ToolNodetools_condition に置き換えます。その後、手動での組み立てをすべて省略し、create_agent の単一呼び出しでグラフ全体を作成します。各ステップで注目すべきは、エージェントの挙動が第15章と同一のままコードが短くなっていくことです。

16.1.1) ToolNodetools_condition

ToolNode は、ツール実行を代わりに処理してくれる事前構築ノードです。State の最後のメッセージ(LLM が返した AIMessage)に tool_calls が含まれているとき、要求されたツールを実行し、その結果を ToolMessage オブジェクトとして messages に追加します。これは第15章で書いた tool_node 関数と同じ仕事をします。加えて、LLM が一度に複数のツールを要求した場合には、それらを並列に実行します。

さらに、ツール実行中の例外処理もサポートしています。ToolNode(tools, handle_tool_errors=True) と設定すると、ツールが例外を発生させてもグラフはクラッシュしません。例外はエラーの詳細を含む ToolMessage に変換され、それが LLM に渡されるので、LLM は失敗を確認し、修正した引数で再試行できます。

ToolNode はツールのリストを渡して作成します。第15章と同じ2つのツールを使います。

python
from langgraph.prebuilt import ToolNode
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """都市の現在の天気を取得します。"""
    fake_data = {"Tokyo": "18°C, cloudy", "Cairo": "31°C, sunny"}
    return fake_data.get(city, f"No weather data for {city}.")
 
@tool
def calculate(expression: str) -> str:
    """単純な算術式を評価します。例: '3 * 21'。"""
    return str(eval(expression))  # 警告: eval() はセキュリティ上のリスクがあります。本番環境では使用しないでください。
 
tools = [get_weather, calculate]
 
# 第15章の tool_map + tool_node 関数がこの1行に置き換わります
tool_node = ToolNode(tools)

ToolNode は、第15章の tool_map とまったく同じように、ツールリストから名前とツールの対応付けを内部的に構築します。実行時には、LLM が要求した名前で各ツールを検索して呼び出します。言い換えると、tool_map 辞書、tool_calls に対する for ループ、そして ToolMessage オブジェクトを構築して収集するコード — それらすべてが ToolNode の内部に収まっているのです。

tools_condition は、第15章の should_continue 関数を置き換える事前構築ルーティング関数です。第15章の should_continue をもう一度見てみましょう。

python
def should_continue(state: AgentState) -> Literal["tool_node", "__end__"]:
    """ツールを実行するかグラフを終了するかを決定します。"""
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tool_node"
    return END

これは、最後のメッセージに tool_calls があるときは "tool_node"(私たちのツールノードの登録名)を返し、そうでなければ END を返していました。tools_condition はまったく同じように動作しますが、返す名前に1つ違いがあります。should_continue はグラフに登録した名前である "tool_node" を返すように書かれていましたが、tools_condition"tools" を返すようにハードコードされています。

セクション15.2.4で学んだように、ルーティング関数が返す値は、次に実行するノードの名前です。その名前のノードがグラフに存在しない場合、ルーティングは失敗します。したがって、tools_condition を使用する場合、ツールノードは "tools" という名前で登録する必要があります。

python
from langgraph.prebuilt import ToolNode, tools_condition
 
builder.add_node("tools", ToolNode(tools))                  # "tools" という名前で登録します
builder.add_conditional_edges("llm_call", tools_condition)  # "tools" または END にルーティングします

こうすることで、tools_condition"tools" を返したとき、たった今登録した ToolNode に正確に接続されます。

では、残りのすべての部品も含めて、第15章の完全なグラフを組み立て直しましょう。ツール、State、そして llm_call ノードは第15章から変わっていません。

python
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """都市の現在の天気を取得します。"""
    fake_data = {"Tokyo": "18°C, cloudy", "Cairo": "31°C, sunny"}
    return fake_data.get(city, f"No weather data for {city}.")
 
@tool
def calculate(expression: str) -> str:
    """単純な算術式を評価します。例: '3 * 21'。"""
    return str(eval(expression))  # 警告: eval() はセキュリティ上のリスクがあります。本番環境では使用しないでください。
 
tools = [get_weather, calculate]
 
class AgentState(MessagesState):
    llm_calls: int
 
llm = ChatOpenAI(model="gpt-5-mini")
model_with_tools = llm.bind_tools(tools)
 
def llm_call(state: AgentState):
    """LLM を呼び出し、その応答を返します。"""
    response = model_with_tools.invoke(state["messages"])
    return {
        "messages": [response],
        "llm_calls": state.get("llm_calls", 0) + 1,
    }
 
builder = StateGraph(AgentState)
 
builder.add_node("llm_call", llm_call)
builder.add_node("tools", ToolNode(tools))  # 第15章の tool_node 関数の代わりに ToolNode
 
builder.add_edge(START, "llm_call")
builder.add_conditional_edges("llm_call", tools_condition)  # 第15章の should_continue の代わりに tools_condition
builder.add_edge("tools", "llm_call")
 
agent = builder.compile()

これを第15章のコードと比較してみてください。tool_map 辞書、tool_node 関数、そして should_continue 関数はすべて消えています。それらが行っていた仕事は、今や ToolNode(tools)tools_condition によって処理されます。ツールノードは、tools_condition がルーティングする名前に合わせて "tools" として登録されています。第15章と同じ質問で実行してみましょう。

python
result = agent.invoke({
    "messages": [HumanMessage(content="カイロの気温を取得して、その数値を3倍してください。")],
    "llm_calls": 0,
})
 
print(result["messages"][-1].content)
print(f"\nTotal LLM calls: {result['llm_calls']}")

出力:

現在のカイロの気温: 31°C。3倍すると = 93。
 
Total LLM calls: 3

結果は第15章と同一です。エージェントは天気を調べ、計算を実行し、最終的な回答を生成します — 同じ挙動が保たれつつ、私たちが書いて保守すべきコードは縮小しています。

ツールノードを "tools" 以外の名前で登録したい場合はどうでしょうか。その場合は、add_conditional_edges の第3引数として対応付けの辞書を渡し、tools_condition の各戻り値がどのノードに接続すべきかを指定します。tools_condition"tools" または END のいずれかを返すので、それらをキーとして使い、ターゲットノードにマッピングします。たとえば、ツールノードを "run_tools" として登録する場合:

python
builder.add_node("run_tools", ToolNode(tools))
builder.add_conditional_edges(
    "llm_call",
    tools_condition,
    {"tools": "run_tools", END: END}   # "tools" の戻り値 → run_tools ノード、END の戻り値 → 終了
)

ToolNodetools_condition は、グラフの個々の部分 — 面倒な部分 — を置き換えますが、ノードを追加してそれらを配線する作業は依然として私たちの手に残っています。その組み立てもまた委ねることはできるでしょうか。それこそが create_agent の役割です。

16.1.2) create_agent

create_agent は、ツール呼び出しエージェントのためのグラフ組み立て全体を処理する LangChain のファクトリ関数です。モデルとツールのリストを渡すと、16.1.1で組み立てたのと同じ構造のグラフを、すでにコンパイル済みで実行できる状態にして構築します。試してみましょう。

python
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.messages import HumanMessage
 
@tool
def get_weather(city: str) -> str:
    """都市の現在の天気を取得します。"""
    fake_data = {"Tokyo": "18°C, cloudy", "Cairo": "31°C, sunny"}
    return fake_data.get(city, f"No weather data for {city}.")
 
@tool
def calculate(expression: str) -> str:
    """単純な算術式を評価します。例: '3 * 21'。"""
    return str(eval(expression))  # 警告: eval() はセキュリティ上のリスクがあります。本番環境では使用しないでください。
 
agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[get_weather, calculate],
    system_prompt="あなたは親切なアシスタントです。",
)
 
result = agent.invoke({
    "messages": [HumanMessage(content="カイロの気温を取得して、その数値を3倍してください。")],
})
print(result["messages"][-1].content)

出力:

現在のカイロの気温: 31°C。3倍すると = 93。

State の定義もなく、ノード関数もなく、add_nodeadd_edge もありません。create_agent の単一呼び出しがそれらすべてを行い、その結果は16.1.1と同一です。

内部で起こっていることは、まさに私たちがすでに知っていることです。create_agent は、渡されたモデルから LLM 呼び出しノードを作成し、ツールリストから ToolNode を構築し、それらを tools_condition エッジとループバックエッジで接続します。その結果は、16.1.1で組み立てたものと同一のループ構造のグラフです。

tool_calls あり

tool_calls なし

START

LLM 呼び出しノード

ToolNode

END

パラメータを見てみましょう。create_agent は実は私たちにとって初めてではありません — 第11章で会話型RAGを構築するときに簡単に使いましたが、パラメータについては詳しく説明しませんでした。1つずつ見ていきましょう。

  • model: エージェントが使用する LLM です。最もシンプルなアプローチは、"openai:gpt-5-mini" のようなプロバイダー文字列を渡すことです。モデルのパラメータを直接設定する必要がある場合は、ChatOpenAI(model="gpt-5-mini") のように初期化済みのモデルインスタンスを渡します。内部的には、LLM 呼び出しノードがこのモデルを使用します。
  • tools: エージェントが使用できるツールのリストです。これらから内部的に ToolNode が構築されます。
  • system_prompt: エージェントに対する動作指示です。これは、LLM を呼び出すたびにメッセージリストの先頭にシステムメッセージとして付加されます。
  • checkpointer: 会話の状態を保存し、エージェントが以前のターンを記憶できるようにします。これは、第11章でマルチターン会話を実装するために InMemorySaver()thread_id とともに使用したのと同じパラメータです。その仕組みについては第17章で詳しく取り上げます。
  • response_format: エージェントの最終的な回答を構造化出力として得たいときに使用します。Pydantic モデル(第7章と同じ概念)を渡すと、検証済みのオブジェクトが result["structured_response"] で利用できるようになります。
  • middleware: エージェントの実行ループの特定のポイントで実行する関数を登録します。これは、第11章で trim_old_messages を登録するために使用したパラメータです。以下で詳しく説明します。

response_format が実際にどのように動作するか見てみましょう。

python
from pydantic import BaseModel
from langchain.agents import create_agent
 
class WeatherReport(BaseModel):
    city: str
    temperature: str
    condition: str
 
agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[get_weather, calculate],
    response_format=WeatherReport,
)
 
result = agent.invoke({
    "messages": [HumanMessage(content="東京の天気はどうですか?")],
})
print(result["structured_response"])

出力:

city='Tokyo' temperature='18°C' condition='cloudy'

エージェントは get_weather ツールを呼び出し、その情報をスキーマに合致する WeatherReport オブジェクトに整理しました。

middleware

エージェントループには、はっきりと区別されるステージがあります。LLM を呼び出し、ツールを実行し、再び LLM を呼び出す — これらのステージが繰り返されます。ミドルウェア(middleware)を使うと、これらのステージの前後に自分の関数を挿入できます。タイミングはデコレータで指定します: @before_model は LLM 呼び出しの直前を意味し、@after_model は LLM が応答した直後を意味します。関数を middleware パラメータに登録すると、指定したポイントで毎回実行されます。

私たちは第11章ですでにミドルウェアを使いました。trim_old_messages 関数を @before_model でデコレートして登録しました — それが LLM 呼び出しのたびに実行されたので、毎回メッセージリストをトリミングできました。

各 LLM 呼び出しの直前にメッセージ数を出力するミドルウェアを構築して、いつ実行されるのかを正確に確認できるようにしましょう。

python
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
 
@before_model
def log_llm_call(state: AgentState, runtime) -> None:
    """各 LLM 呼び出しの直前にメッセージ数を出力します。"""
    print(f"[before_model] About to call LLM, current messages: {len(state['messages'])}")
 
agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[get_weather, calculate],
    middleware=[log_llm_call],
)
 
result = agent.invoke({
    "messages": [HumanMessage(content="カイロの気温を取得して、その数値を3倍してください。")],
})
print(result["messages"][-1].content)

出力:

[before_model] About to call LLM, current messages: 1
[before_model] About to call LLM, current messages: 3
[before_model] About to call LLM, current messages: 5
現在のカイロの気温: 31°C。3倍すると = 93。

log_llm_call ミドルウェアは3回実行されました。ユーザーのリクエストを処理する間に LLM が3回呼び出され、そのたびに直前でミドルウェアが実行されました。メッセージ数は各ポイントでの State を教えてくれます: 最初の呼び出しの前にはユーザーの質問(HumanMessage)しかなく — 1メッセージでした。各ループの反復のあと、ツール呼び出しを要求する AIMessage とその結果を持つ ToolMessage が追加され、3、そして5へと増えていきました。

知っておくべきことが1つあります: LangChain 1.0 より前は、この役割は LangGraph 側の create_react_agent という関数が担っていましたが、現在は非推奨です。古いチュートリアルやブログ記事で from langgraph.prebuilt import create_react_agent を見かけたら、それは今学んでいる create_agent の以前のバージョンだと理解してください。

ToolNodetools_condition、そして create_agent を使って、第15章のエージェントを簡潔に作り直しました。次のセクションでは、これらの事前構築コンポーネントと手動でのグラフ組み立てを組み合わせて、より複雑なエージェントを構築します。

16.2) マルチブランチエージェントの構築

このセクションで構築するマルチブランチエージェント(multi-branch agent)は、まずどんな種類のリクエストを扱っているのかを判定し、それから各種類を異なるモデルや異なるツールのセットで処理します。全体のグラフは第15章のアプローチを使って手動で組み立て、ツール呼び出しループが必要な部分には create_agent を使います。

16.2.1) 要件と設計

この章の冒頭で言及したカスタマーサポートエージェントを構築します。要件は以下のとおりです:

  • 単純な問い合わせ(「営業時間は何時ですか?」) → 低コストの小さいモデルが直接回答します。
  • 複雑な相談(「注文品が破損して届きました — 交換すべきですか、返金すべきですか?」) → 高性能モデルが回答します。
  • 注文照会(「注文番号 #12345 の配送状況は?」) → 注文照会ツールを持つエージェントが処理します。

各問い合わせタイプには異なるモデルとツールの設定が必要なので、まず各リクエストを分類し、それから適切なハンドラーへとルーティングするグラフが必要です。構造は次のとおりです:

単純な問い合わせ

複雑な相談

注文照会

START

classify

問い合わせタイプ?

simple_handler

complex_handler

order_agent

END

リクエストが来ると、classify ノードがそれがどのタイプの問い合わせかを判定し、その結果を State に記録します。次に条件付きエッジが State から記録されたタイプを読み取り、適切なノードへとルーティングします。simple_handler は単純な問い合わせを処理し、complex_handler は複雑な相談を扱います。order_agent は注文照会ツールを使って配送状況を確認し応答します。order_agent は標準的なツール呼び出しノードなので、create_agent で構築します。では、各部品を順に構築していきましょう。

16.2.2) classify ノードとルーティング関数

まず、State を定義しましょう。分類結果を保存するために intent フィールドを追加します。3つのタイプは "simple""complex""order" という値で表現されます。

python
from langgraph.graph import MessagesState
 
class State(MessagesState):
    intent: str    # 分類結果: "simple"、"complex"、"order"

次は classify ノードです。LLM を使って、ユーザーのリクエストが3つのタイプのどれに属するかを判定し、結果を intent に記録します。分類結果は、第7章で学んだ構造化出力を使って受け取ります。スキーマの intent フィールドを Literal 型で宣言すると、LLM の応答は宣言された値のいずれかに制約されます。

python
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
 
class IntentRoute(BaseModel):
    """顧客の問い合わせに対する分類結果。"""
    intent: Literal["simple", "complex", "order"] = Field(
        description=(
            "simple: 営業時間やあいさつなどの一般的な質問。 "
            "complex: 紛争や返金など、慎重な推論が必要な相談。 "
            "order: 特定の注文を照会するリクエスト。"
        )
    )
 
classifier_llm = ChatOpenAI(model="gpt-5.4-nano").with_structured_output(IntentRoute)
 
def classify(state: State):
    """顧客の問い合わせのタイプを分類します。"""
    question = state["messages"][-1].content
    result = classifier_llm.invoke(
        f"顧客のリクエストを分類してください。\n\nリクエスト: {question}"
    )
    return {"intent": result.intent}

分類には最も小さいモデル(gpt-5.4-nano)を使いました。「この問い合わせはどのタイプか?」を判断するのは、高性能モデルを必要としない単純なタスクです。そして、classify ノードはすべてのリクエストが通過するゲートウェイなので、安価で高速なモデルが望ましいのです。

次はルーティング関数です。これは State に保存された分類結果を単に返すだけです。

python
def route_by_intent(state: State) -> Literal["simple", "complex", "order"]:
    """分類結果に基づいて次のノードを決定します。"""
    return state["intent"]

classify ノードはすでにどのノードを次に実行すべきかを決めて intent に記録しているので、ルーティング関数はその値をそのまま返すだけです。

16.2.3) タイプ別のハンドラーノード

では、3つの問い合わせタイプそれぞれのハンドラーを構築しましょう。

単純な問い合わせハンドラーは、小さいモデルを一度だけ呼び出します。実際のカスタマーサポートエージェントでは、内部文書を検索して回答するために RAG を適用しますが、この章のトピックに集中するために、ハンドラーはシンプルなままにしています。

python
simple_llm = ChatOpenAI(model="gpt-5.4-mini")
 
def simple_handler(state: State):
    """単純な問い合わせを小さいモデルで回答します。"""
    response = simple_llm.invoke(state["messages"])
    return {"messages": [response]}

複雑な相談ハンドラーは、高性能モデルを使います。単純な問い合わせハンドラーと同じ理由から、シンプルなままにしています — 単に応答を生成するだけです。

python
complex_llm = ChatOpenAI(model="gpt-5.4")
 
def complex_handler(state: State):
    """複雑な相談を高性能モデルで回答します。"""
    response = complex_llm.invoke(state["messages"])
    return {"messages": [response]}

注文照会ハンドラーは、注文照会ツールを使う必要があり、それはツール呼び出しループを必要とすることを意味します。その構造は標準的なツール呼び出しエージェントと同一なので、create_agent で構築します。

python
from langchain.tools import tool
from langchain.agents import create_agent
 
@tool
def get_order_status(order_id: str) -> str:
    """注文番号で注文の配送状況を照会します。"""
    fake_data = {"12345": "配送中、明日到着予定", "67890": "配達済み"}
    return fake_data.get(order_id, f"注文 {order_id} が見つかりません。")
 
order_agent = create_agent(
    model="openai:gpt-5.4-mini",
    tools=[get_order_status],
)

16.2.4) グラフの組み立てと実行

構築したすべてのノードをグラフに接続しましょう。classify を開始点に配置し、条件付きエッジを介して3つのハンドラーに接続し、各ハンドラーが完了後に終了するように設定します。

python
from langgraph.graph import StateGraph, START, END
 
builder = StateGraph(State)
 
builder.add_node("classify", classify)
builder.add_node("simple", simple_handler)
builder.add_node("complex", complex_handler)
builder.add_node("order", order_agent)      # create_agent のグラフをノードとして登録します
 
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route_by_intent)
builder.add_edge("simple", END)
builder.add_edge("complex", END)
builder.add_edge("order", END)
 
agent = builder.compile()

各タイプの問い合わせを1つずつ実行して、どのハンドラーが処理するか見てみましょう。

python
from langchain_core.messages import HumanMessage
 
for question in [
    "営業時間は何時ですか?",
    "注文品が破損して届きました。交換すべきですか、返金すべきですか?",
    "注文番号 12345 の配送状況は?",
]:
    result = agent.invoke({"messages": [HumanMessage(content=question)]})
    print(f"Q: {question}")
    print(f"[{result['intent']}] A: {result['messages'][-1].content}\n")

出力:

Q: 営業時間は何時ですか?
[simple] A: 決まった営業時間はありません — 24時間365日ご利用いただけます。
...
 
Q: 注文品が破損して届きました。交換すべきですか、返金すべきですか?
[complex] A: 注文品が破損して届いた場合、一般的に **交換または全額返金** を受ける権利があるはずです。
...
 
Q: 注文番号 12345 の配送状況は?
[order] A: 注文 12345 は **配送中** で、**明日到着予定** です。

各問い合わせは異なる経路を通って処理されました。simple_handler は単純な問い合わせを小さいモデルで回答し、complex_handler は複雑な相談を高性能モデルで回答し、order_agentget_order_status ツールを呼び出して注文照会に回答しました。