
Radia Perlman — Planificación Paso a Paso (Puerto 8052)
Por qué Perlman
Radia Perlman es conocida mundialmente como la “Madre de Internet” por inventar el Spanning Tree Protocol (STP). En los primeros días de las redes de ordenadores, cuando se conectaban múltiples puentes y conmutadores para crear redundancia, surgía un problema catastrófico: los bucles infinitos. Un paquete de datos podía dar vueltas eternamente en la red, saturando el ancho de banda y colapsando todo el sistema.
El algoritmo de Perlman transformó las redes de un grafo caótico lleno de ciclos a un árbol estructurado y sin bucles, identificando el camino óptimo entre cualquier origen y destino antes de enviar un solo paquete.
En Inteligencia Artificial y arquitectura de Agentes Autónomos (AI Agents), nos enfrentamos exactamente al mismo problema: si le pides a una IA que resuelva una tarea compleja sin estructurarla previamente, el modelo caerá en bucles de acción repetitivos o dará saltos ilógicos. Aplicar la filosofía de Radia Perlman significa obligar a la IA a construir un plan de ejecución completo y ordenado antes de actuar.
El proyecto
El alumnado introduce un objetivo complejo (ejemplo: “Organiza una excursión escolar al Torcal de Antequera para 60 alumnos: autobús, permisos, ruta y picnic”) y un modelo de lenguaje local impulsado por LM Studio genera un plan de acción dividido en fases secuenciales, precondiciones, riesgos de bucle/fallo y lista de tareas.
- Panel 1 (Entrada de Control): Formulario para definir el objetivo, restricciones de tiempo/presupuesto y nivel de detalle del plan.
- Panel 2 (Diagrama de Planificación): Secuencia visual paso a paso tipo pipeline, indicando orden lógico, requisitos previas de cada paso y comprobación de bucles.
- Panel 3 (Metadatos y Auditoría JSON): Muestra el JSON estricto devuelto por la IA, tiempo de latencia y número de tokens/caracteres procesados.
Motor de razonamiento: LM Studio + Qwen2-VL-2B / Qwen2.5
Para mantener la soberanía de datos y ejecución 100% offline, el backend conecta directamente con LM Studio a través de su servidor local compatible con la API de OpenAI (http://localhost:1234/v1).
Modelos como qwen2-vl-2b-instruct o qwen2.5-coder-7b son ideales para esta tarea gracias a su alta capacidad de razonamiento estructurado y seguimiento de esquemas JSON en entornos locales con recursos moderados.
Instalación previa
1. Dependencias de Python
Instala FastAPI, Uvicorn y las librerías necesarias:
Bash
pip install fastapi uvicorn python-multipart requests pydantic
2. Configuración en LM Studio
- Abre LM Studio y carga el modelo local de tu preferencia (ej.
qwen2-vl-2b-instructo cualquier variante deqwen2.5). - Ve a la pestaña Local Server (icono de desarrollador/red).
- Inicia el servidor en el puerto predeterminado 1234 (
http://localhost:1234).

El código completo (app_perlman.py)
Python
"""
Proyecto Radia Perlman — Planificación Paso a Paso (AI Agents)
IES Monterroso · MonteSteam / AIDARAC
Puerto 8052
Conecta con LM Studio local (puerto 1234) para transformar un objetivo
de alto nivel en una secuencia estructurada de pasos sin bucles,
inspirado en el Spanning Tree Protocol de Radia Perlman.
"""
import json
import re
import time
from typing import Dict, Any
import requests
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse, JSONResponse
app = FastAPI(title="Radia Perlman — Planificador de Agentes")
# --- Configuración del servidor LM Studio ---
LM_STUDIO_URL = "http://localhost:1234/v1/chat/completions"
# --- Valores por defecto para pruebas rápidas ---
OBJETIVO_POR_DEFECTO = (
"Organizar una excursión escolar al Torcal de Antequera para 60 alumnos de secundaria, "
"incluyendo reserva de autobuses, solicitud de permisos al centro, diseño de la ruta "
"senderista guiada y plan de comida y emergencias."
)
RESTRICCIONES_POR_DEFECTO = (
"Presupuesto ajustado, avisar con 2 semanas de antelación y contar con 2 alumnos de movilidad reducida."
)
def extraer_json_de_respuesta(texto: str) -> Dict[str, Any]:
"""
Limpia la respuesta del LLM para extraer un bloque JSON válido
incluso si el modelo añade texto antes o después.
Usa `{3}` en la Expresión Regular para evitar conflictos de comillas triples.
"""
patron_markdown = re.search(r"`{3}(?:json)?\s*(\{[\s\S]*?\})\s*`{3}", texto)
if patron_markdown:
texto_json = patron_markdown.group(1)
else:
inicio = texto.find("{")
fin = texto.rfind("}")
if inicio != -1 and fin != -1 and fin > inicio:
texto_json = texto[inicio : fin + 1]
else:
texto_json = texto
return json.loads(texto_json)
def solicitar_plan_a_lmstudio(objetivo: str, restricciones: str, nivel_detalle: str) -> tuple[Dict[str, Any], float, str]:
system_prompt = (
"Eres un Agente Planificador de Inteligencia Artificial especializado en descomponer "
"objetivos complejos en secuencias lógicas e inmunes a bucles infinitos (algoritmo tipo Spanning Tree).\n"
"Debes responder EXCLUSIVAMENTE con un objeto JSON válido, sin introducciones ni saludos.\n\n"
"El JSON debe seguir strictly este esquema:\n"
"{\n"
' "titulo_plan": "Título descriptivo del plan",\n'
' "resumen_ejecutivo": "Breve explicación de la estrategia general",\n'
' "analisis_bucles": "Explicación de qué riesgos de bucle o bloqueos se evitan en este diseño",\n'
' "pasos": [\n'
" {\n"
' "orden": 1,\n'
' "fase": "Nombre de la fase",\n'
' "accion": "Descripción clara de la acción a ejecutar",\n'
' "precondicion": "Qué debe estar completado antes de iniciar este paso",\n'
' "recursos_necesarios": ["recurso 1", "recurso 2"],\n'
' "riesgo_bloqueo": "Posible fallo o bucle si este paso no se completa"\n'
" }\n"
" ]\n"
"}"
)
user_prompt = f"""
Objetivo a planificar: {objetivo}
Restricciones u observaciones: {restricciones if restricciones else "Ninguna especificada"}
Nivel de detalle requerido: {nivel_detalle}
Genera el plan de ejecución estructurado en formato JSON estricto.
"""
payload = {
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"temperature": 0.2,
"stream": False,
}
inicio = time.perf_counter()
try:
respuesta = requests.post(
LM_STUDIO_URL,
json=payload,
headers={"Content-Type": "application/json"},
timeout=180, # Timeout ampliado a 3 minutos
)
tiempo_transcurrido = time.perf_counter() - inicio
if respuesta.status_code != 200:
raise RuntimeError(f"Error HTTP {respuesta.status_code} desde LM Studio: {respuesta.text}")
datos_respuesta = respuesta.json()
contenido_texto = datos_respuesta["choices"][0]["message"]["content"]
plan_estructurado = extraer_json_de_respuesta(contenido_texto)
return plan_estructurado, tiempo_transcurrido, contenido_texto
except requests.exceptions.ConnectionError:
raise ConnectionError(
"No se pudo conectar con LM Studio. Asegúrate de que el servidor local está "
"activo en http://localhost:1234 (Local Server ON)."
)
except Exception as e:
raise RuntimeError(f"Error procesando la respuesta del planificador: {str(e)}")
HTML_PLANTILLA = """
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Radia Perlman — Planificador paso a paso</title>
<style>
:root {
--fondo: #f4f8f5;
--panel: #ffffff;
--borde: #c8e0d0;
--texto: #1c3525;
--texto-secundario: #4b6855;
--acento: #008539; /* Verde corporativo Junta de Andalucía */
--acento-hover: #00662b;
--acento-suave: #e8f5ed;
--alerta-fondo: #fff8e6;
--alerta-borde: #f0d695;
--alerta-texto: #7a5200;
}
* { box-sizing: border-box; }
body {
background: var(--fondo);
color: var(--texto);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
margin: 0;
padding: 2rem;
}
.cabecera {
max-width: 950px;
margin: 0 auto 2rem auto;
}
.cabecera h1 {
font-size: 1.8rem;
margin-bottom: 0.3rem;
color: var(--acento);
}
.cabecera p {
color: var(--texto-secundario);
font-size: 0.95rem;
line-height: 1.5;
}
.aviso-perlman {
background: var(--acento-suave);
border-left: 4px solid var(--acento);
padding: 1rem 1.2rem;
border-radius: 6px;
margin: 1.2rem 0;
font-size: 0.9rem;
line-height: 1.5;
color: var(--texto);
}
.contenedor-principal {
max-width: 950px;
margin: 0 auto;
}
.panel-entrada {
background: var(--panel);
border: 1px solid var(--borde);
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 4px 12px rgba(0, 133, 57, 0.05);
}
label {
display: block;
margin-bottom: 0.4rem;
font-weight: 600;
font-size: 0.9rem;
color: var(--texto);
}
textarea, input[type="text"], select {
width: 100%;
background: #fafdfb;
color: var(--texto);
border: 1px solid var(--borde);
border-radius: 6px;
padding: 0.8rem;
font-family: inherit;
font-size: 0.95rem;
margin-bottom: 1rem;
}
textarea:focus, input[type="text"]:focus, select:focus {
outline: none;
border-color: var(--acento);
box-shadow: 0 0 0 3px rgba(0, 133, 57, 0.15);
}
textarea { min-height: 90px; resize: vertical; }
.grid-parametros {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1rem;
}
button {
background: var(--acento);
color: #ffffff;
border: none;
border-radius: 6px;
padding: 0.8rem 1.8rem;
font-weight: 600;
cursor: pointer;
font-size: 0.95rem;
transition: background 0.2s ease;
}
button:hover { background: var(--acento-hover); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
/* Modificado a 1fr para que aparezcan apilados verticalmente */
.grid-resultados {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (max-width: 800px) {
.grid-parametros { grid-template-columns: 1fr; }
}
.panel {
background: var(--panel);
border: 1px solid var(--borde);
border-radius: 10px;
padding: 1.5rem;
min-height: 150px;
box-shadow: 0 4px 12px rgba(0, 133, 57, 0.04);
}
.panel h3 {
margin-top: 0;
font-size: 1rem;
color: var(--acento);
border-bottom: 1px solid var(--borde);
padding-bottom: 0.5rem;
}
.paso-card {
background: #ffffff;
border: 1px solid var(--borde);
border-left: 4px solid var(--acento);
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
}
.paso-num {
display: inline-block;
background: var(--acento);
color: #ffffff;
font-weight: bold;
border-radius: 50%;
width: 24px;
height: 24px;
text-align: center;
line-height: 24px;
font-size: 0.8rem;
margin-right: 0.5rem;
}
.paso-titulo {
font-weight: 600;
font-size: 1rem;
color: var(--acento);
}
.paso-detalle {
margin-top: 0.5rem;
font-size: 0.9rem;
color: var(--texto);
line-height: 1.4;
}
.paso-etiqueta {
display: inline-block;
background: var(--acento-suave);
color: var(--texto-secundario);
border: 1px solid #d0e8d9;
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
margin-top: 0.5rem;
margin-right: 0.4rem;
}
.paso-alerta {
background: var(--alerta-fondo);
border: 1px solid var(--alerta-borde);
color: var(--alerta-texto);
font-size: 0.8rem;
padding: 0.5rem;
border-radius: 4px;
margin-top: 0.5rem;
}
.metrica {
display: flex;
justify-content: space-between;
padding: 0.4rem 0;
border-bottom: 1px solid var(--borde);
font-size: 0.88rem;
}
.metrica span:first-child { color: var(--texto-secundario); }
.metrica span:last-child {
font-family: 'Courier New', monospace;
font-weight: bold;
color: var(--acento);
}
pre {
background: #f0f7f2;
border: 1px solid var(--borde);
border-radius: 6px;
padding: 0.8rem;
font-size: 0.8rem;
max-height: 300px;
overflow: auto;
color: #0d5c27;
}
.estado {
color: var(--texto-secundario);
font-style: italic;
text-align: center;
padding: 2rem 0;
}
</style>
</head>
<body>
<div class="cabecera">
<h1>🌐 Radia Perlman — Planificación Paso a Paso</h1>
<p>
Radia Perlman inventó el Spanning Tree Protocol (STP), evitando que la información
quedara atrapada en bucles infinitos en las redes de comunicación. En IA, un
Agente Autónomo debe diseñar la ruta completa de acciones antes de ejecutar nada.
</p>
<div class="aviso-perlman">
<strong>Principios de Planificación en Agentes de IA:</strong><br>
Antes de llamar a herramientas o ejecutar acciones en el mundo real, la IA debe razonar
el grafo de dependencias, definir precondiciones y prever puntos de fallo para evitar bucles.
</div>
</div>
<div class="contenedor-principal">
<div class="panel-entrada">
<label for="objetivo-input">Objetivo Principal del Agente:</label>
<textarea id="objetivo-input" placeholder="Ejemplo: Organiza una excursión escolar...">__OBJETIVO__</textarea>
<div class="grid-parametros">
<div>
<label for="restricciones-input">Restricciones u Observaciones (opcional):</label>
<input type="text" id="restricciones-input" value="__RESTRICCIONES__" placeholder="Ej: Presupuesto ajustado, avisar con 2 semanas de antelación...">
</div>
<div>
<label for="detalle-select">Nivel de Detalle:</label>
<select id="detalle-select">
<option value="estandar">Estándar (4-6 pasos)</option>
<option value="exhaustivo">Exhaustivo (7-10 pasos)</option>
</select>
</div>
</div>
<button id="boton-planificar" onclick="generarPlan()">🚀 Generar Plan de Acción</button>
</div>
<div class="grid-resultados">
<!-- 1. PLAN DE EJECUCIÓN ARRIBA -->
<div class="panel">
<h3>📋 Plan de Ejecución Secuencial</h3>
<div id="contenedor-plan">
<p class="estado">Haz clic en "Generar Plan de Acción" para ejecutar la prueba</p>
</div>
</div>
<!-- 2. ANÁLISIS Y JSON ABAJO -->
<div class="panel">
<h3>🔍 Análisis Antibucle y JSON</h3>
<div id="contenedor-debug">
<p class="estado">Aquí se mostrarán las métricas de inferencia y el JSON estructurado</p>
</div>
</div>
</div>
</div>
<script>
async function generarPlan() {
const objetivo = document.getElementById('objetivo-input').value.trim();
const restricciones = document.getElementById('restricciones-input').value.trim();
const nivelDetalle = document.getElementById('detalle-select').value;
const boton = document.getElementById('boton-planificar');
const contenedorPlan = document.getElementById('contenedor-plan');
const contenedorDebug = document.getElementById('contenedor-debug');
if (!objetivo) {
alert('Escribe un objetivo para el planificador.');
return;
}
boton.disabled = true;
boton.textContent = '⏳ Construyendo árbol de pasos...';
contenedorPlan.innerHTML = '<p class="estado">Trazando ruta de ejecución óptima...</p>';
contenedorDebug.innerHTML = '<p class="estado">Conectando con LM Studio...</p>';
try {
const formData = new FormData();
formData.append('objetivo', objetivo);
formData.append('restricciones', restricciones);
formData.append('nivel_detalle', nivelDetalle);
const respuesta = await fetch('/planificar', {
method: 'POST',
body: formData
});
const datos = await respuesta.json();
if (!respuesta.ok) {
throw new Error(datos.error || 'Error procesando la solicitud');
}
const plan = datos.plan;
let htmlPlan = `
<h4 style="margin-top:0; color:var(--acento); font-size:1.1rem;">${plan.titulo_plan || 'Plan Generado'}</h4>
<p style="font-size:0.9rem; color:var(--texto-secundario);">${plan.resumen_ejecutivo || ''}</p>
<hr style="border:none; border-top:1px solid var(--borde); margin: 1rem 0;">
`;
if (plan.pasos && Array.isArray(plan.pasos)) {
plan.pasos.forEach((paso, idx) => {
htmlPlan += `
<div class="paso-card">
<div>
<span class="paso-num">${paso.orden || idx + 1}</span>
<span class="paso-titulo">${paso.fase || 'Paso ' + (idx + 1)}</span>
</div>
<div class="paso-detalle">${paso.accion}</div>
${paso.precondicion ? `<div class="paso-etiqueta"><strong>Precondición:</strong> ${paso.precondicion}</div>` : ''}
${paso.recursos_necesarios && paso.recursos_necesarios.length > 0 ?
`<div class="paso-etiqueta"><strong>Recursos:</strong> ${paso.recursos_necesarios.join(', ')}</div>` : ''}
${paso.riesgo_bloqueo ?
`<div class="paso-alerta">⚠️ <strong>Riesgo de bloqueo/bucle:</strong> ${paso.riesgo_bloqueo}</div>` : ''}
</div>
`;
});
}
contenedorPlan.innerHTML = htmlPlan;
contenedorDebug.innerHTML = `
<div class="metrica"><span>⏱️ Tiempo de inferencia</span><span>${datos.tiempo_segundos.toFixed(2)} s</span></div>
<div class="metrica"><span>📊 Pasos generados</span><span>${plan.pasos ? plan.pasos.length : 0}</span></div>
<div class="metrica"><span> Servidor IA</span><span>LM Studio Local</span></div>
<div style="margin-top:1rem;">
<label style="font-size:0.85rem; color:var(--texto-secundario);">🛡️ Control de Bucles (STP):</label>
<p style="font-size:0.85rem; background:#f0f7f2; padding:0.6rem; border-radius:4px; border:1px solid var(--borde); color:var(--texto);">
${plan.analisis_bucles || 'No se registraron riesgos de bucle.'}
</p>
</div>
<label style="font-size:0.85rem; color:var(--texto-secundario); margin-top:1rem; display:block;">Salida JSON Cruda:</label>
<pre>${JSON.stringify(plan, null, 2)}</pre>
`;
} catch (error) {
contenedorPlan.innerHTML = `<p style="color:#c53030;">❌ Error: ${error.message}</p>`;
contenedorDebug.innerHTML = `<p style="color:#c53030;">Verifica que LM Studio está ejecutándose en http://localhost:1234 con la API iniciada.</p>`;
} finally {
boton.disabled = false;
boton.textContent = '🚀 Generar Plan de Acción';
}
}
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
def pagina_principal():
contenido = HTML_PLANTILLA.replace("__OBJETIVO__", OBJETIVO_POR_DEFECTO).replace(
"__RESTRICCIONES__", RESTRICCIONES_POR_DEFECTO
)
return contenido
@app.post("/planificar")
def endpoint_planificar(
objetivo: str = Form(...),
restricciones: str = Form(""),
nivel_detalle: str = Form("estandar"),
):
if not objetivo.strip():
return JSONResponse(status_code=400, content={"error": "El objetivo no puede estar vacío."})
try:
plan, tiempo_seg, raw_texto = solicitar_plan_a_lmstudio(
objetivo, restricciones, nivel_detalle
)
return JSONResponse(
content={
"plan": plan,
"tiempo_segundos": tiempo_seg,
}
)
except ConnectionError as e:
return JSONResponse(status_code=503, content={"error": str(e)})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
Puesta en marcha
- Guarda el código en un archivo denominado
app_perlman.py. - Lanza el servidor con Uvicorn en el puerto 8052:
Bash
uvicorn app_perlman:app --reload --port 8052
- Accede en el navegador a: http://localhost:8052
Notas para el aula
- Conexión entre el Spanning Tree Protocol y la IA actual: Radia Perlman resolvió el problema del desbordamiento en redes impidiendo que las tramas Ethernet se propagaran en círculos cerrados. En los Agentes de IA, el equivalente a un bucle de red es cuando el modelo llama repetidamente a una misma API o intenta resolver un problema sin avanzar en la secuencia lógica. La planificación previa actúa como el árbol de expansión de Perlman.
- Integración en la serie de proyectos:
- Loftus: Gestión de contexto y reconstrucción de memoria.
- Lovelace: Extracción estructurada de datos.
- Denning: Seguridad e inyección de instrucciones.
- Derbyshire: Salida de voz y accesibilidad local.
- Perlman: Orquestación y planificación de acciones complejas antes de su ejecución.
- Propuesta de ampliación en el aula: Proponer al alumnado añadir un simulador de fallo donde un paso del plan quede bloqueado (ejemplo: “No hay autobuses disponibles”), obligando al agente a re-planificar dinámicamente únicamente los pasos afectados sin reiniciar la secuencia desde cero.



