DEV Community

Moon Robert
Moon Robert

Posted on • Originally published at blog.rebalai.com

LangChain vs LlamaIndex vs Haystack: Lo que aprendí construyendo RAG en producción

Pasé las últimas dos semanas migrando un sistema RAG entre tres frameworks — y no fue una decisión voluntaria. Empezamos con LangChain, las abstracciones se volvieron difíciles de mantener, alguien del equipo sugirió LlamaIndex, probamos eso, y al final terminé revisando Haystack casi de casualidad mientras buscaba una solución a otro problema. Así que lo hice bien: monté un benchmark real con nuestros datos reales y medí lo que importa.

Trabajo en un equipo de seis personas, construimos un sistema RAG para un cliente fintech. El corpus tiene alrededor de 480k documentos — PDFs escaneados (los peores, siempre), HTML de sus portales internos, y algo de Markdown de sus wikis. Presupuesto máximo de inferencia: $800/mes. Eso descartó varias opciones antes de llegar siquiera a cuestiones de arquitectura.

Las versiones que probé: LangChain 0.3.15, LlamaIndex 0.12.3, Haystack 2.7.1.

LangChain 0.3.15 — El ecosistema que a veces juega en tu contra

LangChain fue mi punto de partida porque ya lo conocía. Y ese mismo conocimiento fue parte del problema — teníamos código de un proyecto de hace ocho meses que hubo que reescribir parcialmente porque las interfaces habían cambiado. Otra vez.

La API ha mejorado, seré honesto. LCEL (LangChain Expression Language) funciona bien cuando lo entiendes de verdad. La composición de cadenas con el operador | queda limpia, y el tracing con LangSmith nos ahorró horas de debugging en staging.

from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_anthropic import ChatAnthropic

# El retriever con score_threshold fue la parte que más tardamos en afinar.
# 0.72 fue nuestro número después de ~200 consultas de evaluación manual.
retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"k": 5, "score_threshold": 0.72}
)

# LCEL en acción — se lee bien y en general funciona bien
chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | ChatAnthropic(model="claude-opus-4-6", temperature=0)
    | StrOutputParser()
)
Enter fullscreen mode Exit fullscreen mode

El problema no es la calidad técnica del framework. LangChain intenta cubrir todo — agentes, memoria, herramientas, cadenas, RAG — y el precio de esa amplitud es que la profundidad en retrieval específicamente no está a la altura de la competencia. Cuando quise implementar sentence window retrieval o hierarchical node parsing, tuve que construirlo yo mismo o buscar en la comunidad. Hay soluciones, sí, pero en cinco versiones distintas de la librería, no siempre queda claro cuál aplica a la tuya.

Un momento concreto que me sacó de quicio: en diciembre intenté usar MultiVectorRetriever con documentos largos y me topé con un bug reportado en el GitHub de LangChain (issue #12847) donde los document IDs se sobreescribían silenciosamente. Funcionaba en local pero no en producción — diferencia en la versión de Chroma, resultó ser. Dos días perdidos en eso.

Para proyectos donde necesitas máxima flexibilidad y tienes equipo dispuesto a mantener código propio, LangChain tiene sentido. Para RAG puro, hay mejores opciones.

LlamaIndex 0.12.3 — Cuando la calidad del retrieval realmente importa

Cambié a LlamaIndex con escepticismo. Me sorprendió.

Está mucho más enfocado en el problema de indexing y retrieval, y eso se nota en los detalles de implementación. La combinación que más movió nuestros números fue SentenceWindowNodeParser con MetadataReplacementPostProcessor. La idea: indexas oraciones individuales para tener precisión en el retrieval, pero al momento de generar la respuesta reemplazas ese fragmento con una ventana de contexto mayor. Bajamos el hallucination rate de ~12% a aproximadamente 4% con este cambio solo. Eso es lo que quiero decir con "profundidad en retrieval" — no una feature de marketing, sino herramientas concretas para un problema concreto.

from llama_index.core import VectorStoreIndex, Settings
from llama_index.core.node_parser import SentenceWindowNodeParser
from llama_index.core.postprocessor import MetadataReplacementPostProcessor

# Esta combinación fue la que realmente cambió nuestras métricas de evaluación
node_parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,                           # 3 oraciones de contexto por lado
    window_metadata_key="window",
    original_text_metadata_key="original_text",
)

Settings.node_parser = node_parser
Settings.chunk_size = 512  # más pequeño de lo que usábamos antes con LangChain

index = VectorStoreIndex.from_documents(
    documents,
    show_progress=True,
)

# El postprocessor reemplaza el nodo recuperado con su ventana de contexto
# justo antes de armar el prompt — la magia está aquí
query_engine = index.as_query_engine(
    similarity_top_k=6,
    node_postprocessors=[
        MetadataReplacementPostProcessor(target_metadata_key="window")
    ],
)
Enter fullscreen mode Exit fullscreen mode

Lo que no me convenció: el sistema de configuración global con Settings se siente torpe cuando tienes múltiples pipelines en el mismo proceso. Cada vez que necesitábamos cambiar algo para un subíndice específico, sobreescribíamos configuraciones globales y rezábamos para que los tests de integración no se pisaran entre sí. El ServiceContext directo era una opción pero está deprecado en 0.12.x, y la historia de migración no es del todo limpia.

También: la documentación asume que usas sus abstracciones de punta a punta. Cuando quisimos integrar LlamaIndex solo para el retrieval y usar nuestro propio sistema de generación, hubo más fricción de lo esperado. No imposible, pero tampoco el camino documentado.

