Módulo 5: ReAct, Self-Consistency y Patrones Avanzados

6. Combinación de Técnicas

Descripción

Las técnicas que hemos visto —CoT, Self-Consistency, ReAct, Tree-of-Thought, Meta-prompting y Self-Refine— no son excluyentes. Combinadas estratégicamente, pueden superar el rendimiento de cualquiera por separado. Pero combinar técnicas indiscriminadamente también puede aumentar el costo sin mejora real.

Esta cápsula explora cuándo y cómo combinar técnicas, con implementaciones prácticas y análisis de cuándo la combinación vale la pena.


Por Qué Combinar Técnicas

Cada técnica resuelve un problema diferente:

TécnicaProblema que resuelve
CoTRazonamiento interno deficiente
Self-ConsistencyUn solo camino puede estar mal
ReActFalta de datos externos
ToTExploración de estrategias alternativas
Meta-promptingPrompt subóptimo para la tarea
Self-RefineRespuesta inicial de baja calidad

Cuando una tarea tiene múltiples problemas simultáneos, combinar técnicas ataca cada uno.


Combinación 1: CoT + Self-Consistency (La más común)

La combinación más usada en producción. Cada una de las N respuestas de Self-Consistency usa CoT internamente, lo que mejora tanto la calidad individual de cada respuesta como el consenso final.

from openai import OpenAI
from collections import Counter
import re

client = OpenAI()

def extraer_respuesta_numerica(texto: str) -> str:
    """Extrae la respuesta numérica final de un texto CoT."""
    patrones = [
        r'(?:respuesta|answer|resultado)[\s:=]+(-?\d+\.?\d*)',
        r'=\s*(-?\d+\.?\d*)\s*$',
        r'\*\*(-?\d+\.?\d*)\*\*'
    ]
    for patron in patrones:
        m = re.search(patron, texto, re.IGNORECASE | re.MULTILINE)
        if m:
            return m.group(1)
    numeros = re.findall(r'-?\d+\.?\d*', texto)
    return numeros[-1] if numeros else texto.strip()[-20:]

def cot_self_consistency(
    problema: str,
    n: int = 5,
    temperatura: float = 0.7,
    incluir_reasoning: bool = True
) -> dict:
    """
    CoT + Self-Consistency: N respuestas con CoT, majority vote.
    
    La diferencia con Self-Consistency básico es que el prompt
    explícitamente fuerza un razonamiento más detallado antes de la respuesta.
    """
    prompt_cot_detallado = f"""{problema}

Instrucciones:
1. Identifica los datos conocidos y desconocidos
2. Establece qué fórmulas o conceptos aplican
3. Resuelve paso a paso, mostrando CADA operación
4. Verifica si la respuesta tiene sentido
5. Al final escribe: "Respuesta final: [valor]"
"""
    
    respuestas = []
    razonamientos = []
    
    for _ in range(n):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt_cot_detallado}],
            temperature=temperatura,
            max_tokens=600
        )
        raw = response.choices[0].message.content
        respuesta = extraer_respuesta_numerica(raw)
        respuestas.append(respuesta)
        razonamientos.append(raw)
    
    conteo = Counter(respuestas)
    ganadora, votos = conteo.most_common(1)[0]
    
    # Encontrar el razonamiento que llevó a la respuesta ganadora
    mejor_razonamiento = next(
        (r for r, resp in zip(razonamientos, respuestas) if resp == ganadora),
        razonamientos[0]
    )
    
    return {
        "respuesta": ganadora,
        "confianza": votos / n,
        "distribucion": dict(conteo),
        "mejor_razonamiento": mejor_razonamiento if incluir_reasoning else None
    }

# Benchmark vs CoT solo:
problemas_math = [
    "María tiene 5 veces más dinero que Juan. Si entre los dos tienen $720, ¿cuánto tiene cada uno?",
    "Un tren sale de A a 80 km/h y otro de B a 60 km/h. Están separados por 420 km. ¿En cuánto tiempo se cruzan?",
    "Si el IVA es 21%, ¿cuál es el precio sin IVA de un artículo que cuesta $242 con IVA?"
]

