
Proyecto Shelley: Explorando la “temperatura” de la IA entre el determinismo y la creación libre
¿Cómo cambia la respuesta de una Inteligencia Artificial cuando ajustamos su hiperparámetro de creatividad? Un experimento didáctico con FastAPI, LM Studio y el legado de Mary Shelley.
1. Por qué Shelley: Más allá de los tópicos de la tecnología
Cuando hablamos de historia de la ciencia y la tecnología, a menudo recurrimos a los mismos nombres consagrados. Sin embargo, si hay una figura en la literatura universal que anticipó de forma profética el debate contemporáneo sobre la creación sintética, la responsabilidad del creador y los límites de la tecnología, es Mary Shelley.
En 1818, con solo 20 años, Shelley publicó Frankenstein o el moderno Prometeo. Más allá de la criatura y el mito popular, la obra explora una pregunta fundamental: ¿Qué ocurre cuando el ser humano da vida a una entidad capaz de generar lenguaje y razonamiento a partir del conocimiento acumulado, y cómo debemos gobernarla?
Inspirándonos en esa intersección entre literatura, ciencia y ética, hemos bautizado este experimento como el Proyecto Shelley: un entorno de pruebas diseñado para que el alumnado experimente de primera mano con el “aliento creador” de los modelos del lenguaje (LLM), observando cómo un simple parámetro numérico modifica radicalmente la personalidad y coherencia de la maquina.
2. El concepto técnico: ¿Qué es la “temperatura” en un LLM?
Los modelos de lenguaje no “piensan” ni “sienten”; calculan la probabilidad de la siguiente palabra (o token) dada una secuencia previa. Cuando un modelo genera texto, asigna una puntuación de probabilidad a miles de palabras posibles.
Aquí es donde entra el parámetro de temperatura ($T$):
- Temperatura baja ($T \to 0$): La distribución de probabilidad se vuelve “aguda”. El modelo elegirá casi siempre la palabra más probable. El resultado es altamente determinista, predecible, preciso y repetitivo.
- Temperatura alta ($T \ge 1.0$): La distribución se “aplana”. Palabras menos frecuentes o más creativas ganan probabilidad de ser elegidas. El resultado es más variado, original e interactivo, pero con un riesgo creciente de alucinación o incoherencia sintáctica.
3. La arquitectura del experimento: app_shelley.py
Para llevar esta experiencia al aula de forma práctica, transparente y respetuosa con la privacidad, hemos optado por una arquitectura 100 % local:
- Ejecución local con LM Studio: Un modelo de lenguaje ejecutándose en la máquina local mediante un servidor compatible con la API de OpenAI (sin depender de claves de pago ni enviar datos a la nube).
- Backend ligero en Python con FastAPI: Una aplicación web en el puerto
8081que toma la consulta del usuario y la envía simultáneamente a 5 niveles de temperatura diferentes:$$\text{Temperaturas} = [0.1, 0.3, 0.5, 0.7, 1.0]$$ - Muestra doble por nivel (Varianza intra-nivel): Para cada nivel de temperatura, la aplicación solicita 2 respuestas independientes para la misma consulta. De este modo, los estudiantes no solo comparan temperaturas distintas, sino que observan si a una misma temperatura el modelo produce el mismo resultado o varía.
┌───────────────────────────┐
│ Prompt del Usuario/Aula │
└─────────────┬─────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
[FastAPI App (Puerto 8081)] ◄───► [LM Studio / Local LLM]
│
┌───────────┼───────────┬───────────┬───────────┐
▼ ▼ ▼ ▼ ▼
T = 0.1 T = 0.3 T = 0.5 T = 0.7 T = 1.0
(2 muestras) (2 muestras) (2 muestras) (2 muestras) (2 muestras)
4. Resultados en el aula: ¿Qué observamos al variar la temperatura?
Al lanzar la misma pregunta a través de las 10 iteraciones (2 muestras $\times$ 5 niveles), los patrones que emergen son fascinantes para el análisis en el aula:
| Nivel de Temperatura | Comportamiento del Modelo | Comparación entre Muestra 1 y Muestra 2 |
| $T = 0.1$ | Determinismo estricto: Respuestas directas, formales y concisas. | Prácticamente idénticas. El margen de variación es casi nulo. Ideal para código, matemáticas o extracción de datos. |
| $T = 0.3$ | Consistencia con leve variación: Estructura idéntica, pequeños cambios de sinonimia. | Variaciones léxicas menores sin alterar el orden del contenido. |
| $T = 0.5$ | Equilibrio óptimo: Fluidez natural, buena sintaxis y respuesta completa. | Estructuras sintácticas diferentes pero ambas respuestas mantienen la misma rigurosidad. |
| $T = 0.7$ | Creatividad y diversidad: Respuestas más elaboradas, uso de analogías y metáforas. | Las dos muestras abordan la respuesta desde ángulos o enfoques narrativos distintos. |
| $T = 1.0$ | Alta divergencia / Riesgo de alucinación: Estilo expresivo, a veces divagante o excesivamente ornamentado. | Totalmente dispares. Una muestra puede ser brillante y la otra desviarse del tema o perder precisión semántica. |

