RAG Frameworks — LangChain & LlamaIndex

Map the pipeline you hand-rolled across this track onto LangChain and LlamaIndex abstractions: what each framework does well, how their RAG idioms compare, and the judgment for when to adopt a framework versus hand-roll the ~40 lines you already understand.

~2hlangchainllamaindexrag
Learning objectives
  • Map each stage of the pipeline you hand-rolled onto LangChain and LlamaIndex abstractions.
  • Build the same RAG query in LlamaIndex (index-first) and LangChain LCEL (composition-first).
  • Judge when a framework earns its keep and when to hand-roll the ~40 lines you already understand.
  • Keep your framework code swappable so you're never locked into one library's opinions.
Note

Python for JavaScript developers — one new idiom: the | pipe. LangChain's LCEL overloads Python's | operator to mean "feed the left component's output into the right one" — retriever | prompt | llm reads like a Unix pipe or an RxJS .pipe(). It's function composition with a nicer syntax, not a bitwise OR. Everything else (dicts, lists, f-strings) you've already seen.

Watch out

These libraries move fast. Exact class names and import paths change release to release. Treat the code below as the shape of the idiom, and check the current LangChain / LlamaIndex docs for today's exact signatures. The concepts — indexes, retrievers, chains — are stable; the imports are not.

You already built a framework (the small version)

Across this track you hand-wrote every stage of RAG: chunk_by_tokens, an embedding call, an index, a retrieve, an answer, plus hybrid fusion, reranking, and an eval harness. A "RAG framework" is just those stages, packaged as reusable, swappable components with connectors to dozens of vector stores and models. Nothing here is new capability — it's less glue code and more batteries included, at the cost of a layer of abstraction between you and the mechanics.

Here's the mapping — read it as "the thing you wrote → the thing the framework calls it":

Your hand-rolled codeLangChainLlamaIndex
chunk_by_tokens / recursive_splitTextSplitter (e.g. RecursiveCharacterTextSplitter)NodeParser / SentenceSplitter
embed()Embeddingsembed_model
the (text, vector) indexVectorStoreVectorStoreIndex
retrieve()RetrieverRetriever / query_engine
rerank / MMRretriever search_type / ContextualCompressionNodePostprocessor (reranker)
the answer() prompt+LLMPromptTemplateLLM (LCEL)response_synthesizer
the eval harnessLangSmith evalsLlamaIndex evaluators
Tip

Exercise 3 has you fill this mapping in for a specific pipeline and swap one component (your custom reranker) into a framework — proving the abstractions are the same ideas you already implemented.

LlamaIndex — index-first

LlamaIndex is built around retrieval. Its happy path gets you a working RAG in a handful of lines because the index owns chunking, embedding, storage, and query orchestration:

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

docs = SimpleDirectoryReader("./corpus").load_data()   # load files from a folder → Documents
index = VectorStoreIndex.from_documents(docs)          # chunk + embed + index, all in one call
query_engine = index.as_query_engine(similarity_top_k=4)  # a retriever + generator bundled together
print(query_engine.query("What does the Pro plan cost?"))  # retrieve top-4 → generate a grounded answer

That one from_documents call hides the entire embed→chunk→index loop from RAG Fundamentals. You trade visibility for speed — great for getting to a baseline fast, and its data connectors (100+ loaders for PDFs, Notion, Slack, SQL, …) are its real strength.

Tip

Exercise 1 has you stand up this 5-line LlamaIndex RAG over your corpus and compare its answer to your hand-rolled pipeline's — same result, far less code, less control.

LangChain — composition-first (LCEL)

LangChain is built around composition. Its LCEL (LangChain Expression Language) pipes components with |, so a RAG chain reads as a data flow you assemble explicitly:

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

# `retriever`, `prompt`, and `llm` are components built earlier (a vector store's retriever, a PromptTemplate, a chat model).
rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}   # run retriever on the input; pass the question through unchanged
    | prompt                                                     # fill the prompt template with {context, question}
    | llm                                                        # generate
    | StrOutputParser()                                          # pull the plain string out of the model message
)
rag_chain.invoke("What does the Pro plan cost?")                 # runs the whole pipe end to end

