Ir para o conteúdo

Lab 043: Agentes Multimodais com GPT-4o Vision

Nível: L300 Trilha: 💻 Pro Code Tempo: ~50 min 💰 Custo: Gratuito — O nível gratuito do GitHub Models suporta GPT-4o vision

O que Você Vai Aprender

  • Enviar imagens para o GPT-4o usando a API de visão da OpenAI (métodos base64 e URL)
  • Construir um agente que pode analisar fotos de produtos e responder perguntas sobre eles
  • Combinar visão + chamada de ferramentas: o modelo vê uma imagem e chama ferramentas com base no que observa
  • Lidar com entradas de múltiplas imagens para comparação de produtos
  • Aplicar visão em cenários reais: identificação de produtos, avaliação de danos, estimativa de tamanho

Introdução

O GPT-4o é nativamente multimodal — ele processa texto e imagens em um único modelo, não como um pipeline de modelos separados. Isso permite agentes que podem "ver" o que o usuário está olhando e responder com contexto.

Casos de uso do OutdoorGear para visão: - O cliente envia uma foto de um produto → o agente identifica e recupera as especificações - O cliente mostra um item danificado → o agente avalia o dano e inicia a devolução - O cliente pergunta "essa barraca cabe nesse carro?" com duas fotos → o agente estima - O cliente compartilha uma foto de trilha → o agente recomenda equipamentos para aquele terreno e clima


Pré-requisitos

pip install openai requests Pillow
export GITHUB_TOKEN=<your PAT>

O GPT-4o com visão está disponível no nível gratuito do GitHub Models — não é necessária assinatura do Azure.


Parte 1: Visão Básica — Analisar uma Foto de Produto

Passo 1: Enviar uma URL de imagem para o GPT-4o

# vision_basics.py
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://models.inference.ai.azure.com",
    api_key=os.environ["GITHUB_TOKEN"],
)

# We'll use a public photo of a tent for demo purposes
# In production, customers upload their own images
TENT_IMAGE_URL = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8e/Tent_at_sunset.jpg/320px-Tent_at_sunset.jpg"

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image_url",
                    "image_url": {"url": TENT_IMAGE_URL},
                },
                {
                    "type": "text",
                    "text": "This is an OutdoorGear customer photo. "
                            "Describe the tent in this image: type, approximate size, condition, "
                            "and whether it appears to be a 3-season or 4-season design.",
                },
            ],
        }
    ],
    max_tokens=300,
)

print("=== Vision Analysis ===")
print(response.choices[0].message.content)

Passo 2: Enviar uma imagem local (base64)

Quando os clientes enviam imagens, você recebe bytes do arquivo — envie-os codificados em base64:

# vision_local_image.py
import base64
import os
from openai import OpenAI

def encode_image(image_path: str) -> str:
    """Encode a local image file to base64 string."""
    with open(image_path, "rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")

def analyze_product_image(image_path: str, question: str) -> str:
    """Ask GPT-4o a question about a local image file."""
    client = OpenAI(
        base_url="https://models.inference.ai.azure.com",
        api_key=os.environ["GITHUB_TOKEN"],
    )

    b64_image = encode_image(image_path)

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "You are an OutdoorGear product expert. "
                           "Analyze customer-submitted product photos and provide helpful, accurate assessments.",
            },
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{b64_image}",
                            "detail": "low",    # "low" saves tokens; "high" for detailed analysis
                        },
                    },
                    {"type": "text", "text": question},
                ],
            },
        ],
        max_tokens=400,
    )
    return response.choices[0].message.content

# Usage (provide your own image):
# result = analyze_product_image("my_tent.jpg", "Is this tent suitable for winter camping?")
# print(result)

Parte 2: Visão + Chamada de Ferramentas

O verdadeiro poder dos agentes multimodais: o modelo vê uma imagem, decide quais ferramentas chamar e toma ação.

Passo 3: Agente de identificação de produtos

# vision_agent.py
import os
import json
from openai import OpenAI

client = OpenAI(
    base_url="https://models.inference.ai.azure.com",
    api_key=os.environ["GITHUB_TOKEN"],
)

