プログラミング・スキルアップ

LangGraph入門2026|Pythonでエージェントを構築する全手順

読了時間: 約24分

LangGraphは、LLMアプリケーションに「制御フロー」を持ち込むフレームワークだ。LangChainが個々のLLM呼び出しを抽象化するのに対し、後者は呼び出しの順序・分岐・ループ・中断と再開をグラフ構造で記述する。

一本道のチェーンでは限界が来る。「ツールの実行結果に応じて次のアクションを変えたい」「人間の承認を挟んでからメールを送りたい」「3回リトライしてダメなら別の戦略に切り替えたい」。こうした制御フローの要求に、LangChainの | パイプだけでは応えられない。

この記事では、LangGraphのインストールからStateGraphの構築、ツール呼び出しAgent、Human-in-the-Loopの実装までをPythonコード付きで進める。公式ドキュメント(v1.0系)の最新APIに準拠しているため、古い記事のコードが動かないという問題にも直面しない。

LangGraphが解決する問題 -- LangChainだけでは足りない理由

まず結論。LangGraphを選ぶべき状況は「LLMの出力に応じてフローが分岐する」ときだ。逆に、固定のプロンプト → LLM → パースという一直線の処理ならLangChainのLCELで十分。

判断軸 LangChain (LCEL) LangGraph
処理フロー 一方向のチェーン 分岐・ループ・合流を含むグラフ
状態管理 入出力の受け渡しのみ グラフ全体で共有するState
人間の介入 対応なし interrupt / Command で一時停止・再開
状態の永続化 対応なし Checkpointer (SQLite, PostgreSQL等)
代表的ユースケース RAG、要約、翻訳 マルチステップAgent、承認フロー、自律型タスク

LangChain社自身が「エージェントを作るならLangGraphを使え」と明言している。公式ドキュメントのAgent関連ページは2024年後半から順次LangGraphへのリダイレクトに切り替わった。もはやLangChain単体でAgentを組むコード例は公式から消えている。

ただし依存関係を誤解しやすいので補足しておく。実はLangChainなしでも動く。ChatModeltoolデコレータを使うときにLangChainのパッケージが必要になるだけで、StateGraphやCheckpointerは単体で動く機能だ。

LangChain記事との接続

LangChainのLCEL・RAG・基本的なAgent構築についてはLangChain入門2026|RAG・Agent実装の最短ルートで扱っている。LangGraphに進む前にLCELの基本を押さえておくと理解が速い。

環境構築: 3行で動く最小セットアップ

前置きなしでいく。Python 3.11以上の仮想環境を前提とする。

pip install langgraph langchain-anthropic langchain-community

langchain-anthropicはClaudeをLLMとして使う場合のパッケージ。OpenAIを使うならlangchain-openaiに差し替える。LangGraph本体はlanggraphの1パッケージに集約されている。

APIキーの設定。

# Claude (Anthropic) の場合
export ANTHROPIC_API_KEY="sk-ant-..."

# OpenAI の場合
export OPENAI_API_KEY="sk-..."

動作確認は次のセクションのコードで兼ねる。別途「Hello World」を挟む必要はない。

State・Node・Edge -- グラフの3要素を手で組む

LangGraphのグラフは、3つの部品だけで成り立つ。

要素 役割 日常の比喩
State グラフ全体で共有するデータ構造 回覧板。各工程で書き加え、次に渡す
Node Stateを受け取り、更新を返す関数 工場の各工程。組み立て、検査、梱包
Edge ノード間の接続(固定 or 条件分岐) ベルトコンベア。次の工程への搬送路

Stateの定義。TypedDictを使う。クラスは不要。

from typing import TypedDict

class Essay(TypedDict):
    topic: str
    content: str | None
    score: float | None

Nodeは関数。それだけだ。引数でStateを受け取り、辞書で更新分だけを返す。変更したいキーだけでいい。

def write_essay(state: Essay):
    # topic を受け取って content を生成
    return {"content": f"{state['topic']}に関するエッセイ本文..."}

def score_essay(state: Essay):
    # content を受け取って score を付与
    return {"score": 8.5}

最後にEdge。add_edgeでノード間をつなぐだけだ。STARTENDはエントリと終了を示す特殊定数で、どのグラフにも登場する。

from langgraph.graph import StateGraph, START, END

builder = StateGraph(Essay)
builder.add_node(write_essay)
builder.add_node(score_essay)

builder.add_edge(START, "write_essay")
builder.add_edge("write_essay", "score_essay")
builder.add_edge("score_essay", END)

