Módulo 2: Vision + LLMs

6. Patrones de Análisis de Imágenes

Descripción

En las cápsulas anteriores aprendiste a enviar imágenes a OpenAI, Claude y Gemini, y comparaste sus fortalezas. Pero saber cómo llamar a una API no es lo mismo que saber qué pedirle. La diferencia entre un resultado útil y uno mediocre casi siempre está en el prompt — en cómo estructuras la instrucción para la tarea específica.

Esta cápsula te enseña 7 patrones de análisis que funcionan con cualquier proveedor. Cada patrón incluye un template de prompt, código funcional, casos de uso y tips para obtener resultados consistentes. Piensa en ellos como recetas probadas: descripción, OCR, clasificación, extracción estructurada, Q&A, comparación y análisis con contexto adicional.

Por qué importa: Sin patrones claros, cada llamada a vision es un experimento. Con patrones, tienes resultados predecibles que puedes testear, versionar y mejorar. Al final combinarás varios en un pipeline multi-paso — la base de cualquier sistema de visión en producción.


Función Base

Todos los ejemplos usan esta función. Cámbiala por el proveedor que prefieras:

import base64
import json
from openai import OpenAI

client = OpenAI()

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


def analyze_image(
    image_path: str,
    prompt: str,
    model: str = "gpt-4o",
    temperature: float = 0.3,
    max_tokens: int = 1024,
) -> str:
    b64 = encode_image(image_path)
    response = client.chat.completions.create(
        model=model,
        temperature=temperature,
        max_tokens=max_tokens,
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": prompt},
                {"type": "image_url", "image_url": {
                    "url": f"data:image/jpeg;base64,{b64}",
                    "detail": "high",
                }},
            ],
        }],
    )
    return response.choices[0].message.content

Patrón 1: Descripción

Objetivo: Generar una descripción textual objetiva de una imagen.

Template

DESCRIPTION_PROMPT = """Describe esta imagen de forma objetiva y estructurada.

Incluye:
- Escena principal y composición general
- Objetos y personas visibles (sin identificar individuos)
- Colores dominantes y condiciones de iluminación
- Texto visible (si lo hay)

Formato: párrafo continuo.
Extensión: {length}.
Tono: descriptivo, neutral, sin interpretaciones subjetivas."""

El placeholder {length} controla la extensión:

NivelValor de {length}Uso típico
Breve"máximo 2 oraciones"Alt-text, accesibilidad
Media"entre 50 y 100 palabras"Indexación y búsqueda
Detallada"entre 150 y 250 palabras"Catalogación, reportes

Código

def describe_image(image_path: str, detail_level: str = "media") -> str:
    lengths = {
        "breve": "máximo 2 oraciones",
        "media": "entre 50 y 100 palabras",
        "detallada": "entre 150 y 250 palabras",
    }
    length = lengths.get(detail_level, lengths["media"])
    prompt = DESCRIPTION_PROMPT.format(length=length)
    return analyze_image(image_path, prompt, temperature=0.3)

Tips

  • Consistencia: Usa siempre el mismo template para obtener descripciones comparables entre imágenes.
  • Alt-text: Para accesibilidad, pide "máximo 2 oraciones" y agrega "no incluyas información subjetiva ni emociones".
  • Batch: Si procesas muchas imágenes, fija temperature=0 para minimizar variación.

Patrón 2: OCR (Extracción de Texto)

Objetivo: Extraer todo el texto visible preservando la estructura original.

Template para texto plano

OCR_RAW_PROMPT = """Extrae TODO el texto visible en esta imagen.

Reglas:
- Mantén el orden de lectura original (arriba a abajo, izquierda a derecha)
- Preserva la estructura: párrafos, listas, encabezados
- Si hay tablas, represéntalas en formato markdown
- NO inventes ni completes texto que no sea legible
- Texto ilegible: márcalo como [ilegible]
- Responde ÚNICAMENTE con el texto extraído, sin comentarios"""

Template para OCR estructurado

OCR_STRUCTURED_PROMPT = """Extrae el texto de esta imagen y organízalo en secciones.

Formato de respuesta:
## Encabezado/Título
[texto del encabezado]

## Cuerpo
[texto principal]

## Tablas
[tablas en formato markdown]

## Notas/Pie
[texto secundario, notas al pie, letras pequeñas]

Si una sección no existe, omítela. NO inventes texto. Marca texto dudoso como [ilegible]."""

Código

