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:
| Nivel | Valor 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=0para 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
| Aspecto | detail=low | detail=high |
|---|---|---|
| Resolución | 512×512 fija | Hasta 2048px, tiles de 512 |
| Costo tokens | ~85 tokens | 85 + 170 por tile |
| Texto grande | Suficiente | Innecesario |
| Texto pequeño/denso | Pierde detalle | Necesario |
| Texto manuscrito | Pobre | Aceptable |
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=0siempre. 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 uso | Prompt 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
| Escenario | Contexto que mejora resultados |
|---|---|
| Documentos técnicos | Dominio (legal, médico, ingeniería) |
| Productos | Catálogo, categoría, marca |
| Dashboards | Métricas esperadas, período, KPIs |
| Imágenes médicas | Historia 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: clasifica → extrae 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ón | Estilo de prompt | Temp. | Salida | Caso de uso |
|---|---|---|---|---|
| Descripción | Instrucción + extensión | 0.3 | Texto libre | Alt-text, indexación |
| OCR | Instrucción + reglas formato | 0 | Texto/Markdown | Documentos, capturas |
| Clasificación | Categorías fijas + restricción | 0 | Texto exacto/JSON | Moderación, routing |
| Extracción JSON | Schema + ejemplo + reglas null | 0 | JSON | Facturas, formularios |
| Q&A Contextual | Pregunta + grounding | 0.2 | Texto libre | Asistentes, análisis |
| Comparación | Estructura análisis + N imágenes | 0.3 | Texto estructurado | Before/after, QA |
| Contexto | Imagen + contexto + tarea | 0.3 | Texto libre | Docs 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 unstr - Soporte para los 7 patrones
- Lanzar
ValueErrorsi 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:
- Extrae el texto crudo con OCR
- Usa ese texto como contexto adicional para la extracción JSON de factura
Requisitos:
- Retorna
dictcon 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=0para 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
- OpenAI Vision Best Practices
- Prompt Engineering for Vision Models
- Anthropic Vision Documentation
- Google Gemini Vision Guide