5. La propuesta didáctica: Preguntas para el debate
Este experimento permite conectar la competencia digital, el pensamiento computacional y la ética filosófica:
- ¿Qué es la “creatividad” artificial? ¿Es la temperatura $1.0$ verdaderamente “creativa” o simplemente un cálculo probabilístico con mayor entropía?
- Gobernanza y uso responsable: Si fueras a diseñar un asistente médico o un tutor de matemáticas, ¿qué temperatura elegirías? ¿Y si diseñaras un generador de poesía o ideas para videojuegos?
- El mito de la conciencia: Al ver cómo dos números cambiados en la configuración cambian la “personalidad” del texto, el alumnado desmitifica la idea de que la IA es un ente sintiente. Es un sistema estadístico parametrizable.
6. Código del servidor FastAPI (app_shelley.py)
A continuación se muestra el núcleo del código utilizado para integrar la API local de LM Studio y generar la comparación graduada:
Python
import asyncio
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx
app = FastAPI(
title="Proyecto Shelley - Comparador de Temperatura LLM",
description="Analizador de creatividad y determinismo en modelos de lenguaje locales."
)
LM_STUDIO_URL = "http://localhost:1234/v1/chat/completions"
TEMPERATURES = [0.1, 0.3, 0.5, 0.7, 1.0]
class PromptRequest(BaseModel):
prompt: str
model: str = "local-model"
async def fetch_sample(client: httpx.AsyncClient, prompt: str, model: str, temp: float, sample_id: int):
payload = {
"model": model,
"messages": [
{"role": "system", "content": "Eres un asistente claro, analítico y preciso."},
{"role": "user", "content": prompt}
],
"temperature": temp,
"max_tokens": 300
}
try:
response = await client.post(LM_STUDIO_URL, json=payload, timeout=60.0)
response.raise_for_status()
data = response.json()
content = data["choices"][0]["message"]["content"]
return {"sample_id": sample_id, "content": content}
except Exception as e:
return {"sample_id": sample_id, "error": str(e)}
@app.post("/compare")
async def compare_temperatures(request: PromptRequest):
async with httpx.AsyncClient() as client:
results = {}
for temp in TEMPERATURES:
# Ejecutamos 2 muestras por cada nivel de temperatura
tasks = [
fetch_sample(client, request.prompt, request.model, temp, 1),
fetch_sample(client, request.prompt, request.model, temp, 2)
]
samples = await asyncio.gather(*tasks)
results[f"temp_{temp}"] = samples
return {"prompt": request.prompt, "results": results}
import asyncio
import httpx
import uvicorn
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel
app = FastAPI(title="Proyecto SHELLEY - El Termómetro de la Creatividad")
LM_STUDIO_URL = "http://127.0.0.1:1234/v1/chat/completions"
# Definimos los 5 niveles de temperatura
TEMP_LEVELS = [
{"temp": 0.1, "label": "Muy Baja", "desc": "Determinista / Rígido", "color": "#0284c7", "bg": "#e0f2fe"},
{"temp": 0.4, "label": "Baja", "desc": "Conservador / Consistente", "color": "#0369a1", "bg": "#f0f9ff"},
{"temp": 0.7, "label": "Media", "desc": "Equilibrado / Estándar", "color": "#d97706", "bg": "#fef3c7"},
{"temp": 1.0, "label": "Alta", "desc": "Creativo / Variado", "color": "#ea580c", "bg": "#ffedd5"},
{"temp": 1.3, "label": "Muy Alta", "desc": "Caótico / Arriesgado", "color": "#dc2626", "bg": "#fee2e2"}
]
class ExperimentRequest(BaseModel):
prompt: str
async def generate_single_response(client: httpx.AsyncClient, prompt: str, temperature: float) -> str:
payload = {
"messages": [
{"role": "user", "content": prompt}
],
"temperature": temperature,
"max_tokens": 60
}
try:
response = await client.post(LM_STUDIO_URL, json=payload, timeout=120.0)
if response.status_code == 200:
data = response.json()
return data["choices"][0]["message"]["content"].strip()
else:
return f"[Error HTTP {response.status_code}: {response.text}]"
except httpx.ReadTimeout:
return "[Error: Tiempo de espera agotado (>120s)]"
except Exception as e:
return f"[Error: {type(e).__name__} - {str(e)}]"
@app.get("/", response_class=HTMLResponse)
async def serve_index():
html_content = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proyecto SHELLEY - El Termómetro de la Creatividad</title>
<style>
:root {
--bg-page: #f8fafc;
--nav-bg: #0f172a;
--border-color: #e2e8f0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg-page); color: #0f172a; line-height: 1.5; padding-bottom: 40px; }
header {
background: var(--nav-bg);
color: white;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.brand-logo { background: #a855f7; color: #ffffff; font-weight: 900; padding: 6px 12px; border-radius: 6px; font-size: 18px; letter-spacing: 1px; }
.badge { background: #f59e0b; color: #0f172a; font-weight: 800; padding: 4px 10px; border-radius: 20px; font-size: 12px; }
.container { max-width: 1300px; margin: 24px auto; padding: 0 16px; }
.intro-box { background: white; border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid var(--border-color); box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.section-title { font-size: 18px; font-weight: 800; color: #1e293b; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
/* PANEL DE CONFIGURACIÓN */
.config-card { background: white; border-radius: 12px; padding: 20px; border: 1px solid var(--border-color); margin-bottom: 24px; }
.mode-selector { display: flex; gap: 12px; margin-bottom: 16px; }
.mode-btn { flex: 1; padding: 10px; border: 2px solid var(--border-color); background: #f1f5f9; border-radius: 8px; font-weight: 700; cursor: pointer; text-align: center; }
.mode-btn.active { border-color: #a855f7; background: #f3e8ff; color: #6b21a8; }
.field-group { margin-bottom: 14px; }
label { display: block; font-weight: 700; font-size: 13px; margin-bottom: 6px; color: #334155; }
select, input[type="text"] { width: 100%; padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px; }
.btn-submit { width: 100%; padding: 14px; background: #7e22ce; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 800; cursor: pointer; transition: background 0.2s; }
.btn-submit:hover { background: #6b21a8; }
/* LOADER */
.loader { display: none; text-align: center; margin: 20px 0; font-weight: 700; color: #7e22ce; }
/* GRID DE 5 NIVELES GRADUALES */
.results-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin-top: 24px; }
@media (max-width: 1024px) {
.results-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
.results-grid { grid-template-columns: 1fr; }
}
.level-card { background: white; border-radius: 12px; padding: 14px; border-top: 6px solid #ccc; box-shadow: 0 2px 4px rgba(0,0,0,0.05); display: flex; flex-direction: column; }
.level-header { margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #f1f5f9; }
.level-temp { font-weight: 900; font-size: 15px; display: block; }
.level-label { font-size: 12px; font-weight: 800; text-transform: uppercase; margin-top: 2px; }
.level-desc { font-size: 11px; color: #64748b; margin-top: 4px; }
.sample-item { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 10px; margin-bottom: 10px; font-size: 12px; line-height: 1.4; flex-grow: 1; }
.sample-tag { font-weight: 800; color: #64748b; font-size: 10px; margin-bottom: 4px; display: block; text-transform: uppercase; }
</style>
</head>
<body>
<header>
<div style="display:flex; align-items:center; gap:12px;">
<div class="brand-logo">SHELLEY</div>
<h1 style="font-size:18px;">El Termómetro de la Creatividad</h1>
</div>
<span class="badge">PUERTO :8081</span>
</header>
<div class="container">
<div class="intro-box">
<div class="section-title">🌡️ Experimento: Evolución Gradual de la Temperatura</div>
<p style="font-size:14px; color:#334155;">
Observa cómo evoluciona la generación de texto a lo largo de <strong>5 niveles progresivos de temperatura</strong>. Para cada nivel generamos <strong>2 textos</strong> para comparar el grado de variación y creatividad.
</p>
</div>
<div class="config-card">
<label>1. Selecciona el Tipo de Experimento:</label>
<div class="mode-selector">
<div class="mode-btn active" id="btnModeCreative" onclick="setMode('creative')">🎨 Modo Creativo (Cuentos / Continuaciones)</div>
<div class="mode-btn" id="btnModeFactual" onclick="setMode('factual')">📌 Modo Datos (Hechos Cerrados)</div>
</div>
<div class="field-group">
<label>2. Elige una propuesta predefinida:</label>
<select id="presetSelect" onchange="applyPreset()">
<!-- Carga dinámica -->
</select>
</div>
<div class="field-group">
<label>3. O escribe tu propia frase/pregunta personalizada:</label>
<input type="text" id="customPrompt" placeholder="Escribe aquí tu texto personalizado...">
</div>
<button class="btn-submit" onclick="runExperiment()">🔥 Lanzar Experimento Gradual (5 Niveles × 2 Textos)</button>
</div>
<div id="loader" class="loader">
⚡ Consultando a LM Studio (Generando 10 respuestas en 5 niveles de temperatura)... Por favor espera unas segundas.
</div>
<div id="resultsArea" class="results-grid" style="display:none;">
<!-- Carga dinámica de las 5 columnas -->
</div>
</div>
<script>
const presets = {
creative: [
"Completa la historia: En mitad del bosque encontré un viejo cofre que contenía...",
"Inventa un nombre original y divertido para una mascota robot.",
"Describe un desayuno típico en el año 2150.",
"Escribe el comienzo de un cuento sobre un dragón que tenía miedo a las alturas."
],
factual: [
"¿Cuál es la capital oficial de Francia?",
"¿Cuántos planetas componen el Sistema Solar?",
"¿En qué año llegó el ser humano a la Luna por primera vez?",
"¿Cuál es el resultado exacto de multiplicar 12 x 12?"
]
};
let currentMode = 'creative';
function setMode(mode) {
currentMode = mode;
document.getElementById('btnModeCreative').classList.toggle('active', mode === 'creative');
document.getElementById('btnModeFactual').classList.toggle('active', mode === 'factual');
updatePresetDropdown();
}
function updatePresetDropdown() {
const select = document.getElementById('presetSelect');
select.innerHTML = '<option value="">-- Selecciona una opción o escribe abajo --</option>';
presets[currentMode].forEach((item) => {
const opt = document.createElement('option');
opt.value = item;
opt.textContent = item;
select.appendChild(opt);
});
if(presets[currentMode].length > 0) {
select.selectedIndex = 1;
applyPreset();
}
}
function applyPreset() {
const select = document.getElementById('presetSelect');
if (select.value) {
document.getElementById('customPrompt').value = select.value;
}
}
async function runExperiment() {
const prompt = document.getElementById('customPrompt').value.trim();
if (!prompt) return alert("Por favor, selecciona o escribe una frase para el experimento.");
document.getElementById('loader').style.display = 'block';
document.getElementById('resultsArea').style.display = 'none';
try {
const res = await fetch('/v1/run_experiment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: prompt })
});
const data = await res.json();
if (data.error) {
alert(data.error);
return;
}
renderGradualResults(data.levels);
document.getElementById('resultsArea').style.display = 'grid';
} catch (err) {
alert("Error al conectar con la aplicación backend.");
} finally {
document.getElementById('loader').style.display = 'none';
}
}
function renderGradualResults(levels) {
const container = document.getElementById('resultsArea');
container.innerHTML = '';
levels.forEach((lvl) => {
const card = document.createElement('div');
card.className = 'level-card';
card.style.borderTopColor = lvl.color;
let samplesHTML = '';
lvl.responses.forEach((text, idx) => {
samplesHTML += `
<div class="sample-item">
<span class="sample-tag">Texto #${idx + 1}</span>
${text}
</div>
`;
});
card.innerHTML = `
<div class="level-header">
<span class="level-temp" style="color:${lvl.color}">Temp: ${lvl.temp}</span>
<div class="level-label" style="color:${lvl.color}">${lvl.label}</div>
<div class="level-desc">${lvl.desc}</div>
</div>
${samplesHTML}
`;
container.appendChild(card);
});
}
updatePresetDropdown();
</script>
</body>
</html>"""
return html_content
@app.post("/v1/run_experiment")
async def run_experiment(req: ExperimentRequest):
levels_result = []
async with httpx.AsyncClient(timeout=120.0) as client:
for lvl in TEMP_LEVELS:
responses = []
# Generamos 2 textos por cada nivel de temperatura
for _ in range(2):
res = await generate_single_response(client, req.prompt, lvl["temp"])
responses.append(res)
levels_result.append({
"temp": lvl["temp"],
"label": lvl["label"],
"desc": lvl["desc"],
"color": lvl["color"],
"bg": lvl["bg"],
"responses": responses
})
return JSONResponse({
"prompt": req.prompt,
"levels": levels_result
})
if __name__ == "__main__":
uvicorn.run("app_shelley:app", host="0.0.0.0", port=8081, reload=True)
Cómo ejecutarlo:
- Inicia LM Studio y activa el servidor local en el puerto
1234. - Lanza la aplicación Shelley en el puerto
8081:Bashuvicorn app_shelley:app --reload --port 8081 - Accede a la documentación interactiva en
http://localhost:8081/docsy prueba tus propios prompts.
Conclusión
El Proyecto Shelley demuestra que la mejor forma de alfabetizar en Inteligencia Artificial no es solo enseñar a usar prompts, sino abrir la caja negra. Entender parámetros como la temperatura permite a los estudiantes pasar de ser meros consumidores de tecnología a usuarios críticos, capaces de calibrar la herramienta según el problema que necesiten resolver.



