LiteLLMとLangGraphでプロバイダー非依存のAIエージェントを構築してみる

LiteLLMとLangGraphでプロバイダー非依存のAIエージェントを構築してみる

2026.05.04

はじめに

データ事業本部のkobayashiです。

前回の記事ではLiteLLMを使って複数のLLMプロバイダーを統一インターフェースで呼び出す方法を紹介しました。今回はその続編として、LiteLLMLangGraphを組み合わせてプロバイダーに依存しないAIエージェントを構築する方法を紹介します。

https://dev.classmethod.jp/articles/python-litellm-cloud-providers/

LangGraphはLLMを使ったステートフルなワークフロー(エージェント)を構築するためのフレームワークです。LiteLLMと組み合わせることで、エージェントのロジックを一切変えずにバックエンドのLLMを自由に切り替えられるようになります。

LangGraphとは

LangGraphは、LangChainチームが開発したエージェントフレームワークです。LLMの呼び出し、ツール実行、条件分岐といった処理をグラフ(有向グラフ)として定義し、ステートフルなワークフローを構築できます。

https://github.com/langchain-ai/langgraph

主な特徴としては以下になります。

  • グラフベースのワークフロー: ノード(処理)とエッジ(遷移)でエージェントのフローを定義
  • ステート管理: 会話履歴やコンテキストを状態として管理
  • ツール呼び出し: LLMが判断して外部ツールを呼び出し、結果を次の推論に活用
  • 条件分岐: LLMの出力に応じて動的にフローを切り替え
  • プリビルトエージェント: create_agent()で手軽にReActエージェントを構築可能

LiteLLM × LangGraph の利点

LiteLLMとLangGraphを組み合わせることで、以下のような利点があります。

  • プロバイダー非依存のエージェント: モデル名を変えるだけでOpenAI・Claude・Geminiのエージェントを切り替え
  • モデル比較が容易: 同じエージェントロジック・同じツールで異なるモデルの性能を比較
  • 本番運用の柔軟性: コスト・性能・可用性に応じてモデルを動的に切り替え
  • ローカル開発: Ollamaのローカルモデルで開発・テストし、本番ではクラウドAPIに切り替え

環境構築

環境

今回使用した環境は以下の通りです。

Python 3.13
litellm 1.83.14
langgraph 1.1.10
langchain-litellm 0.6.4
langchain-core 1.3.2

インストール

pipでインストールします。

$ uv pip install litellm langgraph langchain-litellm langchain-core

APIキーの設定

$ export OPENAI_API_KEY="sk-..."
$ export ANTHROPIC_API_KEY="sk-ant-..."
$ export GEMINI_API_KEY="..."

主要クラスとデコレータ

@tool デコレータ

@toolはLangChainが提供するデコレータで、通常のPython関数をエージェントが呼び出せるツールに変換します。関数のdocstringがツールの説明としてLLMに渡され、LLMはこの説明をもとにどのツールを呼び出すかを判断します。型アノテーションも自動的にスキーマとして解釈されるため、引数名・型・説明を正確に記述することがツール呼び出しの精度に直結します。

ChatLiteLLM クラス

ChatLiteLLMはLiteLLMをLangChainのチャットモデルインターフェースに統合するクラスです。LangChainのBaseChatModelを継承しており、LangGraphやLangChainの各コンポーネントとシームレスに連携できます。

メソッド 説明
ChatLiteLLM(model=...) モデルを指定してインスタンスを生成。LiteLLMのprovider/model形式で指定する
invoke(messages) メッセージリストを渡してLLMを呼び出す。同期的に結果を返す
ainvoke(messages) invokeの非同期版
bind_tools(tools) ツールリストをバインドした新しいインスタンスを返す。StateGraphでツール呼び出しを使う場合に必要
stream(messages) ストリーミングでレスポンスを受け取る

bind_tools()create_agent()を使う場合は内部で自動的に呼ばれるため明示的に呼ぶ必要はありません。StateGraphで手動構築する場合に使用します。

LCEL(パイプライン構文)

LangChainでは**LCEL(LangChain Expression Language)**という|(パイプ)演算子で処理をチェーンする構文が使えます。

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_litellm import ChatLiteLLM

prompt = ChatPromptTemplate.from_messages([
    ("system", "質問に簡潔に回答してください。"),
    ("user", "{question}"),
])
llm = ChatLiteLLM(model="anthropic/claude-sonnet-4-6")

chain = prompt | llm | StrOutputParser()
result = chain.invoke({"question": "東京の人口は?"})

