Módulo 4: Chain-of-Thought y Razonamiento
2. Zero-Shot CoT: "Let's Think Step by Step"
Descripción
La técnica más simple de CoT: añadir "Let's think step by step" (o variantes en español/inglés) al final del prompt. No requiere ejemplos. En esta cápsula aprenderás las variantes más efectivas, cómo funcionan internamente, benchmarks de mejora en datasets reales, y cómo extraer la respuesta final del razonamiento generado.
Tiempo estimado: 60-75 minutos
¿Qué es Zero-Shot CoT y por qué funciona?
Zero-Shot significa que no proporcionas ejemplos en el prompt. Solo añades una instrucción que activa el modo de razonamiento explícito del modelo.
La frase "Let's think step by step" fue descubierta por Kojima et al. (2022) de forma casi accidental durante experimentos con prompts. Lo sorprendente fue que una sola frase añadida al final mejoraba dramáticamente la accuracy en múltiples benchmarks sin ningún otro cambio.
¿Por qué funciona? Hay dos teorías principales:
-
Teoría de activación de patrones: El modelo ha visto millones de textos educativos donde frases como "step by step" preceden a explicaciones detalladas y correctas. Al generar esa frase, activa ese patrón estadístico.
-
Teoría de memoria de trabajo extendida: Al obligar al modelo a escribir pasos intermedios, esos pasos quedan en el contexto (la "ventana" del transformer) y actúan como memoria externa, reduciendo errores de cálculo.
Implementación Base
from openai import OpenAI
client = OpenAI() # Requiere OPENAI_API_KEY en variables de entorno
def resolver_con_cot(pregunta: str, idioma: str = "es") -> str:
"""
Resuelve una pregunta usando Zero-Shot CoT.
Args:
pregunta: El problema a resolver
idioma: "es" para español, "en" para inglés
Returns:
Respuesta del modelo con razonamiento incluido
"""
if idioma == "es":
instruccion_cot = "Piensa paso a paso."
else:
instruccion_cot = "Let's think step by step."
prompt = f"{pregunta}\n\n{instruccion_cot}"
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0, # 0 para máxima consistencia en razonamiento
max_tokens=800 # Suficiente para razonamiento + respuesta
)
return response.choices[0].message.content
# Ejemplo de uso básico
if __name__ == "__main__":
problema = """
Un kilo de manzanas cuesta $3.50. Si compro 2.5 kilos y pago con un billete
de $20, ¿cuánto cambio recibo?
"""
resultado = resolver_con_cot(problema)
print(resultado)
Output esperado:
Para resolver esto necesito:
Paso 1: Calcular el costo de 2.5 kilos de manzanas
- Precio por kilo: $3.50
- Kilos comprados: 2.5
- Costo total: 3.50 × 2.5 = $8.75
Paso 2: Calcular el cambio
- Billete: $20.00
- Costo: $8.75
- Cambio: 20.00 - 8.75 = $11.25
Por lo tanto, recibes $11.25 de cambio.
Variantes Efectivas de Zero-Shot CoT
No todas las frases de activación son igual de efectivas. Aquí tienes las variantes probadas con sus características:
VARIANTES_COT = {
# Variantes en inglés (originales)
"clasica_en": "Let's think step by step.",
"cuidadosa_en": "Let's work through this carefully.",
"razonada_en": "Reason through this step by step.",
"detallada_en": "Think step by step and explain each step.",
# Variantes en español
"clasica_es": "Piensa paso a paso.",
"detallada_es": "Razona paso a paso, explicando cada etapa.",
"verificada_es": "Piensa paso a paso y verifica tu respuesta al final.",
"estructurada_es": "Paso a paso:",
"cuidadosa_es": "Analiza esto cuidadosamente, paso a paso.",
# Variantes especializadas por dominio
"matematica": "Muestra cada operación matemática paso a paso.",
"logica": "Identifica las premisas y razona hacia la conclusión paso a paso.",
"codigo": "Traza la ejecución del código paso a paso.",
"decision": "Evalúa cada factor relevante paso a paso antes de decidir.",
}
def probar_variantes(problema: str, variantes: list[str]) -> dict:
"""Prueba múltiples variantes de CoT en el mismo problema."""
resultados = {}
for nombre, variante in variantes:
prompt = f"{problema}\n\n{variante}"
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=600
)
resultados[nombre] = response.choices[0].message.content
return resultados
Tabla Comparativa de Variantes
| Variante | Mejor para | Tokens generados | Precisión |
|---|---|---|---|
| "Let's think step by step" | General | Moderados | Alta |
| "Think step by step and explain" | Explicaciones pedagógicas | Muchos | Alta |
| "Piensa paso a paso" | General en español | Moderados | Alta |
| "Paso a paso:" | Respuestas cortas estructuradas | Pocos | Media-Alta |
| "Muestra cada operación matemática" | Aritmética/álgebra | Moderados | Muy alta |
| "Identifica premisas y razona" | Lógica formal | Moderados | Muy alta |
El Protocolo de Dos Pasos
Kojima et al. también propusieron una variación importante: el protocolo de dos pasos (two-stage prompting):
Paso 1: Obtener el razonamiento
prompt_paso1 = f"{pregunta}\n\nLet's think step by step."
razonamiento = obtener_respuesta(prompt_paso1)
Paso 2: Extraer la respuesta final
prompt_paso2 = f"""
{pregunta}
{razonamiento}
Por lo tanto, la respuesta final es:
"""
respuesta_final = obtener_respuesta(prompt_paso2)
Este protocolo es especialmente útil cuando el razonamiento es largo y la respuesta final puede perderse en el texto.
def zero_shot_cot_dos_pasos(pregunta: str) -> dict:
"""
Implementa el protocolo de dos pasos de Kojima et al.
Returns:
dict con 'razonamiento' y 'respuesta_final'
"""
# Paso 1: Generar razonamiento
prompt_razonamiento = f"{pregunta}\n\nLet's think step by step."
razonamiento_resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt_razonamiento}],
temperature=0,
max_tokens=600
)
razonamiento = razonamiento_resp.choices[0].message.content
# Paso 2: Extraer respuesta final
prompt_extraccion = f"""
{pregunta}
{razonamiento}
Por lo tanto, la respuesta final es:
"""
respuesta_resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt_extraccion}],
temperature=0,
max_tokens=100
)
respuesta_final = respuesta_resp.choices[0].message.content
return {
"razonamiento": razonamiento,
"respuesta_final": respuesta_final,
"tokens_totales": (
razonamiento_resp.usage.total_tokens +
respuesta_resp.usage.total_tokens
)
}
# Ejemplo de uso
if __name__ == "__main__":
problema = """
Ana, Beatriz y Carlos trabajan juntos en un proyecto.
Ana completa 1/3 del trabajo por día. Beatriz completa 1/4 por día.
Carlos completa 1/6 por día. ¿En cuántos días terminan si trabajan juntos?
"""
resultado = zero_shot_cot_dos_pasos(problema)
print("=== RAZONAMIENTO ===")
print(resultado["razonamiento"])
print("\n=== RESPUESTA FINAL ===")
print(resultado["respuesta_final"])
print(f"\nTokens usados: {resultado['tokens_totales']}")
Extracción de Respuesta Final
Cuando usas Zero-Shot CoT, el modelo mezcla razonamiento y respuesta. Necesitas extraer solo la respuesta final:
import re
from openai import OpenAI
client = OpenAI()
def extraer_respuesta_numerica(output_cot: str) -> str | None:
"""
Extrae el número final de una respuesta CoT.
Busca patrones como:
- "Por lo tanto, X"
- "La respuesta es X"
- "Therefore, X"
- "= X" al final
"""
patrones = [
r'(?:por lo tanto|therefore|la respuesta (?:final )?es|the answer is)[,:]?\s*([-\d,.$€%\.]+)',
r'(?:resultado|result|total)[:\s]+\$?([-\d,\.]+)',
r'=\s*([-\d,\.]+)\s*$',
r'(?:final answer|respuesta final)[:\s]+\$?([-\d,\.]+)',
]
output_lower = output_cot.lower()
for patron in patrones:
match = re.search(patron, output_lower, re.IGNORECASE | re.MULTILINE)
if match:
return match.group(1).strip()
# Fallback: último número en el texto
numeros = re.findall(r'\b\d+(?:[.,]\d+)?\b', output_cot)
return numeros[-1] if numeros else None
def extraer_respuesta_booleana(output_cot: str) -> str | None:
"""
Extrae una respuesta sí/no o válido/inválido de una respuesta CoT.
"""
output_lower = output_cot.lower()
# Buscar en la última parte del texto (donde suele estar la conclusión)
ultima_parte = output_lower[-300:]
patrones_afirmativo = ['válido', 'correcto', 'sí', 'verdadero', 'true', 'valid', 'yes']
patrones_negativo = ['inválido', 'incorrecto', 'no', 'falso', 'false', 'invalid']
for patron in patrones_afirmativo:
if patron in ultima_parte:
return "SÍ/VÁLIDO"
for patron in patrones_negativo:
if patron in ultima_parte:
return "NO/INVÁLIDO"
return None
def resolver_con_extraccion(pregunta: str, tipo: str = "numerico") -> dict:
"""
Resuelve con CoT y extrae la respuesta de forma limpia.
Args:
pregunta: El problema
tipo: "numerico", "booleano", o "texto"
Returns:
dict con 'razonamiento', 'respuesta_extraida', 'respuesta_cruda'
"""
prompt = f"{pregunta}\n\nPiensa paso a paso. Al final, escribe 'Respuesta final: [X]'"
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=700
)
output = response.choices[0].message.content
if tipo == "numerico":
respuesta = extraer_respuesta_numerica(output)
elif tipo == "booleano":
respuesta = extraer_respuesta_booleana(output)
else:
# Para tipo texto, toma la última línea
lineas = [l.strip() for l in output.split('\n') if l.strip()]
respuesta = lineas[-1] if lineas else output
return {
"razonamiento": output,
"respuesta_extraida": respuesta,
"respuesta_cruda": output[-200:] # Últimas 200 chars para debug
}
Benchmarks de Mejora
Estos son los resultados documentados de Zero-Shot CoT vs. prompting estándar:
Dataset GSM8K (Grade School Math)
| Modelo | Sin CoT | Con Zero-Shot CoT | Mejora |
|---|---|---|---|
| GPT-3 (175B) | 14.0% | 40.7% | +26.7pp |
| GPT-3.5 | 57.1% | 70.2% | +13.1pp |
| GPT-4 | 87.1% | 92.0% | +4.9pp |
| Claude 3 Haiku | 71.3% | 83.4% | +12.1pp |
Dataset MultiArith
| Modelo | Sin CoT | Con Zero-Shot CoT | Mejora |
|---|---|---|---|
| GPT-3 (175B) | 17.7% | 78.7% | +61.0pp |
| GPT-4 | 95.2% | 97.1% | +1.9pp |
Dataset SVAMP (variaciones de problemas aritméticos)
| Modelo | Sin CoT | Con Zero-Shot CoT | Mejora |
|---|---|---|---|
| GPT-3 (175B) | 67.7% | 74.2% | +6.5pp |
| GPT-3.5 | 79.1% | 83.2% | +4.1pp |
Observación importante: Los modelos más capaces (GPT-4) ya tienen accuracy alta sin CoT en tareas simples. La ganancia de CoT es mayor en modelos medianos y en problemas más complejos.
Cuándo Funciona Zero-Shot CoT
✅ Funciona bien en:
# Aritmética multi-paso
problema_aritmetica = """
Una tienda tiene descuento del 20% en todos los productos.
Un televisor cuesta $450 original. Además hay IVA del 16%.
¿Cuánto pagas en total?
"""
# Razonamiento lógico secuencial
problema_logica = """
Si está nublado, puede llover. Si llueve, Juan lleva paraguas.
Hoy Juan NO lleva paraguas. ¿Está nublado? ¿Llueve?
"""
# Problemas de secuencia/patrón
problema_patron = """
La secuencia es: 2, 6, 18, 54, ...
¿Cuál es el siguiente número? ¿Y el décimo número de la secuencia?
"""
# Problemas de velocidad, tiempo, distancia
problema_fisica = """
Dos ciclistas salen al mismo tiempo desde ciudades separadas 120 km.
El primero va a 25 km/h. El segundo a 35 km/h. ¿Cuándo se encuentran?
"""
❌ No funciona bien en:
# Creatividad
no_cot_1 = "Escribe un poema sobre el mar"
# CoT genera: "Paso 1: Elegir tema. Paso 2: Buscar rimas..." → poema mecánico
# Opinión/preferencias
no_cot_2 = "¿Python o JavaScript para mi proyecto?"
# No hay razonamiento 'correcto', depende del contexto
# Preguntas factuales directas
no_cot_3 = "¿En qué año se fundó Google?"
# La respuesta es 1998. CoT solo añade tokens: "Google es una empresa... fundada en... 1998"
# Traducción directa
no_cot_4 = "Traduce 'Hello World' al español"
# "Hola Mundo". No hay pasos que mostrar.
Self-Consistency: Mejora Avanzada de Zero-Shot CoT
Una extensión poderosa: generar múltiples respuestas CoT con temperatura > 0 y elegir la más común (votación por mayoría).
from collections import Counter
from openai import OpenAI
client = OpenAI()
def zero_shot_cot_self_consistency(
pregunta: str,
n_muestras: int = 5,
temperatura: float = 0.7
) -> dict:
"""
Self-Consistency: genera N razonamientos y vota la respuesta más frecuente.
Referencia: Wang et al., 2022 - "Self-Consistency Improves CoT Reasoning"
Args:
pregunta: El problema a resolver
n_muestras: Número de caminos de razonamiento a generar
temperatura: Mayor temperatura = mayor diversidad de caminos
Returns:
dict con todas las respuestas y la respuesta por mayoría
"""
respuestas = []
razonamientos = []
prompt = f"{pregunta}\n\nPiensa paso a paso. Al final escribe 'Respuesta: [X]'"
for i in range(n_muestras):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=temperatura,
max_tokens=600
)
output = response.choices[0].message.content
razonamientos.append(output)
# Extraer respuesta final
respuesta = extraer_respuesta_numerica(output)
respuestas.append(respuesta)
# Contar votos (ignorar None)
votos = Counter([r for r in respuestas if r is not None])
respuesta_ganadora = votos.most_common(1)[0][0] if votos else None
return {
"respuesta_final": respuesta_ganadora,
"distribucion_votos": dict(votos),
"confianza": votos[respuesta_ganadora] / n_muestras if respuesta_ganadora else 0,
"todos_razonamientos": razonamientos,
"todas_respuestas": respuestas
}
# Ejemplo de uso
if __name__ == "__main__":
problema = """
Carlos compra acciones: 100 acciones a $15 cada una.
Las vende cuando suben un 30%. ¿Cuál es su ganancia total?
"""
resultado = zero_shot_cot_self_consistency(problema, n_muestras=5)
print(f"Respuesta final (mayoría): {resultado['respuesta_final']}")
print(f"Distribución de votos: {resultado['distribucion_votos']}")
print(f"Confianza: {resultado['confianza']:.0%}")
Cuando usar Self-Consistency:
- Problemas críticos donde un error es costoso
- Cuando la accuracy importa más que la latencia/costo
- Con temperatura 0.5-0.8 para diversidad de caminos
Comparativa Completa: Sin CoT vs. Zero-Shot CoT vs. Self-Consistency
def benchmark_completo(
problemas: list[tuple[str, str]] # (problema, respuesta_correcta)
) -> dict:
"""
Compara tres enfoques en un conjunto de problemas.
"""
resultados = {
"sin_cot": {"correctas": 0, "tokens_promedio": 0},
"zero_shot_cot": {"correctas": 0, "tokens_promedio": 0},
"self_consistency": {"correctas": 0, "tokens_promedio": 0}
}
total_tokens = {k: [] for k in resultados}
for problema, respuesta_correcta in problemas:
# Enfoque 1: Sin CoT
r1 = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": problema}],
temperature=0, max_tokens=100
)
respuesta1 = r1.choices[0].message.content
if respuesta_correcta in respuesta1:
resultados["sin_cot"]["correctas"] += 1
total_tokens["sin_cot"].append(r1.usage.total_tokens)
# Enfoque 2: Zero-Shot CoT
r2 = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{problema}\n\nPiensa paso a paso."}],
temperature=0, max_tokens=500
)
respuesta2 = extraer_respuesta_numerica(r2.choices[0].message.content)
if respuesta2 and respuesta_correcta in str(respuesta2):
resultados["zero_shot_cot"]["correctas"] += 1
total_tokens["zero_shot_cot"].append(r2.usage.total_tokens)
# Enfoque 3: Self-Consistency (3 muestras para este benchmark)
r3 = zero_shot_cot_self_consistency(problema, n_muestras=3)
if r3["respuesta_final"] and respuesta_correcta in str(r3["respuesta_final"]):
resultados["self_consistency"]["correctas"] += 1
total_tokens["self_consistency"].append(
sum(600 for _ in range(3)) # Aproximación
)
n = len(problemas)
for metodo in resultados:
resultados[metodo]["accuracy"] = resultados[metodo]["correctas"] / n
resultados[metodo]["tokens_promedio"] = sum(total_tokens[metodo]) / n
return resultados
Troubleshooting
Problema 1: El modelo ignora la instrucción CoT
Síntomas: El modelo responde directamente sin mostrar pasos.
Causas y soluciones:
# ❌ CoT al inicio puede ser ignorado en prompts largos
prompt_malo = "Piensa paso a paso. Resuelve: 17 × 23"
# ✅ CoT al final del prompt tiene más efecto
prompt_bueno = "Resuelve: 17 × 23\n\nPiensa paso a paso."
# ✅ Alternativamente, usar como system message
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "Siempre razona paso a paso antes de dar tu respuesta final."
},
{"role": "user", "content": "Resuelve: 17 × 23"}
],
temperature=0
)
Problema 2: El razonamiento es muy largo
Síntomas: El modelo genera 10+ párrafos por un problema simple.
Soluciones:
# ✅ Limitar explícitamente los pasos
prompt = f"""
{pregunta}
Piensa paso a paso. Máximo 5 pasos concisos.
"""
# ✅ Pedir formato estructurado
prompt = f"""
{pregunta}
Resuelve en formato:
Paso 1: [acción]
Paso 2: [acción]
...
Respuesta final: [X]
"""
Problema 3: Respuesta incorrecta pero razonamiento correcto
Síntomas: Los pasos son correctos pero la respuesta final extrae mal.
# ✅ Añadir instrucción explícita de formato de respuesta final
prompt = f"""
{pregunta}
Piensa paso a paso.
Al final, escribe EXACTAMENTE en esta línea:
RESPUESTA: [número sin texto adicional]
"""
# ✅ Luego extraer con regex simple
import re
def extraer_respuesta_etiqueta(output: str) -> str | None:
match = re.search(r'RESPUESTA:\s*([\d,.$€%\.+-]+)', output, re.IGNORECASE)
return match.group(1).strip() if match else None
Problema 4: CoT produce respuestas diferentes en cada llamada
Síntomas: Con temperature=0 aún hay variación.
# ✅ Usar seed para reproducibilidad (si disponible)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[...],
temperature=0,
seed=42 # Para reproducibilidad
)
# ✅ Para problemas críticos, usar Self-Consistency
resultado = zero_shot_cot_self_consistency(problema, n_muestras=7)
Ejercicios
Ejercicio 1: Comparar con/sin CoT en 5 problemas aritméticos
Ejecuta el siguiente código, registra los resultados y calcula el accuracy de cada enfoque.
from openai import OpenAI
import re
client = OpenAI()
# Conjunto de prueba
problemas_con_respuesta = [
("¿Cuánto es 15 + 27?", "42"),
("¿Cuánto es 17 × 23?", "391"),
("Si tengo $150 y gasto el 40%, ¿cuánto me queda?", "90"),
("Una pizza de 8 porciones se reparte entre 3 personas. ¿Cuántas porciones enteras recibe cada uno y cuántas sobran?", "2"),
("Un auto va a 80 km/h durante 2.5 horas. ¿Cuántos kilómetros recorre?", "200"),
]
# Tu tarea: completar este código
def resolver(pregunta: str, cot: bool) -> str:
# TODO: Implementar
pass
# Cuenta aciertos para cada enfoque
Ver solución
from openai import OpenAI
import re
client = OpenAI()
problemas_con_respuesta = [
("¿Cuánto es 15 + 27?", "42"),
("¿Cuánto es 17 × 23?", "391"),
("Si tengo $150 y gasto el 40%, ¿cuánto me queda?", "90"),
("Una pizza de 8 porciones se reparte entre 3 personas. ¿Cuántas porciones enteras recibe cada uno y cuántas sobran?", "2"),
("Un auto va a 80 km/h durante 2.5 horas. ¿Cuántos kilómetros recorre?", "200"),
]
def resolver(pregunta: str, cot: bool) -> str:
contenido = pregunta
if cot:
contenido += "\n\nPiensa paso a paso. Al final escribe 'Respuesta: [número]'"
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": contenido}],
temperature=0,
max_tokens=500 if cot else 50
)
return response.choices[0].message.content
aciertos_sin = 0
aciertos_con = 0
for pregunta, correcta in problemas_con_respuesta:
r_sin = resolver(pregunta, cot=False)
r_con = resolver(pregunta, cot=True)
if correcta in r_sin:
aciertos_sin += 1
if correcta in r_con:
aciertos_con += 1
print(f"\nProblema: {pregunta[:50]}...")
print(f" Sin CoT: '{r_sin[:60]}'")
print(f" Con CoT: '{r_con[:60]}'")
n = len(problemas_con_respuesta)
print(f"\n=== RESULTADOS ===")
print(f"Sin CoT: {aciertos_sin}/{n} ({aciertos_sin/n:.0%})")
print(f"Con CoT: {aciertos_con}/{n} ({aciertos_con/n:.0%})")
Ejercicio 2: Extraer la respuesta final de un output CoT
Implementa la función extraer_respuesta que funcione correctamente en los siguientes casos:
casos_prueba = [
("Por lo tanto, el total es 391.", "391"),
("The answer is 42.5 dollars", "42.5"),
("Respuesta final: $89.00", "89.00"),
("En conclusión, hay 15 equipos.\nTotal: 15", "15"),
("RESPUESTA: 200", "200"),
]
Ver solución
import re
def extraer_respuesta(output: str) -> str | None:
"""
Extrae la respuesta numérica final de un output CoT.
Maneja múltiples formatos comunes.
"""
# Normalizar: eliminar espacios extra, convertir a minúsculas para búsqueda
output_clean = output.strip()
output_lower = output_clean.lower()
patrones = [
# "Respuesta: X" o "RESPUESTA: X"
r'respuesta(?:\s+final)?[:\s]+\$?([\d,\.]+)',
# "Por lo tanto, ... X"
r'(?:por lo tanto|therefore|en conclusión)[,.]?\s+(?:el|la|the|hay|son|es|are|is)?[^\d]*([\d,\.]+)',
# "The answer is X" o "La respuesta es X"
r'(?:the answer is|la respuesta es)[:\s]*\$?([\d,\.]+)',
# "Total: X"
r'total[:\s]+\$?([\d,\.]+)',
# "= X" al final de línea
r'=\s*([\d,\.]+)\s*$',
]
for patron in patrones:
match = re.search(patron, output_lower)
if match:
return match.group(1).replace(',', '')
# Fallback: último número en el texto
numeros = re.findall(r'\b\d+(?:[.,]\d+)?\b', output_clean)
if numeros:
return numeros[-1]
return None
# Verificar casos de prueba
casos_prueba = [
("Por lo tanto, el total es 391.", "391"),
("The answer is 42.5 dollars", "42.5"),
("Respuesta final: $89.00", "89.00"),
("En conclusión, hay 15 equipos.\nTotal: 15", "15"),
("RESPUESTA: 200", "200"),
]
for output, esperado in casos_prueba:
resultado = extraer_respuesta(output)
estado = "✓" if resultado == esperado else f"✗ (obtuvo '{resultado}')"
print(f"{estado} | Input: '{output[:40]}' | Esperado: '{esperado}'")
Ejercicio 3: Implementar Self-Consistency con 5 muestras
Implementa Self-Consistency para el problema: "Un vendedor gana comisión del 8% sobre ventas. En enero vendió $12,500. En febrero vendió $9,800. ¿Cuánto ganó en total de comisiones?"
Ver solución
from openai import OpenAI
from collections import Counter
import re
client = OpenAI()
def extraer_numero_final(texto: str) -> str | None:
patrones = [
r'(?:respuesta|total|ganó)[:\s]+\$?([\d,\.]+)',
r'\$\s*([\d,\.]+)',
r'=\s*\$?([\d,\.]+)\s*$',
]
for patron in patrones:
m = re.search(patron, texto.lower())
if m:
return m.group(1).replace(',', '')
numeros = re.findall(r'\b\d+(?:[.,]\d+)?\b', texto)
return numeros[-1] if numeros else None
problema = """
Un vendedor gana comisión del 8% sobre ventas.
En enero vendió $12,500. En febrero vendió $9,800.
¿Cuánto ganó en total de comisiones?
"""
prompt = f"{problema}\n\nPiensa paso a paso. Al final escribe 'Respuesta: $[número]'"
# Respuesta esperada: (12500 + 9800) * 0.08 = 22300 * 0.08 = $1784
respuestas = []
for i in range(5):
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.7, # Algo de diversidad para Self-Consistency
max_tokens=400
)
output = r.choices[0].message.content
respuesta = extraer_numero_final(output)
respuestas.append(respuesta)
print(f"Muestra {i+1}: {respuesta}")
votos = Counter([r for r in respuestas if r])
ganadora, votos_ganadora = votos.most_common(1)[0]
print(f"\nRespuesta final (mayoría): ${ganadora}")
print(f"Confianza: {votos_ganadora}/5 = {votos_ganadora/5:.0%}")
print(f"Respuesta correcta: $1784")
Ejercicio 4: Prueba CoT en razonamiento lógico
Prueba esta secuencia de afirmaciones con y sin CoT. ¿Cuál es la conclusión correcta?
Todas las rosas son flores.
Algunas flores se marchitan rápido.
¿Necesariamente algunas rosas se marchitan rápido?
Ver solución
Respuesta correcta: NO necesariamente.
Razonamiento con CoT:
Premisa 1: Todas las rosas son flores → rosas ⊆ flores
Premisa 2: Algunas flores se marchitan rápido → existe x: flor(x) ∧ marchita_rápido(x)
¿Conclusión? ¿Existe x: rosa(x) ∧ marchita_rápido(x)?
Para que la conclusión sea válida, necesitaría que esas "algunas flores" que se marchitan
rápido pertenezcan al subconjunto "rosas". Pero la premisa 2 solo dice que *algunas* flores
se marchitan, no especifica cuáles.
Contraejemplo: Las flores que se marchitan rápido son solo tulipanes. Las rosas no se
marchitan rápido. Las premisas 1 y 2 se cumplen, pero la conclusión es falsa.
Por lo tanto: INVÁLIDO / NO necesariamente.
Sin CoT, los modelos a veces responden "sí" por el patrón superficial "rosas son flores, flores se marchitan → rosas se marchitan". Con CoT, el modelo detecta la falacia.
pregunta = """
Todas las rosas son flores.
Algunas flores se marchitan rápido.
¿Necesariamente algunas rosas se marchitan rápido? ¿Por qué?
"""
r_sin = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": pregunta}],
temperature=0, max_tokens=100
)
r_con = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": pregunta + "\n\nRazona paso a paso."}],
temperature=0, max_tokens=400
)
print("Sin CoT:", r_sin.choices[0].message.content)
print("\nCon CoT:", r_con.choices[0].message.content)
Ejercicio 5: Midiendo el costo de Zero-Shot CoT
Calcula cuánto cuesta usar Zero-Shot CoT vs. respuesta directa en producción con 10,000 llamadas diarias al problema de las manzanas.
Ver solución
from openai import OpenAI
client = OpenAI()
problema = "Un kilo de manzanas cuesta $3.50. Si compro 2.5 kilos y pago con $20, ¿cuánto cambio recibo?"
# Medir tokens de cada enfoque
r_directo = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": problema}],
temperature=0, max_tokens=50
)
r_cot = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": problema + "\n\nPiensa paso a paso."}],
temperature=0, max_tokens=400
)
tokens_directo = r_directo.usage.total_tokens
tokens_cot = r_cot.usage.total_tokens
# gpt-4o-mini precios (aproximados a marzo 2026)
# Input: $0.15/M tokens, Output: $0.60/M tokens
precio_por_token = 0.0000006 # Promedio input+output
llamadas_por_dia = 10_000
costo_directo_dia = tokens_directo * precio_por_token * llamadas_por_dia
costo_cot_dia = tokens_cot * precio_por_token * llamadas_por_dia
print(f"Tokens por llamada (directo): {tokens_directo}")
print(f"Tokens por llamada (CoT): {tokens_cot}")
print(f"Multiplicador CoT: {tokens_cot/tokens_directo:.1f}x")
print(f"\nCosto diario directo (10k llamadas): ${costo_directo_dia:.2f}")
print(f"Costo diario CoT (10k llamadas): ${costo_cot_dia:.2f}")
print(f"Costo adicional CoT: ${costo_cot_dia - costo_directo_dia:.2f}/día")
print(f"Costo adicional anual: ${(costo_cot_dia - costo_directo_dia) * 365:.0f}")
Conclusión: Para 10,000 llamadas diarias, el costo adicional de CoT suele ser de $1-5/día con gpt-4o-mini. Justificado si mejora la accuracy en casos donde un error es costoso.
Resumen
- Zero-Shot CoT: Añadir "Let's think step by step" o "Piensa paso a paso" al final del prompt
- Mejora típica: +15-60% en math/logic según la complejidad y el modelo base
- Variantes: Múltiples frases funcionan; elige según el dominio
- Protocolo de dos pasos: Razonamiento → Extracción de respuesta final
- Self-Consistency: Genera N razonamientos y vota la respuesta más común para mayor accuracy
- Extracción: Usa regex o instrucción explícita para obtener la respuesta limpia
- Costo: 3-5x más tokens que respuesta directa; usar cuando el error tiene costo alto