0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GPT-4oとGeminiで画像理解AI実装!LangSmithトレーシング付きVLMガイド【牡丹プロジェクト技術解説 Phase 2】

Last updated at Posted at 2025-11-05

はじめに:AI VTuber「牡丹プロジェクト」とは

本記事は、AI VTuber三姉妹(Kasho、牡丹、ユリ)の記憶製造機システムの技術解説シリーズ第2弾です。

プロジェクト概要

「牡丹プロジェクト」は、過去の記憶を持つAI VTuberを実現するプロジェクトです。三姉妹それぞれが固有の記憶・個性・価値観を持ち、配信中に画像を理解して反応するVLM機能を実装しました。

三姉妹の構成

  • Kasho(長女): 論理的・分析的、慎重でリスク重視、保護者的な姉
  • 牡丹(次女): ギャル系、感情的・直感的、明るく率直、行動力抜群
  • ユリ(三女): 統合的・洞察的、調整役、共感力が高い

GitHubリポジトリ

本プロジェクトのコードは以下で公開しています:


Phase 2: VLM統合の重要性

テキスト生成だけでなく、画像を理解して説明できるAIを実装したい。そんなニーズに応えるのがVLM (Vision Language Model) です。

本記事では、GPT-4oとGeminiを使ってVLM機能を実装し、LangSmithでトレーシングする方法を紹介します。

🎯 この記事で分かること

  • VLM (Vision Language Model) とは何か
  • GPT-4o Visionの統合方法
  • Gemini Visionの統合方法(エラー対処含む)
  • LangSmithでVLM呼び出しをトレーシング
  • 実際のベンチマーク結果と性能比較

📦 対象モデル

  • OpenAI GPT-4o: マルチモーダル対応の最新モデル
  • Google Gemini 2.5 Flash: 高速マルチモーダルモデル

VLM (Vision Language Model) とは

VLM (Vision Language Model) は、テキストと画像を同時に理解できるAIモデルです。

従来のLLM vs VLM

機能 従来のLLM VLM
入力 テキストのみ テキスト + 画像
出力 テキスト テキスト
用途 対話、文章生成 画像説明、OCR、視覚的質問応答

VLMのユースケース

  1. 画像キャプション生成: 画像を見て説明文を生成
  2. 視覚的質問応答(VQA): 画像について質問に答える
  3. OCR(文字認識): 画像内の文字を読み取る
  4. 物体検出の説明: 画像内の物体を自然言語で説明
  5. アクセシビリティ: 視覚障害者向けの画像説明

実装:VLM統合

1. 環境構築

パッケージインストール

pip install openai google-generativeai langsmith Pillow requests

環境変数設定

.envファイル:

# OpenAI API
OPENAI_API_KEY=sk-proj-...your_key_here

# Google Gemini API
GOOGLE_API_KEY=AIza...your_key_here

# LangSmith (オプション)
LANGSMITH_API_KEY=lsv2_pt_...
LANGSMITH_TRACING=true
LANGSMITH_PROJECT=botan-vlm-benchmark-v1

2. GPT-4o Vision実装

基本的な実装

GPT-4oはcontentを配列形式で受け取り、テキストと画像を含められます。

import openai
from typing import Optional, Dict, Any

def gpt4o_vision_generate(
    prompt: str,
    image_url: str,
    api_key: str,
    max_tokens: int = 300
) -> Dict[str, Any]:
    """
    GPT-4o Visionで画像を理解して応答生成

    Args:
        prompt: テキストプロンプト
        image_url: 画像URL(httpsまたはbase64)
        api_key: OpenAI APIキー
        max_tokens: 最大トークン数

    Returns:
        応答辞書(response, tokens, latency_ms)
    """
    import time

    client = openai.Client(api_key=api_key)

    # content配列でテキスト+画像を指定
    content = [
        {"type": "text", "text": prompt},
        {"type": "image_url", "image_url": {"url": image_url}}
    ]

    start_time = time.time()

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": content}],
        max_tokens=max_tokens
    )

    latency_ms = (time.time() - start_time) * 1000

    return {
        "response": response.choices[0].message.content,
        "tokens": {
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens,
            "total_tokens": response.usage.total_tokens
        },
        "latency_ms": latency_ms
    }

使用例

import os

