¿Te resulta más fácil leer con fondo crema, un tono de color, más contraste o un texto más grande? Elige tus colores y tu texto una sola vez, para todas las páginas.

Lección gratuita

Construye un chatbot de línea de comandos

Después de esta lección: Puedo juntar llamadas a un modelo en un programa que una persona pueda usar.

Lección de 75 min5 comandosLaboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

Un chatbot es un bucle de llamadas

Un asistente de línea de comandos es un programa que lee una línea que escribes y la envía a un modelo de lenguaje. Imprime la respuesta y repite hasta que salgas. Es el programa útil más pequeño que se construye sobre un modelo.

Importa porque reúne todo lo anterior. Hay mensajes, un historial, un flujo y una llamada fallida que no debe romper el chat.

Al final de esta lección

Nota

Puedo juntar llamadas a un modelo en un programa que una persona pueda usar.

Palabras clave

PalabraQué significaEjemplo
Buclecódigo que se repite hasta que se le dice que pare
Comandouna línea escrita que controla el programa, no el modelo/salir
Historiallos mensajes anteriores que cada llamada reenvía
Recortarquitar los mensajes más antiguos para que el historial siga corto
Salida limpiairse a propósito, sin error y sin perder nada

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

El programa es un bucle con una lista

Cada turno hace los mismos pasos. Leer una línea. Añadirla al historial como mensaje user. Enviar los mensajes. Imprimir la respuesta. Añadir la respuesta al historial como mensaje assistant.

La lista es toda la memoria del programa. Si está mal, todas las respuestas siguientes también lo están.

Escribe la parte 1: el historial

Comando
docker exec -w /home/node/project agent node -e 'require("fs").writeFileSync("assistant.js", ["// assistant.js, parte 1: el historial y un límite a su tamaño.", "const lab = require(\"./lab.js\");", "const readline = require(\"readline\");", "const system = { role: \"system\", content: \"Eres un asistente útil. Responde con una o dos frases cortas.\" };", "const history = [];", "const LIMIT = 6;", "", "// Quedarse solo con los últimos mensajes, sin empezar nunca por un mensaje assistant.", "function trim() {", "  while (history.length > LIMIT) history.shift();", "  while (history.length > 0 && history[0].role === \"assistant\") history.shift();", "}"].join("\n") + "\n")'

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Recorta el historial para que siga corto

Cada llamada envía todo el historial. Un chat largo cuesta más en cada turno y puede pasar del tamaño que el modelo sabe leer. Recórtalo: quédate solo con los últimos mensajes.

Tras el recorte, la lista no debe empezar por un mensaje assistant. Un chat empieza por el usuario, como viste en la primera lección.

Añade la parte 2: el flujo

Comando
docker exec -w /home/node/project agent node -e 'require("fs").appendFileSync("assistant.js", ["", "// Parte 2: enviar el historial e imprimir el flujo según llega.", "const { piecesOf } = require(\"./stream.js\");", "async function streamReply() {", "  let reply = \"\";", "  for await (const piece of piecesOf({ max_tokens: 120, messages: [system, ...history] })) {", "    reply += piece;", "    process.stdout.write(piece);", "  }", "  if (reply === \"\") throw new Error(\"no llegó ninguna palabra\");", "  return reply;", "}"].join("\n") + "\n")'

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Añade la parte 3: un historial en orden

Comando
docker exec -w /home/node/project agent node -e 'require("fs").appendFileSync("assistant.js", ["", "// Parte 3: mantener el historial en orden. Si la llamada falla, se quita la pregunta.", "async function answer(text) {", "  history.push({ role: \"user\", content: text });", "  try {", "    const reply = await streamReply();", "    process.stdout.write(\"\\n\");", "    history.push({ role: \"assistant\", content: reply });", "    trim();", "  } catch (error) {", "    history.pop();", "    console.log(\"\\nMuestra grabada: no puedo llegar al modelo ahora, esta es una respuesta grabada. (\" + error.message + \")\");", "  }", "}"].join("\n") + "\n")'

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Un error frecuente

Advertencia

Mucha gente deja la pregunta en el historial cuando una llamada falla. En el turno siguiente se envían dos mensajes user seguidos y el orden se rompe. Cuando una llamada falla, quita la pregunta, avisa y deja que la persona lo intente de nuevo.

Comandos y una salida limpia

Algunas líneas son para el programa, no para el modelo. Una línea que empieza por / es un comando: /salir se va, /borrar vacía el historial y /historial lo cuenta.

Busca los comandos antes de llamar al modelo. Así una persona siempre puede irse, haga lo que haga el modelo.

Añade la parte 4: el bucle

Comando
docker exec -w /home/node/project agent node -e 'require("fs").appendFileSync("assistant.js", ["", "// Parte 4: el bucle. Una línea que empieza por / es un comando.", "const lines = readline.createInterface({ input: process.stdin });", "const prompt = () => process.stdout.write(\"tú> \");", "(async () => {", "  console.log(\"Asistente listo. Comandos: /salir, /borrar, /historial.\");", "  prompt();", "  for await (const line of lines) {", "    const text = line.trim();", "    if (!process.stdin.isTTY) console.log(text);", "    if (text === \"/salir\") break;", "    if (text === \"/borrar\") { history.length = 0; console.log(\"Historial borrado.\"); }", "    else if (text === \"/historial\") console.log(history.length + \" mensajes guardados.\");", "    else if (text !== \"\") { process.stdout.write(\"asistente> \"); await answer(text); }", "    prompt();", "  }", "  console.log(\"Hasta luego.\");", "  process.exit(0);", "})();"].join("\n") + "\n")'

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Charla con un guion escrito

Comando
printf "Me llamo Marta.\n¿Cómo me llamo?\n/historial\n/borrar\n/historial\n/salir\n" | docker exec -i -w /home/node/project agent node assistant.js

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Ejemplo resuelto: lo que hizo el guion

EscritoLo que hizo el programa
Me llamo Marta.Envió 2 mensajes (el system y el suyo), imprimió una respuesta, guardó 2
¿Cómo me llamo?Envió 4 mensajes (el system y los tres del historial), imprimió una respuesta, guardó 4
/historialImprimió 4 mensajes guardados
/borrarVació el historial
/salirImprimió Hasta luego y salió

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Charla tú mismo con él

El guion de arriba se escribió por ti. Para charlar a mano, abre un terminal en el laboratorio y escribe esta línea: docker exec -it -w /home/node/project agent node assistant.js. Después escribe preguntas y termina con /salir.

Espera a que escribas, así que no es un botón. No escribas contraseñas ni nada personal: lo que escribes se envía a un modelo.

Para ir más lejos: cambia el asistente

Cambia el mensaje system de la parte 1 para darle una personalidad al asistente, y pon LIMIT a 4. Charla durante seis turnos.

Anota qué olvidó el asistente y cuándo. Di qué cuesta mantener un historial largo y qué harías al respecto.

Sobre este laboratorio y su código: Laboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

¿Enseñas a una clase? Asigna este módulo a tus alumnos con guía del profesor, tareas con corrección automática y un laboratorio para cada alumno.