Contenido detallado
🧩 LLM, VLM y VLA: qué hace cada uno en el robot
En todo proyecto de robot con IA aparecen tres siglas, y cada una ocupa un lugar diferente. El LLM (modelo de lenguaje grande) recibe texto y devuelve texto: entiende el pedido y decide qué acción tiene sentido. El VLM (modelo de visión y lenguaje) recibe una imagen junto con el texto y responde sobre ella: «hay un vaso azul a la izquierda». El VLA (visión, lenguaje y acción) va un paso más allá: recibe una imagen y una instrucción, y devuelve el movimiento de las articulaciones, varias veces por segundo.
🌱 ¿Eres nuevo aquí?
Un modelo es un archivo de números (los parámetros, o pesos) aprendidos a partir de muchos ejemplos. Ejecutar el modelo para obtener una respuesta se llama inferencia. El texto entra y sale en fragmentos llamados tokens (partes de palabras). En general, cuantos más parámetros, mejor es la respuesta y más memoria y tiempo requiere la inferencia.
Cómo leer: cada fila es un tipo de modelo. A la izquierda (cian) está lo que recibe; a la derecha, lo que devuelve. El LLM y el VLM se detienen en palabras o en una elección; el VLA, con brillo, devuelve movimiento directo. Este módulo usa los dos primeros como planificadores; el VLA aparece en el módulo 5.4, con SmolVLA.
Traduce «avanza un poco» a avanzar 20 cm. Sirve para entender pedidos, pero no para medir distancias.
Describe la escena de la cámara. Ayuda al LLM a saber qué hay sobre la mesa, pero puede equivocarse con el color, la cantidad y la posición.
Entrenado con demostraciones del propio brazo. Controla las articulaciones, por eso necesita datos, pruebas y límites aún más estrictos.
Por qué aprender
Saber qué hace cada modelo evita el error más común de quienes empiezan: pedirle a un modelo de texto que controle un motor en tiempo real. Cada tipo tiene un lugar en la estructura del robot, y elegir el lugar correcto es la mitad de la seguridad.
Conceptos clave
🛡️ Arquitectura segura: el modelo elige, el robot ejecuta
El modelo de lenguaje se queda en el nivel alto: lee el pedido y elige una acción. Todo lo que viene después es código común, que tú escribiste y probaste. Un validador en la Raspberry Pi o en la PC comprueba si la acción existe y si los números están dentro de los límites. Solo entonces una línea de texto se envía por el puerto serial al ESP32, que tiene sus propios límites y la parada de emergencia. Si el modelo inventa algo, la respuesta se detiene en el validador.
Cómo leer: el recuadro cian con borde discontinuo es la parte que puede equivocarse (el modelo); el recuadro verde azulado es código determinista, que siempre hace lo mismo. La flecha roja discontinua muestra la ruta del rechazo. El botón ALTO actúa directamente en el ESP32, sin pasar por el modelo ni por la Pi.
✓ Hazlo
- ✓Deja que el modelo elija entre pocas acciones con nombres claros.
- ✓Comprueba todo en el código antes de enviarlo por el puerto serial.
- ✓Repite los límites en el ESP32: es la última barrera.
- ✓Registra cada pedido, respuesta y decisión en un log.
✗ No lo hagas
- ✗Dejar que el modelo genere código y lo ejecute sin revisión.
- ✗Enviar el texto del modelo directamente por el puerto serial.
- ✗Pedirle al modelo valores de PWM o ángulos de motor sin procesar.
- ✗Depender del modelo para detener el robot en una emergencia.
Por qué aprender
A veces, los modelos de lenguaje responden con seguridad algo incorrecto (a eso se le llama alucinación). En un chat, eso genera una frase inadecuada; en un robot, puede provocar un choque. Separar «elegir» de «ejecutar» te permite cambiar de modelo, local o en la nube, sin modificar lo que garantiza la seguridad.
Conceptos clave
📋 Lista cerrada de acciones en JSON (function calling)
En vez de pedir una respuesta libre, le das al modelo un menú: las acciones que el robot sabe realizar, los parámetros de cada una y el formato exacto de la respuesta. Esta idea se conoce como function calling (llamada a función) o tool use. Ollama acepta un JSON Schema en el campo format y fuerza la respuesta a seguir ese molde; el campo enum enumera los únicos nombres de acción permitidos.
🌱 ¿Eres nuevo aquí?
JSON es un formato de texto para datos, con claves y valores: {"acao": "girar", "parametros": {"graus": 90}}. Un JSON Schema es la regla que indica qué claves existen y de qué tipo es cada valor. El schema ayuda al modelo a acertar el formato, pero no garantiza que el valor sea seguro: 9000 grados sigue siendo un número válido. Por eso, el validador comprueba los límites después.
Cómo leer: A la izquierda está el menú que escribes en el código. A la derecha, tres posibles respuestas del modelo. Solo la primera se convierte en comando; el validador rechaza las rojas y el robot se queda quieto y explica el motivo.
Objetivo: ver cómo queda una respuesta válida del modelo. Este es el formato que exige el script del tema 4; fíjate en que motivo es solo texto para el registro y nunca se ejecuta.
{
"acao": "girar",
"parametros": { "graus": -90 },
"motivo": "el usuario pidió girar a la izquierda"
}
Cómo verificar: pega el bloque en un validador de JSON (o ejecuta python3 -m json.tool resposta-exemplo.json); debe aceptarlo sin errores. Cambia -90 por "izquierda" y verás que deja de ser un número: el validador del tema 4 lo rechazaría.
Por qué aprender
La lista cerrada transforma un problema abierto («¿qué responderá el modelo?») en uno cerrado y comprobable («¿cuál de las cuatro acciones eligió y el número está dentro del límite?»). Es el mismo principio que usan los asistentes comerciales para llamar herramientas, aplicado al robot.
Conceptos clave
💻 Ejecutar un modelo local (Ollama en el Pi 5 o en la PC)
O Ollama descarga modelos abiertos y los ejecuta en tu máquina mediante una API HTTP en http://localhost:11434. Tu script se comunica con él como lo haría con un servicio en internet, pero nada sale de la computadora. Los modelos en la nube también sirven para esta arquitectura (el validador es el mismo); el curso muestra la opción local porque funciona sin internet, no tiene costo por solicitud y no envía fuera la voz ni las imágenes de tu casa.
Cómo leer: Todo lo que está dentro del marco teal se ejecuta en tu máquina. Las dos flechas entre el script y Ollama representan la pregunta y la respuesta a través de la red local (localhost). El cable cian discontinuo es la conexión serial con el ESP32, la misma del módulo 3.5.
| Dónde se ejecuta | Tamaño de modelo que cabe | En la práctica |
|---|---|---|
| Raspberry Pi 5 (8 GB), solo CPU | pequeños, de 1 a 3 mil millones de parámetros | unos segundos por respuesta corta |
| Pi 5 + AI HAT+ 2 | LLM y VLM pequeños en el acelerador | consulta los modelos compatibles en la documentación del Pi |
| Jetson Orin Nano Super | pequeños y medianos, con GPU | más rápido que el Pi, más caro |
| PC con GPU NVIDIA | medianos (de 7 a 14 mil millones) con holgura | respuesta casi inmediata |
Paso a paso
Instala Ollama
Sigue el instalador de tu sistema en ollama.com/download (en Linux y en el Pi basta con un comando). Inicia un servicio que escucha en el puerto 11434.
Elige y descarga un modelo
En ollama.com/library, filtra los modelos compatibles con «tools» y de un tamaño que quepa en tu memoria. Descárgalo con ollama pull <modelo>. Los nombres y las versiones cambian rápido: consulta la lista actualizada.
Pruébalo desde la terminal e instala pyserial
Ejecuta los comandos de abajo. Si curl devuelve un JSON con "message", el servidor está listo para el script.
ollama pull <modelo> # reemplaza por el nombre elegido
ollama list # el modelo debe aparecer aquí
curl http://localhost:11434/api/chat -d '{"model": "<modelo>", "stream": false,
"messages": [{"role": "user", "content": "Responda só: ok"}]}'
python3 -m venv ~/robo-ia && source ~/robo-ia/bin/activate
pip install pyserial
Objetivo: el script completo de la arquitectura segura. Envía la solicitud y el schema al modelo local, valida la respuesta (acción incluida en la lista, parámetros dentro de los límites, nada más), pide confirmación para movimientos grandes y solo entonces envía una línea de texto al ESP32. Con --simular, imprime la línea en vez de enviarla, para que puedas probar sin hardware.
#!/usr/bin/env python3
"""Cérebro com modelo local: o modelo ESCOLHE uma ação da lista fechada,
o código VALIDA e só então manda uma linha de texto ao ESP32 pela serial.
Uso: python3 cerebro_llm.py (com ESP32)
python3 cerebro_llm.py --simular (sem hardware: só imprime)"""
import json, sys, time, urllib.request, urllib.error
OLLAMA_URL = "http://localhost:11434/api/chat"
MODELO = "<modelo>" # reemplaza por el nombre que aparece en: ollama list
PORTA = "/dev/ttyUSB0" # USB: /dev/ttyUSB0 o /dev/ttyACM0 · UART de Pi: /dev/serial0 · Windows: COM3
BAUD = 115200
CONFIRMAR_ACIMA_CM = 50 # para avanzar más que esto, hay que presionar "s" en el teclado
# Lista cerrada: acción -> {parámetro: (mínimo, máximo)} o {parámetro: [valores permitidos]}
ACOES = {
"andar": {"cm": (5, 100)},
"girar": {"graus": (-180, 180)},
"garra": {"estado": ["abrir", "fechar"]},
"parar": {},
}
ESQUEMA = {
"type": "object",
"properties": {
"acao": {"type": "string", "enum": list(ACOES)},
"parametros": {"type": "object"},
"motivo": {"type": "string"},
},
"required": ["acao", "parametros"],
}
SISTEMA = (
"Controlas un robot pequeño. Elige UNA acción y responde solo en JSON.\n"
"Acciones: andar {cm: 5 a 100, no existe el valor negativo}; girar {grados: -180 a 180, "
"negativo = izquierda}; garra {estado: abrir o cerrar}; parar {}.\n"
"Si el pedido no corresponde a ninguna acción o es peligroso, usa parar y explícalo en el motivo."
)
def perguntar_modelo(pedido):
"""Envía el pedido a Ollama y devuelve el diccionario de la respuesta (o genera un error)."""
corpo = {
"model": MODELO,
"messages": [{"role": "system", "content": SISTEMA},
{"role": "user", "content": pedido}],
"stream": False, # una respuesta completa, no fragmentos
"format": ESQUEMA, # fuerza el formato JSON
"options": {"temperature": 0},
}
req = urllib.request.Request(OLLAMA_URL, data=json.dumps(corpo).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=120) as r:
dados = json.loads(r.read())
return json.loads(dados["message"]["content"])
def validar(resp):
"""Devuelve (ok, línea_o_motivo, necesita_confirmar). Nunca confíes en el modelo."""
if not isinstance(resp, dict):
return False, "la respuesta no es un objeto JSON", False
acao = resp.get("acao")
if acao not in ACOES:
return False, f"la acción '{acao}' no está en la lista", False
params = resp.get("parametros") or {}
if not isinstance(params, dict):
return False, "los parámetros no son un objeto", False
sobra = set(params) - set(ACOES[acao])
if sobra:
return False, f"parámetro desconocido: {', '.join(sorted(sobra))}", False
valores = []
for nome, regra in ACOES[acao].items():
if nome not in params:
return False, f"falta el parámetro '{nome}'", False
v = params[nome]
if isinstance(regra, list): # valores permitidos
if v not in regra:
return False, f"'{v}' no está permitido para {nome}", False
valores.append(str(v).upper())
else: # rango numérico
if isinstance(v, bool) or not isinstance(v, (int, float)):
return False, f"{nome} debe ser un número", False
minimo, maximo = regra
if not (minimo <= v <= maximo):
return False, f"{nome}={v} fuera del límite ({minimo} a {maximo})", False
valores.append(str(int(round(v))))
linha = " ".join([acao.upper()] + valores) # ej.: ANDAR 30 · GARRA ABRIR · PARAR
confirmar = acao == "andar" and params["cm"] > CONFIRMAR_ACIMA_CM
return True, linha, confirmar
def abrir_serial(simular):
if simular:
return None
import serial # pip install pyserial
s = serial.Serial(PORTA, BAUD, timeout=2)
time.sleep(2) # abrir el puerto reinicia el ESP32: espera
s.reset_input_buffer()
return s
def enviar(s, linha):
if s is None:
return f"(simulado) {linha}"
s.write((linha + "\n").encode())
return s.readline().decode(errors="replace").strip() or "(sin respuesta del ESP32)"
def main():
simular = "--simular" in sys.argv
s = abrir_serial(simular)
print("Cerebro listo. Escribe un pedido (o 'salir').")
while True:
pedido = input("Tú: ").strip()
if pedido.lower() in ("sair", "exit"):
break
if not pedido:
continue
try:
resp = perguntar_modelo(pedido)
except urllib.error.URLError as e:
print(f"Ollama no respondió ({e}). ¿Está en ejecución? Prueba: ollama list"); continue
except (json.JSONDecodeError, KeyError) as e:
print(f"Rechazado: respuesta ilegible ({e})."); continue
print("Modelo:", json.dumps(resp, ensure_ascii=False))
ok, linha, confirmar = validar(resp)
if not ok:
print("Recusado:", linha); continue
if confirmar and input(f"¿Confirmas '{linha}'? (s/n) ").strip().lower() != "s":
print("Cancelado por ti."); continue
print("ESP32:", enviar(s, linha))
if s:
s.write(b"PARAR\n"); s.close()
if __name__ == "__main__":
main()
Cómo verificar
- ✓Con --simular, "avanza treinta centímetros" muestra el JSON del modelo y "ESP32: (simulado) ANDAR 30".
- ✓"avanza dos metros" se rechaza (cm fuera del límite) o se convierte en detenerse; "avanza 80 cm" pide confirmación antes.
- ✓"haz un café" termina en rechazo o en PARAR, nunca en una acción inventada.
- ✗¿"Ollama no respondió"? El servicio está detenido o el nombre del modelo no coincide con ollama list.
💡 Prueba el validador sin modelo
En el intérprete de Python, ejecuta from cerebro_llm import validar y llama a validar({"acao": "girar", "parametros": {"graus": 900}}). Escribe unos diez casos inválidos (acción inexistente, texto en lugar de un número, parámetro sobrante). Si todos se rechazan, tu robot está protegido contra cualquier modelo que conectes después.
Por qué aprender
Con el modelo local puedes probar todo lo que quieras, sin límite de uso ni cuenta, y el robot sigue funcionando sin Wi-Fi. Además, como el validador está en tu código, cambiar Ollama por un modelo en la nube solo requiere cambiar la función perguntar_modelo.
Conceptos clave
👁️ VLM: "¿qué ves?" a través de la cámara
Un VLM recibe una foto y una pregunta. En Ollama, la imagen se envía en base64 en el campo images del mensaje. El uso seguro es el mismo de antes: el VLM describe, y esa descripción se incluye como contexto en el pedido al LLM (o en el mismo modelo, si acepta imágenes y herramientas). No controla ningún motor. Para obtener una posición exacta, el detector del módulo 3.3 sigue siendo más confiable; el VLM ayuda cuando la pregunta es abierta: "¿hay algo en el camino?", "¿qué objeto es rojo?".
Objetivo: tomar una foto con la cámara USB (o con la Camera Module 3 a través de OpenCV) y preguntarle al VLM local qué hay en la escena. Necesitas un modelo con visión: en la biblioteca de Ollama, filtra por "vision".
#!/usr/bin/env python3
"""Toma una foto y pregunta al VLM local qué hay en ella. pip install opencv-python"""
import base64, json, sys, urllib.request
import cv2
MODELO_VISAO = "<modelo-de-visao>" # un modelo con 'vision' en ollama.com/library
PERGUNTA = sys.argv[1] if len(sys.argv) > 1 else "Enumera los objetos sobre la mesa, con su color y posición (izquierda, centro, derecha)."
cam = cv2.VideoCapture(0) # 0 = primera cámara
for _ in range(5): # descarta los cuadros oscuros del inicio
ok, quadro = cam.read()
cam.release()
if not ok:
sys.exit("No se pudo abrir la cámara: comprueba el índice (0, 1...) y el cable.")
quadro = cv2.resize(quadro, (640, 480)) # imagen más pequeña = respuesta más rápida
cv2.imwrite("foto.jpg", quadro)
_, jpg = cv2.imencode(".jpg", quadro)
corpo = {
"model": MODELO_VISAO, "stream": False, "options": {"temperature": 0},
"messages": [{"role": "user", "content": PERGUNTA + " Responde en español, en hasta 3 frases.",
"images": [base64.b64encode(jpg.tobytes()).decode()]}],
}
req = urllib.request.Request("http://localhost:11434/api/chat", data=json.dumps(corpo).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=180) as r:
print(json.loads(r.read())["message"]["content"])
Cómo verificar: el archivo foto.jpg aparece en la carpeta y la terminal muestra una descripción breve de la escena. Pon dos objetos de distintos colores sobre la mesa y cámbialos de lado: la descripción debe reflejarlo. Si se equivoca, acabas de ver por qué el VLM no puede controlar el motor.
✓ Buen uso del VLM
- ✓Dar contexto al LLM: "en la mesa hay un vaso azul y una pelota".
- ✓Responder preguntas abiertas del usuario sobre la escena.
- ✓Comprobar después de la acción: "¿la garra está sujetando algo?".
✗ Mal uso del VLM
- ✗Pedir coordenadas en milímetros y mover el brazo hasta allí.
- ✗Usarlo como sensor de seguridad ("¿hay alguien cerca?").
- ✗Llamarlo en cada cuadro de video: es demasiado lento para eso.
Por qué aprender
Combinar visión y lenguaje permite que el robot responda a pedidos que mencionan el mundo ("agarra el objeto rojo"). Saber en qué destaca el VLM y en qué se equivoca te prepara para el VLA del módulo 5.4, que lleva la misma idea hasta el movimiento.
Conceptos clave
🚨 Bloqueos: límites, confirmación y parada de emergencia
La seguridad de un robot con IA se construye en capas, y cada capa desconfía de la anterior. El validador de la Pi es la primera. El ESP32 vuelve a comprobar los límites y rechaza cualquier línea que no entienda. Un watchdog de comandos detiene los motores si no llega ninguna orden en poco tiempo. Y un botón de parada físico detiene el movimiento sin depender de ningún software.
Cómo leer: el motor está en el centro; para que se mueva, ninguna de las cuatro capas que lo rodean debe haber bloqueado el comando. La capa roja, por fuera de todo, es la única que funciona incluso si el programa se bloquea: es física. Si falla una capa, la de afuera aún lo detiene.
Objetivo: el lado ESP32 del protocolo de líneas de texto (el mismo del módulo 3.5), con límites propios, respuesta OK/ERRO, watchdog de 2 segundos y un botón de parada en el GPIO 13. Las funciones del motor solo imprimen; reemplázalas por el código de tus motores (Trilha 1).
// Ejecutor seguro: recibe "ANDAR 30", "GIRAR -90", "GARRA ABRIR", "PARAR" (ESP32, paquete esp32 3.x)
const int PINO_PARADA = 13; // botón a GND (usa el pull-up interno)
const unsigned long WATCHDOG_MS = 2000; // sin comando durante 2 s: se detiene
unsigned long ultimoComando = 0;
bool movendo = false;
void pararMotores() { movendo = false; Serial.println("# motores detenidos"); } // reemplaza por tu driver
void andar(int cm) { movendo = true; Serial.printf("# avanzando %d cm\n", cm); }
void girar(int graus) { movendo = true; Serial.printf("# girando %d grados\n", graus); }
void garra(bool abrir) { Serial.println(abrir ? "# abriendo la garra" : "# cerrando la garra"); }
void setup() {
Serial.begin(115200);
pinMode(PINO_PARADA, INPUT_PULLUP);
pararMotores();
Serial.println("PRONTO");
}
void loop() {
if (digitalRead(PINO_PARADA) == LOW) { pararMotores(); while (digitalRead(PINO_PARADA) == LOW) delay(10); }
if (movendo && millis() - ultimoComando > WATCHDOG_MS) pararMotores();
if (!Serial.available()) return;
String linha = Serial.readStringUntil('\n'); linha.trim();
if (linha.length() == 0 || linha.length() > 32) { Serial.println("ERROR línea inválida"); return; }
ultimoComando = millis();
int esp = linha.indexOf(' ');
String cmd = esp < 0 ? linha : linha.substring(0, esp);
String arg = esp < 0 ? "" : linha.substring(esp + 1);
if (cmd == "PARAR") { pararMotores(); Serial.println("OK PARAR"); }
else if (cmd == "ANDAR") { int v = arg.toInt(); if (v < 5 || v > 100) Serial.println("ERROR límite"); else { andar(v); Serial.println("OK ANDAR"); } }
else if (cmd == "GIRAR") { int v = arg.toInt(); if (v < -180 || v > 180) Serial.println("ERROR límite"); else { girar(v); Serial.println("OK GIRAR"); } }
else if (cmd == "GARRA" && (arg == "ABRIR" || arg == "FECHAR")) { garra(arg == "ABRIR"); Serial.println("OK GARRA"); }
else { pararMotores(); Serial.println("ERROR comando desconocido"); }
}
Cómo verificar
- ✓En el Serial Monitor (115200, fin de línea "Nueva línea"), "ANDAR 30" responde "OK ANDAR" y, 2 s después, "# motores parados".
- ✓"ANDAR 500" responde "ERRO limite" aunque la Pi no haya logrado bloquearlo.
- ✓Con el botón del GPIO 13 presionado, cualquier movimiento se detiene de inmediato. Pruébalo también en Wokwi.
💡 La parada real es física
El botón del GPIO depende de que el programa esté en ejecución. En un robot con fuerza (brazo, base pesada), instala también un interruptor en serie con la alimentación de los motores, al alcance de la mano. Antes de cada prueba, presiónalo una vez para asegurarte de que funciona.
Por qué aprender
Los módulos siguientes incorporan un brazo que recoge objetos y una voz que da órdenes. Con las cuatro capas listas, cualquier modelo que pruebes, local o en la nube, grande o pequeño, funciona dentro de un espacio que tú controlas.
Conceptos clave
✅ Resumen del módulo
Para profundizar
- Ollama — instalación y biblioteca de modelos
- API de Ollama — /api/chat, format, images, tools
- JSON Schema paso a paso — el molde de la respuesta
- IA en Raspberry Pi (AI HAT+ y AI HAT+ 2) — aceleradores para la Pi 5
- pySerial — el puerto serial desde Python
Siguiente módulo
5.2 · Voz local: escucha con Whisper y habla con Piper, conectando la voz con el cerebro de este módulo.