# Tools the agent can call after seeing an image
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "lookup_product",
            "description": "Look up product details by name or description. "
                           "Use after identifying a product in an image.",
            "parameters": {
                "type": "object",
                "properties": {
                    "product_name": {
                        "type": "string",
                        "description": "Product name or description to look up (e.g. 'TrailBlazer Tent 2P')",
                    }
                },
                "required": ["product_name"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "initiate_return",
            "description": "Initiate a product return/warranty claim. "
                           "Use when an image shows damage or defects.",
            "parameters": {
                "type": "object",
                "properties": {
                    "product_id":    {"type": "string"},
                    "damage_type":   {"type": "string", "description": "Description of the observed damage"},
                    "severity":      {"type": "string", "enum": ["minor", "moderate", "severe"]},
                },
                "required": ["product_id", "damage_type", "severity"],
            },
        },
    },
]

PRODUCTS_DB = {
    "trailblazer tent": {"id": "P001", "name": "TrailBlazer Tent 2P", "price": 249.99, "warranty": "lifetime"},
    "summit dome":      {"id": "P002", "name": "Summit Dome 4P",       "price": 549.99, "warranty": "lifetime"},
    "trailblazer solo": {"id": "P003", "name": "TrailBlazer Solo",     "price": 299.99, "warranty": "lifetime"},
    "arcticdown":       {"id": "P004", "name": "ArcticDown Bag",       "price": 389.99, "warranty": "5 years"},
}

def execute_tool(name: str, args: dict) -> str:
    if name == "lookup_product":
        query = args["product_name"].lower()
        for key, product in PRODUCTS_DB.items():
            if key in query or query in key:
                return json.dumps(product)
        return json.dumps({"error": f"Product '{args['product_name']}' not found in catalog"})

    elif name == "initiate_return":
        return json.dumps({
            "return_id": "RTN-2025-00042",
            "status": "initiated",
            "product_id": args["product_id"],
            "damage_type": args["damage_type"],
            "severity": args["severity"],
            "next_steps": "Ship the item to our returns center. Label included via email.",
        })
    return json.dumps({"error": "Unknown tool"})


def vision_agent(image_url: str, user_message: str) -> str:
    """Run a multimodal agent that sees an image and calls tools."""
    messages = [
        {
            "role": "system",
            "content": "You are an OutdoorGear customer service agent. "
                       "When a customer shares a product image: identify the product, "
                       "then use tools to look it up or handle warranty claims.",
        },
        {
            "role": "user",
            "content": [
                {"type": "image_url", "image_url": {"url": image_url}},
                {"type": "text", "text": user_message},
            ],
        },
    ]

    # Agent loop: keep running until no more tool calls
    while True:
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=TOOLS,
            max_tokens=500,
        )

        assistant_msg = response.choices[0].message
        messages.append(assistant_msg)

        if not assistant_msg.tool_calls:
            return assistant_msg.content   # Done!

        # Execute tool calls
        for tool_call in assistant_msg.tool_calls:
            result = execute_tool(
                tool_call.function.name,
                json.loads(tool_call.function.arguments)
            )
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })


# Demo: Customer submits a photo and asks for product info
TENT_URL = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8e/Tent_at_sunset.jpg/320px-Tent_at_sunset.jpg"

print("=== Vision + Tool Calling Agent ===")
answer = vision_agent(
    image_url=TENT_URL,
    user_message="This is my tent from OutdoorGear. Can you tell me its warranty status?",
)
print(answer)

Parte 3: Comparação de Múltiplas Imagens

O GPT-4o pode analisar múltiplas imagens em uma única requisição:

# multi_image.py
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://models.inference.ai.azure.com",
    api_key=os.environ["GITHUB_TOKEN"],
)

def compare_images(image_urls: list[str], comparison_question: str) -> str:
    """Compare multiple images in a single GPT-4o call."""
    content = []
    for i, url in enumerate(image_urls, 1):
        content.append({"type": "text", "text": f"Image {i}:"})
        content.append({"type": "image_url", "image_url": {"url": url}})

    content.append({"type": "text", "text": comparison_question})

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "You are an OutdoorGear product expert."},
            {"role": "user", "content": content},
        ],
        max_tokens=500,
    )
    return response.choices[0].message.content