for problema in problemas_math:
    resultado = cot_self_consistency(problema, n=5)
    print(f"Problema: {problema[:60]}...")
    print(f"  Respuesta: {resultado['respuesta']} (confianza: {resultado['confianza']:.0%})")

Combinación 2: ReAct + Structured Output

ReAct para razonamiento con tools; el output final en formato JSON parseado con Pydantic. Esta combinación es ideal para sistemas de producción que necesitan tanto datos externos como outputs confiables.

from pydantic import BaseModel, Field
from typing import Optional
import json

class RespuestaReAct(BaseModel):
    """Schema para el output estructurado de ReAct."""
    respuesta: str = Field(description="La respuesta al problema")
    confianza: float = Field(ge=0, le=1, description="Confianza en la respuesta (0-1)")
    fuentes_consultadas: list[str] = Field(default_factory=list)
    pasos_razonamiento: list[str] = Field(default_factory=list)
    requiere_aclaracion: bool = Field(default=False)

def react_con_output_estructurado(problema: str) -> RespuestaReAct:
    """
    ReAct que finaliza con output JSON estructurado y validado con Pydantic.
    """
    # Primero ejecutar ReAct normal para obtener el razonamiento
    from .02_react_reasoning_acting import run_react  # En producción, importar correctamente
    
    # Adaptar el prompt final para pedir JSON
    system_prompt = """Eres un asistente que resuelve problemas usando herramientas.
    
Al terminar, responde SIEMPRE en JSON con este formato:
{
    "respuesta": "la respuesta al problema",
    "confianza": 0.9,
    "fuentes_consultadas": ["herramienta1", "herramienta2"],
    "pasos_razonamiento": ["paso1", "paso2"],
    "requiere_aclaracion": false
}"""
    
    # Versión inline para no depender de imports externos
    TOOLS_BASICAS = [
        {
            "type": "function",
            "function": {
                "name": "buscar",
                "description": "Busca información factual",
                "parameters": {
                    "type": "object",
                    "properties": {"query": {"type": "string"}},
                    "required": ["query"]
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "calcular",
                "description": "Evalúa expresiones matemáticas",
                "parameters": {
                    "type": "object",
                    "properties": {"expresion": {"type": "string"}},
                    "required": ["expresion"]
                }
            }
        }
    ]
    
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": problema}
    ]
    
    herramientas_usadas = []
    pasos = []
    
    for _ in range(8):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=TOOLS_BASICAS,
            tool_choice="auto",
            temperature=0
        )
        msg = response.choices[0].message
        
        if not msg.tool_calls:
            # Respuesta final - parsear como JSON
            try:
                # Intentar extraer JSON del texto
                contenido = msg.content or "{}"
                json_match = re.search(r'\{.*\}', contenido, re.DOTALL)
                if json_match:
                    datos = json.loads(json_match.group())
                    datos["fuentes_consultadas"] = herramientas_usadas
                    datos["pasos_razonamiento"] = pasos
                    return RespuestaReAct(**datos)
            except Exception:
                pass
            
            # Fallback
            return RespuestaReAct(
                respuesta=msg.content or "Sin respuesta",
                confianza=0.5,
                fuentes_consultadas=herramientas_usadas,
                pasos_razonamiento=pasos
            )
        
        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)
            
            # Simular herramientas
            if tool_name == "buscar":
                resultado = f"Resultado de búsqueda: {args.get('query', '')} - [datos simulados]"
            elif tool_name == "calcular":
                try:
                    resultado = str(eval(args.get('expresion', '0'), {"__builtins__": {}}))
                except Exception:
                    resultado = "Error de cálculo"
            else:
                resultado = "Herramienta no disponible"
            
            herramientas_usadas.append(tool_name)
            pasos.append(f"{tool_name}({args})")
            
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": resultado
            })
    
    return RespuestaReAct(respuesta="Timeout", confianza=0.0)


# Uso:
resultado = react_con_output_estructurado("¿Cuánto es 15% de 1500 EUR en USD usando el tipo de cambio actual?")
print(f"Respuesta: {resultado.respuesta}")
print(f"Confianza: {resultado.confianza:.0%}")
print(f"Pasos: {resultado.pasos_razonamiento}")

