Módulo 6: Decision Matrix para AI Engineers

Cápsula 04: Matriz de Decisión Práctica

🎯 Objetivo de la cápsula

Aplicar todo el framework (requisitos, pesos, scoring, normalización) a un caso real completo: evaluar 5 vector databases para un proyecto RAG concreto, desde la definición de criterios hasta la recomendación final documentada.

Al finalizar esta cápsula:

  • ✅ Ejecutarás una evaluación completa de 5 proveedores en un caso real
  • ✅ Construirás una matriz de decisión reproducible en Python
  • ✅ Generarás un reporte con ranking, gap analysis y recomendación
  • ✅ Documentarás trade-offs y plan de reevaluación

Tiempo estimado: 30-40 minutos


Descripción de la cápsula

Has construido las piezas: criterios con umbrales (cápsula 02), pesos con metodología (cápsula 03). Ahora es momento de ensamblar todo en un ejercicio práctico completo. En esta cápsula vas a evaluar 5 vector databases reales — Pinecone, Qdrant Cloud, Weaviate Cloud, Milvus y ChromaDB — para un caso de uso concreto: un RAG chatbot para documentación técnica de una startup en crecimiento.

El valor de esta cápsula no está solo en el resultado (cuál "gana"), sino en el proceso. Verás cómo la misma metodología produce resultados diferentes según el contexto del equipo, y cómo documentar la decisión de forma que tu "yo del futuro" entienda por qué elegiste lo que elegiste.

Al terminar, tendrás un template de evaluación que puedes reutilizar cada vez que necesites evaluar un nuevo proveedor o reevaluar tu stack actual.


El caso: RAG Chatbot para documentación técnica

Contexto del proyecto

project_context = {
    "name": "DocBot — RAG para docs técnicas de producto SaaS",
    "description": "Chatbot que responde preguntas de usuarios sobre la documentación "
                   "de un producto SaaS B2B (API docs, guías, troubleshooting)",
    "team": {
        "size": 5,
        "roles": ["2 backend (Python)", "1 frontend", "1 ML engineer", "1 PM"],
        "devops": "No dedicado — backend maneja infra como side-task",
        "vector_db_experience": "Usaron ChromaDB en un PoC hace 3 meses"
    },
    "scale": {
        "current_docs": 2_500,
        "current_vectors": 75_000,       # ~30 chunks por doc
        "projected_docs_12m": 15_000,
        "projected_vectors_12m": 450_000,
        "daily_queries": 500,
        "projected_daily_queries_12m": 5_000
    },
    "requirements": {
        "latency_target": "p95 < 300ms (chatbot con streaming, primer chunk importa)",
        "budget": "$400/mes máximo para vector DB (total infra budget $2K/mes)",
        "compliance": "SOC 2 en roadmap para Q3, no obligatorio aún",
        "features": "Metadata filtering obligatorio, hybrid search deseable",
        "integrations": "LangChain, OpenAI embeddings (text-embedding-3-small, 1536d)"
    },
    "constraints": {
        "timeline": "Producción en 6 semanas",
        "migration_tolerance": "Baja — si eligen mal, migrar cuesta ~2 sprints",
        "risk_tolerance": "Media — pueden tolerar 1-2 incidentes menores/mes"
    }
}

print(f"Proyecto: {project_context['name']}")
print(f"Equipo: {project_context['team']['size']} personas, DevOps: {project_context['team']['devops']}")
print(f"Vectores: {project_context['scale']['current_vectors']:,} → "
      f"{project_context['scale']['projected_vectors_12m']:,} (12m)")
print(f"Budget: {project_context['requirements']['budget']}")

Paso 1: Definir criterios y pesos

Basándote en el contexto del proyecto, define los criterios relevantes:

from dataclasses import dataclass, field
from enum import Enum


class Priority(Enum):
    ELIMINATORY = "eliminatory"
    CRITICAL = "critical"
    IMPORTANT = "important"
    NICE_TO_HAVE = "nice_to_have"


@dataclass
class CriterionConfig:
    name: str
    weight: float
    min_acceptable: float
    ideal_value: float
    lower_is_better: bool = True
    unit: str = ""
    priority: Priority = Priority.IMPORTANT
    rationale: str = ""


