Módulo 6: Decision Matrix para AI Engineers
Cápsula 08: Proyecto - Cuestionario de Decisión
Descripción de la cápsula
Construirás un cuestionario interactivo en Python que recoge requisitos de un proyecto, aplica la matriz de scoring ponderado del módulo, y produce una recomendación justificada con alternativa, trade-offs explícitos y nivel de confianza. No es un formulario estático: es una herramienta ejecutable que cualquier AI engineer puede correr para tomar decisiones defendibles.
Tiempo estimado: 35-45 minutos
🎯 Objetivo del proyecto
- Implementar un cuestionario interactivo con 8 preguntas de opciones cerradas.
- Aplicar scoring ponderado con la fórmula
score_total = sum(peso * score_proveedor). - Producir recomendación principal + alternativa con justificación por criterio.
- Validar contra 3 escenarios distintos y generar reporte legible.
📋 Especificaciones del proyecto
Requisitos funcionales
- Presentar preguntas con opciones numeradas y capturar respuestas válidas.
- Mapear cada respuesta a criterios de la matriz con pesos configurables.
- Calcular score normalizado (0-100) para cada proveedor.
- Producir recomendación top 1 + alternativa top 2 con justificación y trade-offs.
Success Criteria
- ✅ Cubre 5 proveedores: ChromaDB, Pinecone, Weaviate, Qdrant, Milvus
- ✅ 8 preguntas con opciones cerradas (sin respuestas abiertas)
- ✅ 3 escenarios validados con resultado coherente
- ✅ Reporte legible sin contexto adicional
🧠 Contexto antes de empezar
Este proyecto sintetiza las cápsulas 02-07: criterios de decisión (pesos), scoring de la matriz (fórmula), matriz práctica (aplicación), análisis de costos (presupuesto), recomendación por escenarios (validación) y casos reales (contexto).
Diferencia con el Módulo 5: el decision tree usa lógica de ramificación (if/else); este cuestionario usa scoring aditivo ponderado donde todos los criterios contribuyen proporcionalmente al resultado final.
💻 Implementación paso a paso
Paso 1: Definir los criterios y pesos de la matriz
Los criterios vienen de la cápsula 02 y los pesos de la cápsula 03. Cada peso (1-5) refleja cuánto importa ese criterio en la decisión final.
from dataclasses import dataclass, field
CRITERIA_WEIGHTS = {
"scale": {"name": "Escala esperada", "weight": 5},
"latency": {"name": "Latencia objetivo", "weight": 5},
"ops_simplicity": {"name": "Simplicidad operativa", "weight": 4},
"cost_control": {"name": "Control de costos", "weight": 3},
"compliance": {"name": "Compliance y control de datos", "weight": 4},
"hybrid_search": {"name": "Búsqueda híbrida", "weight": 3},
"multi_tenancy": {"name": "Multi-tenancy", "weight": 2},
"ecosystem": {"name": "Ecosistema y madurez", "weight": 2},
}
¿Por qué este diseño? Los pesos están centralizados en un diccionario para poder ajustarlos sin tocar la lógica de scoring.
Paso 2: Definir perfiles de proveedores
Cada proveedor tiene un score por criterio usando la escala de la cápsula 03: 1.0 (cumple plenamente), 0.7 (cumple con trade-off), 0.4 (parcial), 0.1 (no recomendado).
@dataclass
class VectorDBProvider:
"""Perfil de scoring de un proveedor de vector database."""
name: str
scores: dict[str, float]
best_for: list[str] = field(default_factory=list)
risks: list[str] = field(default_factory=list)
typical_cost_range: str = ""
PROVIDERS = {
"ChromaDB": VectorDBProvider(
name="ChromaDB",
scores={"scale": 0.3, "latency": 0.7, "ops_simplicity": 1.0, "cost_control": 1.0,
"compliance": 0.1, "hybrid_search": 0.1, "multi_tenancy": 0.1, "ecosystem": 0.5},
best_for=["PoC y prototipos rápidos", "desarrollo local", "aprendizaje"],
risks=["no diseñado para producción a escala", "sin managed oficial", "features limitadas"],
typical_cost_range="$0 (open source)",
),
"Pinecone": VectorDBProvider(
name="Pinecone",
scores={"scale": 0.9, "latency": 0.9, "ops_simplicity": 1.0, "cost_control": 0.4,
"compliance": 0.7, "hybrid_search": 0.8, "multi_tenancy": 0.9, "ecosystem": 0.9},
best_for=["producción managed sin DevOps", "time-to-market rápido", "SLA exigente"],
risks=["vendor lock-in fuerte", "costos escalan con volumen", "sin self-hosted"],
typical_cost_range="$70-$500+/mes",
),
"Weaviate": VectorDBProvider(
name="Weaviate",
scores={"scale": 0.8, "latency": 0.7, "ops_simplicity": 0.6, "cost_control": 0.7,
"compliance": 0.8, "hybrid_search": 1.0, "multi_tenancy": 0.8, "ecosystem": 0.8},
best_for=["hybrid search avanzado", "flexibilidad managed + self-hosted"],
risks=["curva de aprendizaje mayor", "complejidad operativa en self-hosted"],
typical_cost_range="$25-$300+/mes o infra propia",
),
"Qdrant": VectorDBProvider(
name="Qdrant",
scores={"scale": 0.8, "latency": 0.9, "ops_simplicity": 0.7, "cost_control": 0.8,
"compliance": 0.8, "hybrid_search": 0.8, "multi_tenancy": 0.8, "ecosystem": 0.7},
best_for=["mejor ratio performance/costo", "control self-hosted con buena UX"],
risks=["ecosistema más joven", "self-hosted requiere disciplina operativa"],
typical_cost_range="$25-$200+/mes o infra propia",
),
"Milvus": VectorDBProvider(
name="Milvus",
scores={"scale": 1.0, "latency": 0.6, "ops_simplicity": 0.2, "cost_control": 0.5,
"compliance": 0.9, "hybrid_search": 0.8, "multi_tenancy": 0.9, "ecosystem": 0.7},
best_for=["escalas enterprise masivas (>10M)", "equipo con plataforma dedicada"],
risks=["overkill para proyectos pequeños", "complejidad operativa alta"],
typical_cost_range="$100-$1000+/mes o infra propia",
),
}
Nota: estos scores son estimaciones pedagógicas. En un proyecto real, valida contra benchmarks propios y pricing actualizado.
Paso 3: Definir las preguntas del cuestionario
Cada pregunta tiene opciones cerradas y cada opción ajusta los scores de los criterios. El mapeo entre respuesta y criterio es explícito.
@dataclass
class QuestionOption:
label: str
criteria_adjustments: dict[str, float]
@dataclass
class Question:
id: str
text: str
context: str
options: list[QuestionOption]
QUESTIONS = [
Question(
id="volume",
text="¿Cuántos vectores necesitas almacenar (proyección a 12 meses)?",
context="Incluye crecimiento esperado.",
options=[
QuestionOption("Menos de 500K vectores", {"scale": 0.3}),
QuestionOption("Entre 500K y 5M vectores", {"scale": 0.6}),
QuestionOption("Entre 5M y 50M vectores", {"scale": 0.85}),
QuestionOption("Más de 50M vectores", {"scale": 1.0}),
],
),
Question(
id="latency",
text="¿Cuál es tu requisito de latencia p95?",
context="p95 = el 95% de las consultas deben completarse en este tiempo.",
options=[
QuestionOption("Flexible (> 100ms está bien)", {"latency": 0.3}),
QuestionOption("Moderado (p95 < 50ms)", {"latency": 0.6}),
QuestionOption("Estricto (p95 < 20ms)", {"latency": 0.85}),
QuestionOption("Ultra-bajo (p95 < 10ms)", {"latency": 1.0}),
],
),
Question(
id="budget",
text="¿Cuál es tu presupuesto mensual para vector database?",
context="Incluye infra + servicio.",
options=[
QuestionOption("$0 - Solo opciones gratuitas/open source", {"cost_control": 1.0}),
QuestionOption("Hasta $200/mes", {"cost_control": 0.7}),
QuestionOption("$200-$1000/mes", {"cost_control": 0.4}),
QuestionOption("Más de $1000/mes", {"cost_control": 0.2}),
],
),
Question(
id="team",
text="¿Cuál es la capacidad operativa de tu equipo?",
context="DevOps = capacidad de administrar infra, monitoreo, backups.",
options=[
QuestionOption("Sin DevOps - necesito managed completo", {"ops_simplicity": 1.0}),
QuestionOption("Equipo pequeño con algo de experiencia en infra", {"ops_simplicity": 0.6}),
QuestionOption("Equipo con DevOps o plataforma dedicada", {"ops_simplicity": 0.3}),
],
),
Question(
id="compliance",
text="¿Qué nivel de compliance necesitas?",
context="Compliance = GDPR, HIPAA, SOC2, residencia de datos.",
options=[
QuestionOption("Ninguno - datos no sensibles", {"compliance": 0.1}),
QuestionOption("Básico - GDPR genérico, buenas prácticas", {"compliance": 0.5}),
QuestionOption("Estricto - regulación financiera/salud, residencia", {"compliance": 1.0}),
],
),
Question(
id="operation_model",
text="¿Prefieres managed (cloud del proveedor) o self-hosted?",
context="Managed = menos operación, más dependencia. Self-hosted = más control.",
options=[
QuestionOption("Managed obligatorio", {"ops_simplicity": 1.0, "cost_control": 0.4}),
QuestionOption("Self-hosted preferido", {"ops_simplicity": 0.3, "compliance": 0.9}),
QuestionOption("Flexible", {"ops_simplicity": 0.6, "cost_control": 0.6}),
],
),
Question(
id="hybrid_search",
text="¿Necesitas búsqueda híbrida (semántica + keyword)?",
context="Combina embeddings con filtros de texto exacto (BM25).",
options=[
QuestionOption("No - solo búsqueda semántica", {"hybrid_search": 0.1}),
QuestionOption("Sería útil pero no es crítico", {"hybrid_search": 0.5}),
QuestionOption("Sí - es requisito del producto", {"hybrid_search": 1.0}),
],
),
Question(
id="multi_tenancy",
text="¿Necesitas aislamiento multi-tenant?",
context="Multi-tenant = datos de distintos clientes aislados en la misma instancia.",
options=[
QuestionOption("No - un solo proyecto/cliente", {"multi_tenancy": 0.1}),
QuestionOption("Sí - múltiples clientes con aislamiento de datos", {"multi_tenancy": 1.0}),
],
),
]
¿Por qué opciones cerradas? Eliminan ambigüedad. Cada respuesta tiene un mapeo directo a la matriz, lo que hace el proceso repetible y auditable.
Paso 4: Motor de scoring ponderado
Aquí aplicas la fórmula de la cápsula 03: score_total = sum(peso * score_proveedor), normalizado a 0-100.
@dataclass
class ScoringResult:
provider_name: str
normalized_score: float
criteria_breakdown: dict[str, dict]
strengths: list[str]
weaknesses: list[str]
@dataclass
class QuestionnaireResult:
answers: dict[str, int]
requirement_profile: dict[str, float]
rankings: list[ScoringResult]
primary: ScoringResult
alternative: ScoringResult
confidence: str
warnings: list[str]
def calculate_requirement_profile(answers: dict[str, int]) -> dict[str, float]:
"""Convierte respuestas del cuestionario en perfil de requisitos (0.0-1.0 por criterio)."""
profile: dict[str, float] = {}
for question in QUESTIONS:
selected = question.options[answers[question.id]]
for criterion, value in selected.criteria_adjustments.items():
profile[criterion] = max(profile.get(criterion, 0), value)
for criterion in CRITERIA_WEIGHTS:
if criterion not in profile:
profile[criterion] = 0.5
return profile
def score_provider(provider: VectorDBProvider, profile: dict[str, float]) -> ScoringResult:
"""Calcula score de un proveedor contra el perfil de requisitos."""
raw_score = 0.0
max_possible = 0.0
breakdown = {}
strengths, weaknesses = [], []
for criterion, config in CRITERIA_WEIGHTS.items():
weight = config["weight"]
provider_score = provider.scores.get(criterion, 0.0)
req_level = profile.get(criterion, 0.5)
effective_weight = weight * (0.5 if req_level < 0.3 else req_level)
contribution = effective_weight * provider_score
raw_score += contribution
max_possible += effective_weight * 1.0
breakdown[criterion] = {
"name": config["name"], "weight": weight,
"provider_score": provider_score, "requirement_level": req_level,
"contribution": round(contribution, 2),
}
if provider_score >= 0.8 and req_level >= 0.6:
strengths.append(f"{config['name']}: score {provider_score}")
elif provider_score <= 0.3 and req_level >= 0.6:
weaknesses.append(f"{config['name']}: score {provider_score} — debilidad importante")
normalized = (raw_score / max_possible * 100) if max_possible > 0 else 0.0
return ScoringResult(provider.name, round(normalized, 1), breakdown, strengths, weaknesses)
def evaluate_all_providers(answers: dict[str, int]) -> QuestionnaireResult:
"""Evalúa todos los proveedores y produce resultado completo."""
profile = calculate_requirement_profile(answers)
warnings = _detect_warnings(profile)
results = [score_provider(p, profile) for p in PROVIDERS.values()]
results.sort(key=lambda r: r.normalized_score, reverse=True)
gap = results[0].normalized_score - results[1].normalized_score
confidence = "alta" if gap > 15 else ("media" if gap > 7 else "baja")
return QuestionnaireResult(
answers=answers, requirement_profile=profile,
rankings=results, primary=results[0], alternative=results[1],
confidence=confidence, warnings=warnings,
)
def _detect_warnings(profile: dict[str, float]) -> list[str]:
"""Detecta combinaciones contradictorias de requisitos."""
warnings = []
if profile.get("compliance", 0) >= 0.8 and profile.get("ops_simplicity", 0) >= 0.8:
warnings.append("Compliance estricto + managed puede ser contradictorio.")
if profile.get("scale", 0) >= 0.85 and profile.get("cost_control", 0) >= 0.7:
warnings.append("Escala alta + presupuesto ajustado es difícil de combinar.")
if profile.get("scale", 0) >= 0.85 and profile.get("ops_simplicity", 0) >= 0.8:
warnings.append("Escala alta con equipo sin DevOps es riesgo operativo.")
return warnings
Decisión de diseño: effective_weight ajusta el peso del criterio según cuánto importa al usuario. Si un criterio no es relevante (req_level < 0.3), su peso se reduce a la mitad — esto evita que un proveedor "gane" solo porque ignoras sus debilidades.
Paso 5: Interfaz interactiva del cuestionario
def run_interactive_questionnaire() -> dict[str, int]:
"""Ejecuta el cuestionario interactivo y retorna respuestas."""
print("=" * 65)
print(" CUESTIONARIO DE SELECCIÓN DE VECTOR DATABASE")
print(" Módulo 6 — Decision Matrix para AI Engineers")
print("=" * 65)
print("\nResponde seleccionando el número de la opción.\n")
answers: dict[str, int] = {}
for i, question in enumerate(QUESTIONS, 1):
print(f"─── Pregunta {i}/{len(QUESTIONS)} ───")
print(f"\n {question.text}")
print(f" ({question.context})\n")
for j, option in enumerate(question.options):
print(f" [{j + 1}] {option.label}")
while True:
try:
choice = int(input(f"\n Tu respuesta (1-{len(question.options)}): ").strip()) - 1
if 0 <= choice < len(question.options):
break
print(f" ⚠ Elige un número entre 1 y {len(question.options)}")
except ValueError:
print(" ⚠ Ingresa un número válido")
print(f" ✓ {question.options[choice].label}\n")
answers[question.id] = choice
return answers
Paso 6: Generador de reporte
El reporte debe ser legible sin contexto adicional. Un stakeholder debe entender la recomendación y su justificación.
def generate_report(result: QuestionnaireResult) -> str:
"""Genera reporte completo de la recomendación."""
lines = []
lines.append("=" * 65)
lines.append(" REPORTE DE RECOMENDACIÓN — VECTOR DATABASE")
lines.append("=" * 65)
# ── Perfil de requisitos ──
lines.append("\n📋 PERFIL DE REQUISITOS")
lines.append("-" * 45)
for criterion, level in sorted(result.requirement_profile.items(), key=lambda x: x[1], reverse=True):
name = CRITERIA_WEIGHTS.get(criterion, {}).get("name", criterion)
bar = "█" * int(level * 10) + "░" * (10 - int(level * 10))
lines.append(f" {name:<30} {bar} {level:.1f}")
if result.warnings:
lines.append(f"\n⚠️ ALERTAS ({len(result.warnings)})")
for w in result.warnings:
lines.append(f" ⚠ {w}")
# ── Ranking completo ──
lines.append("\n📊 RANKING DE PROVEEDORES")
lines.append(f" {'#':<4} {'Proveedor':<15} {'Score':>8} {'Ajuste':>10}")
lines.append(" " + "-" * 40)
for rank, r in enumerate(result.rankings, 1):
fit = "excelente" if r.normalized_score >= 80 else ("bueno" if r.normalized_score >= 65 else ("viable" if r.normalized_score >= 50 else "bajo"))
marker = " ← recomendado" if rank == 1 else ""
lines.append(f" {rank:<4} {r.provider_name:<15} {r.normalized_score:>7.1f}% {fit:>10}{marker}")
# ── Recomendación principal ──
prov = PROVIDERS[result.primary.provider_name]
lines.append(f"\n✅ RECOMENDACIÓN PRINCIPAL: {result.primary.provider_name}")
lines.append("-" * 45)
lines.append(f" Score: {result.primary.normalized_score:.1f}% | Confianza: {result.confidence}")
lines.append(f" Costo típico: {prov.typical_cost_range}")
lines.append("\n Fortalezas para tu caso:")
for s in (result.primary.strengths[:4] or ["Buen ajuste general sin fortalezas dominantes"]):
lines.append(f" ✓ {s}")
lines.append("\n Riesgos a considerar:")
for risk in prov.risks:
lines.append(f" ✗ {risk}")
# ── Alternativa ──
alt = PROVIDERS[result.alternative.provider_name]
lines.append(f"\n🔄 ALTERNATIVA: {result.alternative.provider_name}")
lines.append(f" Score: {result.alternative.normalized_score:.1f}% | Costo: {alt.typical_cost_range}")
lines.append(f"\n Preferir {result.alternative.provider_name} si:")
for c in _get_switch_conditions(result.primary.provider_name, result.alternative.provider_name):
lines.append(f" • {c}")
# ── Detalle de scoring top 2 ──
lines.append("\n📐 DETALLE DE SCORING — TOP 2")
lines.append(f" {'Criterio':<25} {'Peso':>5} {'P1':>5} {'P2':>5}")
lines.append(" " + "-" * 42)
for criterion, config in CRITERIA_WEIGHTS.items():
p1 = result.primary.criteria_breakdown[criterion]["provider_score"]
p2 = result.alternative.criteria_breakdown[criterion]["provider_score"]
lines.append(f" {config['name'][:24]:<25} {config['weight']:>4} {p1:>4.1f} {p2:>4.1f}")
lines.append(f" P1={result.primary.provider_name} | P2={result.alternative.provider_name}")
# ── Opciones descartadas ──
lines.append("\n❌ OPCIONES CON MENOR AJUSTE")
for r in result.rankings[2:]:
lines.append(f" {r.provider_name} ({r.normalized_score:.1f}%)")
for w in (r.weaknesses[:2] or ["Puntuó más bajo que las opciones principales"]):
lines.append(f" ✗ {w}")
# ── Próximos pasos ──
lines.append("\n📌 PRÓXIMOS PASOS")
if result.confidence == "baja":
lines.append(" 1. Opciones muy cercanas — PoC comparativo recomendado.")
lines.append(" 2. Define métricas de éxito antes de la prueba.")
elif result.confidence == "media":
lines.append(f" 1. Valida {result.primary.provider_name} con PoC (1-2 semanas).")
lines.append(f" 2. Ten {result.alternative.provider_name} como plan B documentado.")
else:
lines.append(f" 1. Procede con {result.primary.provider_name} para la fase actual.")
lines.append(" 2. Reevalúa en 3-6 meses o al cambiar escala 3x.")
lines.append("\n" + "=" * 65)
return "\n".join(lines)
def _get_switch_conditions(primary: str, alternative: str) -> list[str]:
"""Condiciones para preferir la alternativa sobre la principal."""
switch_map = {
("ChromaDB", "Qdrant"): ["necesitas pasar a producción con SLA", "el volumen supera 500K vectores"],
("Pinecone", "Qdrant"): ["buscas mejor ratio performance/costo", "quieres opción self-hosted"],
("Pinecone", "Weaviate"): ["hybrid search se vuelve requisito central", "necesitas self-hosted futura"],
("Qdrant", "Pinecone"): ["prefieres zero-ops sin DevOps", "necesitas SLA garantizado por contrato"],
("Qdrant", "Weaviate"): ["hybrid search maduro es prioritario", "necesitas ecosistema más amplio"],
("Weaviate", "Qdrant"): ["latencia p95 ultra-baja es prioritaria", "prefieres API más simple"],
("Milvus", "Weaviate"): ["la escala es menor a 10M vectores", "prefieres menor complejidad operativa"],
("Milvus", "Qdrant"): ["el volumen no justifica la complejidad", "tu equipo prefiere operación ligera"],
}
return switch_map.get((primary, alternative), ["cambian requisitos de escala o compliance", "el equipo adquiere capacidad operativa diferente"])
Paso 7: Escenarios de validación
Define 3 escenarios con respuestas predefinidas para verificar coherencia.
def run_validation_scenarios() -> list[dict]:
"""Ejecuta 3 escenarios de validación con respuestas predefinidas."""
scenarios = [
{"name": "Startup MVP — PoC rápido",
"description": "Equipo pequeño, sin presupuesto, desarrollo local",
"answers": {"volume": 0, "latency": 0, "budget": 0, "team": 0,
"compliance": 0, "operation_model": 0, "hybrid_search": 0, "multi_tenancy": 0},
"expected_primary": "ChromaDB"},
{"name": "SaaS en crecimiento — SLA exigente",
"description": "Producto validado, multi-tenant, equipo moderado",
"answers": {"volume": 1, "latency": 2, "budget": 2, "team": 1,
"compliance": 1, "operation_model": 0, "hybrid_search": 1, "multi_tenancy": 1},
"expected_primary": "Pinecone"},
{"name": "Enterprise — Compliance estricto",
"description": "Regulación financiera, self-hosted, equipo fuerte",
"answers": {"volume": 2, "latency": 2, "budget": 3, "team": 2,
"compliance": 2, "operation_model": 1, "hybrid_search": 2, "multi_tenancy": 1},
"expected_primary": "Qdrant"},
]
results = []
for s in scenarios:
qr = evaluate_all_providers(s["answers"])
results.append({
"name": s["name"], "description": s["description"],
"expected": s["expected_primary"], "actual": qr.primary.provider_name,
"score": qr.primary.normalized_score, "confidence": qr.confidence,
"match": qr.primary.provider_name == s["expected_primary"], "full_result": qr,
})
return results
Paso 8: Script principal
Junta todo en un flujo ejecutable con dos modos: interactivo y validación.
import sys
def main():
print("=" * 65)
print(" VECTOR DB DECISION QUESTIONNAIRE")
print(" Módulo 6 — Decision Matrix para AI Engineers")
print("=" * 65)
if len(sys.argv) > 1 and sys.argv[1] == "--validate":
run_validation_mode()
else:
run_interactive_mode()
def run_interactive_mode():
print("\n─── MODO: Cuestionario interactivo ───\n")
answers = run_interactive_questionnaire()
print("\n⏳ Calculando scoring ponderado...\n")
result = evaluate_all_providers(answers)
print(generate_report(result))
print("\n¿El resultado tiene sentido? Si no, revisa pesos o scores de proveedores.")
print("Corre --validate para verificar coherencia del motor.\n")
def run_validation_mode():
print("\n─── MODO: Validación de escenarios ───\n")
results = run_validation_scenarios()
print(f" {'Escenario':<35} {'Esperado':<12} {'Actual':<12} {'Score':>6} {'Conf':>8} {'OK':>4}")
print(" " + "-" * 78)
for r in results:
status = "✅" if r["match"] else "⚠️"
print(f" {r['name']:<35} {r['expected']:<12} {r['actual']:<12} "
f"{r['score']:>5.1f}% {r['confidence']:>8} {status:>4}")
passed = sum(1 for r in results if r["match"])
print(f"\n Resultado: {passed}/{len(results)} escenarios validados")
if passed < len(results):
print("\n ⚠️ Escenarios con discrepancia:")
for r in results:
if not r["match"]:
print(f" • {r['name']}: esperado {r['expected']}, obtuvo {r['actual']}")
print("\n─── DETALLE POR ESCENARIO ───")
for r in results:
print(f"\n{'─' * 65}")
print(generate_report(r["full_result"]))
print(f"\n{'=' * 65}")
print(f" VALIDACIÓN COMPLETADA — {len(PROVIDERS)} proveedores | {passed}/{len(results)} escenarios")
print(f"{'=' * 65}")
if __name__ == "__main__":
main()
📊 Output esperado
Al ejecutar con --validate, verás un reporte con esta estructura:
─── MODO: Validación de escenarios ───
Escenario Esperado Actual Score Conf OK
──────────────────────────────────────────────────────────────────────────────────
Startup MVP — PoC rápido ChromaDB ChromaDB 82.3% alta ✅
SaaS en crecimiento — SLA exigente Pinecone Pinecone 78.5% media ✅
Enterprise — Compliance estricto Qdrant Qdrant 76.1% media ✅
Resultado: 3/3 escenarios validados
✅ RECOMENDACIÓN PRINCIPAL: ChromaDB
─────────────────────────────────────────────
Score: 82.3% | Confianza: alta
Costo típico: $0 (open source)
Fortalezas para tu caso:
✓ Simplicidad operativa: score 1.0
✓ Control de costos: score 1.0
Riesgos a considerar:
✗ no diseñado para producción a escala
✗ sin managed oficial
🔄 ALTERNATIVA: Qdrant
Score: 68.7%
Preferir Qdrant si:
• necesitas pasar a producción con SLA
• el volumen supera 500K vectores
Entregables
Al terminar debes tener:
- Script ejecutable con cuestionario interactivo y modo validación (
--validate). - 3 escenarios validados con resultado coherente documentado.
- Reporte generado para al menos un escenario con justificación completa.
- Pesos y scores revisados — si ajustaste algún valor, documenta por qué.
🔧 Troubleshooting del proyecto
"El cuestionario recomienda lo mismo siempre"
Revisa dos cosas. Primero, verifica que tus escenarios de prueba tengan respuestas genuinamente distintas — si todos eligen opciones similares, el motor converge. Segundo, revisa CRITERIA_WEIGHTS: si un criterio tiene peso 5 y el proveedor lo cumple con 1.0, ese solo criterio puede dominar. Asegúrate de que ningún criterio individual represente más del 25% del score máximo.
"No sé cómo puntuar algunas respuestas"
Usa la escala de la cápsula 03: 1.0 = cumple plenamente, 0.7 = cumple con trade-off, 0.4 = parcial, 0.1 = no recomendado. Si no tienes datos, asigna 0.5 (neutro) y documenta que es un supuesto pendiente.
"Stakeholders no confían en la recomendación"
Comparte: (1) los pesos y de dónde vienen, (2) los scores por proveedor con fuente, (3) los escenarios de validación con resultados. Si un stakeholder cuestiona un peso, ajústalo juntos y re-ejecuta — el motor es determinístico.
"Dos proveedores empatan en score"
Esto es información, no un error. La confianza "baja" indica que necesitas más datos. Tres opciones: agrega un criterio diferenciador, ejecuta un PoC comparativo con ambos finalistas, o aplica la regla de la cápsula 06: ante empate, elige el más simple operativamente.
"Quiero cambiar los pesos según mi contexto"
Para eso está CRITERIA_WEIGHTS centralizado. Cambia los pesos, re-ejecuta --validate, y verifica que los escenarios sigan coherentes. Si un cambio de peso rompe un escenario, tienes un trade-off que documentar.
🏋️ Ejercicios post-proyecto
Ejercicio 1: Agregar criterio de vendor lock-in
Extiende el cuestionario con una pregunta sobre preocupación de portabilidad y un nuevo criterio en la matriz.
Pistas:
- Agrega una entrada
"lock_in"aCRITERIA_WEIGHTS. - Agrega scores de lock-in a cada
VectorDBProvider. - Crea una nueva
Questioncon opciones que mapeen a"lock_in".
Ver solución
# 1. Agregar criterio:
CRITERIA_WEIGHTS["lock_in"] = {"name": "Riesgo de vendor lock-in", "weight": 3}
# 2. Agregar scores a cada proveedor (en su dict "scores"):
# ChromaDB: "lock_in": 1.0 (open source, sin lock-in)
# Pinecone: "lock_in": 0.2 (API propietaria, sin self-hosted)
# Weaviate: "lock_in": 0.8 (open source, API estándar)
# Qdrant: "lock_in": 0.8 (open source, gRPC estándar)
# Milvus: "lock_in": 0.8 (open source, API estándar)
# 3. Agregar pregunta a QUESTIONS:
Question(
id="lock_in",
text="¿Qué tan importante es evitar vendor lock-in?",
context="Lock-in = dificultad de migrar a otro proveedor en el futuro.",
options=[
QuestionOption("No me preocupa - priorizo velocidad", {"lock_in": 0.1}),
QuestionOption("Moderada - quiero opciones pero no urgente", {"lock_in": 0.5}),
QuestionOption("Alta - necesito poder migrar sin reescribir", {"lock_in": 1.0}),
],
)
# 4. Re-ejecutar --validate para verificar coherencia
Ejercicio 2: Exportar resultado a Markdown
Crea una función que convierta el QuestionnaireResult en un documento Markdown listo para compartir en un PR o documento de decisión.
Pistas:
- La función recibe un
QuestionnaireResulty retorna un string Markdown. - Usa tablas Markdown para el ranking y el detalle de scoring.
- Incluye sección de supuestos y fecha de vigencia.
Ver solución
from datetime import date
def export_to_markdown(result: QuestionnaireResult, project_name: str = "Proyecto") -> str:
"""Exporta el resultado del cuestionario a Markdown."""
lines = []
lines.append(f"# Decisión de Vector Database — {project_name}")
lines.append(f"\n**Fecha:** {date.today().isoformat()}")
lines.append(f"**Confianza:** {result.confidence}")
lines.append(f"**Vigencia sugerida:** 3 meses o hasta cambio de escala 2x")
lines.append("\n## Ranking\n")
lines.append("| # | Proveedor | Score | Ajuste |")
lines.append("|---|-----------|------:|--------|")
for rank, r in enumerate(result.rankings, 1):
fit = "excelente" if r.normalized_score >= 80 else ("bueno" if r.normalized_score >= 65 else "viable")
lines.append(f"| {rank} | {r.provider_name} | {r.normalized_score:.1f}% | {fit} |")
prov = PROVIDERS[result.primary.provider_name]
lines.append(f"\n## Recomendación: {result.primary.provider_name}\n")
lines.append(f"**Costo típico:** {prov.typical_cost_range}\n")
lines.append("### Fortalezas\n")
for s in result.primary.strengths:
lines.append(f"- {s}")
lines.append("\n### Riesgos\n")
for risk in prov.risks:
lines.append(f"- {risk}")
lines.append(f"\n## Alternativa: {result.alternative.provider_name}\n")
lines.append(f"Score: {result.alternative.normalized_score:.1f}%\n")
lines.append("Considerar si:")
for c in _get_switch_conditions(result.primary.provider_name, result.alternative.provider_name):
lines.append(f"- {c}")
lines.append("\n## Detalle de scoring\n")
lines.append("| Criterio | Peso | P1 | P2 |")
lines.append("|----------|-----:|---:|---:|")
for criterion, config in CRITERIA_WEIGHTS.items():
p1 = result.primary.criteria_breakdown[criterion]["provider_score"]
p2 = result.alternative.criteria_breakdown[criterion]["provider_score"]
lines.append(f"| {config['name']} | {config['weight']} | {p1:.1f} | {p2:.1f} |")
if result.warnings:
lines.append("\n## Alertas\n")
for w in result.warnings:
lines.append(f"- ⚠️ {w}")
lines.append("\n---\n*Generado con Vector DB Decision Questionnaire — Módulo 6*")
return "\n".join(lines)
# Uso:
# md = export_to_markdown(result, "Mi Proyecto RAG")
# with open("decision-vectordb.md", "w") as f:
# f.write(md)
Ejercicio 3: Análisis de sensibilidad de pesos
Crea una función que varíe cada peso de CRITERIA_WEIGHTS (±1) y muestre cuáles cambian la recomendación — identificando los criterios más sensibles.
Pistas:
- Clona
CRITERIA_WEIGHTS, modifica un peso a la vez, re-calcula scores. - Compara si el
primarycambia con cada variación. - Los criterios que cambian la recomendación son los más sensibles.
Ver solución
import copy
def sensitivity_analysis(answers: dict[str, int]) -> str:
"""Identifica qué pesos son más sensibles al resultado."""
global CRITERIA_WEIGHTS
base_result = evaluate_all_providers(answers)
base_primary = base_result.primary.provider_name
original_weights = copy.deepcopy(CRITERIA_WEIGHTS)
lines = []
lines.append("=" * 65)
lines.append(f" ANÁLISIS DE SENSIBILIDAD — Base: {base_primary} ({base_result.primary.normalized_score:.1f}%)")
lines.append("=" * 65)
sensitive = []
for criterion, config in original_weights.items():
original_w = config["weight"]
lines.append(f"\n📊 {config['name']} (peso base: {original_w})")
lines.append(f" {'Peso':<8} {'Recomendación':<15} {'Score':>7} {'Cambió?':>8}")
lines.append(" " + "-" * 42)
changed = False
for delta in [-2, -1, 0, 1, 2]:
new_w = max(1, min(5, original_w + delta))
CRITERIA_WEIGHTS[criterion]["weight"] = new_w
varied = evaluate_all_providers(answers)
is_diff = varied.primary.provider_name != base_primary
if is_diff:
changed = True
marker = "⚠️ SÍ" if is_diff else " no"
current = " ←" if delta == 0 else ""
lines.append(f" {new_w:<8} {varied.primary.provider_name:<15} "
f"{varied.primary.normalized_score:>6.1f}% {marker:>8}{current}")
CRITERIA_WEIGHTS[criterion]["weight"] = original_w
if changed:
sensitive.append(config["name"])
CRITERIA_WEIGHTS = original_weights
if sensitive:
lines.append(f"\n⚠️ Criterios sensibles:")
for c in sensitive:
lines.append(f" • {c}")
lines.append("Estos pesos debes estimar con mayor cuidado.")
else:
lines.append("\n✅ Decisión robusta — ningún peso individual cambia la recomendación.")
return "\n".join(lines)
# Uso:
# print(sensitivity_analysis(answers))
El output mostrará qué campos marcados con "SÍ" son los inputs más sensibles — los que debes estimar con mayor cuidado.
✅ Checklist de completitud
Estructura del código:
-
CRITERIA_WEIGHTStiene 8 criterios con pesos de 1 a 5. -
PROVIDERScubre 5 proveedores con scores por criterio. -
QUESTIONStiene 8 preguntas con opciones cerradas. - Cada opción mapea a criterios de la matriz.
Motor de scoring:
-
score_provider()aplicasum(peso * score)normalizada a 0-100. -
evaluate_all_providers()rankea proveedores y calcula confianza. -
_detect_warnings()identifica combinaciones contradictorias.
Calidad de la decisión:
- Justificación explica fortalezas (por qué sí) y riesgos (cuidado con).
- La alternativa incluye condiciones de cuándo preferirla.
- Detalle de scoring por criterio visible en el reporte.
Validación:
- 3 escenarios ejecutados (MVP, SaaS, Enterprise).
- Discrepancias entre esperado y actual explicadas.
-
--validateproduce output legible y coherente.
Output:
- Reporte legible sin contexto adicional.
- Ranking completo de 5 proveedores visible.
- Próximos pasos ajustados al nivel de confianza.
Resumen
- Construiste un cuestionario interactivo que transforma respuestas en scoring ponderado sobre 5 proveedores de vector database.
- Cada pregunta tiene opciones cerradas que mapean directamente a criterios de la matriz del módulo.
- El motor aplica la fórmula
score_total = sum(peso * score)con pesos ajustables y normalización a 0-100. - El reporte produce recomendación + alternativa con fortalezas, riesgos, detalle de scoring y condiciones de cambio.
- Validaste con 3 escenarios (MVP, SaaS, Enterprise) que cubren combinaciones representativas de requisitos.
- El nivel de confianza (alta/media/baja) indica cuándo la decisión es clara vs. cuándo necesitas más análisis.
- Los pesos centralizados permiten adaptar la herramienta sin modificar la lógica del motor.
- Este proyecto cierra el Módulo 6 y conecta con el Módulo 7 (consideraciones de producción).
📚 Recursos adicionales
- Decision Matrix — Wikipedia
- Weighted Sum Model — Wikipedia
- Pinecone — Documentación y pricing
- Weaviate — Documentación para developers
- Qdrant — Documentación técnica
- Milvus — Documentación y Zilliz Cloud
- ChromaDB — Documentación
- Martin Fowler — Trade-offs en decisiones técnicas
Tiempo estimado: 35-45 minutos
Siguiente módulo: ../../module-07-production-considerations/es/01-introduccion-modulo.md