Intermedio 11 minStack

RAG con ChromaDB y Mistral

Respuesta directa

Para un RAG local con Mistral, la pila más sencilla es Ollama (generación y embeddings con bge-m3) más ChromaDB en modo archivo, sin servidor ni PyTorch. En cuanto al modelo, ministral-3:8b (6,0 GB, licencia Apache 2.0, contexto de 256K anunciado) es una buena opción por defecto para una tarjeta de 8 a 12 GB, ministral-3:14b para 16 GB y mistral-small3.2:24b (15 GB) para capacidades superiores. El ajuste que no se debe olvidar: la ventana de contexto de Ollama, que hay que aumentar para que los pasajes quepan en el prompt.

Esta guía muestra cómo construir un asistente documental completo en dos scripts Python, con un modelo Mistral ejecutado en tu máquina: tus PDF y archivos de texto se dividen en fragmentos, se indexan en ChromaDB y luego los pasajes recuperados se entregan al modelo, que responde citando sus fuentes. También especifica qué modelo Mistral elegir según tu memoria gráfica y los errores que hacen que un RAG dé respuestas que no vienen al caso.

Por Mohamed Meguedmi·Actualización 2026-09-30·Probado en Windows, macOS y Linux

#Lo que construimos: un RAG Mistral completamente local

El RAG (generación aumentada por recuperación) consiste en buscar los pasajes de tus documentos relacionados con la pregunta y luego pegarlos en el prompt del modelo para que responda a partir de ellos. El resultado esperado aquí es una pequeña herramienta de línea de comandos: un script indexa un directorio de documentos; un segundo lee una pregunta, encuentra los cinco pasajes más cercanos en ChromaDB, los envía a un modelo Mistral a través de Ollama junto con la pregunta y muestra la respuesta seguida de los archivos consultados. Nada sale de la máquina: Ollama sirve el modelo de generación y el modelo de embeddings, y ChromaDB almacena los vectores en un directorio local.

El término «Mistral» se usa con dos sentidos: los modelos de pesos abiertos de Mistral AI, que descargas y ejecutas tú mismo (objeto de esta guía), y las API alojadas de la empresa, que envían tus pasajes a sus servidores. Para documentos confidenciales, solo el primero cumple el requisito de ser «100 % local».

#Qué modelo Mistral elegir para el RAG

El kit RAG Local

Tus documentos, tu IA: un RAG local fiable sobre tus PDF, notas y correos — sin enviar nada a la nube.

  • Espacio en línea de por vida
  • PDF + archivos
  • Reembolsado 30 j

Un RAG tiene necesidades especiales: el modelo debe seguir una instrucción estricta («responde únicamente a partir de los pasajes»), leer varios pasajes sin perderse y responder en francés. El tamaño importa menos que en una conversación libre; en cambio, la memoria disponible para el contexto es más importante. Los tamaños que aparecen a continuación son los que muestra la biblioteca de Ollama, con la cuantización predeterminada.

Modelos Mistral en la biblioteca de Ollama (registro de septiembre de 2026)
ModeloTamaño en OllamaContexto anunciadoPara quién
ministral-3:3b3,0 GB256KSin GPU dedicada; respuestas simples, baja tolerancia a instrucciones complejas
ministral-3:8b6,0 GB256KOpción predeterminada razonable para una tarjeta de 8 a 12 GB o un portátil con 16 GB de memoria
ministral-3:14b9,1 GB256KTarjeta de 16 GB, o 12 GB con un contexto moderado
mistral-nemo (12B)ver la página Ollama128KAlternativa más antigua, aún ampliamente utilizada
mistral-small3.2:24b15 GB128KTarjeta de 24 GB o memoria unificada de 32 GB o más; el más fiable a la hora de seguir instrucciones de formato
mistral (7B, versión 0.3)4,4 GB32KModelo antiguo: reservado para máquinas muy limitadas