result = gpt4o_vision_generate(
    prompt="What do you see in this image? Describe it in detail.",
    image_url="https://example.com/image.jpg",
    api_key=os.getenv("OPENAI_API_KEY")
)

print(f"Response: {result['response']}")
print(f"Tokens: {result['tokens']['total_tokens']}")
print(f"Latency: {result['latency_ms']:.2f}ms")

ポイント解説

  1. content配列: [{"type": "text", ...}, {"type": "image_url", ...}]の形式
  2. 画像URL形式:
    • HTTPSのURL(https://example.com/image.jpg
    • Base64エンコード(data:image/jpeg;base64,...
  3. モデル: gpt-4o(またはgpt-4o-mini

3. Gemini Vision実装

基本的な実装

GeminiはPIL.Imageオブジェクトを直接受け取れます。

import google.generativeai as genai
from PIL import Image
import requests
from io import BytesIO
from typing import Optional, Dict, Any

def gemini_vision_generate(
    prompt: str,
    image_url: str,
    api_key: str,
    model: str = "gemini-2.5-flash",
    max_tokens: int = 300
) -> Dict[str, Any]:
    """
    Gemini Visionで画像を理解して応答生成

    Args:
        prompt: テキストプロンプト
        image_url: 画像URL(httpsまたはローカルパス)
        api_key: Google API キー
        model: Geminiモデル名
        max_tokens: 最大トークン数

    Returns:
        応答辞書(response, tokens, latency_ms)
    """
    import time

    genai.configure(api_key=api_key)

    # 画像をダウンロード
    if image_url.startswith('http'):
        response_img = requests.get(image_url, stream=True)
        response_img.raise_for_status()
        img = Image.open(response_img.raw)
    else:
        # ローカルファイル
        img = Image.open(image_url)

    # コンテンツ = [テキスト, 画像]
    content = [prompt, img]

    # 安全フィルター設定(オプション)
    safety_settings = [
        {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
        {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
        {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
        {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
    ]

    model_instance = genai.GenerativeModel(model, safety_settings=safety_settings)

    start_time = time.time()

    response = model_instance.generate_content(
        content,
        generation_config=genai.types.GenerationConfig(max_output_tokens=max_tokens)
    )

    latency_ms = (time.time() - start_time) * 1000

    # エラーチェック
    if not response.candidates:
        return {
            "response": "",
            "tokens": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
            "latency_ms": latency_ms,
            "error": "Response blocked: No candidates returned"
        }

    return {
        "response": response.text,
        "tokens": {
            "prompt_tokens": response.usage_metadata.prompt_token_count,
            "completion_tokens": response.usage_metadata.candidates_token_count,
            "total_tokens": response.usage_metadata.total_token_count
        },
        "latency_ms": latency_ms
    }

使用例

import os

result = gemini_vision_generate(
    prompt="この画像に何が写っていますか?詳しく説明してください。",
    image_url="https://example.com/image.jpg",
    api_key=os.getenv("GOOGLE_API_KEY")
)

print(f"Response: {result['response']}")
print(f"Tokens: {result['tokens']['total_tokens']}")
print(f"Latency: {result['latency_ms']:.2f}ms")

ポイント解説

  1. PIL.Image: GeminiはPillowのImageオブジェクトを受け取る
  2. content配列: [テキスト, PIL.Image]の順
  3. stream=True: 大きな画像の場合、メモリ効率化のため使用
  4. safety_settings: 必要に応じて安全フィルターを調整

4. LangSmith統合

既存のLangSmithトレーシングモジュールにVLM機能を追加します。

TracedLLMクラスの拡張

from langsmith import traceable
from typing import Optional, Dict, Any
import os

class TracedLLM:
    def __init__(
        self,
        provider: str = "openai",
        model: str = "gpt-4o",
        project_name: str = "botan-project"
    ):
        self.provider = provider
        self.model = model
        self.project_name = project_name
        self.langsmith_enabled = os.getenv("LANGSMITH_TRACING", "false").lower() == "true"

    def generate(
        self,
        prompt: str,
        temperature: float = 0.7,
        max_tokens: int = 1024,
        metadata: Optional[Dict[str, Any]] = None,
        image_url: Optional[str] = None  # ← VLM用パラメータ
    ) -> Dict[str, Any]:
        """
        Generate text with LLM (with automatic tracing)

        Args:
            prompt: Input prompt
            temperature: Sampling temperature
            max_tokens: Maximum tokens to generate
            metadata: Additional metadata for tracing
            image_url: Optional image URL for Vision models

        Returns:
            Response dict with 'response', 'tokens', 'latency_ms'
        """
        trace_name = metadata.get("model_name", self.model) if metadata else self.model

        def do_generate(input_prompt: str) -> str:
            if self.provider == "openai":
                full_result = self._openai_generate(input_prompt, temperature, max_tokens, image_url)
            elif self.provider == "gemini":
                full_result = self._gemini_generate(input_prompt, temperature, max_tokens, image_url)
            else:
                raise ValueError(f"Unknown provider: {self.provider}")

            # エラーがあれば例外を投げる
            if "error" in full_result:
                raise RuntimeError(full_result["error"])

            do_generate.full_result = full_result
            return full_result.get("response", "")

        # トレーシングを適用
        if self.langsmith_enabled:
            traced_func = traceable(
                run_type="llm",
                name=trace_name,
                project_name=self.project_name
            )(do_generate)
            try:
                response_text = traced_func(prompt)
                result = do_generate.full_result
            except RuntimeError:
                result = do_generate.full_result
        else:
            response_text = do_generate(prompt)
            result = do_generate.full_result

        if metadata:
            result["metadata"] = metadata
        result["timestamp"] = datetime.now().isoformat()

        return result

使用例(LangSmith有効)

import os

# LangSmith環境変数設定
os.environ["LANGSMITH_API_KEY"] = "lsv2_pt_..."
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "my-vlm-project"

# VLM実行(自動的にトレース)
llm = TracedLLM(provider="openai", model="gpt-4o")

result = llm.generate(
    prompt="What do you see in this image?",
    image_url="https://example.com/image.jpg",
    max_tokens=300,
    metadata={"model_name": "gpt4o_vision", "has_image": True}
)

print(result["response"])

LangSmithダッシュボードで以下が確認できます:

  • Name: gpt4o_vision
  • Input: プロンプト
  • Output: 生成されたテキスト
  • Metadata: {"has_image": True, ...}

ベンチマーク結果

実行環境

測定結果

GPT-4o Vision

✅ Success
Response: "The image shows a wooden surface, likely a deck or boardwalk,
          with weathered, grayish planks laid horizontally..."
Latency: 6,185ms (6.2秒)
Tokens: 890 (prompt: 約100, completion: 約790)

特徴:

  • ✅ 安定した動作
  • ✅ 詳細な画像説明
  • ⚠️ レイテンシがやや長い(6秒)

Gemini 2.5 Flash Vision

❌ Failed
Error: Generation failed: MAX_TOKENS (no content returned)
Latency: 約2秒
Tokens: 0

問題:

  • finish_reason=2 (MAX_TOKENS) でレスポンスが空
  • これは既知の問題で、Gemini APIの一時的な不具合の可能性
  • 同じコードでGemini 1.5 Flashは動作する場合がある

性能比較まとめ

モデル レイテンシ トークン数 状態
GPT-4o Vision 6.2秒 890 ✅ 成功
Gemini 2.5 Flash Vision 2秒 0 ❌ エラー

結論:

  • GPT-4o: 安定性が高く、詳細な説明が可能
  • Gemini: レイテンシは速いが、現在エラーが発生中

トラブルシューティング

Q1. OpenAI APIで画像が表示されない

原因

画像URLが正しくないか、アクセス権限がありません。

解決策

  1. HTTPS URLを使用: http://ではなくhttps://
  2. 公開アクセス可能な画像: 認証が不要な画像を使用
  3. Base64エンコード: 画像をBase64でエンコードして埋め込む
import base64

def encode_image_to_base64(image_path: str) -> str:
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

# Base64 URLを作成
base64_image = encode_image_to_base64("./image.jpg")
image_url = f"data:image/jpeg;base64,{base64_image}"

Q2. Geminiで"cannot identify image file"エラー

原因

BytesIOから直接Image.open()すると失敗する場合があります。

解決策

stream=Trueを使用してレスポンスを直接読み込みます。

import requests
from PIL import Image

# ❌ 失敗する例
response = requests.get(image_url)
img = Image.open(BytesIO(response.content))  # エラー

# ✅ 成功する例
response = requests.get(image_url, stream=True)
response.raise_for_status()
img = Image.open(response.raw)  # OK

Q3. Geminiで"finish_reason=MAX_TOKENS"エラー

原因

Gemini APIの既知の問題で、レスポンスが空なのにMAX_TOKENSを返す場合があります。

解決策

  1. 別のモデルを試す: gemini-1.5-flashgemini-pro
  2. max_tokensを増やす: デフォルトより大きな値を設定
  3. プロンプトを簡潔に: 長すぎるプロンプトを短くする
# max_tokensを増やす
result = gemini_vision_generate(
    prompt="画像を説明してください。",
    image_url=image_url,
    api_key=api_key,
    max_tokens=1024  # デフォルトより大きく
)

注意: これはGemini APIの一時的な問題の可能性があり、時間が経つと解決する場合があります。


Q4. LangSmithにトレースが表示されない

原因

環境変数が正しく設定されていません。

解決策

# 環境変数を確認
echo $LANGSMITH_API_KEY
echo $LANGSMITH_TRACING
echo $LANGSMITH_PROJECT

# 設定されていない場合
export LANGSMITH_API_KEY=lsv2_pt_...
export LANGSMITH_TRACING=true
export LANGSMITH_PROJECT=my-project-name

または、Pythonコード内で設定:

import os

os.environ["LANGSMITH_API_KEY"] = "lsv2_pt_..."
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "my-project-name"

まとめ

Phase 2 の成果

項目 内容
実装内容 VLM統合(GPT-4o Vision/Gemini Vision)
機能 画像理解、OCR、視覚的質問応答
トレーシング LangSmith完全統合
ベンチマーク GPT-4o: 6.2秒、Gemini: エラー

Phase 1 (LangSmith統合) との関係

項目 Phase 1 Phase 2
入力 テキストのみ テキスト + 画像
出力 テキスト テキスト
トレーシング LangSmith基盤 Phase 1の基盤を活用
用途 応答生成 画像理解、配信画面認識

ベンチマーク結果のまとめ

モデル レイテンシ トークン数 状態
GPT-4o Vision 6.2秒 890 ✅ 成功
Gemini 2.5 Flash Vision 2秒 0 ❌ エラー

得られた知見

  1. GPT-4oの安定性: 画像理解タスクで高い信頼性
  2. Geminiの速度: レイテンシは速いが、現在エラーあり
  3. LangSmithの有用性: VLM呼び出しも可視化できる

Phase 1-5の完成状況

Phase 内容 記事 状態
Phase 1 LangSmithマルチプロバイダートレーシング 記事
Phase 2 VLM (Vision Language Model) 統合 本記事
Phase 3 LLM as a Judge実装 記事
Phase 4 三姉妹討論システム実装(起承転結) 記事
Phase 5 センシティブ判定システム実装 記事

次のステップ

  • Phase 3: LLM as a Judge(品質評価システム)
  • Phase 4: 三姉妹討論システム(起承転結)
  • Phase 5: センシティブ判定システム

応用例

1. AI VTuberの視覚機能

# 配信画面のスクリーンショットを理解
result = llm.generate(
    prompt="この配信画面で何が起きていますか?視聴者に説明してください。",
    image_url="screenshot.jpg",
    metadata={"character": "botan", "task": "stream_narration"}
)

print(f"牡丹: {result['response']}")

2. チャット画像の自動説明

# ユーザーが送った画像を説明
result = llm.generate(
    prompt="この画像について簡潔に説明してください。",
    image_url=user_uploaded_image_url,
    max_tokens=100
)

print(f"AI: {result['response']}")

3. OCR(文字認識)

# 画像内のテキストを抽出
result = llm.generate(
    prompt="この画像に書かれている文字をすべて抽出してください。",
    image_url="document.jpg",
    max_tokens=500
)

print(f"抽出されたテキスト:\n{result['response']}")

参考資料

関連記事


おわりに

VLM (Vision Language Model) を実装することで、テキストだけでなく画像も理解できるAIシステムを構築できました。

特に以下の点が重要でした:

  • 🔍 GPT-4oの安定性で本番環境でも使用可能
  • Geminiの速度は魅力的だがエラー対処が必要
  • 📊 LangSmithトレーシングでVLM呼び出しも可視化

今後は、OCR、物体検出、視覚的質問応答など、さらに高度なVLMタスクにも挑戦していきます。

質問・コメントお待ちしています!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?