def extract_text(image_path: str, structured: bool = False, detail: str = "high") -> str:
    prompt = OCR_STRUCTURED_PROMPT if structured else OCR_RAW_PROMPT
    b64 = encode_image(image_path)
    response = client.chat.completions.create(
        model="gpt-4o",
        temperature=0,
        max_tokens=2048,
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": prompt},
                {"type": "image_url", "image_url": {
                    "url": f"data:image/jpeg;base64,{b64}",
                    "detail": detail,
                }},
            ],
        }],
    )
    return response.choices[0].message.content

detail=low vs detail=high

Aspectodetail=lowdetail=high
Resolución512×512 fijaHasta 2048px, tiles de 512
Costo tokens~85 tokens85 + 170 por tile
Texto grandeSuficienteInnecesario
Texto pequeño/densoPierde detalleNecesario
Texto manuscritoPobreAceptable

Regla práctica: Usa detail=high si el texto tiene fuente menor a ~12pt o es manuscrito. Para capturas de pantalla con texto grande, detail=low ahorra hasta un 90% en tokens.

Tips para texto manuscrito

  • Siempre usa detail=high.
  • Agrega al prompt: "El texto puede ser manuscrito. Transcríbelo lo mejor posible."
  • Pide nivel de confianza: "Marca con [?] las palabras de las que no estés seguro."

Patrón 3: Clasificación

Objetivo: Asignar una o más categorías predefinidas a una imagen.

Template single-label

CLASSIFICATION_PROMPT = """Clasifica esta imagen en EXACTAMENTE una de las siguientes categorías:
{categories}

Reglas:
- Responde ÚNICAMENTE con el nombre exacto de la categoría
- Si ninguna categoría aplica, responde: otro
- No agregues explicación ni puntuación"""

Template multi-label con confianza

MULTI_CLASSIFICATION_PROMPT = """Clasifica esta imagen. Puede pertenecer a una o más categorías.

Categorías posibles: {categories}

Responde en formato JSON:
{{"categorias": ["cat1", "cat2"], "confianza": {{"cat1": 0.95, "cat2": 0.72}}}}

Solo incluye categorías con confianza > 0.5.
Si ninguna aplica: {{"categorias": ["otro"], "confianza": {{"otro": 1.0}}}}"""

Código con validación

def classify_image(
    image_path: str,
    categories: list[str],
    multi_label: bool = False,
) -> dict:
    if multi_label:
        prompt = MULTI_CLASSIFICATION_PROMPT.format(categories=", ".join(categories))
    else:
        prompt = CLASSIFICATION_PROMPT.format(categories=", ".join(categories))

    result = analyze_image(image_path, prompt, temperature=0)

    if multi_label:
        try:
            parsed = json.loads(result)
            valid_cats = [c for c in parsed["categorias"] if c in categories or c == "otro"]
            return {
                "categorias": valid_cats,
                "confianza": {k: v for k, v in parsed.get("confianza", {}).items() if k in valid_cats},
            }
        except (json.JSONDecodeError, KeyError):
            return {"categorias": ["error_parsing"], "confianza": {}}

    category = result.strip().lower()
    if category not in [c.lower() for c in categories] and category != "otro":
        category = "otro"
    return {"categorias": [category], "confianza": {}}


result = classify_image("foto.jpg", categories=["producto", "persona", "documento", "paisaje"])
print(result)

Tips

  • temperature=0 siempre. La clasificación necesita determinismo, no creatividad.
  • Categorías claras y mutuamente excluyentes para single-label. Si hay ambigüedad, usa multi-label.
  • Máximo ~15 categorías en el prompt. Con más, el modelo pierde precisión. Para 50+ categorías, usa clasificación jerárquica (primero categoría general, luego subcategoría).

Patrón 4: Extracción Estructurada (JSON)

Objetivo: Extraer datos específicos de una imagen y devolverlos en formato JSON válido.

Template

EXTRACTION_PROMPT = """Extrae los siguientes campos de esta imagen y devuélvelos como JSON válido.

Campos a extraer:
{field_descriptions}

Formato exacto esperado:
{example_json}

Reglas:
- Responde ÚNICAMENTE con el JSON, sin texto adicional ni markdown
- Usa null para campos que no sean visibles en la imagen
- Fechas en formato YYYY-MM-DD
- Montos como números (sin símbolo de moneda)
- No inventes datos que no estén visibles"""

Schemas para casos comunes

