LiteLLMとLangGraphで画像入力対応のVisionエージェントを構築してみる

LiteLLMとLangGraphで画像入力対応のVisionエージェントを構築してみる

2026.05.21

はじめに

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

Claude SonnetやGPT-5.5、Gemini 2.5 など主要なフラグシップモデルは画像理解(Vision)機能を備えています。今回はLiteLLM経由でこれらのVisionモデルを呼び出し、LangGraphエージェントに画像入力を組み込む方法を紹介します。

マルチモーダル入力の形式

LiteLLMはOpenAIのVision API互換形式で各社のVisionモデルを呼び出せます。messagescontentをリストにして、text要素とimage_url要素を並べる形です。

{
    "role": "user",
    "content": [
        {"type": "text", "text": "..."},
        {"type": "image_url", "image_url": {"url": "..."}}
    ]
}

urlは外部URL(https://...)でも、Base64エンコードされたデータURI(data:image/jpeg;base64,...)でも問題ありません。
なお、実運用では data: URI(Base64)を推奨します。検証中に Wikipedia のような一般的な画像ホスティング先のURLを直接渡したところ、Anthropic からは Unable to download the file (HTTP 400)、OpenAI からは invalid_image_url: Error while downloading で両プロバイダーとも拒否されました。これは User-Agent やレート制限など、各社の画像ダウンローダーが踏まれる外部サーバーのフィルタによるものと推測されます。社内資料・ユーザーアップロードに加え、外部URL画像も一度ローカルに取得して Base64 にしてから渡すのが安定した運用です。

環境

Python 3.13
litellm 1.83.14
langgraph 1.1.10
langchain-litellm 0.6.4

ローカル画像を Base64(data URL)で渡す

image_url フィールドには data:image/jpeg;base64,... 形式の data URI を渡せます。ローカル画像(社内資料・ユーザーアップロード等)を Base64 化してそのまま渡せるので、外部画像サーバーに依存せずどのプロバイダーでも安定して動作するのがメリットです。

image_base64.py
"""ローカル画像ファイルを base64 化して data URL として Vision LLM に送る。

外部のWikipediaやCDNなど一般のサーバーは User-Agent 等の理由で Anthropic /
OpenAI 双方からダウンロードを拒否されることが多いため、ローカルファイルを
読み込んで `data:` URI に変換するパターンが最も安定する。
"""

import base64
from pathlib import Path

from litellm import completion

def encode_image(path: str) -> str:
    """画像を base64 文字列に変換。"""
    return base64.b64encode(Path(path).read_bytes()).decode("utf-8")

# サンプル画像のパス(実行時に存在することを想定)
image_path = "sample.jpg"
if not Path(image_path).exists():
    print(f"{image_path} が見つかりません。任意のJPG画像を配置してください。")
else:
    encoded = encode_image(image_path)
    response = completion(
        model="anthropic/claude-sonnet-4-6",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": "この画像を3行で説明してください"},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{encoded}",
                        },
                    },
                ],
            }
        ],
    )
    print(response.choices[0].message.content)

実行結果は以下のようになります。sample.jpg は青背景に白枠と「Sample Image」テキストの合成画像です。

sample

$ python image_base64.py
1. 画像全体が青みがかったグレーブルーの背景で覆われています。
2. 中央に白い細枠の長方形が描かれています。
3. その長方形の中に「Sample Image (blue rectangle)」という白いテキストが表示されています。

Claude Sonnet 4.6 が画像の構造を3行で正確に説明しました。modelopenai/gpt-5-minigemini/gemini-2.5-flash に変えるだけで、各社の Vision モデルへ切替えられます。社内ストレージ → 一時的に Base64 化 → LLM に送信、というフローはセキュリティ上の理由でも一般的(公開URLにせずに済む)で、Vision 入力の デファクトの渡し方 だと考えてよいです。

Vision エージェント: 画像 × ツール呼び出し

エージェントに画像を渡すと、LLMが画像内容を理解した上でツールを使って情報を補完するパターンが組めます。

vision_agent.py
"""Vision エージェント: 画像を受け取り、ツールで補強情報を取得して回答する。

ローカルの landmark.jpg を base64 化して data URL として渡す。
"""

import base64
from pathlib import Path

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