criteria_config = [
    CriterionConfig(
        name="latency_p95",
        weight=4.0,
        min_acceptable=500,
        ideal_value=150,
        lower_is_better=True,
        unit="ms",
        priority=Priority.CRITICAL,
        rationale="Chatbot: primer chunk debe llegar rápido para UX de streaming"
    ),
    CriterionConfig(
        name="scale_vectors",
        weight=4.0,
        min_acceptable=500_000,
        ideal_value=5_000_000,
        lower_is_better=False,
        unit="vectors",
        priority=Priority.CRITICAL,
        rationale="450K a 12 meses + margen. No queremos migrar en 18 meses"
    ),
    CriterionConfig(
        name="ops_simplicity",
        weight=5.0,
        min_acceptable=15,
        ideal_value=3,
        lower_is_better=True,
        unit="hours/month",
        priority=Priority.CRITICAL,
        rationale="Sin DevOps dedicado. Cada hora de ops = hora menos de producto"
    ),
    CriterionConfig(
        name="monthly_cost",
        weight=4.0,
        min_acceptable=400,
        ideal_value=100,
        lower_is_better=True,
        unit="USD",
        priority=Priority.CRITICAL,
        rationale="Budget de $400/mes. No puede consumir todo el budget de infra"
    ),
    CriterionConfig(
        name="sdk_quality",
        weight=3.0,
        min_acceptable=0.5,
        ideal_value=0.9,
        lower_is_better=False,
        unit="score 0-1",
        priority=Priority.IMPORTANT,
        rationale="5 devs, 2 backend Python. SDK malo = semanas perdidas"
    ),
    CriterionConfig(
        name="metadata_filtering",
        weight=3.0,
        min_acceptable=0.6,
        ideal_value=1.0,
        lower_is_better=False,
        unit="score 0-1",
        priority=Priority.CRITICAL,
        rationale="Filtrar por producto, versión, tipo de doc es obligatorio"
    ),
    CriterionConfig(
        name="hybrid_search",
        weight=2.0,
        min_acceptable=0.0,
        ideal_value=1.0,
        lower_is_better=False,
        unit="score 0-1",
        priority=Priority.NICE_TO_HAVE,
        rationale="Deseable para queries con nombres de API exactos"
    ),
    CriterionConfig(
        name="community_ecosystem",
        weight=2.0,
        min_acceptable=0.4,
        ideal_value=0.9,
        lower_is_better=False,
        unit="score 0-1",
        priority=Priority.IMPORTANT,
        rationale="LangChain integration + respuestas en SO cuando tengamos issues"
    ),
]

print("=== Criterios definidos ===")
print(f"{'Criterio':<25} {'Peso':<6} {'Prioridad':<15} {'Dirección':<15}")
print("-" * 65)
for c in criteria_config:
    direction = "↓ menor mejor" if c.lower_is_better else "↑ mayor mejor"
    print(f"{c.name:<25} {c.weight:<6.1f} {c.priority.value:<15} {direction}")

total_weight = sum(c.weight for c in criteria_config)
print(f"\nPeso total: {total_weight}")

Paso 2: Recolectar datos de proveedores

Los raw scores se basan en documentación oficial, benchmarks públicos y experiencia del PoC:

provider_data = {
    "Pinecone": {
        "latency_p95": 120,
        "scale_vectors": 10_000_000,
        "ops_simplicity": 2,           # h/mes — fully managed
        "monthly_cost": 350,           # Starter tier para 450K vectors 1536d
        "sdk_quality": 0.85,
        "metadata_filtering": 0.95,    # Nativo, robusto, operadores completos
        "hybrid_search": 0.9,          # Sparse-dense nativo
        "community_ecosystem": 0.9,    # LangChain, LlamaIndex, amplia comunidad
        "notes": "Managed puro. Excelente DX. Costo escala con vectores.",
        "evidence": {
            "latency": "ANN-benchmarks + docs oficiales",
            "cost": "Pricing page, calculator para 450K 1536d",
            "ops": "Fully managed, no requiere infra",
            "sdk": "Probado en PoC interno + docs review"
        }
    },
    "Qdrant Cloud": {
        "latency_p95": 140,
        "scale_vectors": 50_000_000,
        "ops_simplicity": 3,           # h/mes — managed con config
        "monthly_cost": 180,           # Cloud tier para 450K vectors
        "sdk_quality": 0.80,
        "metadata_filtering": 0.90,    # Payload filtering robusto
        "hybrid_search": 0.85,         # Sparse vectors + fusion
        "community_ecosystem": 0.75,   # Creciendo rápido, LangChain ok
        "notes": "Buen balance managed/control. Rust engine, rápido.",
        "evidence": {
            "latency": "ANN-benchmarks, community reports",
            "cost": "Qdrant Cloud pricing page",
            "ops": "Dashboard + API, mínima config",
            "sdk": "qdrant-client Python review"
        }
    },
    "Weaviate Cloud": {
        "latency_p95": 180,
        "scale_vectors": 20_000_000,
        "ops_simplicity": 3,
        "monthly_cost": 280,
        "sdk_quality": 0.75,
        "metadata_filtering": 0.85,
        "hybrid_search": 0.95,         # BM25 + vector nativo, mejor hybrid
        "community_ecosystem": 0.80,
        "notes": "Mejor hybrid search nativo. Módulos de vectorización integrados.",
        "evidence": {
            "latency": "Weaviate benchmarks blog + community",
            "cost": "Weaviate pricing, serverless tier",
            "ops": "WCD dashboard, auto-scaling",
            "sdk": "weaviate-client Python v4 review"
        }
    },
    "Milvus (Zilliz Cloud)": {
        "latency_p95": 100,
        "scale_vectors": 100_000_000,
        "ops_simplicity": 5,           # Zilliz Cloud managed
        "monthly_cost": 220,
        "sdk_quality": 0.70,
        "metadata_filtering": 0.80,
        "hybrid_search": 0.75,
        "community_ecosystem": 0.70,
        "notes": "Máxima escala. SDK Python menos pulido que competencia.",
        "evidence": {
            "latency": "ANN-benchmarks (top performer)",
            "cost": "Zilliz Cloud pricing",
            "ops": "Zilliz managed, configuración moderada",
            "sdk": "pymilvus review, docs review"
        }
    },
    "ChromaDB": {
        "latency_p95": 350,
        "scale_vectors": 500_000,
        "ops_simplicity": 12,          # Self-hosted, sin managed maduro
        "monthly_cost": 60,            # Solo costo de VM
        "sdk_quality": 0.88,
        "metadata_filtering": 0.75,    # Básico, operadores limitados
        "hybrid_search": 0.0,          # No soporta hybrid nativo
        "community_ecosystem": 0.85,   # LangChain first-class, gran comunidad
        "notes": "Mejor DX para prototyping. Escala limitada. Sin hybrid.",
        "evidence": {
            "latency": "PoC interno (3 meses atrás)",
            "cost": "Open source + t3.medium AWS",
            "ops": "Experiencia propia: backups manuales, monitoring custom",
            "sdk": "Experiencia directa del equipo"
        }
    }
}

