Inicio / Artículos

Construyendo un agente de IA local: modelos, memoria, almacenamiento e interfaz

Las cinco capas de una pila de agentes de IA locales, con comparaciones entre Ollama, OpenClaw, Mem0 y Qdrant o Chroma, además de soluciones para los problemas con JSON en modo think y las discrepancias silenciosas de dimensiones.

Editor: REACTAPP.TOP10 min de lectura2248 palabras

Fuentes: Building Local AI Agents: A Practical Guide to Models, Memory, and Orchestration

Un agente de IA local mantiene el modelo en su propio hardware, actúa por usted y conserva el contexto de una sesión a otra, sin enviar datos a una API externa. Un chatbot básico responde y olvida. Un agente puede razonar en varios pasos, utilizar herramientas y acumular conocimiento con el tiempo. Ejecutarlo localmente le brinda un asistente sin los compromisos en materia de privacidad ni las tarifas por llamada que impone los servicios alojados.

Son necesarias cinco capas que trabajen juntas para crear uno: una capa de LLM que realiza inferencias en su máquina, un marco de trabajo para agentes que dirige y ejecuta acciones, una capa de memoria que mejora al agente con el tiempo, una capa de almacenamiento que conserva lo que recopila y una capa de interfaz que se adapta a su forma de trabajar actual. A continuación, cada capa presenta sus opciones, compromisos y una recomendación, para que pueda armar un conjunto adecuado a su situación.

Capa 1: el modelo

Este es el motor. El modelo detecta la intención, genera código y extrae datos estructurados en los que depende el resto del sistema, y todo lo demás está diseñado en torno a sus capacidades fiables. Tres herramientas dominan la inferencia local.

Ollama es la forma más sencilla de comenzar. Con un solo comando se instala, con otro se carga un modelo, y de inmediato se dispone de una API compatible con OpenAI en el equipo local. Gestionará los modelos, seleccionará la cuantización y configurará las ventanas de contexto sin necesidad de ajustes. No está diseñado para tráfico a gran escala y es más adecuado como asistente personal o herramienta para desarrolladores.

ollama pull qwen3:8b
ollama pull nomic-embed-text

llama.cpp está en un nivel inferior. Es un motor en C++ que ejecuta directamente modelos GGUF cuantizados, sin daemon ni capa HTTP. Su reducido consumo de memoria lo hace adecuado para dispositivos periféricos como Raspberry Pi o Linux embebido, donde Ollama sería demasiado pesado. El costo es la falta de gestión integrada de modelos y una configuración más manual.

vLLM es la opción para producción. Utiliza PagedAttention y agrupamiento continuo, las técnicas detrás de la inferencia a gran escala en la nube. Si atiendes a muchos usuarios o tienes necesidades reales de rendimiento, esta es la capa adecuada, aunque requiere una infraestructura GPU adecuada y se escala mejor que las otras dos.

Para el asistente local de un único desarrollador, Ollama es la opción por defecto más razonable, ya que la simplicidad es más importante que el rendimiento. El modelo qwen3:8b merece mención: proporciona JSON fiable para la detección de intenciones, escribe código de forma limpia y cabe en 8 GB de VRAM. Un detalle a tener en cuenta es que envuelve su respuesta en bloques <think>…</think> antes del contenido real, lo que impide que cualquier analizador JSON posterior funcione con la respuesta en bruto. Un pequeño wrapper que establece think: False en todas partes soluciona el problema:

_orig_ollama_chat = ollama.chat
def _no_think_chat(*args, **kwargs):
    opts = kwargs.get("options") or {}
    if isinstance(opts, dict):
        opts.setdefault("think", False)
        kwargs["options"] = opts
    return _orig_ollama_chat(*args, **kwargs)
ollama.chat = _no_think_chat

Dado que utiliza setdefault, los usuarios individuales aún pueden volver a activar el modo de razonamiento cuando lo deseen. Para la extracción de JSON y la generación de código en una cadena de herramientas, el modo de razonamiento añade latencia sin mejorar la calidad.

Nivel 2: el marco de trabajo del agente

El modelo genera texto. El marco de trabajo decide qué hacer con ese texto y cuál es el siguiente paso, abarcando la clasificación de intenciones, el enrutamiento y la ejecución.