graph = builder.compile()

compile()を呼ぶと実行可能なグラフオブジェクトが返る。あとはgraph.invoke()に初期Stateを渡すだけ。実際にやってみる。

最初のグラフ: エッセイ採点ワークフロー

「テーマを受け取る → エッセイを書く → 採点する」。LLMは使わず、まずグラフの動きそのものを掴む。

from typing import TypedDict
from langgraph.graph import StateGraph, START, END


class Essay(TypedDict):
    topic: str
    content: str | None
    score: float | None


def write_essay(state: Essay):
    return {"content": f"{state['topic']}に関する800字のエッセイ"}


def score_essay(state: Essay):
    word_count = len(state["content"] or "")
    score = min(10.0, word_count / 20)
    return {"score": score}


builder = StateGraph(Essay)
builder.add_node(write_essay)
builder.add_node(score_essay)

builder.add_edge(START, "write_essay")
builder.add_edge("write_essay", "score_essay")
builder.add_edge("score_essay", END)

graph = builder.compile()

# 実行
result = graph.invoke({"topic": "LangGraphの設計思想", "content": None, "score": None})
print(result)
# {'topic': 'LangGraphの設計思想', 'content': 'LangGraphの設計思想に関する800字のエッセイ', 'score': 1.05}

動いた。invoke()は同期実行で、全ノードを順に処理して最終Stateを返す。

ここで1つ気づくことがある。各ノードはStateの全体を受け取るが、返す辞書には変更するキーだけを含めればいい。write_essaycontentだけ、score_essayscoreだけ。フレームワークが既存のStateにマージしてくれる。

条件分岐を加えるには

add_edgeの代わりにadd_conditional_edgesを使う。ルーティング関数がStateを見て次のノード名を文字列で返す仕組みだ。「スコアが7未満なら書き直し」のようなループも、条件分岐エッジ1本で表現できる。次のセクションで実際に使う。

ツール呼び出しエージェント: ReActパターン実装

ここからが本題。LLMにツールを渡し、「呼ぶかどうかをLLM自身が判断 → 呼んだ結果をLLMに戻す → また判断」というReActループを組む。

ツール定義 → LLMバインド → 条件分岐ルーターの3ステップで組む。

from typing import Literal
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
from langgraph.graph import MessagesState, StateGraph, START, END


# --- 1. ツール定義 ---
@tool
def add(a: int, b: int) -> int:
    """2つの整数を足す"""
    return a + b

@tool
def multiply(a: int, b: int) -> int:
    """2つの整数を掛ける"""
    return a * b

tools = [add, multiply]
tools_by_name = {t.name: t for t in tools}


# --- 2. LLMにツールをバインド ---
llm = ChatAnthropic(model="claude-sonnet-4-6", temperature=0)
llm_with_tools = llm.bind_tools(tools)


# --- 3. ノード定義 ---
def llm_call(state: MessagesState):
    """LLMを呼び出し、ツール呼び出しの有無を含む応答を返す"""
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


def tool_executor(state: MessagesState):
    """LLMが要求したツールを実行し、結果をToolMessageで返す"""
    results = []
    for tc in state["messages"][-1].tool_calls:
        result = tools_by_name[tc["name"]].invoke(tc["args"])
        results.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
    return {"messages": results}


# --- 4. 条件分岐 ---
def should_continue(state: MessagesState) -> Literal["tool_executor", "__end__"]:
    last = state["messages"][-1]
    return "tool_executor" if last.tool_calls else END


# --- グラフ組み立て ---
builder = StateGraph(MessagesState)
builder.add_node("llm_call", llm_call)
builder.add_node("tool_executor", tool_executor)

builder.add_edge(START, "llm_call")
builder.add_conditional_edges("llm_call", should_continue, ["tool_executor", END])
builder.add_edge("tool_executor", "llm_call")  # ツール実行後、LLMに戻る

agent = builder.compile()

# 実行
result = agent.invoke({"messages": [HumanMessage(content="3と7を足して、その結果に5を掛けて")]})
print(result["messages"][-1].content)

MessagesStateは組み込みのState型。messagesキーにadd_messagesリデューサーが設定済みで、メッセージが自動追記される。これだけで十分だ。

should_continue関数に注目してほしい。たった3行。だがこの分岐こそが、LangChainのパイプでは書けなかった制御だ。LLMの出力を見て「もう1周するかここで止めるか」を判断している。パイプ演算子の | には「条件によって接続先を変える」という概念がそもそもない。

