Ir al contenido principal

Diseñando un servidor de IA stateless para un ERP: LLM, memoria y conversaciones

Una arquitectura sencilla para integrar modelos de lenguaje con aplicaciones empresariales sin convertir al LLM en el dueño del estado de la aplicación.

Cuando comenzamos a integrar modelos de lenguaje en una aplicación empresarial, es fácil terminar colocando demasiada lógica alrededor del LLM. El modelo interpreta preguntas, decide qué función ejecutar, consulta información, mantiene el contexto y finalmente genera una respuesta.

Sin embargo, no es necesario que todo eso viva dentro del servidor de IA.

En este artículo voy a plantear una arquitectura diferente: el modelo de lenguaje permanece stateless, el servidor de IA también puede ser stateless y la aplicación empresarial mantiene la conversación, el estado y la memoria.

El resultado es una arquitectura especialmente interesante para un ERP, donde la lógica de negocio y los datos deben permanecer bajo el control de la aplicación.

1. El LLM es stateless

Primero hay que entender algo fundamental sobre los modelos de lenguaje.

Un LLM no tiene una memoria persistente de nuestras conversaciones. Cada llamada recibe una entrada y genera una salida.

Contexto + mensaje
        ↓
       LLM
        ↓
     respuesta

Si enviamos:

Usuario: Hola

y posteriormente hacemos otra llamada enviando solamente:

Usuario: ¿Qué te dije antes?

el modelo no tiene forma de recuperar automáticamente el mensaje anterior.

Para que pueda responder necesitamos enviarle nuevamente el contexto:

Historial:
Usuario: Hola

Usuario: ¿Qué te dije antes?
Idea fundamental: la memoria conversacional no pertenece necesariamente al modelo. Normalmente pertenece a la aplicación que construye el contexto que se envía al modelo.

2. La memoria pertenece a la aplicación

Una aplicación puede aparentar tener memoria porque guarda información externamente y la incorpora a las siguientes llamadas al LLM.

Esa memoria puede estar formada por:

  • historial de mensajes;
  • resúmenes de conversaciones;
  • datos importantes;
  • estado de la conversación;
  • documentos recuperados mediante RAG;
  • resultados obtenidos anteriormente.

Por lo tanto podemos separar claramente dos responsabilidades:

Componente Responsabilidad
LLM Interpretar contexto y generar resultados.
Servidor de IA Procesar las solicitudes contra los modelos.
ERP Mantener estado, memoria y lógica de negocio.

3. ¿Por qué separar el ERP del servidor de IA?

Supongamos que tenemos un ERP desarrollado en Java y queremos utilizar un servidor independiente para acceder a modelos como Ollama, Gemini u otros proveedores.

En lugar de introducir toda la lógica de IA dentro del ERP, podemos crear un servicio independiente.

┌──────────────────────┐
│         ERP          │
│                      │
│  lógica de negocio   │
│  funciones           │
│  memoria              │
│  conversaciones      │
│  estado              │
│  SQLite / BD         │
└──────────┬───────────┘
           │
           │ HTTP API
           ▼
┌──────────────────────┐
│      IA SERVER       │
│                      │
│       LLM 1          │
│       LLM 2          │
│       RAG            │
│       prompts        │
│                      │
│      STATELESS       │
└──────────────────────┘

El servidor de IA no necesita saber quién es el usuario ni qué ocurrió en la conversación anterior.

El ERP le proporciona todo el contexto necesario para cada llamada.

Ventaja: el servidor de IA puede reiniciarse, escalar horizontalmente o incluso tener varias instancias sin perder conversaciones.

4. Una arquitectura de dos llamadas

Una de las decisiones más interesantes de esta arquitectura es limitar cada interacción a un máximo de dos llamadas al LLM.

La primera llamada interpreta la solicitud y produce un plan estructurado.

La segunda llamada utiliza los resultados obtenidos por el ERP y genera la respuesta final.

Usuario
   │
   ▼
ERP
   │
   │ 1. Pregunta + contexto
   ▼