SCHEMAS = {
    "factura": {
        "fields": (
            "- fecha: fecha de emisión\n- numero: número de factura\n"
            "- emisor: nombre de empresa emisora\n- receptor: nombre del cliente\n"
            "- subtotal: monto antes de impuestos\n- impuesto: monto de impuestos\n"
            "- total: monto total\n"
            "- items: lista [{descripcion, cantidad, precio_unitario, monto}]"
        ),
        "example": '{"fecha":"2025-03-15","numero":"FAC-001234","emisor":"Empresa SA",'
                   '"receptor":"Cliente SL","subtotal":1000.00,"impuesto":160.00,'
                   '"total":1160.00,"items":[{"descripcion":"Consultoría",'
                   '"cantidad":1,"precio_unitario":1000.00,"monto":1000.00}]}',
    },
    "recibo": {
        "fields": (
            "- comercio: nombre del establecimiento\n- fecha: fecha de compra\n"
            "- items: lista [{nombre, precio}]\n- total: monto total\n"
            "- metodo_pago: efectivo, tarjeta, etc."
        ),
        "example": '{"comercio":"Tienda XYZ","fecha":"2025-01-20",'
                   '"items":[{"nombre":"Café","precio":4.50}],'
                   '"total":4.50,"metodo_pago":"tarjeta"}',
    },
    "identificacion": {
        "fields": (
            "- tipo: tipo de documento (INE, pasaporte, licencia)\n"
            "- nombre: nombre completo\n- numero_documento: número o clave\n"
            "- fecha_nacimiento: fecha de nacimiento\n"
            "- fecha_expedicion: fecha de expedición\n"
            "- fecha_vencimiento: fecha de vencimiento"
        ),
        "example": '{"tipo":"INE","nombre":"Juan Pérez López",'
                   '"numero_documento":"ABCD123456","fecha_nacimiento":"1990-05-15",'
                   '"fecha_expedicion":"2020-01-10","fecha_vencimiento":"2030-01-10"}',
    },
}

Código con parsing y manejo de errores

def extract_structured(image_path: str, schema_name: str, max_retries: int = 2) -> dict | None:
    schema = SCHEMAS.get(schema_name)
    if not schema:
        raise ValueError(f"Schema desconocido: {schema_name}. Disponibles: {list(SCHEMAS.keys())}")

    prompt = EXTRACTION_PROMPT.format(
        field_descriptions=schema["fields"],
        example_json=schema["example"],
    )

    for attempt in range(max_retries + 1):
        raw = analyze_image(image_path, prompt, temperature=0)
        cleaned = raw.strip()
        if cleaned.startswith("```"):
            cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0].strip()

        try:
            return json.loads(cleaned)
        except json.JSONDecodeError:
            if attempt < max_retries:
                prompt += "\n\nIMPORTANTE: Tu respuesta anterior no fue JSON válido. Responde SOLO con JSON."
                continue
            return None


data = extract_structured("factura_scan.jpg", "factura")
if data:
    print(f"Emisor: {data.get('emisor')} — Total: ${data.get('total')}")
else:
    print("No se pudo extraer JSON válido después de reintentos")

El cleaned maneja el caso más frecuente de JSON inválido: cuando el modelo envuelve la respuesta en bloques markdown (```json ... ```). El loop de reintentos da una segunda oportunidad con una instrucción más enfática.


Patrón 5: Q&A Contextual

Objetivo: Responder preguntas específicas basándose exclusivamente en lo visible en la imagen.

Template

QA_PROMPT = """Responde la siguiente pregunta basándote ÚNICAMENTE en lo visible en la imagen.

Pregunta: {question}

Reglas:
- Si la información NO está visible, responde exactamente: "No visible en la imagen"
- No hagas suposiciones ni uses conocimiento externo
- Sé conciso y directo"""

Código con múltiples preguntas

def ask_about_image(image_path: str, question: str) -> str:
    prompt = QA_PROMPT.format(question=question)
    return analyze_image(image_path, prompt, temperature=0.2)


def ask_multiple(image_path: str, questions: list[str]) -> dict[str, str]:
    combined = (
        "Responde cada pregunta basándote ÚNICAMENTE en lo visible en la imagen.\n"
        "Si la información no está visible, responde 'No visible en la imagen'.\n\n"
        "Responde en formato JSON: {\"1\": \"respuesta\", \"2\": \"respuesta\", ...}\n\n"
    )
    for i, q in enumerate(questions, 1):
        combined += f"{i}. {q}\n"

    raw = analyze_image(image_path, combined, temperature=0.2)
    try:
        answers = json.loads(raw)
        return {questions[int(k) - 1]: v for k, v in answers.items() if int(k) <= len(questions)}
    except (json.JSONDecodeError, ValueError):
        return {questions[0]: raw}