OpenClaw está orientado a las herramientas y funciona a nivel de sistema de archivos. Cada acción es un comando enviado a una habilidad declarada, como crear un archivo, escribir código, abrir una carpeta o ejecutar una orden de shell. Se escribe un SKILL.md que declara el nombre de la habilidad y cómo se envía el comando, y el marco de trabajo se encarga del enrutamiento:

- -
name: local-ai-assistant
description: Local AI coding assistant with persistent memory
command-dispatch: tool
command-tool: exec
command-arg-mode: raw
-

Cuando la mayoría de las operaciones son de entrada/salida de archivos, como en un asistente de programación, este modelo encaja perfectamente. Las líneas command-dispatch: tool y command-tool: exec indican a OpenClaw que ejecute scripts en Python cuando se llama a una habilidad, de modo que el trabajo con archivos pasa a través del framework. Su limitación es que no está diseñado para cadenas de razonamiento largas y multifase ni para coordinar varios agentes.

LangGraph adopta un enfoque opuesto. Se define un grafo de estado explícito en el que los nodos son llamadas al modelo o acciones, las aristas son las transiciones y el framework gestiona el estado entre pasos. El costo es un diseño previo: es necesario definir el esquema de estado y la estructura del grafo antes de implementar cualquier lógica de negocio. Para un uso sencillo de herramientas esto representa sobreingeniería, pero para pipelines complejos y multifase permite tener un registro verificable de lo que ocurrió en cada paso.

CrewAI está un nivel por encima aún. Usted describe a los agentes con roles y tareas junto a sus descripciones, y el marco determina el orden de ejecución y la comunicación entre ellos. Es la vía más rápida para obtener un prototipo multiagente funcional, aunque la abstracción dificulta la depuración de errores.

Cualquiera que sea su elección, la clasificación de intenciones se ejecuta primero en cada mensaje recibido. Combinar la clasificación basada en modelos con una solución alternativa de expresiones regulares permite manejar mejor los fallos que cualquiera de los métodos por separado:

def detect_intent(message: str) -> dict:
    try:
        resp = ollama.chat(
            model=OLLAMA_CHAT_MODEL,
            messages=[{"role": "user", "content": INTENT_PROMPT.format(message=message)}],
            options={"temperature": 0, "num_predict": 1024},
        )
        return extract_json(resp["message"]["content"])
    except Exception:
        return keyword_intent_fallback(message)

La solución de respaldo captura patrones comunes cuando el modelo devuelve una salida mal formada. Con el pensamiento desactivado y la temperatura en 0, la mayoría de las solicitudes son manejadas por el camino del modelo, pero es la solución de respaldo la que mantiene al agente activo.

Capa 3: memoria

La memoria distingue a un agente que se adapta a usted de uno que trata cada conversación como si fuera la primera. Aprende sus convenciones, preferencias y contexto del proyecto, y las aplica sin que se le indique.

Las soluciones habituales se encuentran dentro de la ventana de contexto: un PREFERENCES.md que se lee al iniciar, una larga solicitud del sistema o un archivo CONTEXT.md. Todas comparten una misma deficiencia: dentro de la ventana son restringidas por la compactación, los límites de tokens y los reinicios de sesión. Lo que realmente necesita un agente es memoria fuera de esa ventana, almacenada de forma duradera, a la que se pueda acceder según sea relevante y que se actualice a medida que cambian los hechos. Tres enfoques comunes satisfacen necesidades diferentes.

RAG básico es el más extendido y el más adecuado para principiantes. Se dividen los documentos o el historial de conversaciones en fragmentos, se incrustan estos fragmentos y se recuperan los k más similares cuando llega una consulta; además, requiere poca configuración. Su debilidad radica en que la memoria se convierte en un montón de documentos, sin distinción entre lo que merece conservarse a largo plazo y las preguntas temporales.

Memoria de ámbito de sesión, como por ejemplo ConversationBufferMemory y ConversationSummaryMemory de LangChain, permite mantener la coherencia dentro de una sesión. El agente no olvidará lo que dijiste hace cinco mensajes, y los resúmenes evitan que la ventana se llene. Sin embargo, esta memoria no sobrevive a los reinicios, lo cual representa la principal limitación para un asistente de programación.