IA SERVER
   │
   ▼
LLM 1
   │
   │ análisis estructurado
   ▼
ERP
   │
   │ ejecuta funciones
   │ consulta BD
   │ obtiene resultados
   ▼
IA SERVER
   │
   ▼
LLM 2
   │
   │ respuesta + memoria sugerida
   ▼
ERP
   │
   ▼
Usuario

La ventaja de este modelo es que el flujo es completamente predecible.

No estamos construyendo un agente que puede decidir realizar diez llamadas consecutivas a distintas herramientas. La aplicación controla cuándo se llama al modelo y cuándo se ejecuta código empresarial.

5. Primera llamada: interpretar la pregunta

Supongamos que el usuario escribe:

¿Cuánto vendimos en 2025?

El ERP envía la pregunta al servidor de IA junto con el contexto de la conversación.

El primer LLM puede devolver una estructura como:

{
  "operaciones": [
    {
      "funcion": "ventas_anuales",
      "parametros": {
        "anio": 2025
      }
    }
  ],
  "pregunta": "¿Cuánto vendimos en 2025?"
}

Este JSON no es una respuesta para el usuario. Es un plan de ejecución.

El ERP toma ese plan y decide qué función Java debe ejecutar.

6. Una pregunta puede necesitar varias operaciones

Una ventaja importante de utilizar una lista de operaciones es que una sola pregunta puede requerir varias consultas.

Por ejemplo:

¿Cuánto vendimos en 2025 y cuál fue nuestro mejor cliente?

El primer LLM podría generar:

{
  "operaciones": [
    {
      "funcion": "ventas_anuales",
      "parametros": {
        "anio": 2025
      }
    },
    {
      "funcion": "mejor_cliente",
      "parametros": {
        "anio": 2025
      }
    }
  ],
  "pregunta": "¿Cuánto vendimos en 2025 y cuál fue nuestro mejor cliente?"
}

El ERP ejecuta ambas operaciones sin realizar otra llamada al LLM.

LLM 1
  │
  ├── ventas_anuales(2025) ──→ BD
  │
  └── mejor_cliente(2025) ──→ BD
                 │
                 ▼
             resultados
                 │
                 ▼
               LLM 2

De esta manera podemos realizar varias operaciones de negocio manteniendo solamente dos llamadas al modelo.

7. El ERP es quien ejecuta las funciones

Esta separación es importante.

El servidor de IA no debería necesitar acceso directo a la base de datos del ERP ni conocer sus servicios internos.

El ERP recibe:

{
  "funcion": "ventas_anuales",
  "parametros": {
    "anio": 2025
  }
}

y utiliza su propia lógica:

Resultado resultado =
    servicio.obtenerVentasAnuales(2025);

Luego el resultado vuelve al servidor de IA para la segunda llamada.

Principio: el LLM puede decidir qué información necesita, pero el ERP decide cómo se obtiene esa información.

8. Segunda llamada: generar la respuesta

Una vez que el ERP ejecutó todas las operaciones, construye el contexto para el segundo LLM.

Por ejemplo:

Pregunta:
¿Cuánto vendimos en 2025 y cuál fue nuestro mejor cliente?

Resultados:

Ventas 2025:
₲850.000.000

Mejor cliente:
Empresa ABC

Compras del cliente:
₲120.000.000

El segundo LLM utiliza esa información para generar una respuesta comprensible:

En 2025 las ventas fueron de ₲850.000.000.
El mejor cliente fue Empresa ABC,
con compras por ₲120.000.000.

9. Convertir la segunda respuesta en JSON

Si además queremos que el segundo LLM ayude a mantener la memoria de la conversación, podemos pedirle una respuesta estructurada.

Por ejemplo:

{
  "respuesta": "En 2025 las ventas fueron de ₲850.000.000. El mejor cliente fue Empresa ABC, con compras por ₲120.000.000.",
  "memoria": [
    {
      "contenido": "La conversación está relacionada con las ventas de 2025.",
      "importancia": 0.8
    },
    {
      "contenido": "El mejor cliente de 2025 fue Empresa ABC.",
      "importancia": 0.9
    }
  ],
  "estado": {
    "tema": "ventas",
    "anio": 2025,
    "cliente_actual": "Empresa ABC"
  }
}

El ERP no muestra todo ese JSON al usuario.

Extrae solamente:

resultado.respuesta

y muestra esa parte en la interfaz.

Al mismo tiempo puede guardar:

resultado.memoria
resultado.estado

en su propia base de datos.

10. Memoria conversacional sin enviar todo el historial

Guardar toda la conversación no significa que tengamos que enviarla completa al LLM en cada petición.

Podemos mantener dos niveles de contexto.

CONVERSACIÓN

Memoria relevante
────────────────────────────
• Estamos hablando de ventas de 2025.
• El mejor cliente es Empresa ABC.

Último intercambio
────────────────────────────
Usuario:
¿Cuál fue mi mejor cliente?

Asistente:
El mejor cliente fue Empresa ABC.

Nueva pregunta
────────────────────────────
Usuario:
¿Y cuánto compró?

El modelo recibe únicamente la información necesaria para entender la nueva pregunta.

Esto reduce el tamaño del contexto y evita enviar cientos de mensajes antiguos.

11. Estado de conversación

Además de memoria textual, puede ser útil mantener un estado estructurado.

{
  "tema": "ventas",
  "anio": 2025,
  "cliente_actual": "Empresa ABC"
}

Si posteriormente el usuario pregunta:

¿Y cuánto compró?

el ERP puede proporcionar al LLM:

Tema actual: ventas
Año: 2025
Cliente actual: Empresa ABC

Esto permite resolver referencias como:

  • este año;
  • el año pasado;
  • ese cliente;
  • ese producto;
  • la anterior;
  • el mismo período.

12. SQLite puede ser suficiente

Para almacenar el estado de una conversación no necesitamos comenzar necesariamente con una base de datos vectorial.

Una implementación inicial puede utilizar SQLite, especialmente cuando la memoria que necesitamos conservar es principalmente conversacional y estructurada.

IA del ERP
│
├── conversations
│   ├── id
│   ├── usuario_id
│   └── fecha
│
├── messages
│   ├── id
│   ├── conversation_id
│   ├── role
│   ├── content
│   └── fecha
│
└── conversation_state
    ├── conversation_id
    └── state_json

La tabla messages puede conservar el historial de la conversación, mientras que conversation_state puede almacenar la información que resulta especialmente importante para continuarla.

Por ejemplo, el estado podría contener:

{
  "tema": "ventas",
  "anio": 2025,
  "cliente_actual": "Empresa ABC",
  "memoria_relevante": [
    "El mejor cliente de 2025 fue Empresa ABC."
  ]
}

De esta manera, el ERP no necesita enviar todo el historial al LLM en cada solicitud. Puede seleccionar los últimos mensajes y combinarlos con el estado relevante de la conversación.

Más adelante, si la aplicación necesita buscar información semánticamente entre grandes cantidades de documentos o conversaciones, se puede incorporar RAG, embeddings y una base de datos vectorial.

Importante: una base de datos vectorial no es un requisito para tener memoria conversacional. El estado de una conversación puede almacenarse de forma estructurada, mientras que los embeddings son útiles cuando necesitamos realizar búsquedas semánticas.

13. El servidor de IA puede ser completamente stateless

Si el ERP mantiene el estado, cada petición al servidor de IA contiene todo lo necesario para realizar su trabajo.

Por ejemplo:

POST /ai/analyze

{
  "message": "¿Y cuánto compró?",
  "context": {
    "memoria": [
      "Estamos hablando de ventas de 2025.",
      "El mejor cliente es Empresa ABC."
    ],
    "estado": {
      "tema": "ventas",
      "anio": 2025,
      "cliente_actual": "Empresa ABC"
    },
    "ultimo_intercambio": {
      "usuario": "¿Cuál fue mi mejor cliente?",
      "asistente": "El mejor cliente fue Empresa ABC."
    }
  }
}