print(f"Proveedores a evaluar: {len(provider_data)}")
for name, data in provider_data.items():
    print(f"\n  {name}:")
    print(f"    {data['notes']}")

Paso 3: Normalización y scoring

def normalize_score(raw_value, min_acceptable, ideal_value, lower_is_better=True):
    """Normaliza un valor raw a escala 0-1."""
    if lower_is_better:
        if raw_value <= ideal_value:
            return 1.0
        elif raw_value >= min_acceptable:
            return 0.25
        else:
            range_size = min_acceptable - ideal_value
            if range_size == 0:
                return 1.0
            return max(0.0, min(1.0, round(1.0 - ((raw_value - ideal_value) / range_size), 2)))
    else:
        if raw_value >= ideal_value:
            return 1.0
        elif raw_value <= min_acceptable:
            return 0.25
        else:
            range_size = ideal_value - min_acceptable
            if range_size == 0:
                return 1.0
            return max(0.0, min(1.0, round((raw_value - min_acceptable) / range_size, 2)))


def run_evaluation(criteria: list, providers: dict) -> dict:
    """Ejecuta la evaluación completa."""
    results = {}

    for provider_name, raw_data in providers.items():
        scores = {}
        weighted_scores = {}
        total_weighted = 0
        max_weighted = 0

        for criterion in criteria:
            raw = raw_data.get(criterion.name, 0)
            normalized = normalize_score(
                raw, criterion.min_acceptable,
                criterion.ideal_value, criterion.lower_is_better
            )
            weighted = normalized * criterion.weight

            scores[criterion.name] = {
                "raw": raw,
                "normalized": normalized,
                "weighted": round(weighted, 2)
            }
            total_weighted += weighted
            max_weighted += criterion.weight

        percentage = round((total_weighted / max_weighted) * 100, 1)

        results[provider_name] = {
            "scores": scores,
            "total_weighted": round(total_weighted, 2),
            "max_possible": max_weighted,
            "percentage": percentage
        }

    return results


results = run_evaluation(criteria_config, provider_data)

# Mostrar tabla de resultados
print("\n" + "=" * 100)
print("MATRIZ DE DECISIÓN — DocBot RAG Chatbot")
print("=" * 100)

header = f"{'Criterio':<25} {'Peso':<5}"
for name in provider_data:
    header += f" {name:<16}"
print(header)
print("-" * 100)

for criterion in criteria_config:
    row = f"{criterion.name:<25} {criterion.weight:<5.1f}"
    for provider_name in provider_data:
        data = results[provider_name]["scores"][criterion.name]
        row += f" {data['raw']:>6}{data['normalized']:.2f}  "
    print(row)

print("-" * 100)
total_row = f"{'SCORE FINAL':<25} {'':5}"
for provider_name in provider_data:
    pct = results[provider_name]["percentage"]
    total_row += f" {pct:>12.1f}%    "
print(total_row)
print("=" * 100)

Paso 4: Ranking y análisis

def generate_ranking(results: dict) -> list[tuple]:
    """Genera ranking ordenado."""
    ranking = [
        (name, data["percentage"])
        for name, data in results.items()
    ]
    return sorted(ranking, key=lambda x: -x[1])


