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:
- Recolecta el contexto del proyecto interactivamente (equipo, budget, vectores)
- Genera los criterios basados en las respuestas (usando la lógica de cápsula 02)
- Asigna pesos guiando al usuario por la técnica de distribución fija
- Ejecuta el scoring con datos pre-cargados de proveedores
- 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
- ANN Benchmarks — Benchmarks de performance objetivos para scoring
- Pinecone Pricing Calculator — Estimación de costos para scoring de costo
- Qdrant Cloud Pricing — Datos de pricing para evaluación
- Weaviate Pricing — Pricing y tiers de Weaviate
- Zilliz Cloud (Milvus) — Pricing de Milvus managed
- pgvector GitHub — Alternativa PostgreSQL para evaluación
- VectorDBBench — Benchmark open source para comparar vector DBs
- Decision Matrix Template (Notion) — Template visual de matriz de decisión
Tiempo estimado: 30-40 minutos
Siguiente: 05-analisis-costos-roi.md