Inicio / Artículos

RAG de PDF completamente local con LangChain, Ollama y Chroma

Una aplicación RAG en PDF completamente local: LangChain ingiere los archivos, Ollama los integra, Chroma almacena los vectores, y las respuestas funcionan sin conexión sin necesidad de claves de API en la nube.

Editor: REACTAPP.TOP5 min de lectura1111 palabras

Fuentes: Implement RAG with Langchain and local Ollama models

Crea una pipeline RAG completamente local para PDFs con LangChain, Ollama y ChromaDB: chatea con documentos sin necesidad de APIs en la nube ni de que los datos salgan de la máquina.

Por qué es importante ser local

Los LLM en la nube son prácticos, pero cada consulta envía texto a través de la red de terceros. Los contratos legales, los registros médicos y los informes internos a menudo no pueden aceptar ese compromiso. Una solución local evita fugas de datos mientras permite hacer preguntas y respuestas en lenguaje natural con tus propios archivos.

Qué obtendrás

  • Ingestión de archivos PDF en un almacén vectorial persistente
  • Respuestas a preguntas en lenguaje natural basadas en esos documentos
  • Ejecución sin conexión en el momento de la inferencia: no se requieren claves API una vez que se han descargado los modelos

Stack: LangChain para la orquestación, Ollama para el chat local y los embeddings, ChromaDB como almacén vectorial.

Arquitectura

Ruta de ingesta: los bytes del PDF entran en un cargador basado en PyPDF, se dividen en fragmentos y luego Ollama incrusta cada fragmento en Chroma.

Ruta de consulta: una pregunta llega al recuperador, los fragmentos coincidentes se convierten en contexto y el modelo de chat local Ollama escribe la respuesta.

Dos fases mantienen los costos bajo control:

  1. Ingestión — una vez (o cuando cambian los documentos): analizar, dividir en fragmentos, incrustar, guardar
  2. Recuperación — con cada consulta: los mejores fragmentos se convierten en contexto para el LLM

Pagas por los costos de incrustación cuando cambia el contenido; las consultas interactivas siguen siendo rápidas.

Requisitos previos

Instala Ollama y descarga los modelos utilizados aquí:

ollama pull embeddinggemma:latest   # embedding model
ollama pull gemma4:latest           # chat model

gemma4 está ajustado para seguir instrucciones específicas. Para un uso con menos recursos, pruebe gemma2:2b; para un razonamiento más potente, utilice gemma4:27b — actualice las cadenas de configuración en consecuencia.

Configuración del proyecto

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Paquetes clave de los requisitos:

langchain
langchain-chroma
langchain-community
langchain-ollama
langchain-text-splitters
pypdf
chromadb
python-dotenv

Sobrescriba los valores predeterminados mediante el entorno sin editar código:

OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=gemma4:latest
OLLAMA_EMBED_MODEL=embeddinggemma:latest

Proceso de ingesta (src/ingest.py)

Cargar documentos

def load_documents(pdf_path: str | Path) -> list:
    path = Path(pdf_path)
    loader = (
        PyPDFLoader(str(path))
        if path.is_file()
        else PyPDFDirectoryLoader(str(path))
    )
    return loader.load()py

Un helper sirve para un archivo o directorio individual. Los cargadores de LangChain siempre devuelven una lista uniforme de Document en ambos casos.

Dividir en fragmentos

def split_documents(documents: list) -> list:
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200,
    )
    return splitter.split_documents(documents)

RecursiveCharacterTextSplitter prefiere dividir el texto por párrafos, luego por oraciones y finalmente por palabras para que los límites semánticos se mantengan. Una superposición de 200 caracteres permite que una oración que atraviesa un límite esté presente en ambos fragmentos adyacentes, lo cual reduce los fallos en la recuperación en los bordes.

Incorporar y persistir

def build_vector_store(chunks: list) -> Chroma:
    embeddings = OllamaEmbeddings(model=EMBEDDING_MODEL, base_url=OLLAMA_BASE_URL)
    VECTOR_STORE_DIR.mkdir(parents=True, exist_ok=True)
    return Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        collection_name=COLLECTION_NAME,
        persist_directory=str(VECTOR_STORE_DIR),
    )

Ollama proporciona embeddings localmente en el puerto 11434. Chroma los almacena en vector_store/ (SQLite junto con archivos de índice HNSW) para que los reinicios del proceso no obliguen a realizar una nueva incorporación completa.

Cadena de recuperación (src/retrieval.py)

