RAG Frameworks: LangChain, LlamaIndex, Haystack
On this page
Beyond RAG from Scratch
You have built a complete RAG pipeline in pure Python, and that was the right path: you now know what every line does, where alignment errors hide, and why a poorly sliced chunk ruins an answer. In production, you can also rely on specialized frameworks that handle the plumbing and add advanced features. The three main ones—LangChain, LlamaIndex, and Haystack—are all compatible with Mistral AI, and the choice between them depends less on quality than on the type of application you are targeting.
LangChain with Mistral
LangChain is the most popular framework for LLM applications. Its main strength is the breadth of its integration ecosystem, complemented by the LangGraph module for advanced control flows. The installation bundles the core, the Mistral connector, community integrations, and FAISS.
pip install langchain langchain-mistralai langchain-community faiss-cpu
The minimal RAG then fits in about thirty lines, and the point is to see what has disappeared compared to your manual implementation. Batching, retries, conversion to float32, alignment between vectors and metadata: all of this is absorbed by FAISS.from_texts. The RecursiveCharacterTextSplitter replaces your chunking functions with the same parameters as before, 1500 characters and 200 of overlap. The final chain is written with the pipe operator, which composes steps from left to right: the retriever feeds the context, RunnablePassthrough circulates the question as is, the prompt assembles them, the model generates, and the parser extracts the string.
from langchain_mistralai import ChatMistralAI, MistralAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Mistral models
embeddings = MistralAIEmbeddings(model="mistral-embed")
llm = ChatMistralAI(model="mistral-large-latest", temperature=0.1)
# Splitting and indexing
splitter = RecursiveCharacterTextSplitter(
chunk_size=1500,
chunk_overlap=200,
)
chunks = splitter.split_text(document_text)
# Create the vector store
vectorstore = FAISS.from_texts(chunks, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# RAG chain
prompt = ChatPromptTemplate.from_template(
"""Context: {context}
Question: {question}
Answer based on the context."""
)
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# Usage
answer = rag_chain.invoke("How does FAISS work?")
print(answer)
LangGraph for Corrective, Self, and Adaptive RAG
The linear chain above works for standard RAG, but it cannot loop or branch—yet that is exactly what the architectures in the previous three lessons do. LangGraph addresses this need by defining control flows as graphs. The typed GraphState acts as shared memory: each node receives it, reads what it needs, and returns the fields it updates. You will recognize your own functions behind retrieve_node, grade_node, and generate_node, with grade_document reused as is.
The truly new mechanism is add_conditional_edges. Where your Python class chained if statements, the graph declares a decision function—here route_after_grade—and the table of possible destinations. This separation between the business logic of the nodes and the flow topology makes the pipeline readable at a glance and modifiable without touching the step code. Once compiled, the graph is invoked like a simple function.
from langgraph.graph import StateGraph, END
from typing import TypedDict
class GraphState(TypedDict):
question: str
documents: list
generation: str
web_search_needed: bool
def retrieve_node(state):
"""Retrieval node."""
question = state["question"]
docs = retriever.get_relevant_documents(question)
return {"documents": docs, "question": question}
def grade_node(state):
"""Document grading node."""
question = state["question"]
docs = state["documents"]
relevant = []
web_needed = False
for doc in docs:
# Use the LLM to evaluate relevance
grade = grade_document(question, doc.page_content)
if grade:
relevant.append(doc)
else:
web_needed = True
return {"documents": relevant, "web_search_needed": web_needed}
def generate_node(state):
"""Generation node."""
question = state["question"]
docs = state["documents"]
context = "\n\n".join([d.page_content for d in docs])
generation = llm.invoke(f"Context: {context}\n\nQuestion: {question}")
return {"generation": generation.content}
def route_after_grade(state):
"""Conditional decision after grading."""
if state["web_search_needed"]:
return "web_search"
return "generate"
# Build the graph
workflow = StateGraph(GraphState)
# Add nodes
workflow.add_node("retrieve", retrieve_node)
workflow.add_node("grade", grade_node)
workflow.add_node("generate", generate_node)
# Define the flow
workflow.set_entry_point("retrieve")
workflow.add_edge("retrieve", "grade")
workflow.add_conditional_edges("grade", route_after_grade, {
"generate": "generate",
"web_search": "web_search",
})
workflow.add_edge("generate", END)
# Compile
app = workflow.compile()
# Execute
result = app.invoke({"question": "How does RAG work?"})
print(result["generation"])
LlamaIndex with Mistral
LlamaIndex pushes abstraction further because it is optimized specifically for RAG and structured document management.
pip install llama-index llama-index-llms-mistralai llama-index-embeddings-mistralai
Configuration happens via a global Settings object that sets the generation model and the embedding model once and for all. SimpleDirectoryReader loads an entire directory by guessing formats, and VectorStoreIndex.from_documents chains chunking and encoding without you having to parameterize them. The gain in conciseness is real, but measure what it implies: the splitting strategy, which you now know determines retrieval quality, is chosen for you here. It is comfortable for getting started, and it is the first setting you will need to take back in hand the day answers disappoint. Note that response.source_nodes preserves metadata, hence the traceability you implemented manually.
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.llms.mistralai import MistralAI
from llama_index.embeddings.mistralai import MistralAIEmbedding
from llama_index.core import Settings
# Global configuration
Settings.llm = MistralAI(model="mistral-large-latest", temperature=0.1)
Settings.embed_model = MistralAIEmbedding(model_name="mistral-embed")
# Load documents
documents = SimpleDirectoryReader("./data/").load_data()
# Create the index (automatic chunking + embedding)
index = VectorStoreIndex.from_documents(documents)
# Create the query engine
query_engine = index.as_query_engine(similarity_top_k=3)
# Ask a question
response = query_engine.query("What are the steps of RAG?")
print(response)
print(f"\nSources: {[n.metadata for n in response.source_nodes]}")
Haystack with Mistral
Haystack, developed by deepset, adopts a third philosophy: that of modular pipelines, where each component is named, added, and then explicitly connected to its neighbors.
pip install haystack-ai mistral-haystack
The indexing pipeline below illustrates this approach well. Three components are declared—the splitter, the encoder via MistralDocumentEmbedder, and the writer to the document store—then linked by connect calls that make the data flow visible in the code. This verbosity, which may seem unnecessary on a simple case, becomes an asset as soon as the pipeline branches or you need to replace a link without touching the others. Also note the distinction between MistralDocumentEmbedder and MistralTextEmbedder: the former encodes documents at indexing time, the latter will encode queries at interrogation time, a separation that prevents the classic error of treating both the same way.
from haystack import Pipeline
from haystack.components.writers import DocumentWriter
from haystack.components.preprocessors import DocumentSplitter
from haystack_integrations.components.embedders.mistral import (
MistralDocumentEmbedder,
MistralTextEmbedder,
)
from haystack_integrations.components.generators.mistral import MistralChatGenerator
from haystack.document_stores.in_memory import InMemoryDocumentStore
# Document store
doc_store = InMemoryDocumentStore()
# Indexing pipeline
indexing_pipeline = Pipeline()
indexing_pipeline.add_component("splitter", DocumentSplitter(
split_by="passage", split_length=5
))
indexing_pipeline.add_component("embedder", MistralDocumentEmbedder(
model="mistral-embed"
))
indexing_pipeline.add_component("writer", DocumentWriter(
document_store=doc_store
))
indexing_pipeline.connect("splitter", "embedder")
indexing_pipeline.connect("embedder", "writer")
Which One to Choose
Each framework has its strengths, and the table below summarizes the trade-off.
| Framework | Main Strength | Sweet Spot |
|---|---|---|
| LangChain | Richest ecosystem, LangGraph for complex control flows | Agents and advanced RAG |
| LlamaIndex | Simplest for RAG, native handling of structured documents | Quick start, beginners |
| Haystack | Modular pipeline approach, strong on evaluation | Production deployment |
Next Steps
You now have all the basics to build RAG systems with Mistral AI. You know how to encode text with mistral-embed and its 1024 dimensions, encode code with codestral-embed and its configurable dimensions up to 3072, build a RAG from scratch on FAISS and the Mistral API, then harden it with Corrective, Self, and Adaptive architectures. This final lesson adds the framework path: LangChain, LlamaIndex, and Haystack.
To go further, explore the official Mistral AI cookbooks as well as the notebooks for each framework, which track API evolution more closely than any course. RAG is a fast-moving field: chunking, retrieval, and verification techniques improve constantly, and it is precisely because you have written every step by hand that you will be able to evaluate the next one that appears.
Key Takeaways
- LangChain + LangGraph is the primary choice for advanced RAG control flows
- LlamaIndex is the simplest for standard RAG with Mistral
- Haystack excels in modularity and pipeline evaluation
- All three frameworks natively support mistral-embed and mistral-large
- Start with RAG from scratch to understand, then migrate to a framework for production
Test Your Knowledge
Embeddings, RAG, and advanced variants: the grand final exam.
1. What are Mistral's two embedding models, and what are they for?
Answer: mistral-embed for general text, codestral-embed for code—each projects its domain into a space where semantic similarity is measured.
2. What steps make up the course's RAG pipeline?
Answer: Prepare data (chunking), create embeddings, store them in a vector store (FAISS), then retrieval + generation: the question finds its passages, the model answers based on them.
3. What does Corrective RAG fix?
Answer: Poor retrievals: it evaluates the relevance of retrieved passages and triggers a correction (new search, reformulation) before generating—instead of answering on noise.
4. Self-RAG and Adaptive RAG: what is the common idea?
Answer: Make the pipeline reflexive: decide when to retrieve, critique its own outputs, adapt the strategy to the question—RAG stops being a fixed pipe.
5. What do LangChain, LlamaIndex, or Haystack bring?
Answer: Tooling: ready-made components for ingestion, indexes, chains, and evaluation—useful for speed, provided you understand what they assemble (which this course has taught you).
You now know how to build a RAG by hand and entrust it to a framework with full awareness—that is the right order.