Saltar a contenido

Arquitectura de Mentat — el framework RAG

TL;DR

  • Mentat es un framework de RAG open source y soberano: ingiere documentos, los procesa y deja que unos agentes razonen sobre ellos, con los modelos corriendo en local (Ollama).
  • Nace de un monolito hecho para un cliente (Avicena): se extrae la ingesta reutilizable a una plataforma base y se añade una capa de control —LiteLLM como gateway único de modelos y Langfuse para las trazas— para cumplir el AI Act.
  • Dos principios no negociables: human in the loop (los agentes proponen, el usuario decide) y soberanía de datos (nada sale de la infraestructura propia o del cliente).
  • Se despliega en cuatro escenarios (GPU compartida, cloud, solo-agentes, on-premise). La seguridad de cada uno se trata en su propio artículo.

Estado actual — esto es la arquitectura objetivo

Ahora mismo, en la fase de discovery de Avicena, seguimos trabajando sobre el monolito extendido. La separación en el repositorio base Mentat —y la estructura que describe este documento— se materializará en la fase de build. Lo que sigue es el diseño hacia el que vamos, no el estado desplegado hoy.

Sobre el nombre

Mentat viene de Dune: los Mentat son personas entrenadas como "computadoras humanas" —almacenan, procesan y razonan sobre grandes volúmenes de información— en un mundo donde las máquinas pensantes están prohibidas. Encaja con una plataforma cuyo trabajo es ingerir conocimiento, procesarlo y razonar sobre él. A lo largo del documento se usan indistintamente Mentat y framework de RAG.

Situación inicial

El punto de partida fue un proyecto de análisis de información para un cliente (Avicena), construido a partir de un proyecto personal de Iván Izquierdo.

Ese proyecto era un monolito: un único bloque que englobaba todos los elementos —ingesta de documentos, agentes, gestión— y que además accedía directamente a los modelos LLM, llamando a sus endpoints sin ninguna capa intermedia.

Ese punto de partida tenía dos limitaciones que motivaron rediseñar la arquitectura:

  • La ingesta estaba atada al monolito. Toda la ingesta de documentos y su gestión no tiene nada de específico de Avicena —sirve igual para cualquier proyecto futuro—, pero encerrada dentro del monolito no se podía reutilizar.
  • El acceso a los modelos era directo y opaco. El monolito llamaba a los endpoints de los LLM sin ninguna capa intermedia: sin trazabilidad ni control de acceso. Eso no encaja con el AI Act —que entra en aplicación en agosto de 2026—, que exige, entre otras cosas, trazabilidad de las interacciones con los modelos y control sobre su acceso.

Objetivo

Construir un framework para generar agentes que automaticen tareas, basado en dos principios no negociables:

  • Human in the loop — los agentes proponen, el usuario decide. Ninguna acción se ejecuta sin aprobación explícita del usuario.
  • Soberanía de datos — modelos open source ejecutados en local (Ollama). Los datos nunca salen de la infraestructura propia o del cliente.

Los agentes obtienen información principalmente a través de un sistema RAG (Retrieval-Augmented Generation) que consulta documentos previamente ingestados y almacenados en Qdrant (vectores) y PostgreSQL (datos estructurados).

La plataforma base —Mentat, el framework de RAG— gestiona toda la ingesta, procesamiento y enriquecimiento de documentos. Los agentes se construyen sobre esa base, consumiendo el conocimiento ya procesado.


Qué se añadió y por qué

El rediseño conserva lo que ya funcionaba en el monolito y añade las piezas que faltaban. De lo más estructural a lo más específico:

  • Extracción del framework (Mentat). La ingesta reutilizable se saca a su propio repositorio —Mentat—, y cada proyecto parte de él mediante fork o rama, añadiendo encima lo suyo (agentes, dashboards, acciones). Es lo que convierte un proyecto para un cliente en una plataforma para todos.
  • LiteLLM Proxy — gateway único de modelos. En vez de que cada agente llame directo al endpoint, todas las llamadas pasan por un gateway OpenAI-compatible. Aporta el control de acceso que pide el AI Act, un único punto donde enrutar a los modelos locales y el sitio natural donde enganchar las trazas.
  • Langfuse — trazabilidad. Registra cada llamada a los modelos (la trazabilidad que exige el AI Act), versiona los prompts y permite evaluaciones. En su versión v2 necesita como mínimo PostgreSQL.
  • PostgreSQL — reutilizado, no nuevo. Ya estaba en el monolito, así que se aprovecha como BD compartida: los datos de Mentat, Langfuse y LiteLLM conviven en la misma instancia. Es una decisión de simplicidad con una consecuencia: si en algún escenario se separan estos servicios (p. ej. un LiteLLM central compartido entre varios clientes), cada uno necesitará su propio PostgreSQL.
  • Servicios sparse y reranker — calidad del retrieval (añadidos después). El monolito dependía de un SPLADE remoto para la vectorización sparse; se sustituye por un servicio propio (sparse, BM25 con FastEmbed) que corre en CPU y no depende de nada externo —coherente con la soberanía de datos. Y se suma un reranker (cross-encoder jina-reranker-v2, ONNX, CPU) que reordena los candidatos de la fusión RRF para mejorar la precisión de lo que llega al modelo.