Memoria persistente estructurada, como en Mem0, trata la memoria como datos de primera clase y no como texto sin procesar. En lugar de mensajes literales, el modelo extrae hechos discretos que se incrustan y almacenan en una base de datos vectorial. Al momento de la recuperación, la consulta se incrusta y se devuelven hechos semánticamente similares, incluso si el nuevo mensaje no utiliza la redacción original. Los hechos almacenados permanecen entre sesiones, pueden fusionarse cuando se duplican y se reescriben cuando cambia la realidad. Mem0 admite modelos locales, por lo que es fácil conectarlo a una stack basada en Ollama.

Tomemos a un desarrollador que menciona que le gusta pytest. Un sistema RAG simple mantiene la oración original. Un sistema estructurado extrae y almacena la preferencia en sí. La próxima vez que solicite una función con pruebas, la memoria devolverá “el usuario prefiere pytest”, incluso si el mensaje nunca menciona un marco de pruebas.

Conectar Mem0 a una configuración local de Ollama requiere una breve configuración que indique el modelo local, el modelo de embedding local y un almacén de vectores:

from mem0 import Memory
config = {
    "llm": {
        "provider": "ollama",
        "config": {"model": "qwen3:8b", "ollama_base_url": "http://localhost:11434"},
    },
    "embedder": {
        "provider": "ollama",
        "config": {"model": "nomic-embed-text", "ollama_base_url": "http://localhost:11434"},
    },
    "vector_store": {
        "provider": "qdrant",
        "config": {"host": "localhost", "port": 6333, "embedding_model_dims": 768},
    },
}
memory = Memory.from_config(config)
memory.add("I always use type hints and pytest", user_id="dev")
results = memory.search("write a utility function", user_id="dev")

El bloque llm dirige a Mem0 hacia qwen3:8b para la extracción de hechos, convirtiendo un mensaje en bruto en preferencias almacenadas discretas en lugar de texto literal. El bloque embedder utiliza nomic-embed-text para vectorizar esos hechos al escribirlos y para incrustar consultas al recuperarlos, y memory.search() luego realiza la búsqueda por significado y no por palabras clave.

Una mejora subestimada para cualquier enfoque de memoria es filtrar lo que se almacena antes de escribirlo. Un clasificador ligero que se ejecuta antes de cada escritura mantiene la memoria limpia y garantiza una alta calidad en las búsquedas:

def _is_worth_storing(self, user_message: str) -> bool:
    response = ollama.chat(
        model=OLLAMA_CHAT_MODEL,
        messages=[{"role": "user", "content": SMART_MEMORY_PROMPT.format(
            user_message=user_message
        )}],
        options={"temperature": 0, "num_predict": 512},
    )
    data = self._extract_json_robust(response["message"]["content"])
    return bool(data.get("worth_storing", False))

Su instrucción proporciona al modelo ejemplos claros de la distinción:

HIGH-VALUE (worth storing):
- "I prefer TypeScript over JavaScript"
- "I use pytest, never unittest"
- "Always use Google-style docstrings"

LOW-VALUE (discard):
- "What does enumerate() do?"
- "Write a retry decorator"
- "Thanks"

Si se omite el filtro, la base de datos vectorial se llena gradualmente con solicitudes de uso único que reducen la eficacia de las búsquedas.

Nivel 4: almacenamiento

El nivel de memoria decide qué datos conservar, y el almacenamiento es donde estos se guardan en disco.

FAISS es una biblioteca que funciona dentro del proceso: rápida, bien probada y sin dependencias de infraestructura. Puede integrarse en el proceso Python y ejecutarse en memoria o serializarse para guardarse en disco. Sus limitaciones son operativas: la persistencia requiere llamadas explícitas de serialización, y no cuenta con API HTTP ni replicación integrada. Esto es adecuado para pipelines de investigación y cargas de trabajo por lotes, pero resulta frágil para un asistente que funciona continuamente y se reinicia con frecuencia.

Chroma es la base de datos vectorial más sencilla que puede elegir un desarrollador. Comienza como un proceso local de Python, o se ejecuta incrustado en su propio código, y guarda los datos automáticamente en un directorio local. Úsela con:

pip install chromadb

Miles de hechos memorizados en un asistente personal están perfectamente dentro de la zona de confort de Chroma. Elimina por completo la dependencia de infraestructura, a costa de un filtrado que es más limitado que el de Qdrant.

Qdrant funciona como un servicio real con APIs completas REST y gRPC, filtrado de datos y vectores nombrados. Está orientado a implementaciones con millones de vectores y consultas concurrentes. Almacena los datos de forma nativa en disco y sobrevive a los reinicios de Docker sin necesidad de manipulaciones especiales:

# Qdrant via Docker
docker run -d - name qdrant-local -p 6333:6333 \
-v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant

Un detalle de configuración puede causar problemas de forma silenciosa: la dimensión de incrustación en la configuración del almacén de vectores debe coincidir exactamente con la dimensión de salida de su modelo de incrustación. Por ejemplo, el modelo nomic-embed-text genera vectores de 768 dimensiones. Si cambia el modelo de incrustación sin actualizar ese valor, las inserciones fallarán sin mostrar ningún error visible.

"vector_store": {
    "provider": "qdrant",
    "config": {
        "collection_name": "coding_assistant",
        "host": "localhost",
        "port": 6333,
        "embedding_model_dims": 768,  # must match your embedding model exactly
    },
}

Chroma es un sustituto directo si deseas abandonar Docker: basta con cambiar el proveedor vector_store en la configuración y el resto de la estructura permanece igual.

Capa 5: la interfaz

La capa más externa determina cómo se comunica con el agente, y la elección adecuada depende de su posición en tu flujo de trabajo.

  • Interfaz web. La opción más flexible. Funciona en un navegador, muestra formato Markdown, código resaltado y secciones plegables, y funciona en cualquier sistema operativo sin necesidad de instalación. Un asistente de programación puede mostrar el archivo actual, el código generado y el estado de memoria del agente en la misma pantalla.
  • CLI. Un asistente de terminal se integra en los flujos de trabajo del shell, redirige la salida a otras herramientas y no añade carga visual. La salida detallada requiere más atención, y las conversaciones de varias interacciones son menos fluidas que en una ventana de chat. Es ideal para búsquedas rápidas, operaciones con archivos y tareas programadas.
  • Plugin de IDE. Un asistente dentro de VS Code o JetBrains se encuentra junto al archivo que estás editando, sabe qué archivo está abierto y sugiere modificaciones directamente en el texto. Es necesario aprender las API de la extensión y mantenerse al día con las actualizaciones del IDE, lo cual puede valer la pena para una herramienta de equipo utilizada a diario.
  • Aplicaciones de chat como Slack, Discord o Telegram. Son adecuadas cuando el asistente debe ser accesible desde varios dispositivos o compartido entre un equipo. No son adecuadas para revisión de código ni edición de archivos, sino más bien para preguntas y respuestas, verificaciones de estado y solicitudes sencillas.
  • Cómo se complementan las opciones

    Cada capa restringe a sus vecinas. El modelo elige la forma de API que llama el framework, el enfoque de memoria decide qué tipo de almacenamiento debe soportarse, y la interfaz determina cuán importante es el formato de salida.

    Para el asistente local de programación de un único desarrollador, esta pila minimiza las fricciones al tiempo que mantiene cada capa reemplazable:

    Interface: Web UI on localhost
    │
    Framework: OpenClaw - tool-dispatch, SKILL.md routing
    │
    LLM: Ollama for qwen3:8b
    (think mode disabled for JSON reliability)
    │
    Memory: Structured persistent memory with value filtering
    Embeddings via nomic-embed-text
    │
    Storage: Chroma (zero infrastructure) or Qdrant (more robust)
    

    Dado que cada capa puede intercambiarse de forma independiente, comience con la opción más simple en cada nivel y actualícela a medida que surjan nuevas necesidades. Pasar de Chroma a Qdrant o de Ollama a vLLM no requiere reconstruir nada por encima o por debajo de ellas. Si hay una capa que merece una inversión temprana, esa es la memoria. Un modelo local modesto con una capa de memoria persistente bien diseñada supera de manera fiable a uno más grande que debe comenzar cada sesión desde cero.