LangChain LCEL combina los pasos mediante el operador |.

Plantilla de prompt

RAG_PROMPT = ChatPromptTemplate.from_messages([
    (
        "system",
        "You are an assistant that answers questions based on the provided context "
        "from PDF documents. Use only the information in the context to answer. "
        "If the answer is not in the context, say so clearly.\n\nContext:\n{context}"
    ),
    ("human", "{question}"),
])

El mensaje del sistema ancla las respuestas al contexto recuperado; es la principal medida de seguridad de RAG contra las alucinaciones generadas únicamente a partir del entrenamiento previo.

Construir la cadena

def build_rag_chain(k: int = 4):
    store = load_vector_store()
    retriever = store.as_retriever(search_kwargs={"k": k})
    llm = ChatOllama(model=MODEL_NAME, base_url=OLLAMA_BASE_URL)
    return (
        {"context": retriever | _format_docs, "question": RunnablePassthrough()}
        | RAG_PROMPT
        | llm
        | StrOutputParser()
    )

De izquierda a derecha: el recuperador selecciona los k fragmentos más similares mediante la similitud de embeddings, los asistentes formatan esos fragmentos en una única cadena de contexto, el plantilla de prompt combina ese contexto con la pregunta del usuario y ChatOllama genera la respuesta.

Bucle interactivo de preguntas

# Index a directory of PDFs (default: data/pdfs/)
python main.py ingest

Una interfaz CLI sencilla (o un envoltorio HTTP) carga la cadena una sola vez y procesa las preguntas hasta que se cierre. Mantenga k ajustable para poder equilibrar el tamaño del contexto con la latencia.

Ejecución de principio a fin

# Index a specific file
python main.py ingest --path /path/to/contract.pdf
# Ask a one-off question
python main.py query "What are the payment terms?"
# Interactive session
python main.py chat

Dirija la entrada hacia su carpeta de PDF, confirme que el directorio del almacén de vectores esté completo y luego realice consultas. Todo, incluidos los embeddings, los vectores y la generación, permanece en localhost.

Conclusión

El RAG local ya no es algo exótico. Ollama proporciona los modelos, Chroma almacena los vectores y LangChain conecta el grafo. Para corpus privados, ese trío es suficiente para crear un asistente de documentos útil sin necesidad de facturas por uso en la nube ni de rutas de datos hacia el exterior.

Lecturas relacionadas

  • Ollama en IBM Z y LinuxONE: LLM locales dentro del límite de confianza — La adaptación de Ollama a s390x se basa en cargas de pesos seguras desde el punto de vista endián, para que los datos regulados permanezcan en Z/LinuxONE en lugar de ser enviados a nubes con GPU.
  • RAG solo en VPC en un EC2: Ollama, Qdrant y LangChain — Reemplace el conocimiento tribal por un asistente basado en manuales citados que impide que los documentos salgan de su cuenta de AWS: tres contenedores en un EC2 con GPU.
  • El problema “Modelo no encontrado” de AnythingLLM con Ollama: Cinco soluciones — Los menús desplegables vacíos y las incompatibilidades de etiquetas suelen deberse a conexiones HTTP entre AnythingLLM y Ollama, no a pesos dañados. Siga la lista de verificación de arriba hacia abajo.
  • LangChain plus Ollama: Cadenas locales Qwen, herramientas y estructura — Dirige langchain-ollama hacia una etiqueta local Qwen para cadenas, transmisión en tiempo real, herramientas y salida estructurada, sin necesidad de claves API en la nube.
  • ChromaDB como el cerebro vectorial para chatbots con conocimiento de documentos — Por qué RAG necesita un almacén vectorial, cómo funcionan las colecciones y los filtros de metadatos de Chroma, y cuándo prototipar allí antes de pasar a la fase final.
  • Preparación para entrevistas sobre LangChain con respuestas cortas, análisis profundo y emboscadas — Practica los fundamentos de LangChain con respuestas a preguntas centradas en problemas, junto con ejercicios de código basados en un ticket de interrupción de servicio.
  • RAG con conciencia de dominio con registros MLflow y enrutamiento LLM — Clasificar la intención, seleccionar embeddings de dominio desde MLflow, recuperar datos de la colección Qdrant correspondiente y luego enrutar la generación.
  • RAG de extremo a extremo con Chroma, FastAPI y Llama 3.2 — Desde el particionamiento de PDFs y el reclasificación con cross-encoder hasta FastAPI, Docker, pytest y GitHub Actions para crear un asistente de conocimiento basado en datos reales.