LiteLLMとLangGraphで画像入力対応のVisionエージェントを構築してみる
はじめに
データ事業本部のkobayashiです。
Claude SonnetやGPT-5.5、Gemini 2.5 など主要なフラグシップモデルは画像理解(Vision)機能を備えています。今回はLiteLLM経由でこれらのVisionモデルを呼び出し、LangGraphエージェントに画像入力を組み込む方法を紹介します。
マルチモーダル入力の形式
LiteLLMはOpenAIのVision API互換形式で各社のVisionモデルを呼び出せます。messagesのcontentをリストにして、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 化してそのまま渡せるので、外部画像サーバーに依存せずどのプロバイダーでも安定して動作するのがメリットです。
"""ローカル画像ファイルを 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」テキストの合成画像です。

$ python image_base64.py
1. 画像全体が青みがかったグレーブルーの背景で覆われています。
2. 中央に白い細枠の長方形が描かれています。
3. その長方形の中に「Sample Image (blue rectangle)」という白いテキストが表示されています。
Claude Sonnet 4.6 が画像の構造を3行で正確に説明しました。model を openai/gpt-5-mini や gemini/gemini-2.5-flash に変えるだけで、各社の Vision モデルへ切替えられます。社内ストレージ → 一時的に Base64 化 → LLM に送信、というフローはセキュリティ上の理由でも一般的(公開URLにせずに済む)で、Vision 入力の デファクトの渡し方 だと考えてよいです。
Vision エージェント: 画像 × ツール呼び出し
エージェントに画像を渡すと、LLMが画像内容を理解した上でツールを使って情報を補完するパターンが組めます。
"""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」というラベル付きの合成画像です。

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





