Inteligencia Artificial

Asistente de Terminal IA con LLM Local

Echo Code

Construye un asistente CLI en Bash que traduce lenguaje natural a comandos complejos de Linux usando un LLM local vía API REST de forma segura.

Pingüinos disfrutando en Punta Arenas, Chile.

Problemas Actuales de la Terminal#

Como ingenieros de software, pasamos gran parte de nuestro día en la terminal. Si bien dominamos las operaciones básicas de navegación y gestión de archivos, la realidad es que nadie memoriza la sintaxis exacta de cada bandera de tar, las expresiones regulares complejas de awk, las cadenas de filtros de jq, o los comandos de iptables para enrutamiento de red. La respuesta habitual es romper el flujo de trabajo, abrir el navegador, buscar en la documentación, en StackOverflow o, más recientemente, consultar a un LLM en la nube.

Este salto de contexto constante es ineficiente. Rompe la concentración. Peor aún, pegar fragmentos de logs o configuraciones de red en un servicio de IA alojado en la nube plantea serios riesgos de seguridad y cumplimiento (compliance) al exponer datos sensibles. La solución óptima desde una perspectiva de ingeniería es traer la inteligencia directamente al entorno donde ocurre la acción: un asistente en la propia línea de comandos (CLI) impulsado por un Modelo de Lenguaje Grande (LLM) ejecutándose en local, interactuando con tu shell mediante una API REST.

En este artículo, vamos a diseñar y construir un script robusto en Bash (compatible con Zsh) que toma solicitudes en lenguaje natural, consulta un modelo local y devuelve un comando listo para ser revisado y ejecutado. Cero telemetría, cero latencia de red externa y control absoluto sobre la ejecución.

Arquitectura y Flujo de Datos#

Nuestro objetivo es crear un comando ejecutable (lo llamaremos ask o ia) que se integre de forma transparente en el shell. La arquitectura técnica es directa pero requiere precisión:

  1. Captura de Entrada: El script captura todos los argumentos pasados por el usuario como una única cadena de texto de solicitud.
  2. Inyección de Contexto: Antes de enviar la solicitud, el script recopila información del entorno local (por ejemplo, el sistema operativo, la distribución y el shell actual) para que el LLM genere un comando específico (ej. usando apt vs pacman, o sintaxis específica de Zsh vs Bash).
  3. Construcción del Payload: Se formatea un JSON estricto que contiene el prompt del sistema (crucial para limitar las alucinaciones del modelo) y la consulta del usuario.
  4. Llamada a la API REST: Se utiliza curl para enviar el payload al endpoint del LLM local de forma síncrona.
  5. Parseo de la Respuesta: Mediante jq, extraemos exclusivamente la cadena de texto correspondiente al comando sugerido, ignorando cualquier metadato.
  6. Validación Interactiva: Bajo ninguna circunstancia se auto-ejecuta un comando generado por IA. Se presenta el comando en la salida estándar y se utiliza un prompt interactivo (read -e) que permite al usuario editar el comando en tiempo real, rechazarlo o aprobarlo para su ejecución inmediata.
  7. Inyección en el Historial: Opcionalmente, el comando ejecutado se inyecta en el historial del shell para que las flechas de dirección puedan recuperarlo sin volver a invocar la IA.

Requisitos Previos del Sistema#

Para implementar esta arquitectura, tu entorno de desarrollo Linux debe contar con las siguientes herramientas instaladas:

  • bash o zsh: Modernos (Bash 4.0+ recomendado).
  • curl: Para las peticiones HTTP.
  • jq: El estándar de facto en línea de comandos para manipular JSON.
  • Un servidor LLM local: Para este artículo, asumiremos el uso de Ollama, ya que proporciona una API REST limpia y expone modelos cuantizados eficientemente. Puedes usar cualquier equivalente como llama.cpp server o LM Studio, siempre que ofrezcan un endpoint de inferencia.
  • El Modelo: Sugiero utilizar un modelo optimizado para código, como codellama, qwen2.5-coder o mistral. Deben estar descargados localmente.

