
Proyecto PINSAPIA — Línea Péter: “Piensa Paso a Paso”
¿Acierta más una Inteligencia Artificial cuando se le exige responder de forma inmediata o cuando se le permite “pensar en alto” antes de dar el resultado? En la inferencia de un Modelo de Lenguaje (LLM), la generación de cada palabra sirve como un estado intermedio de cómputo. Cuando le pedimos al modelo que desglose un problema en pasos, no estamos activando un “cerebro digital” distinto: estamos utilizando los propios tokens generados como una memoria de trabajo (Chain-of-Thought).
En esta entrega del Proyecto PINSAPIA, presentamos la Línea Péter, bautizada en honor a Rózsa Péter, matemática húngara y fundadora de la teoría de funciones recursivas. La premisa central de su trabajo es que cualquier problema complejo puede resolverse si se descompone de forma metódica en subproblemas o pasos más pequeños y bien definidos, la base lógica exacta que sostiene la técnica de Prompting por Cadena de Pensamiento.
Mediante nuestra aplicación interactiva (ejecutada localmente en el puerto 8094), el alumnado contrastará de forma empírica la precisión real del modelo ante problemas de lógica y matemáticas simples al comparar dos modos de ejecución: Modo Directo vs. Modo Razonado (Paso a Paso).
🔗 Conexión con las Líneas Anteriores
- Desde la Línea von Neumann: Mantenemos la temperatura fija y baja ($T = 0.3$) en ambos modos. Esto nos asegura que las diferencias en la tasa de acierto se deban al método de razonamiento y no a la varianza aleatoria del muestreo.
- Desde la Línea Gebru: Aplicamos evaluación determinista sin autoevaluación. La aplicación no le pregunta al LLM si ha acertado (lo que introduciría sesgo); en su lugar, un algoritmo en Python extrae la respuesta final mediante expresiones regulares y la contrasta con una solución matemática verificable.
🎯 Guía de Exploración Práctica para el Alumnado
Esta guía está diseñada para auditar la capacidad de razonamiento del modelo local en tiempo real y medir el coste asociado en tiempo y tokens.
Objetivo del Laboratorio
Verificar si la estrategia de “pensar paso a paso” (Chain-of-Thought) incrementa el porcentaje de aciertos en problemas inductivos o engañosos, identificando al mismo tiempo el consumo adicional de tiempo de cómputo.
🧪 Misiones y Retos Prácticos
Reto 1: El Anzuelo Cognitivo (p3: El Bate y la Pelota)
- Selecciona en la aplicación web el problema
p3: Lógica de enunciado engañoso:
“Un bate y una pelota cuestan 1,10€ en total. El bate cuesta 1€ más que la pelota. ¿Cuánto cuesta la pelota?” - Configura 3 Repeticiones por modo y mantén la temperatura en 0.3.
- Haz clic en ▶️ LANZAR LOS DOS MODOS.
- Observa el Marcador Deportivo central y abre los desplegables de las respuestas.
Pregunta de análisis: ¿Qué responde el modelo en Modo Directo? ¿Cae en la trampa intuitiva de decir $0,10€$? ¿Cómo cambia el resultado en Modo Razonado al plantear la ecuación $x + (x + 1) = 1,10$?
Reto 2: Secuencias Inductivas y Proporciones (p1 y p4)
- Marca los problemas
p1 (Proporciones: Ana y Juan)yp4 (Secuencia: 2, 6, 12, 20, 30). - Ejecuta el experimento con 3 Repeticiones.
- Compara la tasa global de aciertos entre ambos enfoques en la tabla comparativa.
Pregunta de análisis: En problemas aritméticos de varios pasos, ¿en qué punto del proceso tiende a equivocarse el Modo Directo en comparación con el desglose del Modo Razonado?
Reto 3: La Balanza del Cómputo (Tiempo vs. Precisión)
- Revisa el Gráfico de Tiempo Medio por Modo (ms) generado en la parte inferior de los resultados.
- Calcula la diferencia porcentual de tiempo transcurrido entre la respuesta directa y la razonada.
Pregunta de análisis: Si el Modo Razonado tarda el triple de tiempo en responder, ¿en qué tipo de tareas reales merece la pena asumir ese coste y en cuáles es preferible la respuesta inmediata?
📋 Ficha de Registro y Resultados en Aula
| Problema | Modo Directo (% Acierto) | Modo Razonado (% Acierto) | Tiempo Directo (ms) | Tiempo Razonado (ms) |
| p1: Proporciones (Ana y Juan) | ||||
| p3: Bate y Pelota (CRT) | ||||
| p4: Secuencia Numérica |
💬 Pregunta de Cierre para el Debate en Clase:
“Si razonar paso a paso mejora tanto la precisión, ¿por qué no pedírselo siempre en cualquier pregunta? ¿Qué se pierde (tiempo, tokens, coste) al hacerlo? ¿Y qué pasa si el problema es tan simple que no necesita pasos — puede el razonamiento forzado empeorar la respuesta?”