Combinación 3: ToT + Self-Consistency

Para problemas donde hay múltiples estrategias válidas Y queremos verificar el resultado por consenso.

def tot_self_consistency(
    problema: str,
    n_arboles: int = 3,
    breadth_por_arbol: int = 2
) -> dict:
    """
    Ejecuta N árboles de ToT independientes y vota por la respuesta más común.
    
    Cada árbol parte de un enfoque diferente (seed de temperatura),
    y la respuesta final es por majority vote sobre los N árboles.
    """
    respuestas_arboles = []
    
    for i in range(n_arboles):
        # Cada árbol usa temperatura diferente para diversidad
        temperatura = 0.5 + (i * 0.2)
        
        prompt_enfoques = f"""Problema: {problema}

Genera {breadth_por_arbol} enfoques distintos para resolverlo.
Elige los enfoques más DIFERENTES entre sí posibles.
Número de enfoque seguido de descripción del primer paso."""
        
        resp_enfoques = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt_enfoques}],
            temperature=temperatura,
            max_tokens=300
        )
        lineas = [l.lstrip('0123456789.-) ').strip() 
                  for l in resp_enfoques.choices[0].message.content.split('\n') 
                  if l.strip() and l.strip()[0].isdigit()][:breadth_por_arbol]
        
        # Evaluar y seleccionar mejor enfoque
        if not lineas:
            continue
        
        scores = []
        for enfoque in lineas:
            eval_resp = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": f"Problema: {problema}\nEnfoque: {enfoque}\n¿Prometedor? (0-1)"}],
                temperature=0, max_tokens=10
            )
            try:
                score = float(eval_resp.choices[0].message.content.strip()[:4])
            except ValueError:
                score = 0.5
            scores.append((score, enfoque))
        
        mejor_enfoque = max(scores, key=lambda x: x[0])[1]
        
        # Resolver desde mejor enfoque
        solucion_resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"{problema}\n\nEnfoque: {mejor_enfoque}\n\nResuelve. Respuesta final: [valor]"}],
            temperature=0,
            max_tokens=400
        )
        respuesta = extraer_respuesta_numerica(solucion_resp.choices[0].message.content)
        respuestas_arboles.append(respuesta)
    
    if not respuestas_arboles:
        return {"respuesta": "Sin resultado", "confianza": 0.0}
    
    # Majority vote sobre resultados de los árboles
    conteo = Counter(respuestas_arboles)
    ganadora, votos = conteo.most_common(1)[0]
    
    return {
        "respuesta": ganadora,
        "confianza": votos / len(respuestas_arboles),
        "respuestas_por_arbol": respuestas_arboles,
        "n_arboles": len(respuestas_arboles)
    }

Combinación 4: Meta-Prompting + Self-Refine + Evaluation

El pipeline completo de optimización automática de prompts:

def pipeline_optimizacion_completo(
    descripcion_tarea: str,
    dataset_evaluacion: list[dict],
    n_iteraciones: int = 3
) -> dict:
    """
    Pipeline completo de optimización:
    1. Meta-prompting genera prompt inicial
    2. Se evalúa en dataset
    3. Self-Refine mejora el prompt basado en errores
    4. Repetir hasta convergencia o N iteraciones
    
    Args:
        descripcion_tarea: Descripción de la tarea
        dataset_evaluacion: Lista de {input: str, expected: str}
        n_iteraciones: Iteraciones de mejora
    
    Returns:
        dict con mejor_prompt, accuracy_historico, mejoras
    """
    # Paso 1: Generar prompt inicial con meta-prompting
    meta_prompt = f"""Crea un prompt óptimo para: {descripcion_tarea}
    
El prompt debe ser específico, incluir formato de salida claro, y manejar edge cases.
Devuelve solo el prompt."""
    
    prompt_actual = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": meta_prompt}],
        temperature=0.3,
        max_tokens=500
    ).choices[0].message.content.strip()
    
    historial = []
    
    for iteracion in range(n_iteraciones):
        # Paso 2: Evaluar prompt actual
        errores = []
        aciertos = 0
        
        for ejemplo in dataset_evaluacion[:5]:  # Limitar para ahorrar llamadas
            resp = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": f"{prompt_actual}\n\nInput: {ejemplo['input']}"}],
                temperature=0,
                max_tokens=200
            ).choices[0].message.content
            
            expected = ejemplo.get("expected", "")
            correcto = expected.lower() in resp.lower() if expected else False
            
            if correcto:
                aciertos += 1
            else:
                errores.append({
                    "input": ejemplo["input"][:100],
                    "esperado": expected,
                    "obtenido": resp[:100]
                })
        
        accuracy = aciertos / min(len(dataset_evaluacion), 5)
        
        historial.append({
            "iteracion": iteracion,
            "accuracy": accuracy,
            "prompt": prompt_actual[:200]
        })
        
        if accuracy >= 0.9:
            break
        
        # Paso 3: Self-Refine del prompt basado en errores
        if errores:
            errores_str = json.dumps(errores[:3], ensure_ascii=False, indent=2)
            refine_prompt = f"""Tarea: {descripcion_tarea}

Prompt actual:
{prompt_actual}

Errores cometidos al usar este prompt:
{errores_str}

Mejora el prompt para corregir estos errores sin romper los casos que funcionan.
Devuelve solo el prompt mejorado."""
            
            prompt_mejorado = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": refine_prompt}],
                temperature=0.2,
                max_tokens=500
            ).choices[0].message.content.strip()
            
            prompt_actual = prompt_mejorado
    
    return {
        "prompt_final": prompt_actual,
        "accuracy_historico": [h["accuracy"] for h in historial],
        "iteraciones": historial,
        "mejora_total": historial[-1]["accuracy"] - historial[0]["accuracy"] if historial else 0
    }

Cuándo Combinar vs Cuando No

Cuándo combinar ES útil

Tarea con MÚLTIPLES requisitos simultáneos:
├── Necesita datos externos + Accuracy crítica
│   └── → ReAct + Self-Consistency
├── Múltiples estrategias + Verificación
│   └── → ToT + Self-Consistency
├── Prompt subóptimo + Respuesta mejorable
│   └── → Meta-prompting + Self-Refine
└── Datos externos + Output estructurado
    └── → ReAct + Structured Output

Cuándo combinar NO es útil (overkill)

# OVERKILL: Tarea simple de clasificación
def clasificar_sentimiento_overkill(texto: str) -> str:
    # INCORRECTO: No necesita ToT ni Self-Consistency para clasificación simple
    return tot_self_consistency(
        f"Clasifica el sentimiento: {texto}",
        n_arboles=5, breadth_por_arbol=3
    )["respuesta"]

# CORRECTO: Few-shot es suficiente
def clasificar_sentimiento_optimo(texto: str) -> str:
    return client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"""
Clasifica el sentimiento como POSITIVO, NEGATIVO o NEUTRAL.
Ejemplos: "Me encantó" → POSITIVO | "Horrible" → NEGATIVO | "Normal" → NEUTRAL

Texto: {texto}
Responde solo con: POSITIVO, NEGATIVO, o NEUTRAL"""}],
        temperature=0
    ).choices[0].message.content.strip()

Regla general: escala de complejidad

Si la tarea es...Técnica
Clasificación/extracción simpleZero-shot o Few-shot
Razonamiento matemáticoCoT
Razonamiento matemático críticoCoT + Self-Consistency
Requiere datos externosReAct
Requiere datos externos + accuracyReAct + Self-Consistency
Múltiples estrategias posiblesToT
Múltiples estrategias + verificaciónToT + Self-Consistency
Prompt subóptimo conocidoMeta-prompting
Calidad de respuesta críticaSelf-Refine
Sistema de producción completoMeta-prompting + Self-Refine + evaluación

Medición del Valor de Combinar