def gap_analysis(results: dict, criteria: list, provider_name: str) -> list[dict]:
    """Identifica dónde pierde más puntos un proveedor."""
    provider = results[provider_name]
    gaps = []

    for criterion in criteria:
        score_data = provider["scores"][criterion.name]
        if score_data["normalized"] < 0.75:
            lost = (1.0 - score_data["normalized"]) * criterion.weight
            gaps.append({
                "criterion": criterion.name,
                "normalized": score_data["normalized"],
                "raw": score_data["raw"],
                "weight": criterion.weight,
                "points_lost": round(lost, 2),
                "unit": criterion.unit
            })

    return sorted(gaps, key=lambda g: -g["points_lost"])


ranking = generate_ranking(results)

print("\n=== RANKING FINAL ===\n")
for i, (name, pct) in enumerate(ranking, 1):
    if pct >= 80:
        verdict = "✅ Excelente fit"
    elif pct >= 65:
        verdict = "🟡 Buen fit con trade-offs"
    elif pct >= 50:
        verdict = "🟠 Fit parcial"
    else:
        verdict = "🔴 No recomendado"
    print(f"  #{i} {name:<25} {pct:>6.1f}%  {verdict}")

# Gap analysis del top 2
print("\n=== GAP ANALYSIS ===")
for name, _ in ranking[:2]:
    gaps = gap_analysis(results, criteria_config, name)
    print(f"\n  {name} — Áreas de mejora:")
    if not gaps:
        print("    Sin gaps significativos")
    for gap in gaps:
        print(f"    ⚠️ {gap['criterion']}: score {gap['normalized']:.2f} "
              f"(raw: {gap['raw']} {gap['unit']}) — pierde {gap['points_lost']} pts")

Paso 5: Comparación head-to-head del top 2

def head_to_head(results: dict, criteria: list, provider_a: str, provider_b: str):
    """Comparación directa entre dos proveedores."""
    print(f"\n{'=' * 70}")
    print(f"HEAD TO HEAD: {provider_a} vs {provider_b}")
    print(f"{'=' * 70}\n")

    a_wins = 0
    b_wins = 0
    ties = 0

    print(f"{'Criterio':<25} {'Peso':<5} {provider_a:<15} {provider_b:<15} {'Ganador':<15}")
    print("-" * 75)

    for criterion in criteria:
        a_score = results[provider_a]["scores"][criterion.name]["normalized"]
        b_score = results[provider_b]["scores"][criterion.name]["normalized"]

        if a_score > b_score:
            winner = provider_a
            a_wins += 1
        elif b_score > a_score:
            winner = provider_b
            b_wins += 1
        else:
            winner = "Empate"
            ties += 1

        print(f"{criterion.name:<25} {criterion.weight:<5.1f} "
              f"{a_score:<15.2f} {b_score:<15.2f} {winner}")

    print("-" * 75)
    a_pct = results[provider_a]["percentage"]
    b_pct = results[provider_b]["percentage"]
    print(f"{'TOTAL':<25} {'':5} {a_pct:<15.1f} {b_pct:<15.1f}")
    print(f"\nVictorias por criterio: {provider_a}={a_wins}, {provider_b}={b_wins}, Empates={ties}")

    gap = abs(a_pct - b_pct)
    if gap < 3:
        print(f"⚠️ Diferencia de {gap:.1f}% — decisión FRÁGIL. Considera criterio de desempate.")
    elif gap < 8:
        print(f"🟡 Diferencia de {gap:.1f}% — ventaja moderada.")
    else:
        print(f"✅ Diferencia de {gap:.1f}% — ventaja clara.")


# Ejecutar head-to-head del top 2
top_2 = [name for name, _ in ranking[:2]]
head_to_head(results, criteria_config, top_2[0], top_2[1])

Paso 6: Análisis de sensibilidad

def sensitivity_analysis(criteria: list, providers: dict, criterion_name: str,
                         weight_range: list[float]) -> dict:
    """¿Cambia el ganador si ajusto el peso de un criterio?"""
    original_weight = None
    target = None

    for c in criteria:
        if c.name == criterion_name:
            original_weight = c.weight
            target = c
            break

    analysis = {}
    for new_weight in weight_range:
        target.weight = new_weight
        temp_results = run_evaluation(criteria, providers)
        temp_ranking = generate_ranking(temp_results)
        analysis[new_weight] = temp_ranking

    target.weight = original_weight
    return analysis


print("\n=== ANÁLISIS DE SENSIBILIDAD ===\n")

sensitive_criteria = ["ops_simplicity", "monthly_cost", "latency_p95"]
for criterion_name in sensitive_criteria:
    analysis = sensitivity_analysis(
        criteria_config, provider_data,
        criterion_name, [1.0, 2.0, 3.0, 4.0, 5.0]
    )

    print(f"\nSensibilidad: '{criterion_name}'")
    for weight, ranking_list in analysis.items():
        leader = ranking_list[0]
        second = ranking_list[1]
        gap = leader[1] - second[1]
        stability = "🟢" if gap > 5 else "🟡" if gap > 2 else "🔴"
        print(f"  Peso {weight:.0f}: #{1} {leader[0]:<20} ({leader[1]:.1f}%) "
              f"vs #{2} {second[0]:<20} ({second[1]:.1f}%) {stability}")