chain = prompt | llm | StrOutputParser() は以下の3ステップを順番に実行するパイプラインを定義しています。

  1. prompt: テンプレートに変数を埋め込んでプロンプトを生成
  2. llm(ChatLiteLLM): プロンプトをLLMに送信して応答を取得
  3. StrOutputParser(): LLMの応答からテキスト部分だけを文字列として抽出

chain.invoke({"question": "東京の人口は?"}) のように呼び出すと、この3ステップが順番に実行され、最終的に文字列が返ります。パイプを使わずに書くと以下と同等です。

# LangChainのクラスを使うがパイプなし
messages = prompt.format_messages(question="東京の人口は?")
response = llm.invoke(messages)
result = response.content

さらにLangChainを使わずLiteLLMだけで書くと以下になります。

from litellm import completion

response = completion(
    model="anthropic/claude-sonnet-4-6",
    messages=[
        {"role": "system", "content": "質問に簡潔に回答してください。"},
        {"role": "user", "content": "東京の人口は?"},
    ],
)
result = response.choices[0].message.content

LCELはRAGパイプラインなど複数のプロンプトとLLM呼び出しを組み合わせる場面で、コードを簡潔に書くのに役立ちます。

基本的なReActエージェント

まずはcreate_agent()を使った最もシンプルなエージェントの例です。LLMがツールを呼び出すかどうかを自律的に判断し、必要に応じてツールを実行して最終回答を生成します。

basic_agent.py
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_litellm import ChatLiteLLM

@tool
def get_weather(city: str) -> str:
    """指定された都市の現在の天気を取得します。"""
    weather_data = {
        "東京": "晴れ、気温25°C、湿度60%",
        "大阪": "曇り、気温23°C、湿度70%",
        "札幌": "雪、気温-2°C、湿度80%",
        "福岡": "晴れ、気温22°C、湿度55%",
    }
    return weather_data.get(city, f"{city}の天気データは見つかりませんでした")

@tool
def calculate(expression: str) -> str:
    """数式を計算します。四則演算に対応しています。"""
    allowed_chars = set("0123456789+-*/.() ")
    if not all(c in allowed_chars for c in expression):
        return "エラー: 許可されていない文字が含まれています"
    try:
        result = eval(expression)  # noqa: S307
        return str(result)
    except Exception as e:
        return f"計算エラー: {e}"

# LiteLLMでモデルを指定 — モデル名を変えるだけでプロバイダーを切り替え可能
llm = ChatLiteLLM(model="anthropic/claude-sonnet-4-6")

# create_agentでReActエージェントを構築
agent = create_agent(llm, tools=[get_weather, calculate])

# 実行
result = agent.invoke(
    {"messages": [{"role": "user", "content": "東京と大阪の天気を調べて、気温の差を計算してください"}]}
)

for msg in result["messages"]:
    print(f"[{msg.type}] {msg.content}")

ポイントはChatLiteLLM(model="anthropic/claude-sonnet-4-6")の部分です。ここのモデル名を"openai/gpt-5.5""gemini/gemini-2.5-flash"に変えるだけで、エージェントのロジックやツール定義を一切変えずにバックエンドのLLMを切り替えられます。

なおcreate_agent()はコンパイル済みのグラフ(CompiledGraph)を返します。agent.invoke()ChatLiteLLMinvoke()とは異なり、グラフ全体のフロー(LLM呼び出し→ツール実行→再度LLM…)を最終回答が生成されるまで繰り返し実行します。

llm.invoke()    → LLMを1回呼び出すだけ
agent.invoke()  → グラフ全体のフローを最後まで実行

create_agent() の主なパラメータ

create_agent()はReActパターンのエージェントを構築する関数です。主なパラメータは以下になります。

パラメータ 説明
model LLMインスタンス(ChatLiteLLMなど)。ツール呼び出しに対応している必要がある
tools エージェントが使用できるツールのリスト。@toolデコレータで定義した関数を渡す
system_prompt システムプロンプト。エージェントの振る舞いを制御する文字列またはSystemMessageを指定できる

内部的にはStateGraphを使ってLLM呼び出し→ツール実行→LLM呼び出し…のループを構築しています。手軽にエージェントを構築したい場合はcreate_agent()、フローを細かく制御したい場合は後述のStateGraphを直接使います。

エージェントは以下の流れで動作します。

  1. ユーザーの質問を受け取る
  2. LLMがget_weatherツールを呼び出して東京と大阪の天気を取得
  3. LLMがcalculateツールを呼び出して気温差を計算
  4. 結果をまとめて最終回答を生成

実行結果は以下のようになりました。