# Demo: Compare two tents for suitability
TENT_1 = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8e/Tent_at_sunset.jpg/320px-Tent_at_sunset.jpg"
TENT_2 = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Bivouac_tent.jpg/320px-Bivouac_tent.jpg"

result = compare_images(
    [TENT_1, TENT_2],
    "Comparing these two tents: which appears more suitable for winter camping, "
    "and which is lighter/smaller for backpacking? Explain your reasoning.",
)
print("=== Multi-Image Comparison ===")
print(result)

Parte 4: Melhores Práticas de Visão

Otimização de tokens

# Vision input token costs:
# "low" detail:  ~85 tokens per image   → use for product identification
# "high" detail: ~1000+ tokens per image → use for damage assessment, fine details

# Always specify detail level:
{
    "type": "image_url",
    "image_url": {
        "url": url,
        "detail": "low"   # or "high" — choose based on task
    }
}

Diretrizes de tamanho de imagem

Caso de uso Detalhe Tamanho máximo da imagem
Identificação de produto low Qualquer (redimensionado automaticamente)
Avaliação de danos high 2048×2048px ideal
Extração de texto (rótulos) high Alta resolução necessária
Perguntas e respostas gerais low Qualquer

Segurança e moderação

def safe_vision_request(image_url: str, user_question: str) -> str:
    """Wraps the vision request with a system prompt that limits scope."""
    # Always constrain vision agents to their domain
    # Prevents misuse (e.g., asking about medical conditions in photos)
    system = (
        "You are an OutdoorGear product assistant. "
        "You ONLY analyze outdoor equipment and gear in images. "
        "If the image does not contain outdoor gear, respond: "
        "'I can only help with OutdoorGear products. This image doesn't appear to show outdoor equipment.'"
    )
    # ... rest of request

🧠 Verificação de Conhecimento

1. Qual é a diferença entre detail: 'low' e detail: 'high' nas requisições de visão?

detail: 'low' redimensiona a imagem para 512×512 pixels e usa ~85 tokens — rápido e barato, adequado para identificação geral de produtos e compreensão de cenas. detail: 'high' divide a imagem em blocos de 512×512 e processa cada um com detalhe completo, usando ~1000+ tokens — necessário para ler texto pequeno (rótulos, números de série), detectar danos finos ou analisar detalhes intrincados. Sempre use low a menos que a tarefa exija explicitamente detalhe fino.

2. Por que combinar visão com chamada de ferramentas é mais poderoso do que apenas visão?

Agentes apenas com visão podem descrever o que veem, mas não podem tomar ação. Combinar visão com chamada de ferramentas significa: o agente uma mochila danificada → chama lookup_product() para identificá-la → chama initiate_return() para iniciar o processo de garantia — tudo em um único turno de conversa. O agente se torna um participante ativo em vez de apenas um narrador.

3. Qual é uma prática-chave de segurança ao implantar agentes multimodais?

Restrição de escopo via prompt de sistema: diga explicitamente ao modelo quais tipos de imagens ele deve e não deve processar. Sem isso, os usuários podem enviar imagens não relacionadas (médicas, pessoais, NSFW) e extrair respostas. Um prompt de sistema com escopo definido como "Apenas analise imagens de equipamentos outdoor — recuse qualquer outra coisa" reduz significativamente o uso indevido. Combine com APIs de moderação de conteúdo (Azure Content Safety) para implantações em produção.


Resumo

Conceito Implementação
Entrada de imagem por URL "type": "image_url", "image_url": {"url": "..."}
Entrada de imagem em Base64 "url": "data:image/jpeg;base64,<encoded>"
Controle de tokens "detail": "low" (~85 tokens) ou "high" (1000+)
Visão + ferramentas Mesmo loop de chamada de ferramentas dos agentes de texto
Múltiplas imagens Múltiplos blocos image_url em um único array content

Próximos Passos