Ingeniería del Prompt del Sistema (System Prompt)#

El punto de fallo más común en estas herramientas no es el script, sino el comportamiento del modelo. Por defecto, los LLMs son “parlanchines”; tienden a añadir introducciones (“¡Claro! Aquí tienes el comando:”) y explicaciones posteriores. Si intentamos pasar esa salida directamente a la shell, causaremos un error de sintaxis y potencialmente romperemos algo.

Para solucionar esto, el Prompt del Sistema debe ser draconiano. Debemos instruir al modelo para que actúe como un transpilador estricto.

Eres un experto administrador de sistemas Linux. Tu única función es traducir peticiones en lenguaje natural a un único comando de terminal válido.
REGLAS ESTRICTAS:
1. Devuelve ÚNICAMENTE el comando. Nada de explicaciones, nada de formato markdown, nada de comillas invertidas.
2. El comando debe ser compatible con la distribución actual y el shell del usuario.
3. Prioriza comandos de una sola línea, utilizando pipes (|) si es necesario.
4. Si la petición requiere múltiples pasos que no pueden encadenarse lógicamente, devuelve los comandos separados por '&&'.
5. Si no sabes cómo hacer algo, devuelve exactamente la palabra "ERROR_MODELO".

Al establecer la “Temperatura” del modelo cerca de 0.0 o 0.1 en el payload JSON, reducimos la entropía, forzando respuestas deterministas que siguen el patrón de la regla estricta.

Construyendo el Script de Bash#

A continuación, vamos a diseccionar y escribir nuestro script. Crea un archivo llamado ia-cmd.sh en tu directorio local de binarios (por ejemplo, ~/.local/bin/ia).