def benchmark_combinaciones(
    problemas: list[dict],  # [{"enunciado": str, "respuesta": str}]
    tecnicas_a_evaluar: list[str] = None
) -> dict:
    """
    Compara múltiples técnicas y combinaciones en un conjunto de problemas.
    
    Args:
        problemas: Lista de problemas con respuesta correcta conocida
        tecnicas_a_evaluar: Lista de técnicas a comparar
    """
    if tecnicas_a_evaluar is None:
        tecnicas_a_evaluar = ["cot_solo", "cot_sc_n3", "cot_sc_n5"]
    
    resultados = {t: {"aciertos": 0, "total": len(problemas), "llamadas": 0} 
                  for t in tecnicas_a_evaluar}
    
    def normalizar(texto: str) -> str:
        try:
            return str(round(float(re.sub(r'[^\d.-]', '', texto.split()[-1] if texto.split() else texto)), 1))
        except ValueError:
            return texto.strip().upper()
    
    for problema in problemas:
        enunciado = problema["enunciado"]
        correcta = normalizar(str(problema["respuesta"]))
        
        for tecnica in tecnicas_a_evaluar:
            if tecnica == "cot_solo":
                resp = client.chat.completions.create(
                    model="gpt-4o-mini",
                    messages=[{"role": "user", "content": f"{enunciado}\nPiensa paso a paso. Respuesta: [número]"}],
                    temperature=0, max_tokens=400
                ).choices[0].message.content
                pred = normalizar(extraer_respuesta_numerica(resp))
                resultados["cot_solo"]["llamadas"] += 1
            
            elif tecnica == "cot_sc_n3":
                r = cot_self_consistency(enunciado, n=3, incluir_reasoning=False)
                pred = normalizar(r["respuesta"])
                resultados["cot_sc_n3"]["llamadas"] += 3
            
            elif tecnica == "cot_sc_n5":
                r = cot_self_consistency(enunciado, n=5, incluir_reasoning=False)
                pred = normalizar(r["respuesta"])
                resultados["cot_sc_n5"]["llamadas"] += 5
            
            else:
                pred = "?"
            
            if pred == correcta or (len(pred) > 0 and correcta in pred):
                resultados[tecnica]["aciertos"] += 1
    
    # Calcular métricas
    for tecnica in resultados:
        r = resultados[tecnica]
        r["accuracy"] = r["aciertos"] / r["total"]
        r["llamadas_por_problema"] = r["llamadas"] / r["total"]
    
    return resultados


# Ejemplo:
benchmark_data = [
    {"enunciado": "¿Cuánto es 15% de 480?", "respuesta": 72},
    {"enunciado": "Si 3x + 5 = 17, ¿cuánto vale x?", "respuesta": 4},
    {"enunciado": "Un tren recorre 240 km en 3 horas. ¿Cuál es su velocidad media en km/h?", "respuesta": 80},
]

metricas = benchmark_combinaciones(benchmark_data)
for tecnica, datos in metricas.items():
    print(f"{tecnica}: accuracy={datos['accuracy']:.0%}, llamadas/problema={datos['llamadas_por_problema']:.1f}")

Combinación Práctica: Sistema de Q&A Robusto

Un sistema de preguntas y respuestas que combina múltiples técnicas según el tipo de pregunta:

def qa_robusto(pregunta: str) -> dict:
    """
    Sistema de Q&A que selecciona automáticamente la combinación correcta.
    """
    # Paso 1: Clasificar la pregunta
    clasificacion = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"""
Clasifica esta pregunta. Responde JSON: {{"tipo": "...", "necesita_datos_externos": true/false, "accuracy_critica": true/false}}

Tipos: math (operaciones numéricas), logica (razonamiento), factual (hechos del mundo), creative (sin respuesta correcta única)

Pregunta: {pregunta}"""}],
        temperature=0,
        response_format={"type": "json_object"}
    ).choices[0].message.content
    
    try:
        clasif = json.loads(clasificacion)
    except Exception:
        clasif = {"tipo": "logica", "necesita_datos_externos": False, "accuracy_critica": False}
    
    tipo = clasif.get("tipo", "logica")
    necesita_externos = clasif.get("necesita_datos_externos", False)
    accuracy_critica = clasif.get("accuracy_critica", False)
    
    # Paso 2: Seleccionar técnica óptima
    if necesita_externos and accuracy_critica:
        # ReAct + Self-Consistency (más caro pero más preciso)
        # Simplificado: usar CoT + Self-Consistency con nota de que faltan datos externos
        resultado = cot_self_consistency(pregunta, n=5)
        tecnica_usada = "cot_sc_n5 (idealmente: react+sc)"
    
    elif necesita_externos:
        # ReAct
        from openai import OpenAI
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"{pregunta}\nNota: si necesitas datos externos, indícalo explícitamente."}],
            temperature=0, max_tokens=400
        ).choices[0].message.content
        resultado = {"respuesta": resp, "confianza": 0.7}
        tecnica_usada = "react_simplificado"
    
    elif tipo == "math" and accuracy_critica:
        # CoT + Self-Consistency
        resultado = cot_self_consistency(pregunta, n=5)
        tecnica_usada = "cot_sc_n5"
    
    elif tipo in ["math", "logica"]:
        # CoT solo
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"{pregunta}\nPiensa paso a paso."}],
            temperature=0, max_tokens=400
        ).choices[0].message.content
        resultado = {"respuesta": resp, "confianza": 0.8}
        tecnica_usada = "cot_solo"
    
    else:
        # Zero-shot/few-shot para factual y creative
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": pregunta}],
            temperature=0.3, max_tokens=400
        ).choices[0].message.content
        resultado = {"respuesta": resp, "confianza": 0.75}
        tecnica_usada = "zero_shot"
    
    return {
        "respuesta": resultado.get("respuesta", ""),
        "confianza": resultado.get("confianza", 0.5),
        "clasificacion": clasif,
        "tecnica_usada": tecnica_usada
    }


# Prueba:
preguntas = [
    "¿Cuánto es el 23% de 1,540?",
    "¿Cuáles son las tendencias en renewable energy en 2026?",
    "Si todos los A son B, y algunos B son C, ¿necesariamente algunos A son C?"
]

for p in preguntas:
    r = qa_robusto(p)
    print(f"\nPregunta: {p}")
    print(f"Técnica: {r['tecnica_usada']}")
    print(f"Respuesta: {r['respuesta'][:100]}...")

Troubleshooting

Problema 1: La combinación es más lenta sin mejora perceptible

Síntoma: Combinar CoT + Self-Consistency para tareas simples no mejora nada y multiplica el costo 5x.

Diagnóstico y solución:

def combinar_solo_si_vale(problema: str) -> dict:
    """Primero intenta CoT simple; solo usa SC si la confianza es baja."""
    # Intento 1: CoT simple
    resp_inicial = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"{problema}\nPiensa paso a paso. Respuesta: [valor]"}],
        temperature=0, max_tokens=400
    ).choices[0].message.content
    
    # Evaluar confianza del modelo (a través de la longitud del razonamiento)
    # Respuestas muy cortas o con "no sé" indican baja confianza
    confianza_baja = (
        len(resp_inicial.split()) < 30 or
        any(p in resp_inicial.lower() for p in ["no sé", "not sure", "unclear", "ambiguous"])
    )
    
    if not confianza_baja:
        return {"respuesta": extraer_respuesta_numerica(resp_inicial), "tecnica": "cot_solo"}
    
    # Si confianza baja, usar Self-Consistency
    resultado = cot_self_consistency(problema, n=3)
    return {**resultado, "tecnica": "cot_sc"}

Problema 2: ReAct + Self-Consistency es demasiado caro

Síntoma: ReAct con 4 pasos * 5 muestras = 20+ llamadas por pregunta.

Solución: Usar Self-Consistency solo para el resultado final de ReAct:

def react_con_verificacion(problema: str, n_verificaciones: int = 3) -> dict:
    """ReAct una vez, luego verificar el resultado con SC."""
    # ReAct una sola vez
    resultado_react = run_react(problema, verbose=False)
    respuesta_react = resultado_react["respuesta"]
    
    # Verificar la respuesta de ReAct con SC
    verificaciones = []
    for _ in range(n_verificaciones):
        v = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"Verifica si esta respuesta es correcta para el problema dado:\nProblema: {problema}\nRespuesta propuesta: {respuesta_react}\n\nResponde SOLO: CORRECTO o INCORRECTO"}],
            temperature=0.3
        ).choices[0].message.content.strip().upper()
        verificaciones.append("CORRECTO" in v)
    
    confianza = sum(verificaciones) / len(verificaciones)
    return {
        "respuesta": respuesta_react,
        "verificada": confianza > 0.6,
        "confianza_verificacion": confianza,
        "n_llamadas_total": resultado_react.get("steps_used", 4) + n_verificaciones
    }