筆者も初めてこのパターンを書いたとき、無限ループに落ちた。原因は単純で、LLMがツール呼び出しを止めないプロンプト設計だった。should_continueの条件が正しくても、LLMが毎回ツールを呼び続ければ永遠に回る。ツールのdocstringを「〜する。結果が得られたら返答せよ」と具体的に書くことで解消した。

ただしループ制御だけでは解決できない問題がある。人間が判断を挟む必要があるケースだ。

Human-in-the-Loop: 人間が承認を挟むワークフロー

日本語の入門記事でほとんど扱われていない領域がある。Human-in-the-Loop(HITL)だ。自律型エージェントを本番に載せるとき、「メール送信前に人間が確認する」「課金APIの前に承認を取る」。こうした要件は避けて通れない。

部品は3つだ。

  1. Checkpointer -- グラフの状態をDBに保存する。InMemorySaver(開発用)かSqliteSaver(永続化)を選ぶ
  2. interrupt() -- ノード内で呼ぶとグラフが一時停止する。引数に渡したデータが中断理由として外部に返る
  3. Command(resume=...) -- 一時停止したグラフを再開する。人間の判断結果を渡せる

メール送信前の承認フローを実装する。

import sqlite3
from typing import TypedDict
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_anthropic import ChatAnthropic
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import Command, interrupt


@tool
def send_email(to: str, subject: str, body: str) -> str:
    """メールを送信する(送信前に人間の承認が必要)"""

    # ここでグラフが一時停止する
    response = interrupt({
        "action": "send_email",
        "to": to,
        "subject": subject,
        "body": body,
        "message": "このメールを送信しますか?",
    })

    if response.get("action") == "approve":
        final_to = response.get("to", to)
        final_subject = response.get("subject", subject)
        return f"送信完了: {final_to} / {final_subject}"

    return "送信キャンセル"


llm = ChatAnthropic(model="claude-sonnet-4-6").bind_tools([send_email])


def agent_node(state: MessagesState):
    return {"messages": [llm.invoke(state["messages"])]}


def tool_node(state: MessagesState):
    results = []
    for tc in state["messages"][-1].tool_calls:
        result = send_email.invoke(tc["args"])
        results.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
    return {"messages": results}


def should_continue(state: MessagesState):
    last = state["messages"][-1]
    return "tool_node" if getattr(last, "tool_calls", None) else END


builder = StateGraph(MessagesState)
builder.add_node("agent", agent_node)
builder.add_node("tool_node", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, ["tool_node", END])
builder.add_edge("tool_node", "agent")

# Checkpointer付きでコンパイル(これがないとinterruptが使えない)
checkpointer = SqliteSaver(sqlite3.connect("email_approval.db"))
agent = builder.compile(checkpointer=checkpointer)


# --- 実行 ---
config = {"configurable": {"thread_id": "email-001"}}

# Step 1: エージェントが動く → send_email内のinterruptで停止
result = agent.invoke(
    {"messages": [HumanMessage(content="田中さん([email protected])に会議の件でメールして")]},
    config=config,
)
print("承認待ち:", result["__interrupt__"])

# Step 2: 人間が内容を確認し、承認(件名を修正して再開)
resumed = agent.invoke(
    Command(resume={"action": "approve", "subject": "【4/10】定例会議のご案内"}),
    config=config,
)
print(resumed["messages"][-1].content)

thread_idは手紙の宛名のようなものだ。同じthread_idで投げれば、昨日の続きとして扱われる。CheckpointerがState全体をSQLiteに書き出しているため、Pythonプロセスが死んでも関係ない。

これは地味だが強力な設計だ。interruptのタイミングでサーバーを再起動しようが、翌日に別のプロセスからCommand(resume=...)を投げても中断時点から再開する。金曜に上司の承認待ちで止めて、月曜に承認ボタンを押す。そういうフローが自然に組める。

注意: Checkpointerなしではinterruptが使えない

builder.compile()にcheckpointerを渡さずにinterrupt()を呼ぶと、RuntimeErrorが出る。開発中はInMemorySaver()を使い、本番ではSqliteSaverPostgresSaverを選ぶ。

Graph API vs Functional API -- どちらを選ぶか

グラフを組む方法は2つある。

1つ目はここまで使ってきたStateGraphベースのGraph API。もう1つがデコレータベースのFunctional APIだ。Functional APIで先ほどのReActエージェントを書き直すとどう変わるか。

from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, BaseMessage
from langchain_anthropic import ChatAnthropic
from langgraph.graph import add_messages
from langgraph.func import entrypoint, task