#!/usr/bin/env bash
# Configuración del Endpoint
API_URL="[http://127.0.0.1:11434/api/generate](http://127.0.0.1:11434/api/generate)"
MODEL_NAME="qwen2.5-coder" # Asegúrate de tener este modelo instalado localmente
# Verificación de dependencias
for req in curl jq; do
if ! command -v $req &> /dev/null; then
echo -e "\033[0;31mError: $req no está instalado. Por favor, instálalo primero.\033[0m"
exit 1
fi
done
# Validación de argumentos
if [ $# -eq 0 ]; then
echo "Uso: ia <lo que quieres hacer en la terminal>"
echo "Ejemplo: ia encuentra todos los archivos mayores a 50MB y muestra su tamaño"
exit 1
fi
# Capturar toda la entrada del usuario
USER_PROMPT="$*"
# Detección del contexto del sistema operativo para dar precisión al LLM
OS_NAME=$(grep "^PRETTY_NAME=" /etc/os-release | cut -d= -f2 | tr -d '"')
SHELL_NAME=$(basename "$SHELL")
# Construcción del prompt del sistema (escapado para JSON)
SYSTEM_PROMPT="Eres un transpilador estricto de comandos Linux. \
Sistema actual: $OS_NAME. Shell actual: $SHELL_NAME. \
Tu salida será ejecutada directamente. NO uses bloques de código markdown (\`\`\`). \
NO des explicaciones. Devuelve SOLAMENTE el comando en texto plano."
# Escapar comillas dobles en el prompt del usuario para evitar romper el JSON
ESCAPED_USER_PROMPT=$(echo "$USER_PROMPT" | sed 's/"/\\"/g')
# Construcción del Payload JSON usando jq para asegurar un formato válido
PAYLOAD=$(jq -n \
--arg model "$MODEL_NAME" \
--arg system "$SYSTEM_PROMPT" \
--arg prompt "$ESCAPED_USER_PROMPT" \
'{
"model": $model,
"system": $system,
"prompt": $prompt,
"stream": false,
"options": {
"temperature": 0.1,
"top_k": 10
}
}')
echo -e "\033[0;36mPensando...\033[0m"
# Realizar la llamada a la API (Timeout configurado a 15 segundos)
RESPONSE=$(curl -s -m 15 -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
# Manejo de errores de conexión
if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then
echo -e "\033[0;31mError: No se pudo conectar al servidor LLM local en $API_URL\033[0m"
exit 1
fi
# Extraer el comando generado
GENERATED_COMMAND=$(echo "$RESPONSE" | jq -r '.response' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
# Limpieza adicional: A veces los modelos fallan e incluyen markdown de todas formas
GENERATED_COMMAND=$(echo "$GENERATED_COMMAND" | sed -E 's/^```[a-z]*//' | sed -E 's/```$//' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
if [ -z "$GENERATED_COMMAND" ] || [ "$GENERATED_COMMAND" == "null" ]; then
echo -e "\033[0;31mError: El modelo devolvió una respuesta vacía o inválida.\033[0m"
exit 1
fi
# Presentación interactiva del comando
echo -e "\n\033[1;32mComando Sugerido:\033[0m"
echo -e "\033[1;37m$GENERATED_COMMAND\033[0m\n"
# Usar read con soporte de readline (-e) e inicialización (-i)
# Esto permite al usuario editar el comando antes de ejecutarlo
read -e -i "$GENERATED_COMMAND" -p "Presiona ENTER para ejecutar o edita el comando (Ctrl+C para cancelar): " FINAL_COMMAND
if [ -n "$FINAL_COMMAND" ]; then
echo -e "\033[0;33mEjecutando:\033[0m $FINAL_COMMAND"
# Guardar en el historial del shell (requiere configuración según el shell)
if [[ "$SHELL_NAME" == "zsh" ]]; then
print -s "$FINAL_COMMAND"
elif [[ "$SHELL_NAME" == "bash" ]]; then
history -s "$FINAL_COMMAND"
fi
# Ejecución mediante eval para soportar pipes y redirecciones
eval "$FINAL_COMMAND"
fi

No olvides darle permisos de ejecución con chmod +x ~/.local/bin/ia.

Análisis Profundo de la Implementación#

El script anterior parece simple a primera vista, pero contiene decisiones de diseño críticas para entornos profesionales:

1. Construcción del JSON con jq#

En lugar de concatenar cadenas estáticas como "{ \"prompt\": \"$USER_PROMPT\" }", lo cual es extremadamente frágil y susceptible a inyecciones si el usuario escribe doillas o saltos de línea, utilizamos jq -n --arg. Esto delega la responsabilidad de escapar caracteres especiales y construir un JSON válido al propio analizador JSON, garantizando que ninguna petición maliciosa o mal formateada rompa la llamada a la API local.

2. Timeout Estricto (-m 15)#

El desarrollo local significa que el servidor LLM (Ollama) podría estar inactivo, ocupando VRAM con otra tarea, o sufriendo de un “cold start” masivo si el modelo tuvo que cargarse en memoria desde disco. Establecer un tiempo de espera en el comando curl previene que el shell se quede colgado indefinidamente, devolviendo el control al usuario rápidamente con un mensaje claro.

3. Limpieza de Fallos en Prompts#

A pesar de la temperatura de 0.1 y el prompt del sistema estricto, algunos modelos menores (parámetros sub-7B) siguen siendo tercos e intentarán encapsular la respuesta en comillas invertidas de markdown (```bash). La tubería sed después de extraer la respuesta con jq funciona como una segunda capa de seguridad (fallback) que limpia estos caracteres espurios, asegurando que eval reciba sintaxis pura de bash.

4. Ejecución Interactiva con Modificación In-situ#

La línea más valiosa del script es: read -e -i "$GENERATED_COMMAND" .... A diferencia de los scripts estándar que simplemente te preguntan “[Y/n] para ejecutar”, aquí inyectamos el comando sugerido directamente en la entrada de línea de comandos de la instrucción read.

Si pides “Busca todos los puertos en uso y muestra el PID” y el modelo sugiere lsof -i -P -n | grep LISTEN, pero te das cuenta de que quieres limitar la búsqueda al puerto 8080, no tienes que rechazar el comando y volver a escribir el prompt. Simplemente usas las flechas del teclado en la misma línea para añadir | grep 8080 antes de presionar ENTER. Esto transforma la IA de un “oráculo” a una herramienta de autocompletado avanzado.

5. Inyección en el Historial (history -s / print -s)#

Una fricción recurrente en asistentes CLI es que el comando generado por la IA se ejecuta, pero no queda registrado en el historial de comandos habitual (~/.bash_history o ~/.zsh_history). Si presionas “Arriba” en tu teclado después de ejecutarlo, verás la llamada al script de IA (ia mata el proceso...), no el comando subyacente (kill -9 ...). Agregando history -s "$FINAL_COMMAND" resolvemos este problema, asegurando la trazabilidad de los comandos reales ejecutados en el sistema y permitiendo reutilizarlos inmediatamente.

Casos de Uso Complejos en el Mundo Real#

Una vez integrado, el asistente brilla especialmente en tareas de análisis de texto, gestión de red y control de procesos. Veamos algunas pruebas reales utilizando este flujo:

Ejemplo 1: Análisis de logs de acceso web

  • Prompt: ia extrae las 10 direcciones IP con más errores 404 de /var/log/nginx/access.log
  • Resultado devuelto: awk '($9 ~ /404/)' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 10
  • Valor de ingeniería: Escribir esta cadena de filtrado y ordenamiento de memoria toma típicamente minutos y varios intentos de prueba y error. El modelo local lo procesa en unos 400 milisegundos.

Ejemplo 2: Gestión de certificados SSL

  • Prompt: ia verifica la fecha de expiración del certificado local ssl en el puerto 443 del servidor mi-dominio.local
  • Resultado devuelto: echo | openssl s_client -servername mi-dominio.local -connect mi-dominio.local:443 2>/dev/null | openssl x509 -noout -dates
  • Valor de ingeniería: Recordar las banderas de openssl es un dolor de cabeza crónico. Este caso demuestra cómo el inyector de contexto asegura que obtengamos sintaxis válida de openssl para la tarea específica sin abrir una página del manual man.

Ejemplo 3: Gestión de Docker y Limpieza profunda

  • Prompt: ia elimina de forma forzada todas las imágenes de docker sin nombre tag y contenedores detenidos
  • Resultado devuelto: docker container prune -f && docker image prune -a --filter "dangling=true" -f

Consideraciones de Seguridad Avanzadas#

Aunque utilizar un LLM local elimina la fuga de datos hacia corporaciones de IA en la nube, persisten riesgos de seguridad operacional a nivel local.

Peligros de la función eval Utilizar eval "$FINAL_COMMAND" es necesario en Bash para procesar tuberías (|) y redirecciones (>, >>) dinámicamente. Sin embargo, esto expone la terminal a la ejecución de código arbitrario. Al incluir un paso manual obligatorio (read -e), mitigamos en gran medida este riesgo, transformando el problema de un riesgo de ejecución automática a un riesgo de error humano. Sin embargo, nunca debes encadenar este script con cron jobs o integraciones automáticas. Debe ser invocado por humanos de manera síncrona.

Manejo del Contexto Sensible En versiones futuras de tu script, podrías sentir la tentación de pasar más contexto, como las variables de entorno actuales (printenv) o el estado completo del árbol de directorios de git (git status). Ten en cuenta las limitaciones de ventana de contexto del LLM que ejecutes. Modelos menores en local (ej. 8 mil millones de parámetros cuantizados a 4 bits) sufren degradación de rendimiento cuando el prompt inicial (system + user + context) supera los 4096 tokens, resultando en alucinaciones severas o el truncamiento del comando resultante.

Conclusión#

La implementación de un Asistente CLI mediante un script en Bash conectado a un LLM alojado localmente representa un retorno de inversión (ROI) masivo para los equipos de plataforma y DevOps. Redefine la barrera de fricción para crear operaciones de mantenimiento complejas sin perder la trazabilidad de los comandos reales. Mantienes tus datos seguros en tu hardware, preservas tus flujos de trabajo en el teclado, y consolidas las prácticas modernas de automatización de inteligencia artificial con los pilares del diseño clásico y confiable de Unix.