El servidor procesa la solicitud y devuelve el resultado. No necesita conservar la conversación.

14. Escalabilidad

Esta característica facilita mucho la escalabilidad.

                    ┌────────────────┐
                    │      ERP       │
                    │   memoria/BD   │
                    └───────┬────────┘
                            │
                 ┌──────────┼──────────┐
                 │          │          │
                 ▼          ▼          ▼
             IA Server  IA Server  IA Server
                 1          2          3
                 │          │          │
                 └──────────┼──────────┘
                            ▼
                          LLM

Cualquier instancia del servidor de IA puede atender cualquier solicitud porque ninguna depende de una sesión almacenada localmente.

El estado viaja con la petición. Por eso el servidor de IA puede ser reemplazado, reiniciado o replicado sin perder la conversación.

15. ¿Dónde entra LangChain4j?

Después de analizar esta arquitectura aparece una pregunta natural: ¿realmente necesitamos LangChain4j?

La respuesta es: no necesariamente.

LangChain4j puede aportar abstracciones para:

  • Tool Calling;
  • memoria;
  • RAG;
  • embeddings;
  • structured output;
  • integración con diferentes modelos.

Pero una biblioteca no debería incorporarse solamente porque proporciona funcionalidades que también podemos implementar directamente.

En esta arquitectura ya tenemos una decisión importante:

LLM 1
  ↓
plan estructurado
  ↓
ERP ejecuta operaciones
  ↓
LLM 2
  ↓
respuesta estructurada

El flujo es sencillo, controlado y tiene un número máximo de llamadas al modelo.

Esto es diferente de un agente autónomo. Un agente puede decidir llamar a una herramienta, analizar el resultado, llamar a otra herramienta y continuar ejecutando pasos. En esta arquitectura el ERP controla explícitamente el flujo.

16. La diferencia con Tool Calling

Con Tool Calling, el modelo puede recibir herramientas como:

ventasAnuales(anio)
mejorCliente(anio)
listarVentas(anio)

y seleccionar directamente una de ellas.

En la arquitectura propuesta, en cambio, el primer LLM produce un plan:

{
  "operaciones": [
    {
      "funcion": "ventas_anuales",
      "parametros": {
        "anio": 2025
      }
    }
  ]
}

El ERP interpreta ese plan y ejecuta la operación.

La segunda opción proporciona un nivel de control muy claro sobre la ejecución.

17. Una arquitectura sencilla y controlable

Finalmente, podemos resumir todo el diseño de esta manera:

                         USUARIO
                            │
                            ▼
                    ┌──────────────┐
                    │     ERP      │
                    │              │
                    │ memoria      │
                    │ estado       │
                    │ historial    │
                    └──────┬───────┘
                           │
                    pregunta + contexto
                           │
                           ▼
                    ┌──────────────┐
                    │  IA SERVER   │
                    │              │
                    │    LLM 1     │
                    └──────┬───────┘
                           │
                    plan estructurado
                           │
                           ▼
                    ┌──────────────┐
                    │     ERP      │
                    │              │
                    │ ejecuta      │
                    │ operaciones  │
                    └──────┬───────┘
                           │
                    resultados + contexto
                           │
                           ▼
                    ┌──────────────┐
                    │  IA SERVER   │
                    │              │
                    │    LLM 2     │
                    └──────┬───────┘
                           │
                  respuesta + memoria
                           │
                           ▼
                    ┌──────────────┐
                    │     ERP      │
                    │              │
                    │ guarda       │
                    │ memoria      │
                    │ muestra      │
                    │ respuesta    │
                    └──────────────┘

Conclusión

Una arquitectura de IA para un ERP no tiene por qué convertirse necesariamente en un sistema de agentes complejo.

Un modelo de lenguaje es esencialmente stateless: recibe contexto y produce una salida. La memoria puede mantenerse perfectamente en la aplicación empresarial.

