Módulo 5: ReAct, Self-Consistency y Patrones Avanzados
8. Proyecto: Multi-Strategy Problem Solver
Descripción
En este proyecto construirás un sistema completo que recibe cualquier tipo de problema, lo clasifica automáticamente, selecciona la técnica de prompt engineering más adecuada, ejecuta esa técnica, y opcionalmente compara los resultados de múltiples técnicas para validar la respuesta.
Este proyecto integra todo lo aprendido en el módulo: ReAct, Self-Consistency, Tree-of-Thought, Meta-prompting y Self-Refine.
Arquitectura del Sistema
MULTI-STRATEGY PROBLEM SOLVER
Input: Problema/Pregunta
│
▼
┌─────────────────┐
│ CLASIFICADOR │ → tipo: math/factual/reasoning/creative/code/planning
└────────┬────────┘
│
▼
┌─────────────────┐
│ ROUTER │ → selecciona técnica óptima según tipo + constraints
└────────┬────────┘
│
┌────────┴──────────────────────────────────┐
│ │
▼ ▼
Técnica A Técnica B (si comparación)
(principal) (alternativa)
│ │
└────────────────────┬──────────────────────┘
▼
┌──────────────┐
│ EVALUADOR │ → score calidad, confianza
└──────┬───────┘
│
▼
Output estructurado
{respuesta, tecnica_usada, confianza, metricas}
Implementación Completa
from openai import OpenAI
from collections import Counter
from dataclasses import dataclass, field
from typing import Optional, Any
import json
import re
import time
import math
client = OpenAI()
# ============================================================
# MODELOS DE DATOS
# ============================================================
@dataclass
class ProblemaClasificado:
texto: str
tipo: str # math, factual, reasoning, creative, code, planning, qa_realtime
complejidad: str # simple, media, alta
necesita_externos: bool
accuracy_critica: bool
tiene_respuesta_unica: bool # False para creative
@dataclass
class ResultadoTecnica:
tecnica: str
respuesta: str
tiempo_segundos: float
n_llamadas: int
confianza: float
metadata: dict = field(default_factory=dict)
@dataclass
class ResultadoFinal:
problema: str
clasificacion: ProblemaClasificado
resultado_principal: ResultadoTecnica
resultado_alternativo: Optional[ResultadoTecnica]
respuesta_consenso: str
metricas: dict
recomendacion_tecnica: str
# ============================================================
# MÓDULO 1: CLASIFICADOR
# ============================================================
def clasificar_problema(problema: str) -> ProblemaClasificado:
"""
Analiza un problema y lo clasifica para el routing.
Args:
problema: El texto del problema a clasificar
Returns:
ProblemaClasificado con todos los atributos necesarios para el routing
"""
prompt_clasificacion = f"""Analiza el siguiente problema y clasifícalo con precisión.
Problema: {problema}
Responde en JSON con este schema exacto:
{{
"tipo": "math|factual|reasoning|creative|code|planning|qa_realtime",
"complejidad": "simple|media|alta",
"necesita_externos": true|false,
"accuracy_critica": true|false,
"tiene_respuesta_unica": true|false,
"razon_tipo": "explicación en 10 palabras"
}}
Guía de tipos:
- math: operaciones numéricas, álgebra, cálculo, estadística
- factual: hechos que el modelo conoce del entrenamiento
- reasoning: deducciones lógicas, inferencias, multi-paso sin cálculo
- creative: escritura, generación, sin respuesta correcta única
- code: escribir, revisar, o depurar código
- planning: diseñar estrategias, arquitecturas, roadmaps
- qa_realtime: preguntas sobre datos actuales (precios, noticias, fechas)
Guía necesita_externos: true si la respuesta cambia con el tiempo o requiere APIs/BD
Guía accuracy_critica: true si errores tienen alto impacto (finanzas, medicina, legal)"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt_clasificacion}],
temperature=0,
response_format={"type": "json_object"}
)
try:
datos = json.loads(response.choices[0].message.content)
return ProblemaClasificado(
texto=problema,
tipo=datos.get("tipo", "reasoning"),
complejidad=datos.get("complejidad", "media"),
necesita_externos=datos.get("necesita_externos", False),
accuracy_critica=datos.get("accuracy_critica", False),
tiene_respuesta_unica=datos.get("tiene_respuesta_unica", True)
)
except Exception as e:
# Fallback conservador
return ProblemaClasificado(
texto=problema,
tipo="reasoning",
complejidad="media",
necesita_externos=False,
accuracy_critica=False,
tiene_respuesta_unica=True
)
# ============================================================
# MÓDULO 2: IMPLEMENTACIONES DE TÉCNICAS
# ============================================================
def _extraer_numero(texto: str) -> str:
"""Extrae el número final de un texto de razonamiento."""
patrones = [
r'(?:respuesta|answer|resultado)[:\s=]+(-?\d+\.?\d*)',
r'\*\*(-?\d+\.?\d*)\*\*',
r'=\s*(-?\d+\.?\d*)\s*$'
]
for p in patrones:
m = re.search(p, texto, re.IGNORECASE | re.MULTILINE)
if m:
return m.group(1)
nums = re.findall(r'-?\d+\.?\d*', texto)
return nums[-1] if nums else texto.strip()[-20:]
def ejecutar_zero_shot(problema: str) -> ResultadoTecnica:
"""Resolución directa sin técnica especial."""
t0 = time.time()
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": problema}],
temperature=0.3,
max_tokens=400
).choices[0].message.content
return ResultadoTecnica(
tecnica="zero_shot",
respuesta=resp,
tiempo_segundos=time.time() - t0,
n_llamadas=1,
confianza=0.7
)
def ejecutar_cot(problema: str) -> ResultadoTecnica:
"""Chain-of-Thought: razonamiento paso a paso."""
t0 = time.time()
prompt = f"""{problema}
Piensa paso a paso:
1. Identifica qué sabes y qué necesitas encontrar
2. Aplica el proceso correcto paso a paso
3. Verifica si tu respuesta tiene sentido
4. Escribe: "Respuesta final: [respuesta]"
"""
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=500
).choices[0].message.content
return ResultadoTecnica(
tecnica="chain_of_thought",
respuesta=resp,
tiempo_segundos=time.time() - t0,
n_llamadas=1,
confianza=0.82,
metadata={"tiene_razonamiento": True}
)
def ejecutar_self_consistency(problema: str, n: int = 5) -> ResultadoTecnica:
"""Self-Consistency: N respuestas con majority vote."""
t0 = time.time()
respuestas = []
for _ in range(n):
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{problema}\nPiensa paso a paso. Respuesta: [valor]"}],
temperature=0.7,
max_tokens=400
).choices[0].message.content
respuestas.append(_extraer_numero(resp))
conteo = Counter(respuestas)
ganadora, votos = conteo.most_common(1)[0]
confianza = votos / n
return ResultadoTecnica(
tecnica="self_consistency",
respuesta=ganadora,
tiempo_segundos=time.time() - t0,
n_llamadas=n,
confianza=confianza,
metadata={"n_muestras": n, "distribucion": dict(conteo), "votos_ganador": votos}
)
def ejecutar_react(problema: str) -> ResultadoTecnica:
"""
ReAct simplificado: razonamiento con herramientas de cálculo y búsqueda.
"""
t0 = time.time()
TOOLS = [
{
"type": "function",
"function": {
"name": "calcular",
"description": "Evalúa expresiones matemáticas. Usa para cálculos numéricos.",
"parameters": {
"type": "object",
"properties": {
"expresion": {"type": "string", "description": "Expresión a calcular"}
},
"required": ["expresion"]
}
}
},
{
"type": "function",
"function": {
"name": "obtener_dato",
"description": "Simula obtener un dato factual actual. En producción: llama API real.",
"parameters": {
"type": "object",
"properties": {
"consulta": {"type": "string"}
},
"required": ["consulta"]
}
}
}
]
messages = [
{
"role": "system",
"content": "Usa herramientas para obtener datos y calcular. No inventes números. Cuando tengas la respuesta, termina con 'Respuesta: [resultado]'"
},
{"role": "user", "content": problema}
]
n_llamadas = 1
herramientas_usadas = []
for _ in range(6):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
tool_choice="auto",
temperature=0
)
msg = response.choices[0].message
n_llamadas += 1
if not msg.tool_calls:
respuesta_final = msg.content or "Sin respuesta"
break
messages.append({
"role": "assistant",
"content": msg.content or "",
"tool_calls": [tc.model_dump() for tc in msg.tool_calls]
})
for tc in msg.tool_calls:
tool_name = tc.function.name
args = json.loads(tc.function.arguments)
herramientas_usadas.append(tool_name)
if tool_name == "calcular":
expr = args.get("expresion", "0")
try:
# Safe eval solo operaciones matemáticas
allowed = set("0123456789+-*/.() ")
if all(c in allowed for c in expr):
resultado = str(round(eval(expr, {"__builtins__": {}}, {"math": math}), 4))
else:
resultado = "Expresión no permitida"
except Exception as e:
resultado = f"Error: {e}"
else:
resultado = f"[Dato simulado para: {args.get('consulta', '')}]"
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": resultado
})
else:
respuesta_final = "Límite de pasos alcanzado"
return ResultadoTecnica(
tecnica="react",
respuesta=respuesta_final,
tiempo_segundos=time.time() - t0,
n_llamadas=n_llamadas,
confianza=0.85 if herramientas_usadas else 0.7,
metadata={"herramientas_usadas": herramientas_usadas}
)
def ejecutar_tot_simplificado(problema: str, breadth: int = 3) -> ResultadoTecnica:
"""Tree-of-Thought simplificado: generar enfoques, evaluar, continuar con el mejor."""
t0 = time.time()
n_llamadas = 0
# Generar enfoques
resp_enfoques = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"""Problema: {problema}
Genera {breadth} enfoques DISTINTOS para resolver este problema.
Cada uno debe ser una estrategia diferente.
Formato: "1. [enfoque]" por línea."""}],
temperature=0.8,
max_tokens=300
)
n_llamadas += 1
lineas = [l.lstrip('0123456789.-) ').strip()
for l in resp_enfoques.choices[0].message.content.split('\n')
if l.strip() and (l.strip()[0].isdigit() or l.strip()[0] == '-')]
enfoques = lineas[:breadth]
if not enfoques:
enfoques = ["Enfoque directo paso a paso"]
# Evaluar enfoques
mejores = []
for enfoque in enfoques:
eval_resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"Problema: {problema}\nEnfoque: {enfoque}\n¿Prometedor? Score 0.0-1.0. Solo el número."}],
temperature=0,
max_tokens=10
)
n_llamadas += 1
try:
score = float(eval_resp.choices[0].message.content.strip()[:4])
score = max(0.0, min(1.0, score))
except ValueError:
score = 0.5
mejores.append((score, enfoque))
mejor_enfoque = max(mejores, key=lambda x: x[0])[1]
# Resolver con el mejor enfoque
solucion = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"""Problema: {problema}
Empezando con: {mejor_enfoque}
Continúa paso a paso hasta la solución completa.
Al final: "Respuesta: [respuesta]"
"""}],
temperature=0,
max_tokens=500
)
n_llamadas += 1
return ResultadoTecnica(
tecnica="tree_of_thought",
respuesta=solucion.choices[0].message.content,
tiempo_segundos=time.time() - t0,
n_llamadas=n_llamadas,
confianza=0.78,
metadata={"mejor_enfoque": mejor_enfoque, "n_enfoques_evaluados": len(enfoques)}
)
def ejecutar_self_refine(problema: str, max_iter: int = 2) -> ResultadoTecnica:
"""Self-Refine: generar, criticar, mejorar."""
t0 = time.time()
n_llamadas = 0
# Generación inicial
resp_inicial = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": problema}],
temperature=0.3,
max_tokens=500
).choices[0].message.content
n_llamadas += 1
respuesta = resp_inicial
score_actual = 0.5
for _ in range(max_iter):
# Crítica
critica_resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"""Problema: {problema}
Respuesta propuesta: {respuesta}
Evalúa: ¿Hay errores? ¿Falta algo? ¿Se puede mejorar?
Responde en JSON: {{"score": 0.0-1.0, "mejoras": ["mejora1", "mejora2"], "puede_mejorar": true/false}}"""}],
temperature=0,
response_format={"type": "json_object"}
).choices[0].message.content
n_llamadas += 1
try:
critica = json.loads(critica_resp)
score = float(critica.get("score", 0.5))
mejoras = critica.get("mejoras", [])
puede_mejorar = critica.get("puede_mejorar", True)
except Exception:
break
if not puede_mejorar or score >= 0.88:
score_actual = score
break
# Refinamiento
mejoras_str = "\n".join([f"- {m}" for m in mejoras[:3]])
refinada = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"""Problema: {problema}
Respuesta anterior: {respuesta}
Mejoras requeridas:
{mejoras_str}
Genera respuesta mejorada incorporando todas las mejoras."""}],
temperature=0.2,
max_tokens=500
).choices[0].message.content
n_llamadas += 1
respuesta = refinada
score_actual = score
return ResultadoTecnica(
tecnica="self_refine",
respuesta=respuesta,
tiempo_segundos=time.time() - t0,
n_llamadas=n_llamadas,
confianza=min(score_actual, 0.95),
metadata={"iteraciones": max_iter}
)
# ============================================================
# MÓDULO 3: ROUTER
# ============================================================
def seleccionar_y_ejecutar(
clasificacion: ProblemaClasificado,
modo_comparacion: bool = False,
n_sc: int = 5
) -> tuple[ResultadoTecnica, Optional[ResultadoTecnica]]:
"""
Selecciona y ejecuta la técnica según la clasificación del problema.
Args:
clasificacion: Resultado del clasificador
modo_comparacion: Si True, también ejecuta una técnica alternativa para comparar
n_sc: Número de muestras para Self-Consistency
Returns:
Tupla (resultado_principal, resultado_alternativo)
"""
tipo = clasificacion.tipo
accuracy = clasificacion.accuracy_critica
externos = clasificacion.necesita_externos
complejidad = clasificacion.complejidad
tiene_unica = clasificacion.tiene_respuesta_unica
# Selección de técnica principal
if externos:
principal = ejecutar_react(clasificacion.texto)
elif tipo == "math" and accuracy and complejidad in ["media", "alta"]:
principal = ejecutar_self_consistency(clasificacion.texto, n=n_sc)
elif tipo == "math":
principal = ejecutar_cot(clasificacion.texto)
elif tipo in ["reasoning", "logico"] and complejidad == "alta":
principal = ejecutar_self_consistency(clasificacion.texto, n=3)
elif tipo in ["reasoning", "logico"]:
principal = ejecutar_cot(clasificacion.texto)
elif tipo == "planning" and complejidad == "alta":
principal = ejecutar_tot_simplificado(clasificacion.texto)
elif tipo == "code":
principal = ejecutar_self_refine(clasificacion.texto, max_iter=2)
elif tipo == "creative" or not tiene_unica:
principal = ejecutar_zero_shot(clasificacion.texto)
else:
# factual, qa_realtime, default
principal = ejecutar_cot(clasificacion.texto) if externos else ejecutar_zero_shot(clasificacion.texto)
# Técnica alternativa para comparación
alternativa = None
if modo_comparacion:
if principal.tecnica == "chain_of_thought":
alternativa = ejecutar_self_consistency(clasificacion.texto, n=3)
elif principal.tecnica == "self_consistency":
alternativa = ejecutar_cot(clasificacion.texto)
elif principal.tecnica == "tree_of_thought":
alternativa = ejecutar_cot(clasificacion.texto)
elif principal.tecnica == "self_refine":
alternativa = ejecutar_cot(clasificacion.texto)
return principal, alternativa
# ============================================================
# MÓDULO 4: EVALUADOR Y CONSENSO
# ============================================================
def evaluar_calidad_respuesta(problema: str, respuesta: str) -> dict:
"""
Evalúa la calidad de una respuesta sin conocer la respuesta correcta.
"""
prompt = f"""Evalúa esta respuesta:
Problema: {problema}
Respuesta: {respuesta}
Responde en JSON:
{{
"score_completitud": 0.0-1.0,
"score_precision": 0.0-1.0,
"score_claridad": 0.0-1.0,
"tiene_respuesta": true/false,
"posibles_errores": ["error si existe"]
}}"""
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
response_format={"type": "json_object"}
).choices[0].message.content
try:
datos = json.loads(resp)
score_promedio = (
datos.get("score_completitud", 0.5) * 0.4 +
datos.get("score_precision", 0.5) * 0.4 +
datos.get("score_claridad", 0.5) * 0.2
)
return {**datos, "score_promedio": score_promedio}
except Exception:
return {"score_promedio": 0.5, "tiene_respuesta": True, "posibles_errores": []}
def determinar_consenso(
resultado_principal: ResultadoTecnica,
resultado_alternativo: Optional[ResultadoTecnica],
clasificacion: ProblemaClasificado
) -> str:
"""
Determina la respuesta de consenso cuando hay dos técnicas.
"""
if resultado_alternativo is None:
return resultado_principal.respuesta
# Para math: comparar respuestas numéricas
if clasificacion.tipo == "math":
num_p = _extraer_numero(resultado_principal.respuesta)
num_a = _extraer_numero(resultado_alternativo.respuesta)
if num_p == num_a:
return resultado_principal.respuesta # Consenso
# Diferencia: elegir la de mayor confianza
if resultado_principal.confianza >= resultado_alternativo.confianza:
return resultado_principal.respuesta
return resultado_alternativo.respuesta
# Para otras: elegir la de mayor score de calidad
eval_p = evaluar_calidad_respuesta(clasificacion.texto, resultado_principal.respuesta)
eval_a = evaluar_calidad_respuesta(clasificacion.texto, resultado_alternativo.respuesta)
if eval_p["score_promedio"] >= eval_a["score_promedio"]:
return resultado_principal.respuesta
return resultado_alternativo.respuesta
# ============================================================
# SISTEMA PRINCIPAL
# ============================================================
def resolver_problema(
problema: str,
modo_comparacion: bool = False,
n_sc: int = 5,
verbose: bool = True
) -> ResultadoFinal:
"""
Sistema principal del Multi-Strategy Problem Solver.
Args:
problema: El problema o pregunta a resolver
modo_comparacion: Si True, ejecuta dos técnicas y compara
n_sc: N para Self-Consistency si aplica
verbose: Si True, imprime el proceso
Returns:
ResultadoFinal con respuesta, métricas y metadata completa
"""
t_inicio = time.time()
if verbose:
print(f"\n{'='*60}")
print(f"MULTI-STRATEGY PROBLEM SOLVER")
print(f"{'='*60}")
print(f"Problema: {problema[:80]}...")
# Paso 1: Clasificar
if verbose:
print("\n[1/4] Clasificando problema...")
clasificacion = clasificar_problema(problema)
if verbose:
print(f" Tipo: {clasificacion.tipo}")
print(f" Complejidad: {clasificacion.complejidad}")
print(f" Datos externos: {clasificacion.necesita_externos}")
print(f" Accuracy crítica: {clasificacion.accuracy_critica}")
# Paso 2: Seleccionar y ejecutar técnica
if verbose:
print("\n[2/4] Seleccionando y ejecutando técnica...")
resultado_principal, resultado_alternativo = seleccionar_y_ejecutar(
clasificacion, modo_comparacion, n_sc
)
if verbose:
print(f" Técnica principal: {resultado_principal.tecnica}")
print(f" Llamadas API: {resultado_principal.n_llamadas}")
print(f" Tiempo: {resultado_principal.tiempo_segundos:.2f}s")
if resultado_alternativo:
print(f" Técnica alternativa: {resultado_alternativo.tecnica}")
# Paso 3: Evaluar calidad
if verbose:
print("\n[3/4] Evaluando calidad...")
evaluacion = evaluar_calidad_respuesta(problema, resultado_principal.respuesta)
if verbose:
print(f" Score calidad: {evaluacion.get('score_promedio', 0):.2f}")
# Paso 4: Determinar respuesta de consenso
if verbose:
print("\n[4/4] Determinando respuesta final...")
respuesta_consenso = determinar_consenso(
resultado_principal, resultado_alternativo, clasificacion
)
# Calcular métricas
n_llamadas_total = resultado_principal.n_llamadas
if resultado_alternativo:
n_llamadas_total += resultado_alternativo.n_llamadas
# Incluir llamadas de clasificación y evaluación
n_llamadas_total += 2
metricas = {
"tiempo_total_segundos": time.time() - t_inicio,
"n_llamadas_total": n_llamadas_total,
"costo_estimado_usd": n_llamadas_total * 0.0002, # Estimación gpt-4o-mini
"score_calidad": evaluacion.get("score_promedio", 0),
"confianza_tecnica": resultado_principal.confianza,
"hay_comparacion": resultado_alternativo is not None
}
if verbose:
print(f"\n{'='*60}")
print(f"RESULTADO FINAL:")
print(f"Técnica usada: {resultado_principal.tecnica}")
print(f"Respuesta: {respuesta_consenso[:200]}...")
print(f"Tiempo total: {metricas['tiempo_total_segundos']:.2f}s")
print(f"Llamadas API: {n_llamadas_total}")
print(f"Costo estimado: ${metricas['costo_estimado_usd']:.4f}")
print(f"{'='*60}")
return ResultadoFinal(
problema=problema,
clasificacion=clasificacion,
resultado_principal=resultado_principal,
resultado_alternativo=resultado_alternativo,
respuesta_consenso=respuesta_consenso,
metricas=metricas,
recomendacion_tecnica=resultado_principal.tecnica
)
Suite de Pruebas
# ============================================================
# TESTS DEL SISTEMA
# ============================================================
CASOS_PRUEBA = [
{
"categoria": "Math simple",
"problema": "¿Cuánto es 15% de 2,400?",
"respuesta_esperada": "360",
"tecnica_esperada": "chain_of_thought"
},
{
"categoria": "Math crítico",
"problema": "Si invierto €10,000 con interés compuesto al 6% anual durante 5 años, ¿cuánto tendré?",
"respuesta_esperada": "13382",
"tecnica_esperada": "self_consistency"
},
{
"categoria": "Razonamiento lógico",
"problema": "Todos los managers trabajan los lunes. Alicia trabaja los lunes. ¿Alicia es manager?",
"respuesta_esperada": "no necesariamente",
"tecnica_esperada": "chain_of_thought"
},
{
"categoria": "Planificación",
"problema": "Diseña un plan de 3 meses para aprender Machine Learning partiendo de Python básico. Incluye recursos específicos.",
"respuesta_esperada": None, # Evaluación cualitativa
"tecnica_esperada": "tree_of_thought"
},
{
"categoria": "Código",
"problema": "Escribe una función Python que encuentre todos los números primos hasta N usando el método Sieve of Eratosthenes.",
"respuesta_esperada": None,
"tecnica_esperada": "self_refine"
},
{
"categoria": "Creativo",
"problema": "Escribe un haiku sobre la programación.",
"respuesta_esperada": None,
"tecnica_esperada": "zero_shot"
}
]
def ejecutar_suite_pruebas(verbose: bool = True) -> dict:
"""
Ejecuta todos los casos de prueba y genera un reporte.
"""
print("\n" + "="*70)
print("SUITE DE PRUEBAS: MULTI-STRATEGY PROBLEM SOLVER")
print("="*70)
resultados_suite = []
for i, caso in enumerate(CASOS_PRUEBA, 1):
print(f"\n[Test {i}/{len(CASOS_PRUEBA)}] {caso['categoria']}")
print(f"Problema: {caso['problema'][:60]}...")
# Ejecutar
resultado = resolver_problema(
caso["problema"],
modo_comparacion=False,
verbose=False # Silencioso en la suite
)
# Evaluar
tecnica_correcta = resultado.recomendacion_tecnica == caso.get("tecnica_esperada")
respuesta_correcta = None
if caso.get("respuesta_esperada"):
resp_num = _extraer_numero(resultado.respuesta_consenso)
try:
correcta_num = case.get("respuesta_esperada", "")
respuesta_correcta = abs(float(resp_num) - float(correcta_num)) < 1.0
except ValueError:
respuesta_correcta = caso["respuesta_esperada"].lower() in resultado.respuesta_consenso.lower()
resultado_test = {
"categoria": caso["categoria"],
"tecnica_usada": resultado.recomendacion_tecnica,
"tecnica_esperada": caso.get("tecnica_esperada"),
"tecnica_correcta": tecnica_correcta,
"respuesta_correcta": respuesta_correcta,
"confianza": resultado.metricas["confianza_tecnica"],
"llamadas": resultado.metricas["n_llamadas_total"],
"tiempo": resultado.metricas["tiempo_total_segundos"]
}
resultados_suite.append(resultado_test)
if verbose:
print(f" Técnica: {resultado.recomendacion_tecnica} ({'✓' if tecnica_correcta else '✗'})")
if respuesta_correcta is not None:
print(f" Respuesta: {'✓ Correcta' if respuesta_correcta else '✗ Incorrecta'}")
print(f" Llamadas: {resultado.metricas['n_llamadas_total']}, Tiempo: {resultado.metricas['tiempo_total_segundos']:.2f}s")
# Resumen
tecnicas_correctas = sum(1 for r in resultados_suite if r["tecnica_correcta"])
respuestas_evaluables = [r for r in resultados_suite if r["respuesta_correcta"] is not None]
respuestas_correctas = sum(1 for r in respuestas_evaluables if r["respuesta_correcta"])
total_llamadas = sum(r["llamadas"] for r in resultados_suite)
tiempo_promedio = sum(r["tiempo"] for r in resultados_suite) / len(resultados_suite)
print(f"\n{'='*70}")
print("RESUMEN DE PRUEBAS")
print(f"{'='*70}")
print(f"Tests ejecutados: {len(CASOS_PRUEBA)}")
print(f"Técnica correcta: {tecnicas_correctas}/{len(CASOS_PRUEBA)} ({tecnicas_correctas/len(CASOS_PRUEBA):.0%})")
if respuestas_evaluables:
print(f"Respuesta correcta: {respuestas_correctas}/{len(respuestas_evaluables)} ({respuestas_correctas/len(respuestas_evaluables):.0%})")
print(f"Total llamadas API: {total_llamadas}")
print(f"Tiempo promedio: {tiempo_promedio:.2f}s")
print(f"Costo total estimado: ${total_llamadas * 0.0002:.4f}")
return {
"detalle": resultados_suite,
"accuracy_tecnica": tecnicas_correctas / len(CASOS_PRUEBA),
"total_llamadas": total_llamadas,
"tiempo_promedio": tiempo_promedio
}
Extensiones Opcionales
Extensión 1: Interfaz de línea de comandos
import sys
def cli_interactive():
"""
Interfaz interactiva para usar el solver desde terminal.
Escribe 'salir' para terminar.
"""
print("\n=== MULTI-STRATEGY PROBLEM SOLVER ===")
print("Escribe cualquier problema o pregunta.")
print("Comandos: 'comparar' (activa modo comparación), 'salir'\n")
modo_comparacion = False
while True:
problema = input("Tu pregunta: ").strip()
if problema.lower() == "salir":
print("¡Hasta luego!")
break
if problema.lower() == "comparar":
modo_comparacion = not modo_comparacion
print(f"Modo comparación: {'ON' if modo_comparacion else 'OFF'}")
continue
if not problema:
continue
resultado = resolver_problema(problema, modo_comparacion=modo_comparacion, verbose=True)
print(f"\nRESPUESTA FINAL:\n{resultado.respuesta_consenso}\n")
Extensión 2: Logging y métricas persistentes
import json
from datetime import datetime
from pathlib import Path
class SolverLogger:
def __init__(self, log_file: str = "solver_logs.jsonl"):
self.log_file = Path(log_file)
def log(self, resultado: ResultadoFinal):
"""Guarda cada resolución en un archivo JSONL para análisis posterior."""
entrada = {
"timestamp": datetime.now().isoformat(),
"problema": resultado.problema[:200],
"tipo": resultado.clasificacion.tipo,
"tecnica": resultado.recomendacion_tecnica,
"n_llamadas": resultado.metricas["n_llamadas_total"],
"tiempo_seg": resultado.metricas["tiempo_total_segundos"],
"score_calidad": resultado.metricas["score_calidad"],
"confianza": resultado.metricas["confianza_tecnica"]
}
with self.log_file.open("a") as f:
f.write(json.dumps(entrada, ensure_ascii=False) + "\n")
def estadisticas(self) -> dict:
"""Calcula estadísticas del uso del solver."""
if not self.log_file.exists():
return {}
logs = [json.loads(l) for l in self.log_file.read_text().strip().split('\n') if l]
from collections import Counter
tecnicas_conteo = Counter(l["tecnica"] for l in logs)
tipos_conteo = Counter(l["tipo"] for l in logs)
return {
"total_resoluciones": len(logs),
"tecnicas_mas_usadas": tecnicas_conteo.most_common(5),
"tipos_mas_comunes": tipos_conteo.most_common(5),
"tiempo_promedio": sum(l["tiempo_seg"] for l in logs) / len(logs),
"score_promedio": sum(l["score_calidad"] for l in logs) / len(logs),
"llamadas_promedio": sum(l["n_llamadas"] for l in logs) / len(logs)
}
# Uso con logging:
logger = SolverLogger()
resultado = resolver_problema("¿Cuánto es la raíz cuadrada de 2025?")
logger.log(resultado)
print(logger.estadisticas())
Extensión 3: Cache de respuestas similares
from hashlib import md5
import shelve
class CachedSolver:
def __init__(self, cache_file: str = "solver_cache"):
self.cache_file = cache_file
def _hash_problema(self, problema: str) -> str:
"""Crea un hash único para el problema."""
return md5(problema.strip().lower().encode()).hexdigest()
def resolver_con_cache(self, problema: str, ttl_hours: int = 24) -> ResultadoFinal:
"""
Resolver con cache: si el mismo problema fue resuelto recientemente,
retorna el resultado cacheado.
"""
clave = self._hash_problema(problema)
with shelve.open(self.cache_file) as cache:
if clave in cache:
entrada = cache[clave]
# Verificar TTL
edad_horas = (time.time() - entrada["timestamp"]) / 3600
if edad_horas < ttl_hours:
print(f"[Cache hit] Resultado encontrado ({edad_horas:.1f}h de antigüedad)")
return entrada["resultado"]
# No en cache: resolver y guardar
resultado = resolver_problema(problema, verbose=False)
cache[clave] = {
"resultado": resultado,
"timestamp": time.time()
}
return resultado
Criterios de Éxito del Proyecto
Verifica que tu implementación cumple estos criterios:
- El clasificador identifica correctamente el tipo de problema (math/factual/reasoning/creative/code/planning) en >80% de los casos
- El router selecciona la técnica apropiada según el tipo y la complejidad
- Cada técnica produce resultados de calidad consistente
- El sistema de evaluación asigna scores de calidad razonables
- El modo comparación ejecuta dos técnicas y determina la mejor respuesta
- El sistema maneja errores graciosamente (sin crash en casos edge)
- Los logs y métricas se registran correctamente (extensión)
- El cache funciona para evitar llamadas repetidas (extensión)
Punto de Entrada Principal
if __name__ == "__main__":
# Demo básico
problemas_demo = [
"¿Cuánto es el 30% de 1,750?",
"Explica la diferencia entre una lista y una tupla en Python",
"Diseña la arquitectura de una API REST para un sistema de inventario",
"Escribe una función que invierta una cadena de texto sin usar reversed()",
]
print("=== DEMO: MULTI-STRATEGY PROBLEM SOLVER ===\n")
for problema in problemas_demo:
print(f"\n{'─'*50}")
resultado = resolver_problema(problema, verbose=True)
# Suite de pruebas
print("\n\n=== EJECUTANDO SUITE DE PRUEBAS ===")
reporte = ejecutar_suite_pruebas(verbose=True)
print(f"\nAccuracy en selección de técnica: {reporte['accuracy_tecnica']:.0%}")
Resumen
En este proyecto construiste un Multi-Strategy Problem Solver completo que:
- Clasifica automáticamente el tipo de problema (math, reasoning, código, planificación, etc.)
- Selecciona la técnica óptima basándose en el tipo, complejidad, y si requiere datos externos
- Ejecuta la técnica seleccionada con parámetros apropiados
- Evalúa la calidad de la respuesta
- Determina consenso cuando hay comparación entre técnicas
- Registra métricas de uso, costo y calidad
Las técnicas implementadas son: Zero-shot, CoT, Self-Consistency, ReAct, Tree-of-Thought, y Self-Refine.
Este sistema es extensible: puedes agregar nuevos tipos de problemas, nuevas técnicas, y conectar herramientas externas reales en el módulo ReAct.