La familia Ministral 3 (3B, 8B y 14B) está publicada bajo licencia Apache 2.0, como indica el anuncio de Mistral 3, y la página de Ollama la describe como diseñada para un despliegue en el borde, capaz de funcionar en una amplia gama de hardware. Mistral Small 4, publicado en 2026 con 119 mil millones de parámetros en total según el nombre de su ficha de Hugging Face, está dirigido a hardware de servidor: no es un candidato para un equipo personal. Para hacerse una idea del orden de magnitud de la memoria necesaria, la calculadora de VRAM del sitio ofrece el tamaño del modelo más la caché de contexto.

i
Contexto anunciado y contexto útil
Un contexto de 128K o 256K es una capacidad máxima del modelo, no un ajuste: Ollama utiliza mucho menos por defecto, y un contexto grande consume más memoria. Para un RAG de cinco pasajes, 8.000 tokens son suficientes.

#La pila tecnológica

Generación
Un modelo Mistral servido por Ollama, a través de la API HTTP local en el puerto 11434.
Embeddings
bge-m3 servido por Ollama: la página de la biblioteca lo describe como un modelo de BAAI versátil, multilingüe y con múltiples niveles de granularidad, de 567 millones de parámetros. Evita tener que instalar PyTorch y sentence-transformers.
Base de datos vectorial
ChromaDB en modo local (PersistentClient): un directorio, sin servidor. Chroma proporciona un wrapper, OllamaEmbeddingFunction, que llama a la API de embeddings de Ollama.
Lectura de archivos
pypdf para PDFs que contienen texto, lectura directa para Markdown y texto plano. Un PDF escaneado es una imagen: primero se necesita reconocimiento de caracteres.

#Preparar el entorno

  1. 01
    Instalar Ollama y descargar los modelos
    Instala Ollama y luego descarga el modelo de generación y el modelo de embeddings con los dos comandos que aparecen a continuación.
  2. 02
    Crear el entorno Python
    Python 3.10 o superior. Un entorno virtual mantiene las dependencias del proyecto separadas.
  3. 03
    Colocar los documentos
    Copia tus PDF y tus archivos Markdown y de texto en una carpeta docs/ junto a los scripts.
Modelos y dependencias
ollama pull ministral-3:8b
ollama pull bge-m3

mkdir mon-rag && cd mon-rag
python3 -m venv venv
source venv/bin/activate   # .\venv\Scripts\activate sous Windows
pip install chromadb pypdf requests

#2. Indexar los documentos en ChromaDB

El script lee cada archivo, divide el texto en fragmentos de aproximadamente 1.800 caracteres cortando entre párrafos, luego los entrega a Chroma, que llama a bge-m3 por Ollama para calcular los vectores. Dos detalles importan: cada fragmento mantiene el nombre del archivo como metadato (para citar la fuente) y los añadidos se realizan por lotes en lugar de uno a la vez.

index.py
from pathlib import Path
import chromadb
from chromadb.utils.embedding_functions.ollama_embedding_function import OllamaEmbeddingFunction
from pypdf import PdfReader

ef = OllamaEmbeddingFunction(url="http://localhost:11434", model_name="bge-m3")
coll = chromadb.PersistentClient(path="./chroma_db").get_or_create_collection("mes_docs", embedding_function=ef)

def lire(path: Path) -> str:
    if path.suffix.lower() == ".pdf":
        return "\n\n".join(p.extract_text() or "" for p in PdfReader(str(path)).pages)
    return path.read_text(encoding="utf-8", errors="ignore")

def decouper(texte: str, max_chars=1800):
    """Regroupe des paragraphes entiers jusqu'à max_chars ; un paragraphe trop long est coupé."""
    chunks, courant = [], ""
    for para in (p.strip() for p in texte.split("\n\n")):
        if not para:
            continue
        while len(para) > max_chars:
            if courant:
                chunks.append(courant); courant = ""
            chunks.append(para[:max_chars]); para = para[max_chars:]
        if len(courant) + len(para) + 2 > max_chars and courant:
            chunks.append(courant); courant = ""
        courant = (courant + "\n\n" + para).strip()
    if courant:
        chunks.append(courant)
    return chunks