Esto permite separar responsabilidades de una forma muy clara:

  • El LLM interpreta y genera lenguaje.
  • El IA Server proporciona los modelos y servicios de IA.
  • El ERP mantiene la conversación y el estado.
  • El ERP ejecuta las funciones de negocio.
  • La base de datos del ERP conserva la memoria.

Con dos llamadas al LLM se puede construir un flujo bastante potente: una primera llamada para interpretar y planificar las operaciones y una segunda para utilizar los resultados y generar la respuesta final.

Además, al permitir que la primera llamada produzca múltiples operaciones, una pregunta compleja puede resolverse mediante varias consultas al sistema sin aumentar el número de llamadas al modelo.

El resultado es una arquitectura sencilla, controlable y escalable, donde la IA no se convierte en el dueño de los datos ni del estado de la aplicación.

Principio final: el LLM piensa sobre el contexto que recibe; el ERP conserva ese contexto y controla lo que realmente ocurre en el sistema.

Comentarios

Entradas populares de este blog

Instalar Evolution API en Docker con Redis y PostgreSQL Local

Instalar Evolution API en Docker con Redis y PostgreSQL Local En este tutorial vamos a levantar Evolution API usando Docker , con soporte de Redis para sesiones y PostgreSQL local para almacenar datos de manera persistente y compartida entre varios usuarios. 1. Estructura del proyecto Crea una carpeta para tu proyecto y colócate en ella: mkdir -p ~/docker/evolution-api cd ~/docker/evolution-api 2. Archivo docker-compose.yml Este compose levanta Redis y Evolution API : version: "3.9" services: # ✅ SERVICIO REDIS redis: container_name: evolution_redis image: redis:7-alpine restart: unless-stopped ports: - "6379:6379" volumes: - redis_data:/data command: redis-server --save 60 1 --loglevel warning # ✅ SERVICIO EVOLUTION API evolution-api: container_name: evolution_api image: atendai/evolution-api restart: unless-stopped ports: - "8085:8080" env_file: - .env ...

Instalación y Configuración de MySQL 5.7 en Ubuntu 24.04 LTS

Instalar MySQL 5.7 en Ubuntu 24.04 1. Descargar e instalar MySQL Copiar mkdir ~/mysql57 cd ~/mysql57 wget https://cdn.mysql.com/archives/mysql-5.7/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz tar -zxvf mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz sudo mv mysql-5.7.44-linux-glibc2.12-x86_64 /usr/local/mysql sudo ln -s /usr/local/mysql/bin/mysql /usr/local/bin/mysql 2. Instalar dependencias necesarias IMPORTANTE: Se descargan las versiones nuevas de las librerías y se las vincula con las librerías que necesita MySQL. Copiar sudo apt update # Reemplazo de libaio sudo apt install libaio1t64 # Reemplazo de libtinfo y ncurses sudo apt install libtinfo6 libncurses6 Copiar # Crear los enlaces simbólicos sudo ln -sf /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/libaio.so.1 sudo ln -sf /usr/lib/x86_64-linux-gnu/libtinfo.so.6 /usr/lib/x86_64-linux-gnu/libtinfo.so.5 sudo ln -sf /usr/lib/x86_64-linux-gnu/libncurses.so.6 /usr/lib/x86_64...

Tutorial: n8n con Docker Compose en Linux

Tutorial Completo: Instalar y correr n8n en Linux con Docker Compose Este tutorial te guiará paso a paso para instalar y ejecutar n8n usando Docker Compose en un sistema Linux (como Ubuntu), con buenas prácticas y configuraciones recomendadas. --- 1. Actualizar el sistema Es buena práctica empezar actualizando los paquetes y sistema: Copiar sudo apt update sudo apt upgrade -y --- 2. Instalar dependencias necesarias para Docker Copiar sudo apt install -y ca-certificates curl gnupg lsb-release --- 3. Agregar repositorio oficial de Docker Copiar sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update --- 4. ...