answers = ask_multiple("dashboard.png", [
    "¿Cuál es el total de ventas?",
    "¿Qué período cubre el reporte?",
    "¿Cuál es la categoría con mayor ingreso?",
])
for q, a in answers.items():
    print(f"Q: {q}\nA: {a}\n")

Tips

  • Grounding: La instrucción "ÚNICAMENTE en lo visible" reduce alucinaciones significativamente.
  • Batch: Enviar varias preguntas en una sola llamada es más barato, pero pierde precisión con más de 5-6 preguntas.
  • Especificidad: "¿Cuál es el número en la esquina superior derecha?" es mejor que "¿Cuál es el número?".

Patrón 6: Comparación de Imágenes

Objetivo: Analizar similitudes y diferencias entre dos o más imágenes.

Template

COMPARISON_PROMPT = """Compara las {n} imágenes proporcionadas.

Estructura tu respuesta:
1. **Similitudes:** Elementos comunes entre las imágenes
2. **Diferencias:** Cambios específicos entre cada imagen
3. **Conclusión:** Resumen en 1-2 oraciones

Sé específico: menciona ubicaciones, colores, texto y elementos concretos."""

Código

def compare_images(image_paths: list[str], custom_prompt: str | None = None) -> str:
    prompt = custom_prompt or COMPARISON_PROMPT.format(n=len(image_paths))
    content = [{"type": "text", "text": prompt}]
    for path in image_paths:
        b64 = encode_image(path)
        content.append({"type": "image_url", "image_url": {
            "url": f"data:image/jpeg;base64,{b64}", "detail": "high",
        }})

    response = client.chat.completions.create(
        model="gpt-4o", temperature=0.3, max_tokens=1024,
        messages=[{"role": "user", "content": content}],
    )
    return response.choices[0].message.content

Casos de uso

Caso de usoPrompt adicional sugerido
Before/After"Enfócate en qué cambió entre la primera y segunda imagen"
Product matching"¿Son el mismo producto? Lista diferencias de apariencia"
Change detection"Lista TODOS los cambios, por pequeños que sean"
Brand consistency"¿Ambas imágenes siguen la misma línea visual?"

Patrón 7: Análisis con Contexto Adicional

Objetivo: Mejorar la precisión combinando la imagen con información textual relevante.

Template y código

CONTEXT_ANALYSIS_PROMPT = """Analiza esta imagen considerando el siguiente contexto:

Contexto: {context}

Tarea: {task}

Usa el contexto para interpretar mejor la imagen, pero basa tus observaciones en lo que realmente es visible."""


def analyze_with_context(image_path: str, context: str, task: str) -> str:
    prompt = CONTEXT_ANALYSIS_PROMPT.format(context=context, task=task)
    return analyze_image(image_path, prompt, temperature=0.3)


result = analyze_with_context(
    image_path="plano_arquitectura.jpg",
    context="Plano del segundo piso de un edificio comercial. "
            "El cliente solicitó 3 oficinas privadas y un área común.",
    task="Verifica si el plano cumple con los requisitos. "
         "Identifica cada oficina y el área común.",
)

Cuándo agregar contexto

EscenarioContexto que mejora resultados
Documentos técnicosDominio (legal, médico, ingeniería)
ProductosCatálogo, categoría, marca
DashboardsMétricas esperadas, período, KPIs
Imágenes médicasHistoria clínica relevante (anonimizada)

Precaución: El contexto ayuda al modelo a interpretar, pero también puede sesgarlo. Si dices "esta imagen muestra un defecto", el modelo buscará uno aunque no exista. Usa contexto factual, no conclusiones.


Combinando Patrones

El verdadero poder emerge al encadenarlos. Pipeline típico: clasificaextrae según clasificación → valida el resultado.