@tool
def get_landmark_info(landmark_name: str) -> str:
    """ランドマーク(建造物・観光地)の補足情報を返します。"""
    db = {
        "東京タワー": "高さ333m、1958年完成、特別展望台がある",
        "スカイツリー": "高さ634m、2012年開業、世界一の自立式電波塔",
        "金閣寺": "正式名称は鹿苑寺、1397年創建、世界遺産",
    }
    return db.get(landmark_name, f"{landmark_name}に関する情報は見つかりませんでした")

vision_llm = ChatLiteLLM(model="openai/gpt-5-mini")
agent = create_agent(
    model=vision_llm,
    tools=[get_landmark_info],
    system_prompt=(
        "画像を見てランドマーク名を特定し、必ず get_landmark_info ツールで"
        "補足情報を取得してから、画像の内容と補足情報を統合して回答してください。"
    ),
)

# ローカル画像を base64 化
encoded = base64.b64encode(Path("landmark.jpg").read_bytes()).decode("utf-8")
data_url = f"data:image/jpeg;base64,{encoded}"

# 画像 + 質問をマルチモーダルで送る
result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": "この画像のランドマークについて教えてください",
                    },
                    {
                        "type": "image_url",
                        "image_url": {"url": data_url},
                    },
                ],
            }
        ]
    }
)

print(result["messages"][-1].content)

実行結果は以下のようになります。
landmark.jpg は赤橙色の塔シルエット+「TOKYO TOWER」というラベル付きの合成画像です。

landmark

$ python vision_agent.py
画像は「TOKYO TOWER」とラベルされている、東京タワーを簡略化したイラストと判断します
(赤橙色のテーパー状の塔に先端のアンテナ)。ご指定どおり補助情報を取得するために
get_landmark_info を呼び出しましたが、補足情報は見つかりませんでした。

そのため、以下は一般に知られている東京タワーの基本情報と画像の説明を統合した内容です。

- ランドマーク名: 東京タワー (Tokyo Tower)
- 位置: 東京都港区(主に芝公園周辺)
- 高さ: 約333メートル
- 建造年: 1958年竣工
(中略)

LLMが画像から「東京タワー」を識別し、get_landmark_infoツールを呼びに行きました。今回はツールが日本語キー (東京タワー) で検索する一方、エージェントが渡した引数が Tokyo Tower などの別表記だったために 補足情報は見つかりませんでした が返り、エージェントは自分の知識ベースから情報を補って回答しています。画像の内容理解 → 構造化情報の取得 → 統合というパターンは、ECサイトの商品認識、文書OCR、医療画像の補助診断など幅広く応用できます。

実運用では tool 呼び出し前にエージェントが「正規化された日本語ランドマーク名」を必ず使うようにシステムプロンプトを補強する、もしくはツール側で英語名/日本語名の両方を許容する辞書を用意するなど、ツール仕様とエージェント挙動のすり合わせが必要になります。

モデル別の対応状況(2026年5月時点)

モデル 画像 動画 音声入力
anthropic/claude-sonnet-4-6 ×
openai/gpt-5.5
openai/gpt-5-mini × ×
gemini/gemini-2.5-flash

LiteLLMはこれらの差異を吸収しつつも、対応していない入力形式を渡すとエラーになります。fallbacksと組み合わせて「Visionが必須なモデルが落ちたら別Visionモデルへ」とすると安全です。

まとめ

LiteLLM × LangGraph でマルチモーダル(画像)入力を扱う方法を、completion() 直叩きと create_agent への組み込みの両方で紹介しました。

messages.contenttextimage_url を並べる OpenAI Vision 互換形式を LiteLLM が吸収してくれるおかげで、Claude / GPT / Gemini といった主要 Vision モデルを モデル名1行の差し替えで 切替えられ、create_agent に渡せば「画像入力 × ツール呼び出し」もそのまま実現できます。実運用では Wikipedia など外部URLが Anthropic / OpenAI 双方からダウンロード拒否されるケースが多いため、「URL → 中継サーバーで取得 → Base64(data URL)にして LLM へ」 という3段階フローが最も安定します。

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


生成AI活用はクラスメソッドにお任せ

過去に支援してきた生成AIの支援実績100+を元にホワイトペーパーを作成しました。御社が抱えている課題のうち、どれが解決できて、どのようなサービスが受けられるのか?4つのフェーズに分けてまとめています。どうぞお気軽にご覧ください。

生成AI資料イメージ

無料でダウンロードする

この記事をシェアする

関連記事