Paso 7: Recomendación final documentada

def generate_recommendation(ranking: list, results: dict, criteria: list,
                            project_context: dict) -> str:
    """Genera documento de recomendación formal."""
    winner = ranking[0]
    alternative = ranking[1]

    lines = [
        "=" * 70,
        "RECOMENDACIÓN DE VECTOR DATABASE",
        f"Proyecto: {project_context['name']}",
        f"Fecha: 2025-03 (reevaluar: 2025-09)",
        "=" * 70,
        "",
        f"RECOMENDACIÓN PRINCIPAL: {winner[0]} ({winner[1]:.1f}%)",
        f"ALTERNATIVA: {alternative[0]} ({alternative[1]:.1f}%)",
        "",
        "--- JUSTIFICACIÓN ---",
    ]

    winner_gaps = gap_analysis(results, criteria, winner[0])
    alt_gaps = gap_analysis(results, criteria, alternative[0])

    lines.append(f"\n{winner[0]} gana porque:")
    winner_strengths = [
        c.name for c in criteria
        if results[winner[0]]["scores"][c.name]["normalized"] >= 0.75
    ]
    for s in winner_strengths[:4]:
        score = results[winner[0]]["scores"][s]["normalized"]
        lines.append(f"  ✅ {s}: {score:.2f}")

    if winner_gaps:
        lines.append(f"\nRiesgos de {winner[0]}:")
        for gap in winner_gaps[:3]:
            lines.append(f"  ⚠️ {gap['criterion']}: score {gap['normalized']:.2f} "
                         f"(raw: {gap['raw']} {gap['unit']})")

    lines.extend([
        "",
        "--- PLAN DE ACCIÓN ---",
        "1. PoC de 3 días con el proveedor recomendado",
        "2. Benchmark con dataset real (1000 docs del proyecto)",
        "3. Validar latencia p95 con carga simulada",
        "4. Revisar pricing para 450K vectores (proyección 12m)",
        "5. Decision final en sprint planning",
        "",
        "--- TRIGGERS DE REEVALUACIÓN ---",
        f"• Vectores superan {project_context['scale']['projected_vectors_12m']:,}",
        f"• Costo mensual supera ${project_context['requirements']['budget'].split('$')[1].split('/')[0]}",
        "• SOC 2 se vuelve obligatorio (Q3 roadmap)",
        "• Latencia p95 > 400ms sostenido por 1 semana",
        "• Nuevo proveedor disruptivo entra al mercado",
    ])

    return "\n".join(lines)


recommendation = generate_recommendation(ranking, results, criteria_config, project_context)
print(recommendation)

Paso completo: función all-in-one

Aquí tienes una función que ejecuta todo el proceso de principio a fin:

def full_evaluation_pipeline(project_name: str, criteria: list,
                             providers: dict) -> dict:
    """
    Pipeline completo de evaluación.
    Retorna resultados, ranking, gaps y recomendación.
    """
    # 1. Evaluación
    results = run_evaluation(criteria, providers)

    # 2. Ranking
    ranking = generate_ranking(results)

    # 3. Gap analysis para top 3
    gaps = {}
    for name, _ in ranking[:3]:
        gaps[name] = gap_analysis(results, criteria, name)

    # 4. Head-to-head del top 2
    top_2 = [name for name, _ in ranking[:2]]

    # 5. Resumen
    summary = {
        "project": project_name,
        "providers_evaluated": len(providers),
        "criteria_count": len(criteria),
        "total_weight": sum(c.weight for c in criteria),
        "ranking": ranking,
        "recommendation": ranking[0][0],
        "alternative": ranking[1][0],
        "gap_top_2": abs(ranking[0][1] - ranking[1][1]),
        "decision_stability": "robust" if abs(ranking[0][1] - ranking[1][1]) > 5 else "fragile",
        "gaps": gaps
    }

    return summary


pipeline_result = full_evaluation_pipeline(
    "DocBot RAG", criteria_config, provider_data
)

print(f"\nResumen del pipeline:")
print(f"  Recomendación: {pipeline_result['recommendation']}")
print(f"  Alternativa: {pipeline_result['alternative']}")
print(f"  Gap: {pipeline_result['gap_top_2']:.1f}%")
print(f"  Estabilidad: {pipeline_result['decision_stability']}")

Template reutilizable

Cada vez que necesites evaluar un nuevo proveedor o reevaluar tu stack:

EVALUATION_TEMPLATE = """
# Evaluación de Vector Database
# Fecha: {date}
# Proyecto: {project}
# Evaluador: {evaluator}

## 1. Contexto
- Equipo: {team_size} personas, DevOps: {devops}
- Vectores: {current_vectors:,} → {projected_vectors:,} (12m)
- Budget: ${budget}/mes
- Timeline: {timeline}

## 2. Criterios (del Requirements Document)
{criteria_table}

## 3. Proveedores evaluados
{providers_list}

## 4. Resultados
{results_table}

## 5. Recomendación
- Principal: {recommendation}
- Alternativa: {alternative}
- Justificación: {justification}

## 6. Triggers de reevaluación
{triggers}

## 7. Firma
- Decisión tomada por: {decision_makers}
- Fecha de reevaluación programada: {review_date}
"""

def generate_template(context: dict) -> str:
    """Genera el documento de evaluación a partir del template."""
    return EVALUATION_TEMPLATE.format(**context)

🔧 Troubleshooting

Problema 1: "La matriz se vuelve muy compleja"

Síntoma: Más de 10 criterios, 6+ proveedores, la tabla no cabe en ninguna pantalla.

Solución: Aplica eliminación por fases. Primero filtra con criterios eliminatorios (reduce proveedores). Luego limita a 6 criterios ponderados. Si aún tienes 5+ proveedores, haz un primer corte con 3 criterios principales y luego evalúa top 3 con la matriz completa.

Problema 2: "No sabemos puntuar objetivamente"

Síntoma: Los scores se basan en "creo que..." en lugar de evidencia.

Solución: Define la fuente de evidencia para cada score: benchmark público (ANN-benchmarks), documentación oficial (pricing, features), PoC propio (mediciones reales). Marca cada score con su fuente. Si no hay fuente → score provisional con ⚠️.

Problema 3: "El negocio cambió a mitad del análisis"

Síntoma: Empezaste con "MVP sin compliance" y ahora piden SOC 2.

Solución: No rehagas toda la evaluación. Actualiza solo los pesos y umbrales afectados. Si el cambio agrega un criterio eliminatorio (como SOC 2), re-filtra proveedores primero y luego recalcula scores solo para los que pasan.

Problema 4: "Dos proveedores están empatados técnicamente"

Síntoma: Diferencia < 3% entre el #1 y #2.

Solución: Un empate técnico no se resuelve con más decimales. Usa criterios de desempate en este orden: (1) simplicidad operativa, (2) experiencia previa del equipo, (3) velocidad de setup para PoC. Si todo sigue empatado, elige el que tenga menor costo de migración futura.

Problema 5: "Un stakeholder quiere overridear la matriz"

Síntoma: El CTO dice "usemos X porque lo conozco" ignorando los scores.

Solución: El override es válido pero debe documentarse. Crea una sección "Override justification" en el documento de decisión que explique por qué la experiencia/relación/strategic fit supera el scoring. Esto protege al equipo si la decisión resulta ser incorrecta.


🏋️ Ejercicios

Ejercicio 1: Evaluación para un escenario diferente

Cambia el contexto: ahora eres una empresa enterprise con 50 developers, DevOps dedicado, 5M vectores actuales, budget de $5K/mes y SOC 2 obligatorio. ¿Cómo cambian los pesos? ¿Cambia el ganador?

Solución
enterprise_criteria = [
    CriterionConfig("latency_p95", weight=5.0, min_acceptable=300,
                    ideal_value=50, lower_is_better=True, unit="ms"),
    CriterionConfig("scale_vectors", weight=5.0, min_acceptable=10_000_000,
                    ideal_value=100_000_000, lower_is_better=False, unit="vectors"),
    CriterionConfig("ops_simplicity", weight=2.0, min_acceptable=25,
                    ideal_value=5, lower_is_better=True, unit="hours/month"),
    CriterionConfig("monthly_cost", weight=3.0, min_acceptable=5000,
                    ideal_value=1000, lower_is_better=True, unit="USD"),
    CriterionConfig("sdk_quality", weight=2.0, min_acceptable=0.5,
                    ideal_value=0.9, lower_is_better=False, unit="score"),
    CriterionConfig("metadata_filtering", weight=4.0, min_acceptable=0.7,
                    ideal_value=1.0, lower_is_better=False, unit="score"),
    CriterionConfig("hybrid_search", weight=3.0, min_acceptable=0.5,
                    ideal_value=1.0, lower_is_better=False, unit="score"),
    CriterionConfig("community_ecosystem", weight=2.0, min_acceptable=0.5,
                    ideal_value=0.9, lower_is_better=False, unit="score"),
]

enterprise_results = run_evaluation(enterprise_criteria, provider_data)
enterprise_ranking = generate_ranking(enterprise_results)

print("=== Enterprise Ranking ===")
for i, (name, pct) in enumerate(enterprise_ranking, 1):
    print(f"  #{i} {name}: {pct:.1f}%")

# Con enterprise: scale domina → Milvus/Qdrant suben
# ops_simplicity baja → ChromaDB self-hosted ya no penaliza tanto
# hybrid_search sube → Weaviate se beneficia