$ python basic_agent.py 
[human] 東京と大阪の天気を調べて、気温の差を計算してください
[ai] 
[tool] 晴れ、気温25°C、湿度60%
[tool] 曇り、気温23°C、湿度70%
[ai] 
[tool] 2
[ai] 東京の天気は「晴れ、気温25°C、湿度60%」です。  
大阪の天気は「曇り、気温23°C、湿度70%」です。  

気温の差は 2°C です。

StateGraphでカスタムエージェントを構築

create_agent()は手軽ですが、より細かい制御が必要な場合はStateGraphを使ってエージェントのフローを自分で定義できます。以下は商品検索と在庫確認を行うエージェントの例です。

custom_graph_agent.py
from langchain_core.tools import tool
from langchain_litellm import ChatLiteLLM
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode

@tool
def search_product(query: str) -> str:
    """商品を検索します。"""
    products = {
        "ノートPC": "商品A: 15.6インチ、メモリ16GB、SSD 512GB — ¥120,000",
        "キーボード": "商品B: メカニカル、日本語配列、Cherry MX赤軸 — ¥15,000",
        "モニター": "商品C: 27インチ、4K、IPS液晶 — ¥45,000",
    }
    for key, value in products.items():
        if key in query:
            return value
    return f"'{query}'に一致する商品は見つかりませんでした"

@tool
def check_stock(product_name: str) -> str:
    """商品の在庫状況を確認します。"""
    stock = {
        "商品A": "在庫あり(残り3台)",
        "商品B": "在庫あり(残り12個)",
        "商品C": "在庫なし(入荷予定: 1週間後)",
    }
    for key, value in stock.items():
        if key in product_name:
            return value
    return "在庫情報が見つかりません"

# ツールとLLMの設定
tools = [search_product, check_stock]
llm = ChatLiteLLM(model="anthropic/claude-sonnet-4-6").bind_tools(tools)

SYSTEM_PROMPT = (
    "あなたは商品検索アシスタントです。"
    "ユーザーの質問に対して、必ずツールを使って情報を取得してから回答してください。"
    "推測で回答せず、ツールの結果に基づいて回答してください。"
)

# エージェントノード: LLMを呼び出してレスポンスを返す
def agent_node(state: MessagesState):
    messages = [{"role": "system", "content": SYSTEM_PROMPT}] + state["messages"]
    response = llm.invoke(messages)
    return {"messages": [response]}

# 条件分岐: ツール呼び出しがあるかどうかで次のノードを決定
def should_continue(state: MessagesState):
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tools"
    return END

# StateGraphを手動で構築
graph = StateGraph(MessagesState)

# ノードを追加
graph.add_node("agent", agent_node)
graph.add_node("tools", ToolNode(tools))

# エッジを定義
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")

# グラフをコンパイル
app = graph.compile()

# 実行
result = app.invoke(
    {"messages": [{"role": "user", "content": "ノートPCを探しています。在庫はありますか?"}]}
)

print("=== エージェントの実行結果 ===")
for msg in result["messages"]:
    role = msg.type
    content = getattr(msg, "content", "")
    if content:
        print(f"\n[{role}] {content}")

StateGraphを使ったエージェントの構成要素を解説します。

StateGraph の主要メソッド

メソッド 説明
StateGraph(state_schema) グラフを初期化する。MessagesStateを渡すとメッセージ履歴を自動管理する
add_node(name, func) ノード(処理単位)を追加する。funcはステートを受け取りステートの更新を返す関数
add_edge(from, to) ノード間の無条件遷移を定義する。STARTENDは特殊定数
add_conditional_edges(from, func, mapping) 条件分岐を定義する。funcの戻り値に応じて遷移先を切り替える
compile() グラフをコンパイルして実行可能なアプリケーションを生成する

MessagesState

MessagesStateはLangGraphが提供する組み込みのステートスキーマです。messagesキーにメッセージ履歴を保持し、ノードが返す{"messages": [response]}を自動的に既存の履歴に追加します。

ToolNode

ToolNode(tools)はLangGraphが提供するプリビルトのノードです。LLMのレスポンスに含まれるツール呼び出しリクエスト(tool_calls)を解析し、対応するツールを実行して結果をメッセージに追加します。

グラフの構成

上記のコード例では以下のようなグラフを構築しています。

ノード(処理単位)

  • agent: LLMを呼び出し、ツール呼び出しを含むレスポンスを返す
  • tools: ToolNodeがLLMの要求したツールを実行し、結果をメッセージに追加する

エッジ(遷移)

  • START → agent: 最初にLLMを呼び出す
  • agent → tools(条件付き): LLMがツール呼び出しを要求した場合
  • agent → END(条件付き): ツール呼び出しがない場合(最終回答)
  • tools → agent: ツール実行結果をLLMに渡して次の判断を促す