def document_pipeline(image_path: str) -> dict:
    classification = classify_image(
        image_path,
        categories=["factura", "recibo", "identificacion", "otro"],
    )
    doc_type = classification["categorias"][0]

    if doc_type == "otro":
        return {
            "tipo": "no_reconocido",
            "descripcion": describe_image(image_path, detail_level="detallada"),
            "datos": None,
        }

    extracted = extract_structured(image_path, doc_type)

    validation_questions = {
        "factura": "¿El total coincide con la suma de subtotal e impuesto?",
        "recibo": "¿El total coincide con la suma de los items?",
        "identificacion": "¿Todas las fechas son legibles y en formato válido?",
    }
    validation = ask_about_image(
        image_path,
        validation_questions.get(doc_type, "¿Los datos son consistentes?"),
    )

    return {"tipo": doc_type, "datos": extracted, "validacion": validation}


result = document_pipeline("documento_scan.jpg")
print(f"Tipo: {result['tipo']}")
if result["datos"]:
    print(f"Datos: {json.dumps(result['datos'], indent=2, ensure_ascii=False)}")
print(f"Validación: {result.get('validacion', 'N/A')}")

Este pipeline demuestra el flujo clasificar → extraer → validar que es la base de los sistemas de procesamiento de documentos con visión.


Tabla Resumen de Patrones

PatrónEstilo de promptTemp.SalidaCaso de uso
DescripciónInstrucción + extensión0.3Texto libreAlt-text, indexación
OCRInstrucción + reglas formato0Texto/MarkdownDocumentos, capturas
ClasificaciónCategorías fijas + restricción0Texto exacto/JSONModeración, routing
Extracción JSONSchema + ejemplo + reglas null0JSONFacturas, formularios
Q&A ContextualPregunta + grounding0.2Texto libreAsistentes, análisis
ComparaciónEstructura análisis + N imágenes0.3Texto estructuradoBefore/after, QA
ContextoImagen + contexto + tarea0.3Texto libreDocs técnicos, dominio

Troubleshooting

Problema 1: OCR "inventa" texto que no existe

Síntoma: El modelo devuelve texto plausible que no está en la imagen.

Solución: Agrega "Si un fragmento no es legible, escribe [ilegible]. NUNCA completes o adivines texto." Usa temperature=0 y detail=high — la resolución baja causa más alucinaciones.

Problema 2: Clasificación inconsistente

Síntoma: La misma imagen devuelve "producto" a veces y "objeto" otras.

Solución: temperature=0 obligatorio. Revisa que las categorías sean mutuamente excluyentes. Agrega definiciones breves: "producto: artículo comercial; objeto: elemento no comercial."

Problema 3: JSON malformado en extracción

Síntoma: json.loads() falla con el response.

Solución: El código ya maneja bloques ```json ```. Además: agrega "Responde ÚNICAMENTE con JSON, sin texto ni bloques de código." Implementa reintentos (Patrón 4). Si persiste, usa regex para extraer el primer {...}.

Problema 4: Descripciones vagas o genéricas

Síntoma: "La imagen muestra un objeto sobre una superficie" sin detalles.

Solución: Usa el template con {length} explícito. Agrega categorías: "Incluye: materiales, texturas, marcas visibles, estado." Aumenta max_tokens — a veces el modelo trunca.


Ejercicios

Ejercicio 1 (Fácil): PromptBuilder

Crea una clase PromptBuilder que genere el template de prompt correcto para cada patrón. Debe aceptar el nombre del patrón y sus parámetros (categorías para clasificación, schema para extracción, pregunta para Q&A).

Requisitos:

  • Método build(pattern_name, **kwargs) que retorna un str
  • Soporte para los 7 patrones
  • Lanzar ValueError si falta un parámetro requerido
Ver solución
class PromptBuilder:
    TEMPLATES = {
        "descripcion": DESCRIPTION_PROMPT,
        "ocr": OCR_RAW_PROMPT,
        "ocr_estructurado": OCR_STRUCTURED_PROMPT,
        "clasificacion": CLASSIFICATION_PROMPT,
        "clasificacion_multi": MULTI_CLASSIFICATION_PROMPT,
        "extraccion": EXTRACTION_PROMPT,
        "qa": QA_PROMPT,
        "comparacion": COMPARISON_PROMPT,
        "contexto": CONTEXT_ANALYSIS_PROMPT,
    }
    REQUIRED_PARAMS = {
        "descripcion": ["length"], "ocr": [], "ocr_estructurado": [],
        "clasificacion": ["categories"], "clasificacion_multi": ["categories"],
        "extraccion": ["field_descriptions", "example_json"],
        "qa": ["question"], "comparacion": ["n"], "contexto": ["context", "task"],
    }

    def build(self, pattern_name: str, **kwargs) -> str:
        if pattern_name not in self.TEMPLATES:
            raise ValueError(f"Patrón desconocido: {pattern_name}")

        missing = [p for p in self.REQUIRED_PARAMS[pattern_name] if p not in kwargs]
        if missing:
            raise ValueError(f"Faltan parámetros para '{pattern_name}': {missing}")

        if "categories" in kwargs and isinstance(kwargs["categories"], list):
            kwargs["categories"] = ", ".join(kwargs["categories"])

        return self.TEMPLATES[pattern_name].format(**kwargs)