Ejercicio 2: Agrega un proveedor nuevo

pgvector (PostgreSQL con extensión vector) acaba de entrar en tu radar. Investiga y agrega sus raw scores a la evaluación. ¿Dónde queda en el ranking?

Solución
provider_data["pgvector"] = {
    "latency_p95": 250,             # Depende de PostgreSQL tuning
    "scale_vectors": 2_000_000,     # Limitado por RAM/configuración
    "ops_simplicity": 8,            # Si ya tienes PostgreSQL, menos overhead
    "monthly_cost": 80,             # Solo costo del PostgreSQL (ya lo tienes)
    "sdk_quality": 0.65,            # SQLAlchemy + pgvector, no SDK dedicado
    "metadata_filtering": 0.70,     # SQL WHERE — poderoso pero diferente
    "hybrid_search": 0.6,           # pg_trgm + vector, manual pero funcional
    "community_ecosystem": 0.60,    # PostgreSQL enorme, pgvector específico es menor
    "notes": "Si ya tienes PostgreSQL, es la opción de menor fricción. "
             "No es un vector DB dedicado, pero suficiente para muchos casos.",
    "evidence": {
        "latency": "Benchmarks community pgvector vs dedicated",
        "cost": "Ya pagas PostgreSQL — costo incremental mínimo",
        "ops": "Tu DBA ya opera PostgreSQL",
        "sdk": "No hay SDK dedicado — usas SQL + extensión"
    }
}

updated_results = run_evaluation(criteria_config, provider_data)
updated_ranking = generate_ranking(updated_results)

print("=== Ranking con pgvector ===")
for i, (name, pct) in enumerate(updated_ranking, 1):
    print(f"  #{i} {name}: {pct:.1f}%")

# pgvector típicamente queda en medio: bajo costo pero
# ops/latencia/features no compiten con dedicated vector DBs
# Excepto si ya tienes PostgreSQL en producción y quieres minimizar stack

del provider_data["pgvector"]  # limpiar para siguientes ejercicios

Ejercicio 3: Simulación de falla de criterio eliminatorio

SOC 2 acaba de volverse obligatorio (antes era nice-to-have). Agrega un criterio eliminatorio y re-evalúa. ¿Cuántos proveedores sobreviven?

Solución
soc2_compliance = {
    "Pinecone": True,            # SOC 2 Type II certified
    "Qdrant Cloud": True,        # SOC 2 certified
    "Weaviate Cloud": True,      # SOC 2 in progress / available
    "Milvus (Zilliz Cloud)": True, # Zilliz: SOC 2 certified
    "ChromaDB": False,           # Open source, sin certificaciones
}

print("=== Filtro eliminatorio: SOC 2 ===\n")
surviving = []
for provider, compliant in soc2_compliance.items():
    status = "✅ PASA" if compliant else "❌ ELIMINADO"
    print(f"  {provider}: {status}")
    if compliant:
        surviving.append(provider)

print(f"\nSobreviven: {len(surviving)}/{len(soc2_compliance)}")
print(f"Eliminados: ChromaDB (open source sin SOC 2)")

# Re-evaluar solo los sobrevivientes
filtered_providers = {k: v for k, v in provider_data.items() if k in surviving}
filtered_results = run_evaluation(criteria_config, filtered_providers)
filtered_ranking = generate_ranking(filtered_results)

print("\n=== Ranking post-eliminatorio ===")
for i, (name, pct) in enumerate(filtered_ranking, 1):
    print(f"  #{i} {name}: {pct:.1f}%")

Ejercicio 4: Documento de decisión completo

Genera el documento de decisión completo para el caso DocBot usando el template. Incluye contexto, criterios, resultados, recomendación y triggers de reevaluación.

Solución
decision_document = {
    "date": "2025-03-15",
    "project": "DocBot — RAG Chatbot para docs técnicas",
    "evaluator": "Backend Team",
    "team_size": 5,
    "devops": "No dedicado",
    "current_vectors": 75_000,
    "projected_vectors": 450_000,
    "budget": 400,
    "timeline": "6 semanas a producción",
    "criteria_table": "\n".join([
        f"| {c.name} | {c.weight} | {c.priority.value} | {c.unit} |"
        for c in criteria_config
    ]),
    "providers_list": ", ".join(provider_data.keys()),
    "results_table": "\n".join([
        f"| #{i} | {name} | {pct:.1f}% |"
        for i, (name, pct) in enumerate(ranking, 1)
    ]),
    "recommendation": ranking[0][0],
    "alternative": ranking[1][0],
    "justification": (
        f"{ranking[0][0]} gana con {ranking[0][1]:.1f}% vs "
        f"{ranking[1][0]} con {ranking[1][1]:.1f}%. "
        f"Diferencia de {abs(ranking[0][1] - ranking[1][1]):.1f}%. "
        f"{'Decisión robusta.' if abs(ranking[0][1] - ranking[1][1]) > 5 else 'Decisión frágil — validar con PoC.'}"
    ),
    "triggers": (
        "- Vectores > 500K\n"
        "- Costo > $400/mes\n"
        "- SOC 2 obligatorio\n"
        "- p95 > 400ms sostenido\n"
        "- Nuevo release mayor del proveedor alternativo"
    ),
    "decision_makers": "CTO + Backend Lead",
    "review_date": "2025-09-15 (6 meses)"
}