The win is explicit control and composability: every stage is visible and swappable, streaming and async come for free, and the same idiom extends into agents and tools (the Agentic Engineering track). The cost is a steeper learning curve and more moving parts than LlamaIndex's index-first path.

Tip

Exercise 2 has you build this LCEL chain and then swap the retriever for a hybrid one — feeling how composition makes each stage a replaceable part.

Choosing — and the third option

LlamaIndexLangChainHand-roll
Best whenRAG/search is the core; you want fast indexing + many data loaderscomplex multi-step chains/agents; you want explicit controla simple, stable pipeline you fully understand
Strengthindex-first ergonomics, connectorscomposition, agents, ecosystemzero abstraction, zero dependency risk, total control
Costless control over the internalsmore concepts, faster-moving APIyou maintain the glue and the connectors

They aren't mutually exclusive — teams routinely use LlamaIndex for ingestion/retrieval inside a LangChain agent. And "hand-roll" is a legitimate answer: you've seen that a competent RAG core is ~40 lines. If your pipeline is a single vector store, a fixed prompt, and no agentic branching, a framework can be more code and a moving dependency for no gain.

Note

When to hand-roll: the pipeline is simple and stable, you want zero dependency/version churn, you need to understand every line for debugging or compliance, or the framework's abstraction is fighting you. When to adopt a framework: you need many data connectors, complex multi-step/agentic flows, or built-in streaming/observability/eval you'd otherwise rebuild. Rule of thumb: start hand-rolled to learn and for simple cases; adopt a framework when the glue code you're writing is exactly what the framework already provides.

Tip

Exercise 4 has you write the choice — framework or hand-roll — for four concrete projects, justifying each against these trade-offs.

Keeping it swappable

Whichever you pick, wrap the framework behind your own thin interface (retrieve(question) -> chunks, answer(question) -> str) so your application code doesn't depend on framework classes directly. Then a framework migration — or a drop back to hand-rolled — touches one adapter, not your whole codebase. The evaluation harness from the last topic is what makes such a swap safe: run the golden set before and after and confirm the scorecard didn't regress.

Pitfalls

  • Reaching for a framework reflexively. For a simple, stable pipeline it can be more code and a moving dependency than the ~40 lines you understand.
  • Letting framework classes leak everywhere. Bind your app to your own retrieve/answer interface, not to VectorStoreIndex or LCEL runnables, so you can swap later.
  • Copying tutorial imports blindly. These APIs change fast — verify against current docs, don't trust a months-old snippet (including this one).
  • Skipping eval after adoption. Moving to a framework changes defaults (chunking, k, prompt); re-run the golden set to confirm quality held.
  • Framework lock-in as a decision. Neither library is a one-way door if you keep the adapter thin.

Recap

  • A RAG framework is the same embed → chunk → index → retrieve → generate stages you hand-built, packaged as reusable, connector-rich components.
  • LlamaIndex is index-first — fastest path to a retrieval baseline, strongest data connectors.
  • LangChain is composition-first (LCEL) — explicit, swappable stages that extend into agents; more concepts, faster-moving API.
  • Hand-rolling is valid: a competent RAG core is ~40 lines; a framework earns its place when you need connectors, agentic complexity, or built-in streaming/observability/eval.
  • Keep the framework behind a thin adapter and re-run the golden set after any swap.
  • Worked examples map to the exercises: LlamaIndex 5-line RAG (Ex 1), LangChain LCEL chain (Ex 2), the hand-rolled→framework mapping + component swap (Ex 3), and the framework-vs-hand-roll decision (Ex 4).

RAG Frameworks — Exercises

Install what you try: pip install llama-index (Ex 1) and/or pip install langchain langchain-openai langchain-community (Ex 2). APIs shift between releases — if an import fails, check the current docs; the shape of each idiom is what matters here.


Exercise 1 — Five-line LlamaIndex RAG