builder = PromptBuilder()
prompt = builder.build("clasificacion", categories=["gato", "perro", "ave"])
print(prompt)

Ejercicio 2 (Medio): Invoice Extractor Pipeline

Construye extract_invoice(image_path) que combine OCR + extracción estructurada:

  1. Extrae el texto crudo con OCR
  2. Usa ese texto como contexto adicional para la extracción JSON de factura

Requisitos:

  • Retorna dict con campos del schema de factura
  • Si OCR no detecta texto, retorna {"error": "No se detectó texto"}
  • Incluye el texto OCR bajo la clave "_ocr_raw"
Ver solución
def extract_invoice(image_path: str) -> dict:
    ocr_text = extract_text(image_path, structured=True)
    if not ocr_text or not ocr_text.strip():
        return {"error": "No se detectó texto"}

    enriched_prompt = EXTRACTION_PROMPT.format(
        field_descriptions=SCHEMAS["factura"]["fields"],
        example_json=SCHEMAS["factura"]["example"],
    )
    enriched_prompt += (
        f"\n\nContexto — texto ya extraído de la imagen:\n---\n{ocr_text}\n---\n"
        f"Usa tanto la imagen como el texto extraído para mayor precisión."
    )

    for attempt in range(3):
        raw = analyze_image(image_path, enriched_prompt, temperature=0)
        cleaned = raw.strip()
        if cleaned.startswith("```"):
            cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
        try:
            data = json.loads(cleaned)
            data["_ocr_raw"] = ocr_text
            return data
        except json.JSONDecodeError:
            if attempt < 2:
                enriched_prompt += "\nJSON inválido. Responde SOLO con JSON."

    return {"error": "No se pudo parsear JSON", "_ocr_raw": ocr_text}


invoice = extract_invoice("factura_scan.jpg")
if "error" not in invoice:
    print(f"Emisor: {invoice.get('emisor')} — Total: {invoice.get('total')}")

Ejercicio 3 (Medio): Multi-Pattern Analyzer

Crea full_analysis(image_path, categories) que ejecute 3 patrones en secuencia: descripción → clasificación → OCR.

Requisitos:

  • Descripción nivel "media", clasificación single-label, OCR raw
  • Retornar {"descripcion": str, "clasificacion": dict, "texto_ocr": str}
  • Si un paso falla, incluir el error en el campo correspondiente sin detener el pipeline
Ver solución
def full_analysis(image_path: str, categories: list[str]) -> dict:
    result = {"descripcion": None, "clasificacion": None, "texto_ocr": None}

    for key, fn in [
        ("descripcion", lambda: describe_image(image_path, detail_level="media")),
        ("clasificacion", lambda: classify_image(image_path, categories)),
        ("texto_ocr", lambda: extract_text(image_path, structured=False)),
    ]:
        try:
            result[key] = fn()
        except Exception as e:
            result[key] = f"Error: {e}" if key != "clasificacion" else {"error": str(e)}

    return result


analysis = full_analysis(
    "documento.jpg",
    categories=["factura", "recibo", "contrato", "identificacion"],
)
print(f"Tipo: {analysis['clasificacion']}")
print(f"Descripción: {str(analysis['descripcion'])[:100]}...")

Resumen

  • Cada tarea de visión tiene un patrón óptimo con su propio template, temperature y formato de salida.
  • temperature=0 para tareas deterministas (clasificación, OCR, extracción). 0.2-0.3 para descriptivas.
  • Los patrones son provider-agnostic: funcionan con OpenAI, Claude y Gemini cambiando solo la función base.
  • Combinar patrones en pipelines (clasificar → extraer → validar) es cómo se construyen sistemas reales.
  • Siempre valida la salida: categorías en la lista permitida, JSON parseable, OCR sin texto inventado.

Recursos adicionales

  1. OpenAI Vision Best Practices
  2. Prompt Engineering for Vision Models
  3. Anthropic Vision Documentation
  4. Google Gemini Vision Guide