@tool
def add(a: int, b: int) -> int:
    """2つの整数を足す"""
    return a + b

@tool
def multiply(a: int, b: int) -> int:
    """2つの整数を掛ける"""
    return a * b

tools = [add, multiply]
tools_by_name = {t.name: t for t in tools}
llm = ChatAnthropic(model="claude-sonnet-4-6", temperature=0).bind_tools(tools)


@task
def call_llm(messages: list[BaseMessage]):
    return llm.invoke(messages)


@task
def call_tool(tool_call):
    return tools_by_name[tool_call["name"]].invoke(tool_call)


@entrypoint()
def agent(messages: list[BaseMessage]):
    llm_response = call_llm(messages).result()

    while True:
        if not llm_response.tool_calls:
            break
        # ツール呼び出しを並列実行
        tool_futures = [call_tool(tc) for tc in llm_response.tool_calls]
        tool_results = [f.result() for f in tool_futures]
        messages = add_messages(messages, [llm_response, *tool_results])
        llm_response = call_llm(messages).result()

    return add_messages(messages, llm_response)


# 実行
result = agent.invoke([HumanMessage(content="3と7を足して、その結果に5を掛けて")])
print(result[-1].content)

StateGraphadd_nodeadd_edgeも登場しない。Pythonのwhileifでフローを制御し、@taskで非同期実行の単位を切る。

自分ならどちらを選ぶか。判断基準は1つ。分岐が2つ以上あるかどうか

状況 推奨API 理由
シンプルなReActループ Functional API whileループで済む。グラフ定義は冗長
複数の条件分岐・並列パス Graph API フロー図としての可読性が高い。可視化もしやすい
Human-in-the-Loop どちらでも interrupt / Commandは両APIで共通
マルチエージェント(Supervisor構成) Graph API サブグラフの合成が必要になる

内部は両API共通。同じPregelランタイム上で動き、Checkpointer・ストリーミング・LangSmith連携もすべて使える。パフォーマンス差はゼロ。選択は見通しだけの話だ。

よくある質問

Q. LangGraphは無料で使えるか?

OSS版は完全無料(MITライセンス)。LangGraph Platform(ホスティング・モニタリング)は別サービスで有料プランがある。

Q. LangChainを知らなくても使えるか?

使える。StateGraphやCheckpointerは単体で動く機能だ。ただしChatModelやtoolデコレータを使う場面ではLangChainのパッケージをインポートするため、LCEL(パイプ演算子|での連結)の基本を知っておくとスムーズ。LangChain入門記事で30分あれば押さえられる。

Q. 古い記事のコードが動かない。バージョン互換は?

LangGraphはv0.x系からv1.0でAPIの破壊的変更があった。MessageGraphは廃止されStateGraph(MessagesState)に統一。ToolInvocationToolCallに改名。2025年10月以前の記事のコードはそのままでは動かないことが多い。この記事のコードはv1.0系(1.0.3以降)に準拠している。

Q. デバッグが難しい。ノードの実行順を確認する方法は?

stream_mode="updates"を使うとノードごとの出力を逐次確認できる。LangSmithを有効にすれば、各ノードの入出力・トークン消費・レイテンシがトレースとして記録される。無料枠でも十分使える。

Q. CrewAIやAutoGenとの違いは?

抽象度が違う。CrewAIやAutoGenは「ロール(役割)を持つエージェント間の会話」を設計する高レベルフレームワーク。LangGraphはその下のレイヤーで、ノードとエッジで自由にワークフローを組む低レベルオーケストレーター。LangGraphはノード単位でロジックを書けるため、CrewAIでは不可能な条件分岐やリトライも自在に組める。一方、3人のエージェントに順番に話させるだけならCrewAIが10行で片付く。

まとめ

  • State・Node・Edgeの3要素でグラフを構築する基本パターン
  • 条件分岐エッジでLLMの出力に応じたフロー制御
  • ReActエージェント: ツール呼び出しループの実装
  • Human-in-the-Loop: interrupt / Command / Checkpointerによる承認フロー
  • Graph API vs Functional API: 分岐の数で使い分ける

LangChainで一直線の処理を書いていて「ここで分岐したい」「ここで止めたい」と感じた瞬間が、LangGraphの出番だ。should_continueの戻り値をEND固定にして動かすと、ループしない最小形が手元で確認できる。その状態から条件を1つ足すたびに挙動が変わる。それが一番早い理解ルートだ。

関連記事

参考書籍

LangGraphの文献はまだ薄い。LangChain側から入るなら以下が現状では一番まとまっている。