Igual, si el retrieval de calidad es tu prioridad principal, LlamaIndex tiene la mejor caja de herramientas de los tres. Por bastante margen.

Haystack 2.7.1 — El que nadie menciona en los tutoriales

Seré directo: no esperaba que Haystack me sorprendiera tanto.

Tiene una fracción del mindshare de los otros dos, y eso es una lástima porque para ambientes de producción tiene ventajas reales. El modelo mental es distinto al de LangChain y LlamaIndex: todo es un pipeline de componentes conectados, y ese pipeline es un ciudadano de primera clase — lo puedes serializar a YAML, versionarlo, desplegarlo con configuración externa.

# pipeline.yaml — esto es real, no pseudocódigo de blog
components:
  retriever:
    type: haystack.components.retrievers.InMemoryEmbeddingRetriever
    init_parameters:
      document_store: !component document_store
      top_k: 5
  prompt_builder:
    type: haystack.components.builders.PromptBuilder
    init_parameters:
      template: |
        Contexto: {% for doc in documents %}{{ doc.content }}{% endfor %}
        Pregunta: {{ question }}
        Respuesta:
connections:
  - sender: retriever.documents
    receiver: prompt_builder.documents
Enter fullscreen mode Exit fullscreen mode

Eso suena aburrido hasta que estás en tu tercer deployment del día y alguien de QA quiere correr la versión exacta del pipeline que falló en staging. Ser capaz de versionar el pipeline como un artefacto separado del código de aplicación es algo que los otros dos no tienen de serie.

Donde me costó caro: la comunidad es significativamente más pequeña. Pasé cuatro horas un martes intentando entender por qué mi DocumentSplitter ignoraba los saltos de página en PDFs. Nada en Stack Overflow. El Discord de Haystack tenía una pregunta similar sin respuesta de hace tres meses. Al final leí el código fuente — hay un parámetro split_by="page" que no está en la guía de inicio rápido pero sí en el API reference si sabes dónde buscar. Ese tipo de fricción se acumula cuando hay plazos encima.

La observabilidad está mejor pensada, especialmente si ya usas OpenTelemetry. El tracing sale de caja con detalle sobre qué componente tomó cuánto tiempo, sin pagar por herramientas externas.

No sé si Haystack escala más allá de lo que nosotros probamos — alrededor de 2,000 queries diarios en producción. Mi intuición dice que sí, pero no tengo datos propios para afirmarlo.

El Viernes que Casi Me Hace Cambiar de Opinión

Estaba casi decidido por LlamaIndex cuando pasó algo que me hizo repensar el criterio de evaluación completo.

Un viernes por la tarde — clásico — empujé una actualización al servicio de indexación. El proceso de background para indexar documentos nuevos empezó a consumir memoria de forma inconsistente. No todos los runs, tal vez uno de cada cuatro. El problema: en LlamaIndex 0.12.x, hay edge cases en el manejo de memoria al indexar documentos con embeddings de páginas muy largas — PDFs de más de 200 páginas en nuestro caso. Encontré la issue en GitHub pero no había fix todavía.

La solución que terminé usando fue procesar esos documentos en batches más pequeños con un wrapper propio. Funcionó, pero me dejó pensando: ¿cuánto tiempo debería gastar parchando comportamiento de framework versus construyendo producto?

Pensaba que la calidad del retrieval era el criterio más importante, pero en producción la operabilidad importa tanto o más. La pregunta que no me había hecho al inicio del benchmark.

Con Haystack, el mismo escenario hubiera sido más predecible — el pipeline explícito hace más obvio dónde puede fallar y dónde intervenir. Con LangChain hubiera tenido más opciones de configuración pero también más superficie de error. No hay respuesta perfecta. Solo hay tradeoffs que vale la pena conocer antes de comprometer el stack de un cliente.

Mi Recomendación (sin el "depende de tu caso de uso")

Voy directo: si en 2026 estás arrancando un nuevo proyecto RAG de cero con un corpus de escala media o grande, te recomiendo LlamaIndex para la capa de indexación y retrieval. La toolbox de retrieval avanzado no tiene competencia real en los otros dos frameworks, y esa diferencia se traduce en métricas concretas.

Si tu equipo valora la operabilidad y la reproducibilidad de pipelines sobre la velocidad de prototipado inicial — y especialmente si ya tienen cultura de infraestructura-como-código — considera Haystack. La curva de adopción es más alta, pero lo que ganas en predictibilidad en producción compensa.

LangChain tiene sentido en un escenario específico: si necesitas construir agentes con herramientas complejas, o si tu equipo ya lo conoce bien y el costo de migración supera el beneficio. El ecosistema de integraciones es el más amplio de los tres. Pero para RAG puro, es la opción con más overhead de mantenimiento.

Aquí lo que no esperaba aprender: el framework que eliges afecta cómo piensas el problema, no solo cómo lo implementas. LangChain te hace pensar en cadenas. LlamaIndex te hace pensar en nodos y retrieval. Haystack te hace pensar en pipelines y componentes. Si tu modelo mental ya encaja con uno de esos paradigmas, eso es información válida para la decisión — más válida que cualquier benchmark sintético.

Mi setup actual: LlamaIndex para indexación y retrieval, FastAPI por encima, métricas propias con Prometheus. Ninguno de los tres me convenció con su historia de observabilidad nativa, así que ahí construí algo propio. En los próximos meses voy a mirar si la integración OpenTelemetry de Haystack madura lo suficiente para reemplazar eso — pero por ahora, lo que tenemos funciona.

Top comments (0)