Decisiones de stack

Componente Tecnología elegida Motivo
Orquestación de agentes LangGraph Grafos de estado explícitos, interrupt_after nativo para human-in-the-loop, persistencia de checkpoints
Recuperación RAG LlamaIndex QdrantVectorStore con búsqueda híbrida, gestiona query engine y síntesis de contexto
Gateway LLM LiteLLM Proxy Endpoint OpenAI-compatible único para todos los agentes, enruta a modelos locales, integración nativa con Langfuse
Modelos LLM Ollama (gemma4:e4b + gemma 31B fallback) Open source, ejecución local, soberanía de datos
Embeddings Ollama (bge-m3) 1024 dimensiones, mismo servidor que el LLM, sin dependencias externas
Vector store Qdrant Búsqueda híbrida denso (bge-m3) + sparse (BM25) fusionada con RRF, filtrado por metadatos, ya integrado en Mentat
Vectorización sparse FastAPI + FastEmbed (BM25) Servicio sparse auto-hospedado (/vectorize); reemplaza el SPLADE remoto, corre en CPU sin GPU
Reranking FastAPI + FastEmbed (cross-encoder) Servicio reranker auto-hospedado (/rerank), jina-reranker-v2-base-multilingual (ONNX) sobre CPU; reordena los candidatos de la RRF
Observabilidad Langfuse v2 (self-hosted) Trazabilidad de llamadas LLM, gestión de prompts con versiones, evaluaciones y scores de feedback humano
Persistencia de estado Redis Checkpoints de LangGraph (estado del grafo entre interacciones), ya disponible en el stack
API de agentes FastAPI Consistente con doc-tools, Python nativo para LangGraph y LlamaIndex

Calidad del retrieval

Las palancas que determinan la calidad del retrieval (modelo denso, sparse, fusión RRF, reranking, chunking) y cómo configurarlas están en calidad-retrieval.md.

Decisiones pendientes para producción

  • Langfuse v3 — requiere ClickHouse + MinIO/S3. Necesario cuando el volumen de trazas sea alto. Actualmente usamos v2 (solo PostgreSQL) por simplicidad.
  • Checkpoints Redis → PostgreSQL — migración trivial en LangGraph si se necesita persistencia histórica de conversaciones.
  • Multi-tenancy — actualmente una instancia por cliente. Multi-tenant (un solo stack, varios clientes) es una evolución futura si el volumen lo justifica.

Escenarios de despliegue

Escenario 1 — Múltiples clientes, GPU compartida

Cada cliente tiene sus propios datos (Qdrant, PostgreSQL) pero comparte el servidor de modelos.

  • LiteLLM Proxy se despliega como servicio centralizado
  • Solo cambia LITELLM_BASE_URL en el .env de cada cliente
  • Langfuse puede ser compartido (visibilidad total del equipo) o por cliente

Escenario 2 — Cliente autogestionado en cloud

El cliente tiene acceso completo: sube documentos, gestiona prompts, administra usuarios.

  • Un docker-compose completo por cliente (máximo aislamiento)
  • Aprovisionamiento via script o Terraform
  • Evolución futura: multi-tenant si el volumen lo justifica

Escenario 3 — Cliente solo consume agentes

El cliente final solo usa el chat de agentes. La ingesta y administración las gestiona el equipo interno.

  • UI separada para el cliente (solo chat + aprobación)
  • El equipo interno usa la UI del Framework completa
  • Nuevo rol consumer en el sistema de auth (solo ve la UI de agentes)

Escenario 4 — Instalación on-premise en el cliente

Todo en la infraestructura del cliente, sin dependencias externas.

  • El cliente aporta su propia GPU con Ollama
  • Solo hay que cambiar las variables de entorno (GEMMA_E4B_URL, EMBEDDINGS_URL)
  • Langfuse self-hosted garantiza que las trazas no salen de su infraestructura
  • Arranque: docker compose up -d

1. Estructura de repositorios (objetivo)

Visión general

Estructura de repositorios: Mentat como plataforma base y los repos de cliente que parten de él por fork o rama

Repo Mentat (IP empresa)