Ejercicios

Ejercicio 1: Diseñar la combinación óptima

Para cada uno de estos sistemas, diseña qué combinación de técnicas usarías y justifica tu elección:

a) Sistema de tutoring de matemáticas para estudiantes de secundaria b) Chatbot de soporte técnico que consulta una base de conocimientos c) Generador de código que debe producir código sin bugs d) Sistema de análisis de riesgo financiero

Ver solución

a) Tutoring de matemáticas:

  • CoT + Self-Consistency (N=3-5) para validar respuestas matemáticas
  • Self-Refine para explicaciones pedagógicas
  • Justificación: la accuracy es crítica, y las explicaciones deben ser claras

b) Chatbot de soporte técnico:

  • ReAct (para consultar base de conocimientos) + Structured Output
  • Clasificación previa para rutear preguntas simples vs complejas
  • Justificación: necesita datos externos, output debe ser parseable

c) Generador de código:

  • Self-Refine con criterios técnicos específicos (correctitud, eficiencia, seguridad)
  • Meta-prompting para encontrar el mejor prompt de generación
  • Justificación: calidad crítica, múltiples iteraciones mejoran el código

d) Análisis de riesgo financiero:

  • ReAct (datos de mercado) + CoT + Self-Consistency
  • Structured Output para el reporte final
  • Justificación: necesita datos actuales, accuracy crítica, output formal

Ejercicio 2: Benchmark de costos

Implementa un tracker de costos que registre cuántas llamadas API hace cada combinación y calcula el costo estimado por problema.

Ver solución
class CostTracker:
    PRECIOS_POR_1K_TOKENS = {
        "gpt-4o-mini": {"input": 0.00015, "output": 0.00060},
        "gpt-4o": {"input": 0.0025, "output": 0.01}
    }
    
    def __init__(self):
        self.llamadas = []
    
    def registrar(self, modelo: str, tokens_entrada: int, tokens_salida: int):
        precios = self.PRECIOS_POR_1K_TOKENS.get(modelo, self.PRECIOS_POR_1K_TOKENS["gpt-4o-mini"])
        costo = (tokens_entrada / 1000 * precios["input"] + 
                 tokens_salida / 1000 * precios["output"])
        self.llamadas.append({
            "modelo": modelo,
            "tokens_entrada": tokens_entrada,
            "tokens_salida": tokens_salida,
            "costo_usd": costo
        })
    
    def costo_total(self) -> float:
        return sum(l["costo_usd"] for l in self.llamadas)
    
    def resumen(self) -> dict:
        return {
            "n_llamadas": len(self.llamadas),
            "costo_total_usd": self.costo_total(),
            "costo_promedio_usd": self.costo_total() / len(self.llamadas) if self.llamadas else 0
        }

tracker = CostTracker()
# Para usar: cada vez que haces una llamada, registrar tokens
# response = client.chat.completions.create(...)
# tracker.registrar("gpt-4o-mini", response.usage.prompt_tokens, response.usage.completion_tokens)

Resumen

  • CoT + Self-Consistency: La combinación más útil en producción. Cada muestra de SC usa CoT internamente. Mejora +5-15% con N=5.
  • ReAct + Structured Output: Para sistemas de producción. Datos externos + output parseable.
  • ToT + Self-Consistency: Para problemas muy difíciles con múltiples estrategias. Muy caro, usar con moderación.
  • Meta-prompting + Self-Refine: Para optimización sistemática de prompts.
  • La regla de oro: Combinar solo cuando cada técnica resuelve un problema distinto y real. No combinar por combinar.

Recursos adicionales

  1. ReAct (Yao et al., 2022)
  2. Self-Consistency (Wang et al., 2022)
  3. Tree of Thoughts (Yao et al., 2023)
  4. Self-Refine (Madaan et al., 2023)
  5. LLM-Blender: Ensembling LLMs with Pairwise Ranking - Combinar salidas de múltiples LLMs
  6. Prompt Engineering Guide - Combining Techniques