n = 0
for path in sorted(Path("docs").rglob("*")):
    if path.suffix.lower() not in {".pdf", ".md", ".txt"}:
        continue
    chunks = decouper(lire(path))
    if not chunks:
        print(f"  ! {path.name} : aucun texte extrait (PDF scanné ?)")
        continue
    for i in range(0, len(chunks), 32):  # par lots de 32
        lot = chunks[i:i + 32]
        coll.upsert(
            ids=[f"{path.name}-{i + j}" for j in range(len(lot))],
            documents=lot,
            metadatas=[{"source": path.name}] * len(lot),
        )
    n += len(chunks)
    print(f"  + {path.name} : {len(chunks)} passages")
print(f"Terminé : {n} passages indexés")

El uso de upsert con identificadores construidos a partir del nombre de archivo y del número de pasaje permite volver a ejecutar el script: reindexar la misma carpeta actualiza los pasajes en lugar de duplicarlos. Sin embargo, ten en cuenta que, si un documento se acorta, los pasajes antiguos que sobran permanecen en la base de datos; para un cambio importante, elimina la carpeta chroma_db y vuelve a indexar. La elección del tamaño de los pasajes se detalla en la guía sobre estrategias de chunking.

#3. Consultar: búsqueda y después generación

El segundo script incorpora la pregunta, recupera los cinco pasajes más cercanos y construye el prompt. La instrucción es decisiva: pide responder únicamente a partir de los pasajes, reconocer cuando falta información y citar el archivo. El parámetro num_ctx amplía la ventana de contexto: la documentación de Ollama indica que la ventana predeterminada es de 4096 tokens y que la variable OLLAMA_CONTEXT_LENGTH o el parámetro num_ctx la modifican. Con cinco pasajes de 400 a 500 tokens, la instrucción y la respuesta, 4096 tokens están al límite: un contexto demasiado corto se trunca silenciosamente y el modelo responde sin haber leído el final de tus pasajes.

ask.py
import sys, requests
import chromadb
from chromadb.utils.embedding_functions.ollama_embedding_function import OllamaEmbeddingFunction

MODELE = "ministral-3:8b"
ef = OllamaEmbeddingFunction(url="http://localhost:11434", model_name="bge-m3")
coll = chromadb.PersistentClient(path="./chroma_db").get_collection("mes_docs", embedding_function=ef)

SYSTEME = (
    "Tu réponds en français, uniquement à partir des passages fournis. "
    "Si la réponse n'y figure pas, dis-le clairement au lieu de deviner. "
    "Termine chaque affirmation par le nom du fichier source entre crochets."
)

def repondre(question: str, k: int = 5):
    res = coll.query(query_texts=[question], n_results=k)
    passages = list(zip(res["documents"][0], res["metadatas"][0]))
    contexte = "\n\n---\n\n".join(f"[{m['source']}]\n{p}" for p, m in passages)
    r = requests.post("http://localhost:11434/api/chat", json={
        "model": MODELE,
        "stream": False,
        "options": {"temperature": 0.2, "num_ctx": 8192},
        "messages": [
            {"role": "system", "content": SYSTEME},
            {"role": "user", "content": f"PASSAGES :\n{contexte}\n\nQUESTION : {question}"},
        ],
    }, timeout=300)
    r.raise_for_status()
    return r.json()["message"]["content"], sorted({m["source"] for _, m in passages})

if __name__ == "__main__":
    q = " ".join(sys.argv[1:]) or input("Question : ")
    reponse, sources = repondre(q)
    print("\n" + reponse)
    print("\nSources consultées :", ", ".join(sources))
Iniciar
python index.py
python ask.py "Quel est le délai de préavis prévu au contrat ?"