Mentat/
├── backend/               NestJS — auth, roles, BullMQ, API REST
│   └── src/
│       ├── auth/          sesiones, guards, roles (admin / user_avc / consumer)
│       ├── users/         gestión de usuarios
│       ├── documents/     pipeline de fragmentación (PDF, Word, PPT, Excel)
│       ├── enrichment/    enriquecimiento de CSVs con LLM
│       ├── prompts/       gestión de prompts con versionado inmutable
│       ├── verification/  QA manual de chunks
│       ├── mix/           fusión de CSVs
│       └── llm/           multi-provider LLM con circuit breaker
├── frontend/              Next.js 15 — UI de administración (UI de Mentat)
│   └── src/
│       ├── features/      fragmentación, enriquecimiento, validación, prompts...
│       └── app/agentes/   UI del agente RAG (chat + aprobación)
├── doc-tools/             FastAPI — extracción de documentos (MarkItDown, pymupdf4llm, LibreOffice) + detección/clasificación de figuras (transformers/torch CPU)
├── agents/                FastAPI — framework base de agentes
│   ├── core/
│   │   ├── llm.py         cliente LiteLLM (OpenAILike)
│   │   ├── rag.py         LlamaIndex + QdrantVectorStore
│   │   └── tracing.py     cliente Langfuse
│   └── agents/
│       └── rag_chat/      Agente 0: RAG Query Agent (genérico, reutilizable)
├── litellm/
│   └── config.yaml        configuración del proxy LLM
├── docker-compose.yml     orquestación completa
└── .env.example           variables de entorno documentadas

Servicios en docker-compose.yml:

Servicio Puerto Tecnología Descripción
postgres 8973 PostgreSQL 18 BD principal (mentat + langfuse + litellm)
redis 8974 Redis 8 Colas BullMQ + sesiones + checkpoints
qdrant 6333 Qdrant latest Vector store (chunks + figuras + metadatos)
doc-tools 8976 FastAPI + MarkItDown Extracción de documentos + figuras
sparse 8979 FastAPI + FastEmbed (BM25) Vectorización sparse (/vectorize), CPU
reranker 8980 FastAPI + FastEmbed (jina) Reranking cross-encoder (/rerank), CPU
backend 8972 NestJS 11 API REST + workers BullMQ
frontend 8971 Next.js 15 UI de administración
companion 8977 Uppy Companion Subidas desde Drive/OneDrive/Box
litellm 4000 LiteLLM Proxy Gateway centralizado de LLMs
langfuse-web 3002 Langfuse v2 Trazabilidad, prompts, evaluaciones
agents-api 8978 FastAPI + LangGraph API de agentes

Repo clienteX (IP cliente / proyecto)

clienteX/
├── agents/                agentes específicos del proyecto
│   └── agents/
│       ├── doc_analysis/  agente de análisis de documentos
│       └── jira_assistant/ agente asistente de tickets Jira
├── config/
│   ├── .env               credenciales y endpoints del cliente
│   ├── prompts/           prompts específicos del proyecto
│   └── metadata.xlsx      metadatos de proyecto (zona, fase, cliente...)
└── docker-compose.override.yml   extiende el docker-compose del Mentat

2. Arquitectura de componentes (instancia única)

Así se ven los contenedores de una instalación de Mentat y cómo se relacionan: el núcleo (frontend + backend NestJS), el plano de IA (agentes, LiteLLM y Langfuse) y la capa de infraestructura y datos (Qdrant, PostgreSQL, Redis, más los servicios doc-tools, sparse y reranker). Las líneas discontinuas son dependencias y referencias por variable de entorno.

Arquitectura de contenedores del Framework de RAG

Flujo del Agente RAG (paso a paso)

El agente RAG genérico recorre siempre el mismo camino, desde la pregunta del usuario hasta la respuesta aprobada. La pieza clave es el interrupt_after: el grafo se detiene, guarda su estado en Redis y espera decisión humana —aprobar, descartar o refinar— antes de dar nada por bueno. Es el human in the loop hecho arquitectura.

Flujo RAG — retrieve_and_generate


3. Arquitectura de componentes en producción (multi-cliente)

Escenario A — GPU compartida, datos aislados

Cada cliente tiene su propia instancia de la plataforma pero comparte el servidor de modelos (Ollama en máquina con GPU).

Escenario A — GPU compartida, datos aislados

Variables que cambian por cliente:

LITELLM_BASE_URL=http://<ip-litellm-central>:4000
QDRANT_URL=http://qdrant-clienteA:6333
DOCS_COLLECTION=clienteA_docs


Escenario B — On-premise en el cliente

Todo en la infraestructura del cliente. Solo cambian las variables de entorno.

Escenario B — On-premise en el cliente

Variables que cambian:

GEMMA_E4B_URL=http://host.docker.internal:<puerto-ollama-cliente>
EMBEDDINGS_URL=http://host.docker.internal:<puerto-ollama-cliente>
LANGFUSE_NEXTAUTH_URL=http://<ip-interna>:3002


Seguridad

Cada escenario de despliegue tiene su propio perfil de riesgo —autenticación, autorización, aislamiento de datos, trazas de Langfuse y gestión de sesión— con sus recomendaciones concretas. Esas consideraciones viven en un artículo aparte para no cargar este:

Seguridad por escenario de despliegue en Mentat