この構造により、LLMは必要なだけツールを繰り返し呼び出し、十分な情報が集まった時点で最終回答を生成します。create_agent()も内部的にはこれと同等のグラフを構築しています。

実行してみると search_productcheck_stock の順にツールが呼ばれ、結果に基づいて回答を生成していることがわかります。

$ python custom_graph_agent.py
=== エージェントの実行結果 ===

[human] ノートPCを探しています。在庫はありますか?

[tool] 商品A: 15.6インチ、メモリ16GB、SSD 512GB — ¥120,000

[tool] 在庫あり(残り3台)

[ai] ノートPCは1件見つかりました。

- 商品A: 15.6インチ、メモリ16GB、SSD 512GB ¥120,000
- 在庫: あり(残り3台)

他の条件
- 予算
- 画面サイズ
- メモリ容量
- メーカー

があれば、絞り込んでお探しできます。

同じエージェントを複数モデルで実行

LiteLLMの真価は、エージェントのロジックを一切変えずにモデルを切り替えられる点にあります。以下の例では、同じツール・同じ質問で3つのプロバイダーのモデルを比較しています。

multi_model_agent.py
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_litellm import ChatLiteLLM

@tool
def search_docs(query: str) -> str:
    """社内ドキュメントを検索します。"""
    docs = {
        "有給休暇": "有給休暇は入社6ヶ月後に10日付与されます。申請は3営業日前までに上長承認が必要です。",
        "リモートワーク": "リモートワークは週3日まで可能です。事前申請は不要ですが、勤怠システムへの記録が必要です。",
        "経費精算": "経費精算は月末締め、翌月15日払いです。領収書の原本またはスキャンデータの提出が必要です。",
    }
    for key, value in docs.items():
        if key in query:
            return value
    return f"'{query}'に関するドキュメントは見つかりませんでした"

# LiteLLMの強み: モデル名を変えるだけで同じエージェントを異なるプロバイダーで実行
models = [
    "openai/gpt-5.5",
    "anthropic/claude-sonnet-4-6",
    "gemini/gemini-2.5-flash",
]

query = "リモートワークのルールを教えてください"

for model_name in models:
    print(f"\n{'='*60}")
    print(f"モデル: {model_name}")
    print(f"{'='*60}")

    llm = ChatLiteLLM(model=model_name)
    agent = create_agent(llm, tools=[search_docs])

    result = agent.invoke({"messages": [{"role": "user", "content": query}]})

    # 最終回答を表示
    final_message = result["messages"][-1]
    print(final_message.content[:300])

このように、モデルの切り替えはChatLiteLLM(model=model_name)の1行だけです。エージェントのフロー、ツール定義、入出力の処理はすべて共通のまま、バックエンドのLLMだけを差し替えられます。

これは実際の開発で以下のようなシーンで役立ちます。

  • モデル評価: 同じタスクで各モデルのツール呼び出し精度や回答品質を比較
  • コスト最適化: 簡単なタスクは安価なモデル、複雑なタスクはハイエンドモデルに振り分け
  • フォールバック: 特定プロバイダーの障害時に別モデルへ切り替え

実行結果は以下のようになりました。

$ python multi_model_agent.py 

============================================================
モデル: openai/gpt-5.5
============================================================
リモートワークは週に3日まで可能です。事前の申請は不要ですが、勤怠システムへの記録が必要です。

============================================================
モデル: anthropic/claude-sonnet-4-6
============================================================
社内ドキュメントに基づいた、リモートワークのルールをご案内します。

- **利用可能日数**:週3日までリモートワークが可能です。
- **事前申請**:不要です。
- **勤怠記録**:勤怠システムへの記録が必要です。

ご不明な点があれば、お気軽にご質問ください!

============================================================
モデル: gemini/gemini-2.5-flash
============================================================
週3日までリモートワークが可能です。事前申請は不要ですが、勤怠システムへの記録が必要です。

まとめ

LiteLLMとLangGraphを組み合わせたエージェント開発の方法を紹介しました。

LiteLLMの統一インターフェースにより、LangGraphで構築したエージェントのロジックやツール定義を一切変えずに、バックエンドのLLMをOpenAI・Claude・Geminiなど自由に切り替えられます。create_agent()を使えば数行でReActエージェントを構築でき、より細かい制御が必要な場合はStateGraphでカスタムフローを定義することも可能です。

複数モデルの比較評価、コスト最適化、プロバイダー障害時のフォールバックなど、実運用を見据えたエージェント開発にこの組み合わせは非常に有効です。

最後まで読んでいただきありがとうございました。

この記事をシェアする

関連記事