Point LlamaIndex at the corpus folder you've used all track, build an index, and answer a question. Compare the answer to your hand-rolled pipeline's for the same question.

Reference solution
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

docs = SimpleDirectoryReader("./corpus").load_data()        # load every file in ./corpus as Documents
index = VectorStoreIndex.from_documents(docs)               # chunk + embed + index in one call
qe = index.as_query_engine(similarity_top_k=4)              # bundle retriever + generator
print(qe.query("What does the Pro plan cost?"))             # grounded answer from the top-4 chunks

The single from_documents call replaces the entire embed→chunk→index loop you wrote in RAG Fundamentals. You get to a baseline in five lines — but the chunking strategy, embedding model, and prompt are now framework defaults you'd override to regain the control you had by hand.


Exercise 2 — LangChain LCEL chain, then swap the retriever

Build a RAG chain with LCEL (retriever | prompt | llm), run it, then replace the retriever with a different search_type (e.g. MMR) — changing one line.

Reference solution
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

retriever = vectorstore.as_retriever(search_kwargs={"k": 4})      # baseline similarity retriever
rag = ({"context": retriever, "question": RunnablePassthrough()}
       | prompt | llm | StrOutputParser())
print(rag.invoke("What does the Pro plan cost?"))

# Swap to MMR for diversity — only the retriever line changes; the chain is untouched:
retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 4, "fetch_k": 20})
rag = ({"context": retriever, "question": RunnablePassthrough()}
       | prompt | llm | StrOutputParser())

The | pipe makes each stage a replaceable part: retriever, prompt, model, and parser compose independently. That composability is LangChain's core value — and the same idiom carries into agents later.


Exercise 3 — Map your pipeline to framework abstractions

For your hand-rolled pipeline, fill in the table: which framework abstraction replaces each of your functions? Then swap one hand-rolled component (your custom reranker) into a framework pipeline.

Reference solution
Your codeLangChainLlamaIndex
chunk_by_tokens / recursive_splitRecursiveCharacterTextSplitterSentenceSplitter
embed()OpenAIEmbeddingsOpenAIEmbedding (embed_model)
(text, vector) listVectorStore (pgvector, FAISS…)VectorStoreIndex
retrieve().as_retriever().as_retriever() / query_engine
rerank() (cross-encoder)ContextualCompressionRetriever + rerankerreranker NodePostprocessor
answer()promptllmStrOutputParser (LCEL)response_synthesizer

Swapping your reranker in (LangChain):

from langchain.retrievers import ContextualCompressionRetriever
# Wrap your existing cross-encoder as a compressor, then layer it over the base retriever:
compression = ContextualCompressionRetriever(base_compressor=my_reranker, base_retriever=retriever)
rag = ({"context": compression, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser())

The point: every framework "concept" here is something you already implemented by hand. The abstraction names are new; the ideas are not.


Exercise 4 — Framework or hand-roll?

For each project, choose LlamaIndex, LangChain, or hand-roll, and justify in one line.

  1. A weekend prototype: chat over a folder of PDFs, quality secondary, ship today.
  2. A production single-vector-store RAG with a fixed prompt, no agents, strict compliance audit of every line.
  3. A multi-step assistant that retrieves, calls tools, branches on results, and streams responses.
  4. An ingestion-heavy app pulling from Notion, Slack, Google Drive, and SQL into one searchable index.
Reference solution
  1. LlamaIndex. Index-first ergonomics + built-in PDF loaders get a working demo in minutes; control isn't the priority.
  2. Hand-roll. Simple, stable pipeline where you must understand and audit every line; a framework adds a moving dependency and hidden defaults for no benefit.
  3. LangChain. Multi-step chains, tool calls, branching, and streaming are exactly what LCEL/agents are built for.
  4. LlamaIndex. Its 100+ data connectors are the strongest reason to adopt it — connector coverage is the deciding factor here.

Rule of thumb: hand-roll simple/stable/audited pipelines; adopt a framework when the glue you'd write is exactly what it already provides (connectors, agentic flows, streaming/observability). Keep it behind a thin adapter either way.