#Verificar lo que devuelve ChromaDB antes de culpar al modelo

Cuando una respuesta es mala, la causa está en uno de dos puntos: la búsqueda no ha recuperado el pasaje correcto o el modelo lo ha usado mal. Puedes distinguir ambos casos mostrando los pasajes recuperados con su distancia, sin llamar al modelo. Si el pasaje correcto no está entre los cinco primeros, cambia la segmentación, añade una búsqueda por palabras clave o un reranker. Si está presente y la respuesta sigue siendo incorrecta, el problema viene del prompt, del contexto truncado o del modelo: prueba un modelo de mayor tamaño antes de sacar conclusiones.

debug.py : mostrar los pasajes y su distancia
import sys
import chromadb
from chromadb.utils.embedding_functions.ollama_embedding_function import OllamaEmbeddingFunction

ef = OllamaEmbeddingFunction(url="http://localhost:11434", model_name="bge-m3")
coll = chromadb.PersistentClient(path="./chroma_db").get_collection("mes_docs", embedding_function=ef)
res = coll.query(query_texts=[" ".join(sys.argv[1:])], n_results=8)
for doc, meta, dist in zip(res["documents"][0], res["metadatas"][0], res["distances"][0]):
    print(f"{dist:.3f}  {meta['source']}  {doc[:120]!r}")

#Presupuesto de memoria: lo que debe caber al mismo tiempo

El RAG hace coexistir dos modelos, el que genera y el que calcula los vectores, además de la caché de contexto del primero. Ollama carga cada modelo bajo demanda y puede liberar de la memoria uno para hacer espacio al otro, lo que añade un retraso en cada cambio si la memoria es escasa. La tabla ofrece un orden de magnitud para tres configuraciones; el tamaño del modelo procede de la biblioteca de Ollama y el resto es un cálculo que hay que afinar con la calculadora de VRAM del sitio.

Memoria necesaria (pesos de Ollama, contexto de 8 192 tokens)
ConfiguraciónPeso del modelo de generaciónA añadirTarjeta objetivo
ministral-3:8b + bge-m36,0 GBCaché de contexto, modelo de embeddings (567 millones de parámetros, apenas más de un GB en media precisión), margen del sistema8 a 12 GB
ministral-3:14b + bge-m39,1 GBÍdem; el contexto largo se convierte en el factor limitante con 12 GB12 a 16 GB
mistral-small3.2:24b + bge-m315 GBLo mismo; prever un margen holgado24 GB o más
→
Si falta memoria
Reduce primero num_ctx (8192 ya es generoso para cinco fragmentos) y luego pasa a un modelo de menor tamaño. Evita también aumentar el número de fragmentos: demasiados fragmentos diluyen la respuesta tanto como llenan la memoria.

#Las trampas que llevan a respuestas que no vienen al caso

El contexto predeterminado es demasiado corto
Ver más arriba: si no se aumenta num_ctx, los últimos pasajes se truncan. Síntoma típico: ChromaDB recupera correctamente la respuesta, pero el modelo dice que no la encuentra.
PDF escaneados
pypdf solo lee texto ya presente. Un documento escaneado devuelve un resultado vacío: el script lo muestra. Primero procesa el documento con OCR, como se describe en la guía sobre Tesseract.
Fragmentos sin contexto
Un fragmento extraído de su documento («el plazo es de 30 días») no indica de qué trata. Antepón a cada fragmento el título del documento o de la sección.
Pregunta sin respuesta en los documentos
Sin la instrucción «dilo claramente», un modelo rellena el vacío con lo que sabe. Prueba siempre una pregunta cuya respuesta no esté en tus archivos.
Identificadores y términos exactos
Un número de contrato o de expediente no se encuentra bien mediante los embeddings: añade una búsqueda por palabras clave, como se describe en la guía sobre búsqueda híbrida.
!
Verificar antes de confiar
Un RAG cita sus fuentes, pero eso no prueba que la respuesta sea correcta: abre el archivo citado para las decisiones importantes (contratos, cifras, plazos).