doc = generate_template(decision_document)
print(doc)

Ejercicio 5: Reevaluación trimestral

Han pasado 3 meses desde la decisión. Los vectores crecieron a 200K, el costo subió a $280/mes y el equipo reporta 4h/mes de mantenimiento. ¿La decisión original sigue siendo válida? Re-ejecuta la evaluación con datos actualizados.

Solución
# Datos actualizados a 3 meses
updated_context = {
    "vectors_now": 200_000,
    "vectors_projected_9m": 350_000,   # ajuste de proyección
    "actual_cost": 280,
    "actual_ops_hours": 4,
    "actual_latency_p95": 160,
    "issues_reported": 1,              # un incidente menor
}

print("=== Reevaluación Trimestral ===\n")

# ¿Algún trigger se activó?
triggers_check = {
    "Vectores > 500K": updated_context["vectors_now"] > 500_000,
    "Costo > $400/mes": updated_context["actual_cost"] > 400,
    "SOC 2 obligatorio": False,  # aún no
    "p95 > 400ms": updated_context["actual_latency_p95"] > 400,
}

print("Triggers de reevaluación:")
any_triggered = False
for trigger, activated in triggers_check.items():
    status = "🔴 ACTIVADO" if activated else "🟢 OK"
    print(f"  {trigger}: {status}")
    if activated:
        any_triggered = True

if not any_triggered:
    print("\n✅ Ningún trigger activado. Decisión original sigue válida.")
    print("Próxima reevaluación programada: +3 meses")
else:
    print("\n⚠️ Trigger activado. Iniciar proceso de reevaluación.")

# Comparar métricas esperadas vs reales
print("\n=== Métricas: esperado vs real ===")
comparisons = [
    ("Latencia p95", "< 300ms", f"{updated_context['actual_latency_p95']}ms", "✅"),
    ("Costo mensual", "< $400/mes", f"${updated_context['actual_cost']}/mes", "✅"),
    ("Ops horas", "< 5h/mes", f"{updated_context['actual_ops_hours']}h/mes", "✅"),
    ("Incidentes", "< 2/mes", f"{updated_context['issues_reported']}/mes", "✅"),
]
for metric, expected, actual, status in comparisons:
    print(f"  {metric}: esperado {expected}, real {actual} {status}")

🔗 Conexión con proyecto: Decision Questionnaire

Tu Decision Questionnaire implementará exactamente este pipeline. La diferencia es que en lugar de hardcodear los valores, tu cuestionario:

  1. Recolecta el contexto del proyecto interactivamente (equipo, budget, vectores)
  2. Genera los criterios basados en las respuestas (usando la lógica de cápsula 02)
  3. Asigna pesos guiando al usuario por la técnica de distribución fija
  4. Ejecuta el scoring con datos pre-cargados de proveedores
  5. Genera el reporte con ranking, gaps y recomendación

El full_evaluation_pipeline() de esta cápsula es el core de tu proyecto.


Resumen

  • El proceso completo tiene 7 pasos: contexto → criterios → pesos → datos → scoring → análisis → recomendación.
  • Los datos de proveedores necesitan fuentes. Benchmark, docs, PoC. Nunca marketing.
  • El gap analysis revela dónde están los riesgos de tu opción ganadora.
  • Head-to-head del top 2 es obligatorio: si están a < 3%, la decisión es frágil.
  • El análisis de sensibilidad te dice si tu decisión depende de un solo peso.
  • La recomendación incluye triggers de reevaluación. No es "para siempre".
  • Documenta TODO: criterios, pesos, scores, fuentes, justificación, override si aplica.
  • Reevalúa trimestralmente o cuando un trigger se active.

Recursos adicionales

  1. ANN Benchmarks — Benchmarks de performance objetivos para scoring
  2. Pinecone Pricing Calculator — Estimación de costos para scoring de costo
  3. Qdrant Cloud Pricing — Datos de pricing para evaluación
  4. Weaviate Pricing — Pricing y tiers de Weaviate
  5. Zilliz Cloud (Milvus) — Pricing de Milvus managed
  6. pgvector GitHub — Alternativa PostgreSQL para evaluación
  7. VectorDBBench — Benchmark open source para comparar vector DBs
  8. Decision Matrix Template (Notion) — Template visual de matriz de decisión

Tiempo estimado: 30-40 minutos Siguiente: 05-analisis-costos-roi.md