Load, chunk, embed, store, retrieve, rerank, generate — the full retrieval-augmented-generation pipeline with the choices that matter.
83 items
◷ 7 min
Chunk
Embed
Retrieve
Start hereQuick start · 6 you’ll reach for daily
LoadLoader(path).load()
ChunkRecursiveCharacterTextSplitter()
EmbedOpenAIEmbeddings(model=…)
StoreChroma.from_documents(…)
Retrievestore.as_retriever(k=4)
Generateretriever | prompt | llm
scope · stack-neutralVersions
Targets:any modern chat modelany vector DBpython ≥ 3.10
Concepts on this page transfer across frameworks. Snippets use LangChain because it has the broadest
coverage, but every step (load → chunk → embed → store → retrieve → rerank → generate) maps cleanly
to LlamaIndex
and Haystack.
install · smoke testSetup
bash
# Minimum stack — works for >90% of starter projects
pip install langchain langchain-openai langchain-chroma langchain-text-splitters
# Optional reranker + sparse retriever (hybrid)
pip install sentence-transformers cohere rank-bm25
# Provider keys
export OPENAI_API_KEY=sk-...
export COHERE_API_KEY=co-...
# Sanity check the stack
python - <<'PY'
from langchain_openai import OpenAIEmbeddings
emb = OpenAIEmbeddings(model="text-embedding-3-small")
print(len(emb.embed_query("hello"))) # 1536 for 3-small
PY
the seven stagesPipeline
1. Load
Source → Document. PDF, web, S3, Notion, DB …
2. Chunk
Document → Node / chunk. Drives recall.
3. Embed
Chunk → vector. Pick a model that matches your domain.
Cross-encoder pass over the top-k. Highest accuracy lever.
7. Generate
Prompt + chunks → answer. Cite. Refuse when unsure.
Each stage has its own failure mode. Bad answer? Diagnose top-down: was the right chunk
retrieved, was it reranked into the prompt, did the model use it?
If you change one thing, add a reranker. Single biggest accuracy lever per dollar —
typical lift is 10-20 points NDCG@5 vs raw dense retrieval, at one extra hosted API call.
prompt the answerGeneration
stuff
Single LLM call with all chunks. Cheapest. Default for top-k ≤ 5.
refine
Walk chunks one-by-one, refining the answer. Use for long answers.
map_reduce
Answer per chunk → aggregate. Best for summarisation across many docs.
map_rerank
Answer per chunk + self-score → pick highest. Single-answer tasks.
Citations
Inject source IDs in the context; ask for them back: [file:page].
Refusal path
“If unsure, say ‘I don’t know’.” Reduces hallucination.
Strict-context instruction
“Use ONLY the context.” Repeat at the end of the prompt.
Stream
Generate tokens as they arrive. Big UX win for long answers.
measure RAG qualityEvaluation
Retrieval metrics
Recall@k, MRR, nDCG@k. Cheap, model-free.
Faithfulness / groundedness
Does the answer follow from the retrieved context?
Answer relevance
Does the answer match the question intent?
Context precision / recall
Were the right chunks retrieved? Did they cover everything?
RAGAS
Library that bundles the above as LLM-judged metrics.
TruLens / Phoenix / LangSmith
Observability + eval over real traces.
Golden set: 20–100 Q/A
Hand-curated, sourced from real questions.
A/B retrieval before A/B prompts
Test retrieval changes in isolation; prompts are downstream.
A folder of PDFs to an answering chain with citations. Replace the path and you have a working doc-bot.
python
# Load -> split -> embed -> retrieve -> rerank -> generate -> cite.
from pathlib import Path
from langchain_community.document_loaders import PyPDFDirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
docs = PyPDFDirectoryLoader("./pdfs").load()
chunks = RecursiveCharacterTextSplitter(
chunk_size=900, chunk_overlap=120,
).split_documents(docs)
emb = OpenAIEmbeddings(model="text-embedding-3-small")
store = Chroma.from_documents(chunks, emb, persist_directory="./chroma")
retriever = store.as_retriever(search_kwargs={"k": 6})
PROMPT = ChatPromptTemplate.from_messages([
("system",
"Answer using ONLY the context. Cite sources as [filename:page]. "
"If unsure, say 'I don't know'.\n\nContext:\n{context}"),
("human", "{question}"),
])
def format_ctx(docs):
return "\n\n".join(
f"[{d.metadata.get('source')}:{d.metadata.get('page')}] {d.page_content}"
for d in docs
)
chain = (
{"context": retriever | format_ctx, "question": RunnablePassthrough()}
| PROMPT
| ChatOpenAI(model="gpt-4o-mini", temperature=0)
| StrOutputParser()
)
print(chain.invoke("Summarise the access policy in 3 bullets."))
Best practiceGood to know
Hybrid retrieval + reranker is the strong default.
Dense alone misses keyword-heavy queries; sparse alone misses paraphrases. A cross-encoder rerank on
top of both reliably beats either branch.
Most “bad RAG” is bad chunking.
Before tweaking embeddings or LLMs, inspect what chunks look like. Headers stripped? Tables fragmented?
Code snippets cut in half? Fix at the splitter.
Always pass source IDs through.
Stitch metadata["source"], page,
heading into every chunk. Citations are 80% of user trust.
Common trapsWatch out for
Mismatched embedding model on reload.
If you ingest with text-embedding-3-small (1536-d) and query with
3-large (3072-d), the store raises on dim mismatch — or worse, returns
garbage with a homemade wrapper. Pin the model name in metadata.
Don’t mix tenant data in one collection.
Even with metadata filtering, accidental leakage is a query-rewriter bug away. Use separate collections
or partition keys at the store level.
Large top_k dilutes the prompt.
Past ~8 chunks, recall stops helping and signal-to-noise drops — the model gets confused. Retrieve
wide, rerank narrow.
RAG is a pattern that improves LLM responses by injecting relevant retrieved documents into the prompt at inference time. Instead of relying on the model's training knowledge, RAG fetches up-to-date or domain-specific chunks from a vector store (or BM25 index), reranks them, and passes them as context. This reduces hallucinations and keeps answers grounded in real data.
What are the main steps in a RAG pipeline?
The pipeline has two phases. Indexing: load documents, chunk them (by token count, sentence, or semantic boundary), embed each chunk with an embedding model, and store vectors in a vector database. Retrieval: embed the query, search the vector store for top-k chunks, optionally rerank with a cross-encoder, then pass the chunks and query to an LLM to generate the final answer.
What is the best chunking strategy for RAG?
There is no single best strategy. Fixed-size token chunks (256-512 tokens with 10-20% overlap) are the safe default. Recursive character splitting respects natural text boundaries. Semantic chunking groups sentences by embedding similarity for more coherent chunks. Parent-document retrieval stores small chunks but returns their larger parent for context. Experiment with chunk size — it is one of the highest-impact RAG parameters.
What is reranking in RAG and why does it matter?
A vector search retrieves top-k candidates by embedding similarity, which is fast but imprecise. A reranker (cross-encoder) scores each candidate against the query in a joint forward pass, producing much better relevance ordering. Cohere Rerank, BGE-Reranker, and Jina Reranker are popular options. Retrieve 20-50 candidates from ANN, rerank, then pass only the top 3-5 to the LLM to save tokens.
What is the difference between naive RAG and advanced RAG?
Naive RAG is a single retrieval step: embed query, retrieve top-k, generate. Advanced RAG adds query rewriting (HyDE, step-back prompting), multi-query expansion, hybrid search (dense + sparse), reranking, iterative retrieval (self-RAG, FLARE), and citation grounding. Start naive and add complexity only where evaluation shows it improves answer quality.
Which frameworks support RAG pipelines?
LangChain and LlamaIndex are the two most popular frameworks with built-in RAG abstractions — loaders, splitters, retrievers, and chain/query engine primitives. Haystack offers a pipeline-component model. For production, many teams build custom pipelines directly using embedding model APIs (OpenAI, Cohere, Google) and vector DBs (Qdrant, Chroma, Milvus, Pinecone) to avoid framework lock-in.