#Para ir más allá

Mejoras ordenadas según la relación entre esfuerzo y efecto
MejoraEsfuerzoCuándo hacerlo
Aumentar el número de pasajes (k) de 5 a 8Una líneaLa respuesta se distribuye en varios pasajes
Chunking por títulos en lugar de párrafosMedioDocumentos estructurados (documentación, contratos por artículos)
Búsqueda híbrida BM25 + vectorialMedioPreguntas por identificador, sigla o nombre propio
Reranker (bge-reranker-v2-m3)MedioLa respuesta correcta se recupera pero se coloca más allá del 5.º puesto
Interfaz de chat (Open WebUI, API FastAPI)VariableOtras personas deben usar la herramienta
Copia de seguridad y reindexación programadasBajoEl directorio de documentos evoluciona cada semana

Cada mejora tiene su propia guía: mide el recall con entre 30 y 50 preguntas reales antes y después, en lugar de acumular técnicas. Si prefieres una interfaz ya preparada sin escribir código, la guía sobre RAG sin programar presenta Open WebUI y AnythingLLM.

#Preguntas frecuentes sobre RAG con Mistral

FAQ
¿Qué modelo Mistral para un RAG local?+
Para una tarjeta de 8 a 12 GB, ministral-3:8b (6,0 GB en Ollama, licencia Apache 2.0) es un buen punto de partida; ministral-3:14b (9,1 GB) para 16 GB; mistral-small3.2:24b (15 GB) para 24 GB o más. Elige según la memoria que quede libre una vez cargado el modelo: se necesita espacio para el contexto.
¿Puede Ollama calcular los embeddings en lugar de sentence-transformers?+
Sí: Ollama expone una API de embeddings y ofrece bge-m3, un modelo multilingüe de 567 millones de parámetros. ChromaDB proporciona el adaptador OllamaEmbeddingFunction para realizar las llamadas. La ventaja es tener solo un motor que instalar, sin PyTorch; la desventaja es que hay que mantener Ollama activo durante la indexación.
¿Por qué el modelo dice que no encuentra la respuesta si está en mis documentos?+
Dos causas frecuentes. O bien la ventana de contexto de Ollama es demasiado corta y los pasajes se truncan: aumenta num_ctx a 8.192. O bien los pasajes recuperados no contienen la respuesta: comprueba lo que devuelve ChromaDB antes de la generación, luego ajusta la división en fragmentos o la búsqueda.
¿Se puede usar la API Mistral en lugar de Ollama?+
Técnicamente sí, pero tus fragmentos de texto se enviarían entonces a los servidores de la empresa, lo que contradice el requisito de confidencialidad para documentos sensibles. Para seguir trabajando en local, conserva los modelos de pesos abiertos ejecutados por Ollama. Para documentos no sensibles, puedes usar la API; en ese caso, debes consultar las condiciones de tratamiento de los datos del proveedor.
¿Cómo agregar nuevos documentos sin volver a indexarlo todo?+
Copia los archivos a docs/ y vuelve a ejecutar index.py: gracias a upsert y a los identificadores estables, los fragmentos existentes se actualizan y los nuevos se añaden. Si modificas el tamaño de los fragmentos o el modelo de embeddings, elimina el directorio chroma_db y vuelve a indexarlo todo: los vectores antiguos ya no son comparables.
¿Se necesita un GPU para este RAG?+
No necesariamente: ministral-3:3b funciona en un procesador reciente con 8 GB de memoria, con respuestas lentas. La indexación, en cambio, se ejecuta una sola vez. Un GPU mejora principalmente la respuesta: más velocidad y la posibilidad de usar un modelo más grande. Mide el tiempo de respuesta en tu máquina antes de decidir invertir.
¿Esta guía te ha ayudado?

¿Un comentario, un error, una precisión? Avísanos, eso mejora la guía para todos.