🛠️ Arquitectura Técnica del Proyecto (Puerto 8094)
- Banco de Problemas con Evaluación Oculta: Las respuestas correctas están almacenadas exclusivamente en el backend (
app_peter.py) para garantizar una corrección objetiva. - Extracción Robusta por Expresiones Regulares:
- Números: Extrae el último valor numérico del texto (contemplando formato decimal con coma o punto).
- Texto Corto (SÍ / NO): Escanea la presencia de confirmación/negación priorizando la mención final para evitar falsos positivos si el LLM repite el enunciado.
- Mapeo de Prompts Controlado:
PROMPT_DIRECTO: Enuncia el problema y exige responder únicamente con el dato final.PROMPT_RAZONADO: Obliga a incluir el razonamiento paso a paso y declarar explícitamenteRespuesta final: X.
- Métricas de Trade-off Integradas: Calcula la mejora en puntos porcentuales de precisión frente al incremento relativo en milisegundos de inferencia.
💻 Código Fuente Completo (app_peter.py)
Crea un archivo llamado app_peter.py e introduce el siguiente código:
Python
import json
import re
import statistics
import time
import urllib.request
from typing import Any, Dict, List, Optional
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import uvicorn
app = FastAPI(
title="Proyecto PINSAPIA - Línea Péter",
description="Evaluación de Chain-of-Thought (Piensa Paso a Paso) vs Modo Directo",
version="1.0.0",
)
# -----------------------------------------------------------------------------
# 1. BANCO DE PROBLEMAS (EVALUACIÓN AUTOMÁTICA)
# -----------------------------------------------------------------------------
BANCO_PROBLEMAS = [
{
"id": "p1",
"categoria": "Proporciones",
"enunciado": "Ana tiene el doble de manzanas que Juan. Entre los dos tienen 12 manzanas. ¿Cuántas manzanas tiene Juan?",
"respuesta_correcta": 4.0,
"tipo_respuesta": "numero"
},
{
"id": "p2",
"categoria": "Edades",
"enunciado": "Dentro de 5 años, María tendrá 20 años. ¿Cuántos años tiene ahora?",
"respuesta_correcta": 15.0,
"tipo_respuesta": "numero"
},
{
"id": "p3",
"categoria": "Lógica de enunciado engañoso",
"enunciado": "Un bate y una pelota cuestan 1,10€ en total. El bate cuesta 1€ más que la pelota. ¿Cuánto cuesta la pelota?",
"respuesta_correcta": 0.05,
"tipo_respuesta": "numero"
},
{
"id": "p4",
"categoria": "Secuencias",
"enunciado": "¿Qué número sigue en la secuencia 2, 6, 12, 20, 30?",
"respuesta_correcta": 42.0,
"tipo_respuesta": "numero"
},
{
"id": "p5",
"categoria": "Deducción simple",
"enunciado": "Todos los alumnos de 4ºESO han hecho el examen de Física. Pedro no ha hecho el examen de Física. ¿Pedro está en 4ºESO? Responde solo SÍ o NO.",
"respuesta_correcta": "NO",
"tipo_respuesta": "texto_corto"
}
]
PROMPT_DIRECTO = "{enunciado}\nResponde SOLO con la respuesta final, sin explicación."
PROMPT_RAZONADO = "{enunciado}\nPiensa paso a paso, muestra tu razonamiento, y al final escribe claramente: 'Respuesta final: X'."
# -----------------------------------------------------------------------------
# 2. MODELOS DE DATOS (PYDANTIC)
# -----------------------------------------------------------------------------
class EjecutarExperimentoRequest(BaseModel):
problemas_ids: List[str]
n_repeticiones: int = 3
temperatura: float = 0.3
class NuevoProblemaRequest(BaseModel):
categoria: str
enunciado: str
respuesta_correcta: str
tipo_respuesta: str
# -----------------------------------------------------------------------------
# 3. FUNCIONES DE EXTRACCIÓN Y VERIFICACIÓN
# -----------------------------------------------------------------------------
def extraer_respuesta_numero(texto: str) -> Optional[float]:
if not texto:
return None
# Búsqueda prioritaria si incluye 'Respuesta final:'
match_rf = re.search(r'Respuesta final:\s*(-?\d+(?:[.,]\d+)?)', texto, re.IGNORECASE)
if match_rf:
val_str = match_rf.group(1).replace(',', '.')
try:
return float(val_str)
except ValueError:
pass
# Fallback: capturar el último número del texto completo
numeros = re.findall(r'-?\d+(?:[.,]\d+)?', texto)
if numeros:
val_str = numeros[-1].replace(',', '.')
try:
return float(val_str)
except ValueError:
return None
return None
def extraer_respuesta_texto_corto(texto: str) -> Optional[str]:
if not texto:
return None
texto_upper = texto.upper()
# Priorizar presencia final
pos_si = max(texto_upper.rfind("SÍ"), texto_upper.rfind("SI"))
pos_no = texto_upper.rfind("NO")
if pos_si == -1 and pos_no == -1:
return None
if pos_si > pos_no:
return "SÍ"
return "NO"
def es_correcta(extraida: Any, correcta: Any, tipo_respuesta: str) -> bool:
if extraida is None:
return False
if tipo_respuesta == "numero":
try:
return abs(float(extraida) - float(correcta)) < 0.001
except (ValueError, TypeError):
return False
elif tipo_respuesta == "texto_corto":
ext_str = str(extraida).strip().upper().replace("SI", "SÍ")
cor_str = str(correcta).strip().upper().replace("SI", "SÍ")
return ext_str == cor_str
return False
def consultar_lmstudio(prompt: str, temperatura: float, timeout_sec: int = 40) -> Dict[str, Any]:
inicio = time.perf_counter()
url = "http://localhost:1234/v1/chat/completions"
payload = {
"messages": [{"role": "user", "content": prompt}],
"temperature": float(temperatura),
"top_p": 1.0,
"max_tokens": 250
}
try:
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout_sec) as response:
res_body = json.loads(response.read().decode("utf-8"))
texto = res_body["choices"][0]["message"]["content"].strip()
tiempo_ms = round((time.perf_counter() - inicio) * 1000, 2)
return {"ok": True, "texto": texto, "tiempo_ms": tiempo_ms}
except Exception as e:
tiempo_ms = round((time.perf_counter() - inicio) * 1000, 2)
return {"ok": False, "texto": f"Error: {str(e)}", "tiempo_ms": tiempo_ms}
# -----------------------------------------------------------------------------
# 4. ENDPOINTS DE LA API
# -----------------------------------------------------------------------------
@app.get("/api/banco_problemas")
def get_banco_problemas():
# Ocultar respuesta_correcta en el listado inicial
return [
{
"id": p["id"],
"categoria": p["categoria"],
"enunciado": p["enunciado"],
"tipo_respuesta": p["tipo_respuesta"]
}
for p in BANCO_PROBLEMAS
]
@app.post("/api/anadir_problema")
def anadir_problema(req: NuevoProblemaRequest):
nuevo_id = f"p{len(BANCO_PROBLEMAS) + 1}"
resp_cor: Any = req.respuesta_correcta
if req.tipo_respuesta == "numero":
try:
resp_cor = float(req.respuesta_correcta.replace(',', '.'))
except ValueError:
raise HTTPException(status_code=400, detail="La respuesta correcta debe ser un número válido.")
nuevo_item = {
"id": nuevo_id,
"categoria": req.categoria.strip(),
"enunciado": req.enunciado.strip(),
"respuesta_correcta": resp_cor,
"tipo_respuesta": req.tipo_respuesta
}
BANCO_PROBLEMAS.append(nuevo_item)
return {"ok": True, "id": nuevo_id, "mensaje": "Problema añadido con éxito."}
@app.post("/api/ejecutar_experimento")
def api_ejecutar_experimento(req: EjecutarExperimentoRequest):
if not req.problemas_ids:
raise HTTPException(status_code=400, detail="Debes seleccionar al menos un problema.")
problemas_sel = [p for p in BANCO_PROBLEMAS if p["id"] in req.problemas_ids]
if not problemas_sel:
raise HTTPException(status_code=400, detail="No se encontraron los problemas solicitados.")
inicio_exp = time.perf_counter()
resultados_por_problema = []
total_aciertos_dir = 0
total_intentos_dir = 0
tiempos_dir = []
total_aciertos_raz = 0
total_intentos_raz = 0
tiempos_raz = []
for prob in problemas_sel:
# Modo Directo
prompt_dir = PROMPT_DIRECTO.format(enunciado=prob["enunciado"])
intentos_dir = []
aciertos_dir = 0
for _ in range(req.n_repeticiones):
res = consultar_lmstudio(prompt_dir, req.temperatura)
txt = res["texto"]
t_ms = res["tiempo_ms"]
tiempos_dir.append(t_ms)
if prob["tipo_respuesta"] == "numero":
ext = extraer_respuesta_numero(txt)
else:
ext = extraer_respuesta_texto_corto(txt)
correct = es_correcta(ext, prob["respuesta_correcta"], prob["tipo_respuesta"])
if correct:
aciertos_dir += 1
intentos_dir.append({
"texto_completo": txt,
"respuesta_extraida": ext,
"correcta": correct,
"tiempo_ms": t_ms
})
total_aciertos_dir += aciertos_dir
total_intentos_dir += req.n_repeticiones
# Modo Razonado
prompt_raz = PROMPT_RAZONADO.format(enunciado=prob["enunciado"])
intentos_raz = []
aciertos_raz = 0
for _ in range(req.n_repeticiones):
res = consultar_lmstudio(prompt_raz, req.temperatura)
txt = res["texto"]
t_ms = res["tiempo_ms"]
tiempos_raz.append(t_ms)
if prob["tipo_respuesta"] == "numero":
ext = extraer_respuesta_numero(txt)
else:
ext = extraer_respuesta_texto_corto(txt)
correct = es_correcta(ext, prob["respuesta_correcta"], prob["tipo_respuesta"])
if correct:
aciertos_raz += 1
intentos_raz.append({
"texto_completo": txt,
"respuesta_extraida": ext,
"correcta": correct,
"tiempo_ms": t_ms
})
total_aciertos_raz += aciertos_raz
total_intentos_raz += req.n_repeticiones
resultados_por_problema.append({
"id": prob["id"],
"categoria": prob["categoria"],
"enunciado": prob["enunciado"],
"respuesta_correcta": prob["respuesta_correcta"],
"tipo_respuesta": prob["tipo_respuesta"],
"modo_directo": {
"intentos": intentos_dir,
"aciertos": aciertos_dir,
"total": req.n_repeticiones,
"porcentaje_acierto": round(aciertos_dir / req.n_repeticiones, 2)
},
"modo_razonado": {
"intentos": intentos_raz,
"aciertos": aciertos_raz,
"total": req.n_repeticiones,
"porcentaje_acierto": round(aciertos_raz / req.n_repeticiones, 2)
}
})
pct_dir = round((total_aciertos_dir / total_intentos_dir) * 100, 1) if total_intentos_dir > 0 else 0.0
pct_raz = round((total_aciertos_raz / total_intentos_raz) * 100, 1) if total_intentos_raz > 0 else 0.0
t_med_dir = round(statistics.mean(tiempos_dir), 1) if tiempos_dir else 0.0
t_med_raz = round(statistics.mean(tiempos_raz), 1) if tiempos_raz else 0.0
dif_pct_tiempo = round(((t_med_raz - t_med_dir) / t_med_dir) * 100, 1) if t_med_dir > 0 else 0.0
tiempo_total_ms = round((time.perf_counter() - inicio_exp) * 1000, 2)
return {
"temperatura": req.temperatura,
"n_repeticiones": req.n_repeticiones,
"resultados_por_problema": resultados_por_problema,
"resumen_global": {
"porcentaje_acierto_directo": pct_dir,
"porcentaje_acierto_razonado": pct_raz,
"diferencia_puntos_porcentuales": round(pct_raz - pct_dir, 1),
"tiempo_medio_directo_ms": t_med_dir,
"tiempo_medio_razonado_ms": t_med_raz,
"tiempo_extra_razonado_pct": dif_pct_tiempo
},
"tiempo_total_ms": tiempo_total_ms
}
# -----------------------------------------------------------------------------
# 5. FRONTEND HTML EMBEDDED
# -----------------------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
def index():
return HTML_CONTENT
HTML_CONTENT = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PINSAPIA - Línea Péter: Piensa Paso a Paso</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="bg-slate-100 font-sans min-h-screen pb-12">
<div class="max-w-7xl mx-auto px-4 py-6">
<header class="bg-slate-900 text-white rounded-2xl p-6 shadow-xl border border-slate-800 mb-6">
<div class="flex items-center justify-between">
<div>
<span class="text-xs font-bold uppercase tracking-widest text-emerald-400 bg-emerald-950/60 px-3 py-1 rounded-full border border-emerald-800">
PROYECTO PINSAPIA - LÍNEA PÉTER
</span>
<h1 class="text-2xl md:text-3xl font-extrabold mt-2 flex items-center gap-3">
<span>🧠 "Piensa Paso a Paso": Razonamiento CoT vs Modo Directo</span>
</h1>
<p class="text-slate-400 text-xs mt-1">Homenaje a Rózsa Péter: funciones recursivas y descomposición metódica de problemas</p>
</div>
<i class="fa-solid fa-brain text-emerald-400 text-5xl opacity-80 hidden sm:block"></i>
</div>
</header>
<section class="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm mb-6">
<div class="mb-4">
<label class="block font-bold text-slate-800 text-sm mb-2 flex items-center gap-2">
<i class="fa-solid fa-list-check text-emerald-600"></i>
SELECCIONA PROBLEMAS DEL BANCO EVALUABLE:
</label>
<div id="problemas-container" class="grid grid-cols-1 md:grid-cols-2 gap-3"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200">
<div class="flex justify-between items-center mb-2">
<label class="font-bold text-slate-700 text-xs uppercase">Temperatura (T):</label>
<span id="temp-val" class="font-mono font-extrabold text-lg text-emerald-600">0.3</span>
</div>
<input type="range" id="temp-range" min="0.0" max="1.0" step="0.1" value="0.3" oninput="document.getElementById('temp-val').innerText = this.value" class="w-full h-2 bg-slate-300 rounded-lg appearance-none cursor-pointer accent-emerald-600">
<p class="text-[11px] text-slate-500 mt-2">💡 <strong>Nota metodológica:</strong> Temperatura baja fija (0.3) para atribuir el contraste al modo de pensamiento y no al azar (conexión con línea von Neumann).</p>
</div>
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200 flex flex-col justify-between">
<div>
<label class="block font-bold text-slate-700 text-xs uppercase mb-2">Repeticiones por modo (N):</label>
<select id="n-repeticiones" class="w-full border border-slate-300 rounded-lg p-2 font-mono text-sm bg-white focus:ring-2 focus:ring-emerald-500 focus:outline-none">
<option value="1">1 Muestra (Rápido)</option>
<option value="3" selected>3 Muestras (Recomendado)</option>
<option value="5">5 Muestras (Alta precisión)</option>
</select>
</div>
<p class="text-[11px] text-slate-500 mt-2">Permite calcular el % medio de aciertos por cada problema y estrategia.</p>
</div>
</div>
<button id="btn-lanzar" onclick="ejecutarExperimento()" class="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3.5 px-6 rounded-xl shadow-md transition flex items-center justify-center gap-2">
<i class="fa-solid fa-play"></i> ▶️ LANZAR LOS DOS MODOS (DIRECTO VS RAZONADO)
</button>
<div id="progress-container" class="hidden mt-4">
<div class="flex justify-between text-xs font-bold text-slate-600 mb-1">
<span id="progress-text">Iniciando evaluación...</span>
</div>
<div class="w-full bg-slate-200 rounded-full h-3 overflow-hidden">
<div id="progress-bar" class="bg-emerald-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</section>
<div id="results-section" class="hidden space-y-6">
<!-- MARCADOR DEPORTIVO -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-red-50 border-2 border-red-300 p-6 rounded-2xl shadow-sm flex items-center justify-between">
<div>
<span class="text-xs font-bold text-red-700 uppercase tracking-wider block">MODO DIRECTO</span>
<span id="score-directo" class="text-4xl font-black text-red-600 font-mono">0.0%</span>
<span id="time-directo" class="text-xs text-red-500 block mt-1 font-mono">0 ms med / respuesta</span>
</div>
<i class="fa-solid fa-bolt text-red-300 text-5xl"></i>
</div>
<div class="bg-emerald-50 border-2 border-emerald-300 p-6 rounded-2xl shadow-sm flex items-center justify-between">
<div>
<span class="text-xs font-bold text-emerald-700 uppercase tracking-wider block">MODO RAZONADO (CoT)</span>
<span id="score-razonado" class="text-4xl font-black text-emerald-600 font-mono">0.0%</span>
<span id="time-razonado" class="text-xs text-emerald-500 block mt-1 font-mono">0 ms med / respuesta</span>
</div>
<i class="fa-solid fa-brain text-emerald-300 text-5xl"></i>
</div>
</div>
<!-- TABLA POR PROBLEMA -->
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
<h3 class="font-bold text-slate-800 text-xs uppercase mb-4 flex items-center gap-2">
<i class="fa-solid fa-table-list text-emerald-600"></i>
DESGLOSE Y AUDITORÍA POR PROBLEMA
</h3>
<div id="tabla-problemas" class="space-y-4"></div>
</div>
<!-- GRÁFICO COMPARATIVO DE TIEMPO -->
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
<h3 class="font-bold text-slate-800 text-xs uppercase mb-4 flex items-center gap-2">
<i class="fa-solid fa-clock text-emerald-600"></i>
TRADE-OFF DE TIEMPO DE CÓMPUTO (MS)
</h3>
<div class="h-60 relative">
<canvas id="chart-tiempos"></canvas>
</div>
</div>
</div>
<section class="bg-slate-900 text-slate-200 rounded-2xl p-6 shadow-xl border border-slate-800 mt-6">
<h3 class="text-xs font-bold uppercase tracking-widest text-emerald-400 mb-3 flex items-center gap-2">
<i class="fa-solid fa-terminal"></i>
METADATOS Y REGISTRO RAW
</h3>
<div class="p-3 bg-slate-800 rounded-lg border border-slate-700 text-xs text-slate-300 mb-4">
⚠️ <strong>Aviso metodológico:</strong> La extracción de la respuesta numérica usa una expresión regular sobre el texto generado (buscando la etiqueta final o el último número). Si el texto generado es atípico, revisa el acordeón de respuestas crudas.
</div>
<details class="cursor-pointer">
<summary class="text-xs text-slate-300 font-bold hover:text-emerald-400 transition mb-2">
▼ Ver JSON crudo completo devuelto por el backend
</summary>
<pre id="raw-json" class="p-4 bg-slate-950 text-emerald-300 rounded-xl text-xs font-mono overflow-x-auto max-h-80 border border-slate-800 whitespace-pre-wrap break-all">{}</pre>
</details>
</section>
</div>
<script>
let bancoProblemas = [];
let chartInstance = null;
document.addEventListener("DOMContentLoaded", cargarBanco);
async function cargarBanco() {
try {
const res = await fetch("/api/banco_problemas");
bancoProblemas = await res.json();
const container = document.getElementById("problemas-container");
container.innerHTML = "";
bancoProblemas.forEach(p => {
const checked = (p.id === "p3") ? "checked" : "";
const card = document.createElement("label");
card.className = "bg-slate-50 p-3 rounded-xl border border-slate-200 hover:border-emerald-500 cursor-pointer transition flex items-start gap-3";
card.innerHTML = `
<input type="checkbox" value="${p.id}" ${checked} class="chk-problema mt-1 accent-emerald-600">
<div>
<div class="flex items-center gap-2">
<span class="text-[10px] font-bold uppercase bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded">${p.categoria}</span>
<span class="text-xs font-mono font-bold text-slate-500">${p.id}</span>
</div>
<p class="text-xs text-slate-800 mt-1 font-mono leading-relaxed">"${p.enunciado}"</p>
</div>
`;
container.appendChild(card);
});
} catch (e) { console.error(e); }
}
async function ejecutarExperimento() {
const chks = document.querySelectorAll(".chk-problema:checked");
const ids = Array.from(chks).map(c => c.value);
const temp = parseFloat(document.getElementById("temp-range").value);
const n = parseInt(document.getElementById("n-repeticiones").value);
if (ids.length === 0) {
alert("Selecciona al menos un problema para evaluar.");
return;
}
document.getElementById("btn-lanzar").disabled = true;
document.getElementById("progress-container").classList.remove("hidden");
document.getElementById("results-section").classList.add("hidden");
document.getElementById("progress-bar").style.width = "40%";
document.getElementById("progress-text").innerText = "Ejecutando inferencias en LM Studio...";
try {
const res = await fetch("/api/ejecutar_experimento", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
problemas_ids: ids,
n_repeticiones: n,
temperatura: temp
})
});
document.getElementById("progress-bar").style.width = "100%";
const data = await res.json();
renderizarResultados(data);
} catch (e) {
alert("Error de conexión al ejecutar el experimento.");
console.error(e);
} finally {
document.getElementById("btn-lanzar").disabled = false;
document.getElementById("progress-container").classList.add("hidden");
}
}
function renderizarResultados(data) {
document.getElementById("results-section").classList.remove("hidden");
document.getElementById("raw-json").textContent = JSON.stringify(data, null, 2);
const resGlobal = data.resumen_global;
document.getElementById("score-directo").innerText = `${resGlobal.porcentaje_acierto_directo}%`;
document.getElementById("time-directo").innerText = `${resGlobal.tiempo_medio_directo_ms} ms / respuesta`;
document.getElementById("score-razonado").innerText = `${resGlobal.porcentaje_acierto_razonado}%`;
document.getElementById("time-razonado").innerText = `${resGlobal.tiempo_medio_razonado_ms} ms (+${resGlobal.tiempo_extra_razonado_pct}%)`;
// Tabla por problemas
const containerTabla = document.getElementById("tabla-problemas");
containerTabla.innerHTML = "";
data.resultados_por_problema.forEach(p => {
const card = document.createElement("div");
card.className = "border border-slate-200 rounded-xl p-4 bg-slate-50 space-y-3";
const accDir = `${p.modo_directo.aciertos}/${p.modo_directo.total}`;
const accRaz = `${p.modo_razonado.aciertos}/${p.modo_razonado.total}`;
const colorDir = p.modo_directo.aciertos === p.modo_directo.total ? "text-emerald-600" : "text-red-600";
const colorRaz = p.modo_razonado.aciertos === p.modo_razonado.total ? "text-emerald-600" : "text-red-600";
let htmlIntentosDir = p.modo_directo.intentos.map((it, idx) => `
<div class="p-2 bg-white rounded border border-slate-200 text-xs font-mono mb-1">
<div class="flex justify-between items-center text-[10px] text-slate-500 mb-1">
<span>Muestra #${idx+1} (${it.tiempo_ms} ms)</span>
<span class="${it.correcta ? 'text-emerald-600 font-bold' : 'text-red-600 font-bold'}">${it.correcta ? '✓ ACERTADO' : '✗ FALLADO'}</span>
</div>
<div class="text-slate-800">${escapeHtml(it.texto_completo)}</div>
<div class="text-[10px] text-slate-500 mt-1">Extraído: <strong>${it.respuesta_extraida !== null ? it.respuesta_extraida : 'Ninguno'}</strong></div>
</div>
`).join("");
let htmlIntentosRaz = p.modo_razonado.intentos.map((it, idx) => `
<div class="p-2 bg-white rounded border border-slate-200 text-xs font-mono mb-1">
<div class="flex justify-between items-center text-[10px] text-slate-500 mb-1">
<span>Muestra #${idx+1} (${it.tiempo_ms} ms)</span>
<span class="${it.correcta ? 'text-emerald-600 font-bold' : 'text-red-600 font-bold'}">${it.correcta ? '✓ ACERTADO' : '✗ FALLADO'}</span>
</div>
<div class="text-slate-800">${resaltarFraseFinal(escapeHtml(it.texto_completo))}</div>
<div class="text-[10px] text-slate-500 mt-1">Extraído: <strong>${it.respuesta_extraida !== null ? it.respuesta_extraida : 'Ninguno'}</strong></div>
</div>
`).join("");
card.innerHTML = `
<div class="flex flex-col sm:flex-row justify-between sm:items-center gap-2 border-b border-slate-200 pb-2">
<div>
<span class="text-[10px] font-bold uppercase bg-slate-200 text-slate-700 px-2 py-0.5 rounded">${p.categoria}</span>
<span class="text-xs font-bold text-slate-800 ml-2">Respuesta Correcta: <code class="bg-emerald-100 text-emerald-800 px-1 rounded">${p.respuesta_correcta}</code></span>
</div>
<div class="flex items-center gap-4 text-xs font-mono font-bold">
<span>Directo: <span class="${colorDir}">${accDir}</span></span>
<span>Razonado: <span class="${colorRaz}">${accRaz}</span></span>
</div>
</div>
<p class="text-xs font-mono text-slate-700">"${p.enunciado}"</p>
<details class="cursor-pointer">
<summary class="text-xs text-indigo-600 font-bold hover:underline">
▼ Desplegar respuestas crudas y trazabilidad
</summary>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<div>
<span class="text-[10px] font-bold text-red-600 block mb-1">RESPUESTAS MODO DIRECTO</span>
${htmlIntentosDir}
</div>
<div>
<span class="text-[10px] font-bold text-emerald-600 block mb-1">RESPUESTAS MODO RAZONADO</span>
${htmlIntentosRaz}
</div>
</div>
</details>
`;
containerTabla.appendChild(card);
});
renderizarGrafico(data.resumen_global);
}
function renderizarGrafico(resumen) {
const ctx = document.getElementById("chart-tiempos").getContext("2d");
if (chartInstance) chartInstance.destroy();
chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Modo Directo', 'Modo Razonado (CoT)'],
datasets: [{
label: 'Tiempo Medio de Inferencia (ms)',
data: [resumen.tiempo_medio_directo_ms, resumen.tiempo_medio_razonado_ms],
backgroundColor: ['#EF4444', '#10B981'],
borderRadius: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true, title: { display: true, text: 'Milisegundos (ms)' } } }
}
});
}
function resaltarFraseFinal(str) {
return str.replace(/Respuesta final:/g, '<strong class="bg-yellow-200 text-yellow-900 px-1 rounded">Respuesta final:</strong>');
}
function escapeHtml(str) {
return String(str || '').replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
</script>
</body>
</html>
"""
if __name__ == "__main__":
uvicorn.run("app_peter:app", host="0.0.0.0", port=8094, reload=True)
🚀 Instrucciones de Ejecución
- Inicia LM Studio con un modelo cargado y el servidor local activado en el puerto
1234. - Instala las dependencias en tu entorno Python:
Bashpip install fastapi uvicorn pydantic - Ejecuta la aplicación desde la terminal:
Bashuvicorn app_peter:app --host 0.0.0.0 --port 8094 --reload - Abre tu navegador e ingresa a:
http://localhost:8094
Slug:
piensa-paso-a-paso-razonamiento-cot-linea-peter-pinsapia
Extracto (Excerpt):
Descubre cómo la técnica ‘Piensa paso a paso’ (Chain-of-Thought) altera la precisión real de un LLM en problemas de lógica y matemáticas. Incluye la guía didáctica de la Línea Péter del Proyecto PINSAPIA y la aplicación completa en Python con FastAPI y LM Studio en el puerto 8094.
Palabras clave:
aidarac, montesteam, ies monterroso, PINSAPIA, Línea Péter, Rózsa Péter, Chain of Thought, razonamiento LLM, FastAPI, Python, LM Studio, funciones recursivas, evaluación de IA
🤖 “Piensa Paso a Paso”: ¿Mejora la IA cuando razona en alto? 🧠
Llega a PINSAPIA la Línea Péter 🚀, un laboratorio interactivo diseñado para medir el impacto de la técnica Chain-of-Thought (Cadena de Pensamiento) frente al modo de respuesta directa.
Inspirándonos en la matemática Rózsa Péter y la teoría de funciones recursivas, comparamos en tiempo real cómo resuelve un modelo los mismos problemas de lógica según la forma de preguntarle:
1️⃣ Modo Directo: Exigimos solo el resultado final sin explicación.
2️⃣ Modo Razonado: Pedimos desglosar el problema paso a paso.
3️⃣ Evaluación Autónoma: Python extrae mediante regex la respuesta y calcula la precisión y el consumo de tiempo.
📊 Los datos muestran cómo el razonamiento desglosado permite superar “trampas cognitivas” como el famoso problema del bate y la pelota, visibilizando al mismo tiempo el trade-off en tiempo de inferencia.
👉 ¿Quieres probarlo en el aula? Código completo en Python/FastAPI listo para correr en local en el blog.
#PINSAPIA #MonteSTEAM #ChainOfThought #InteligenciaArtificial #RozsaPeter #AIdarac #IESMonterroso #Python #EducacionDigital #TechInEducation



