LangChain for RAG: A Beginner's Guide

Learn LangChain for RAG with Python: load and split documents, embed them, build a retriever, stream answers with sources, and try agentic RAG.

LangChain for RAG beginner's guide showing a langchain_rag.py code card with a text splitter, vector store, prompt chain and agent

Introduction

LangChain RAG tutorials have a reputation for being confusing.

Half of them import things that no longer exist.

The other half wrap everything in so many layers that you can't tell what's happening.

So let's do it differently.

In the previous two articles, we built a RAG app by hand.

Build a Simple RAG Application Using Python covered loading, chunking, embedding and retrieval with plain Python and NumPy.

How to Use the OpenAI API in a RAG Pipeline made the generation side production-ready.

Now you know every step.

That makes this the perfect time to see what a framework actually does for you.

In this tutorial, we'll rebuild the same RAG app with LangChain, using current LangChain 1.x code.

By the end, you'll know how to:

· Load files into LangChain Document objects

· Split them with RecursiveCharacterTextSplitter

· Embed and store chunks in a LangChain vector store

· Retrieve relevant chunks with scores, or with a retriever

· Build a RAG chain with a prompt template, a chat model and an output parser

· Stream answers with sources

· Turn the same search into an agent tool for agentic RAG

· Avoid the old LangChain imports that break in version 1

If RAG itself is new to you, read What Is RAG? Retrieval-Augmented Generation Explained first.

We won't repeat the theory here.


Quick Answer

To build RAG with LangChain, load your files as Document objects, split them with a text splitter, embed and store the chunks in a vector store, then retrieve the most relevant chunks for each question and pass them to a chat model through a prompt template.

The workflow looks like this:

Your Files
    ↓
Document objects (page_content + metadata)
    ↓
RecursiveCharacterTextSplitter → chunks
    ↓
OpenAIEmbeddings + InMemoryVectorStore
    ↓
Similarity search → top-k chunks
    ↓
ChatPromptTemplate → chat model → answer with sources

Same pipeline as Article 7.

Different building blocks.


What We're Building

We'll add one new file to the Article 7 project:

langchain_rag.py

It has two commands.

Build the index:

python langchain_rag.py index docs/

Ask a question:

python langchain_rag.py ask "Can I get a refund after 30 days?"

Add --show-sources to print the retrieved chunks, or --agent to let an AI agent decide when to search.

Architecture

Every LangChain RAG app has two phases.

INDEXING (run once)

docs/*.md
    │
    ▼
Document objects
    │
    ▼
RecursiveCharacterTextSplitter
    │
    ▼
OpenAIEmbeddings ──► InMemoryVectorStore ──► lc_index.json

ANSWERING (every question)

User question
    │
    ├── default ──► similarity_search_with_score ──► prompt | model | parser
    │
    └── --agent ──► create_agent ──► calls search_docs when needed
    │
    ▼
Answer with cited sources


1. Prerequisites

You'll need:

· Python 3.10 or newer, which LangChain requires according to its installation guide

· An OpenAI API key, from the OpenAI API keys page

· The docs/ folder from Article 7, or any folder of .md and .txt files

We'll use the same two models as the earlier articles.

Embedding model

text-embedding-3-small

Chat model

gpt-6-luna

Both can be changed with the EMBED_MODEL and CHAT_MODEL environment variables.

You don't need a vector database account.

We'll start with LangChain's in-memory vector store and save it to a file.


2. Install LangChain for RAG

Move into your Article 7 project:

cd simple-rag


Activate the virtual environment on macOS or Linux:

source .venv/bin/activate


On Windows:

.venv\Scripts\activate


Install the packages:

pip install -U langchain langchain-openai langchain-text-splitters numpy


Here's what each one does:

langchain

The main package.

It brings in langchain-core (documents, prompts, vector store interfaces) and the agent runtime.

langchain-openai

The OpenAI integration: OpenAIEmbeddings and the chat model.

LangChain keeps every provider in its own package, so you install only what you use.

langchain-text-splitters

The text splitters, including RecursiveCharacterTextSplitter.

numpy

The in-memory vector store needs it for cosine similarity.

Skip it, and your first search fails with an ImportError.

This tutorial was tested with langchain 1.4, langchain-core 1.6 and langchain-openai 1.7.

Check yours:

python -c "import langchain, langchain_core; print(langchain.__version__, langchain_core.__version__)"

Make sure your API key is set:

export OPENAI_API_KEY="sk-..."

Your project now looks like this:

simple-rag/
├── .venv/
├── docs/
│   ├── refunds.md
│   └── shipping.md
├── rag.py
├── openai_rag.py
└── langchain_rag.py

We'll build langchain_rag.py piece by piece.

The complete file is in Final Code.


3. Load Documents Into LangChain

LangChain represents every piece of text as a Document.

A Document has two fields:

page_content   the text itself
metadata       a dict: source file, page number, anything you want

Metadata is what makes citations possible later.

Add the imports and settings:

import argparse
import os
from pathlib import Path

import openai
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

EMBED_MODEL = os.getenv("EMBED_MODEL", "text-embedding-3-small")
CHAT_MODEL = os.getenv("CHAT_MODEL", "openai:gpt-6-luna")
INDEX_FILE = Path("lc_index.json")
NO_ANSWER = "I don't know based on the provided documents."

Now the loader:

def load_documents(folder: str) -> list[Document]:
    docs = []
    for path in sorted(Path(folder).rglob("*")):
        if path.suffix.lower() in {".md", ".txt"}:
            text = path.read_text(encoding="utf-8")
            docs.append(Document(page_content=text, metadata={"source": path.as_posix()}))
    return docs

That's the same loop as Article 7.

The only difference is the return type.

Why not a LangChain document loader?

LangChain has loaders for PDFs, web pages, Notion and many more.

For plain text files, pathlib is all you need.

LangChain's own semantic search tutorial builds Document objects directly too.

We'll use a real PDF loader in Article 11.


4. Split Documents With RecursiveCharacterTextSplitter

Next, we split each document into chunks.

Add this:

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    add_start_index=True,
)

LangChain's documentation calls RecursiveCharacterTextSplitter the recommended splitter for generic text.

It tries to split on paragraphs first, then lines, then words, until every chunk fits chunk_size.

That's very close to the paragraph-packing function we wrote by hand.

Look at this line:

add_start_index=True,

It stores each chunk's character position in metadata["start_index"].

Now every chunk knows exactly where it came from.

The settings match Article 7: about 1,000 characters per chunk with 200 characters of overlap.

If you want to tune them, read How Document Chunking Works in RAG.


5. Create a LangChain Vector Store

Now we embed the chunks and store them.

Create the embedding model:

embeddings = OpenAIEmbeddings(model=EMBED_MODEL)

Then the indexing function:

def build_index(folder: str) -> None:
    docs = load_documents(folder)
    chunks = splitter.split_documents(docs)
    store = InMemoryVectorStore(embeddings)
    store.add_documents(chunks)
    store.dump(str(INDEX_FILE))
    print(f"Indexed {len(chunks)} chunks from {len(docs)} files into {INDEX_FILE}")

Three LangChain calls replace about thirty lines from Article 7:

splitter.split_documents(docs)

Splits every document and copies its metadata to each chunk.

store.add_documents(chunks)

Embeds every chunk with OpenAIEmbeddings and stores the vectors.

No manual batching.

No manual normalization.

store.dump(...)

Saves the documents and vectors to a JSON file.

Without it, the in-memory store disappears when the script ends, and you'd pay to re-embed everything on every run.

Loading it back is one line:

def load_store() -> InMemoryVectorStore:
    if not INDEX_FILE.exists():
        raise FileNotFoundError(INDEX_FILE)
    return InMemoryVectorStore.load(str(INDEX_FILE), embeddings)

Why InMemoryVectorStore?

It needs no server and no account.

That makes it perfect for learning.

Every LangChain vector store shares the same interface, so moving to Chroma, Qdrant or pgvector later changes the store line, not your RAG code.

The vector store integrations list shows the options.

If vector stores are new to you, read Vector Databases Explained.


6. Retrieve Relevant Chunks

Now the search.

Add this:

def retrieve(store: InMemoryVectorStore, question: str, k: int = 4, min_score: float = 0.2) -> list[Document]:
    results = store.similarity_search_with_score(question, k=k)
    return [doc for doc, score in results if score >= min_score]

similarity_search_with_score() embeds the question, compares it to every chunk, and returns the top k with their scores.

For InMemoryVectorStore, the score is cosine similarity.

We keep the same min_score rule as Article 7.

If nothing scores high enough, we skip the model call and say "I don't know".

Then a helper that formats chunks for the prompt:

def format_docs(docs: list[Document]) -> str:
    return "\n\n---\n\n".join(f"[{d.metadata['source']}]\n{d.page_content}" for d in docs)

Each chunk is labelled with its source file, so the model can cite it.

What about retrievers?

You'll see as_retriever() in almost every LangChain RAG tutorial:

retriever = store.as_retriever(search_kwargs={"k": 4})
docs = retriever.invoke("Can I get a refund after 30 days?")

A retriever is a standard LangChain interface for "give me documents for this query".

The semantic search tutorial explains the key difference: vector stores aren't Runnables, but retrievers are.

That means retrievers plug straight into chains and support invoke and batch.

They also support search_type="mmr" for more diverse results, and "similarity_score_threshold" for score filtering.

We call the store directly here only because we want to see the scores.

Both approaches are fine.


7. Build a RAG Chain With a Prompt Template

Retrieval found the facts.

Now the model needs to answer from them.

Create the chat model:

model = init_chat_model(CHAT_MODEL)

init_chat_model() takes a provider:model string, as explained in the LangChain models guide.

"openai:gpt-6-luna" uses OpenAI.

To switch providers, install that provider's package and change the string.

Your RAG code stays the same.

Now the prompt:

prompt = ChatPromptTemplate.from_messages([
    ("system",
     "You are a support assistant for our company documents.\n"
     "Answer using ONLY the context below.\n"
     "Cite the sources you used in square brackets, like [docs/refunds.md].\n"
     "If the context does not contain the answer, reply exactly: {no_answer}\n\n"
     "Context:\n{context}"),
    ("human", "{question}"),
]).partial(no_answer=NO_ANSWER)

A ChatPromptTemplate is a reusable prompt with placeholders.

{context} and {question} get filled in on every call.

.partial() fills {no_answer} once, up front.

These are the same three rules from Article 7: only the context, cite sources, and a clear way to say "I don't know".

Now the chain:

rag_chain = prompt | model | StrOutputParser()

This one line is the heart of LangChain.

The | pipe connects components, like a Unix pipeline:

{"context": ..., "question": ...}
        ↓
prompt            fills the template → chat messages
        ↓
model             calls the chat model → AI message
        ↓
StrOutputParser   extracts the text → plain string

Every piece is a Runnable, so the whole chain is a Runnable too.

That gives you invoke(), batch() and stream() for free.


8. Stream Answers With Sources

Now we connect retrieval and the chain.

Add this:

def ask(question: str, k: int = 4, show_sources: bool = False) -> None:
    store = load_store()
    docs = retrieve(store, question, k=k)
    if not docs:
        print(NO_ANSWER)
        return
    if show_sources:
        for d in docs:
            print(f"source: {d.metadata['source']} (start {d.metadata['start_index']})")
    for chunk in rag_chain.stream({"context": format_docs(docs), "question": question}):
        print(chunk, end="", flush=True)
    print()

Look at this line:

for chunk in rag_chain.stream({...}):

Because the chain ends with StrOutputParser, each streamed chunk is already a plain string.

We print it as it arrives.

No event types to check.

Compare that with Article 8, where we handled response.output_text.delta events ourselves.

LangChain hides that detail.

That's convenient.

It's also exactly the kind of detail you can't see when something breaks, which is why the next sections show you how to look inside.

Retrieve first, generate second

This pattern is what LangChain's retrieval docs call 2-step RAG.

Retrieval always runs before generation.

The docs describe it as simple and predictable, with predictable latency because the number of model calls is known.

For FAQ bots and documentation search, it's usually the right default.


9. Add Agentic RAG With create_agent

There's a second way to do RAG in LangChain.

Instead of always searching first, you give the model a search tool and let it decide when to use it.

That's agentic RAG.

LangChain's retrieval docs put it simply: the only thing an agent needs for RAG behavior is access to one or more tools that fetch external knowledge.

Add this:

def build_agent(k: int = 4):
    store = load_store()

    @tool
    def search_docs(query: str) -> str:
        """Search the company support documents about refunds, shipping and policies."""
        docs = retrieve(store, query, k=k)
        return format_docs(docs) if docs else "No matching documents found."

    return create_agent(
        model=model,
        tools=[search_docs],
        system_prompt=(
            "You answer questions about our company documents. "
            "Use search_docs before answering, and cite sources like [docs/refunds.md]. "
            f"If the documents don't cover it, reply exactly: {NO_ANSWER}"
        ),
    )

Two things are new.

@tool

Turns a normal Python function into a tool the model can call.

The docstring becomes the tool description.

The model reads it to decide when the tool is useful, so write it clearly.

create_agent()

Builds an agent loop: call the model, run any tool it asks for, send the result back, and repeat until the model gives a final answer.

See the LangChain agents guide for everything it supports.

Now run it:

def ask_agent(question: str, k: int = 4) -> None:
    agent = build_agent(k=k)
    result = agent.invoke({"messages": [{"role": "user", "content": question}]})
    print(result["messages"][-1].text)

The result holds the whole conversation, something like this:

HumanMessage   "Can I get a refund after 30 days?"
AIMessage      tool call: search_docs(query="refund after 30 days")
ToolMessage    [docs/refunds.md] Refund policy ...
AIMessage      "After 30 days, refunds are issued as store credit only [docs/refunds.md]."

The last message is the answer.

2-step RAG vs agentic RAG

So which one should you use?

2-Step RAG

Best for: FAQ bots, support assistants, documentation search.

Pros: one search and one model call per question. Fast, cheap and easy to debug.

Cons: it searches even for "hi", and it can't search twice for a complex question.

Agentic RAG

Best for: questions that need several searches, or apps with several tools.

Pros: the model can rewrite the query, search again, or skip search entirely.

Cons: more model calls, more tokens, and less predictable behavior.

Start with 2-step RAG.

Move to agents when your questions actually need them.

We'll go much deeper into this in Article 21, Agentic RAG Explained.


10. What Actually Happened?

Here's the full lifecycle in default mode:

python langchain_rag.py index docs/
        ↓
load_documents() builds Document objects with a source
        ↓
split_documents() creates chunks with start_index
        ↓
add_documents() embeds and stores every chunk
        ↓
dump() saves lc_index.json

python langchain_rag.py ask "..."
        ↓
load() reads the saved index
        ↓
similarity_search_with_score() finds the top chunks
        ↓
rag_chain.stream() fills the prompt, calls the model, streams text


11. Final Code

Here's the complete langchain_rag.py:

"""langchain_rag.py - the Article 7 RAG app, rebuilt with LangChain.

Load -> split -> embed -> store -> retrieve -> generate, plus an agentic mode.
"""
import argparse
import os
from pathlib import Path

import openai
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

EMBED_MODEL = os.getenv("EMBED_MODEL", "text-embedding-3-small")
CHAT_MODEL = os.getenv("CHAT_MODEL", "openai:gpt-6-luna")
INDEX_FILE = Path("lc_index.json")
NO_ANSWER = "I don't know based on the provided documents."

embeddings = OpenAIEmbeddings(model=EMBED_MODEL)
model = init_chat_model(CHAT_MODEL)


# 1. Load -----------------------------------------------------------------
def load_documents(folder: str) -> list[Document]:
    docs = []
    for path in sorted(Path(folder).rglob("*")):
        if path.suffix.lower() in {".md", ".txt"}:
            text = path.read_text(encoding="utf-8")
            docs.append(Document(page_content=text, metadata={"source": path.as_posix()}))
    return docs


# 2. Split ----------------------------------------------------------------
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    add_start_index=True,
)


# 3 + 4. Embed and store ---------------------------------------------------
def build_index(folder: str) -> None:
    docs = load_documents(folder)
    chunks = splitter.split_documents(docs)
    store = InMemoryVectorStore(embeddings)
    store.add_documents(chunks)
    store.dump(str(INDEX_FILE))
    print(f"Indexed {len(chunks)} chunks from {len(docs)} files into {INDEX_FILE}")


def load_store() -> InMemoryVectorStore:
    if not INDEX_FILE.exists():
        raise FileNotFoundError(INDEX_FILE)
    return InMemoryVectorStore.load(str(INDEX_FILE), embeddings)


# 5. Retrieve -------------------------------------------------------------
def retrieve(store: InMemoryVectorStore, question: str, k: int = 4, min_score: float = 0.2) -> list[Document]:
    results = store.similarity_search_with_score(question, k=k)
    return [doc for doc, score in results if score >= min_score]


def format_docs(docs: list[Document]) -> str:
    return "\n\n---\n\n".join(f"[{d.metadata['source']}]\n{d.page_content}" for d in docs)


# 6. Generate -------------------------------------------------------------
prompt = ChatPromptTemplate.from_messages([
    ("system",
     "You are a support assistant for our company documents.\n"
     "Answer using ONLY the context below.\n"
     "Cite the sources you used in square brackets, like [docs/refunds.md].\n"
     "If the context does not contain the answer, reply exactly: {no_answer}\n\n"
     "Context:\n{context}"),
    ("human", "{question}"),
]).partial(no_answer=NO_ANSWER)

rag_chain = prompt | model | StrOutputParser()


def ask(question: str, k: int = 4, show_sources: bool = False) -> None:
    store = load_store()
    docs = retrieve(store, question, k=k)
    if not docs:
        print(NO_ANSWER)
        return
    if show_sources:
        for d in docs:
            print(f"source: {d.metadata['source']} (start {d.metadata['start_index']})")
    for chunk in rag_chain.stream({"context": format_docs(docs), "question": question}):
        print(chunk, end="", flush=True)
    print()


# 7. Agentic RAG ----------------------------------------------------------
def build_agent(k: int = 4):
    store = load_store()

    @tool
    def search_docs(query: str) -> str:
        """Search the company support documents about refunds, shipping and policies."""
        docs = retrieve(store, query, k=k)
        return format_docs(docs) if docs else "No matching documents found."

    return create_agent(
        model=model,
        tools=[search_docs],
        system_prompt=(
            "You answer questions about our company documents. "
            "Use search_docs before answering, and cite sources like [docs/refunds.md]. "
            f"If the documents don't cover it, reply exactly: {NO_ANSWER}"
        ),
    )


def ask_agent(question: str, k: int = 4) -> None:
    agent = build_agent(k=k)
    result = agent.invoke({"messages": [{"role": "user", "content": question}]})
    print(result["messages"][-1].text)


# CLI ---------------------------------------------------------------------
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="RAG with LangChain")
    sub = parser.add_subparsers(dest="command", required=True)
    idx = sub.add_parser("index")
    idx.add_argument("folder")
    one = sub.add_parser("ask")
    one.add_argument("question")
    one.add_argument("-k", type=int, default=4)
    one.add_argument("--show-sources", action="store_true")
    one.add_argument("--agent", action="store_true", help="let an agent decide when to search")
    args = parser.parse_args()

    try:
        if args.command == "index":
            build_index(args.folder)
        elif args.agent:
            ask_agent(args.question, k=args.k)
        else:
            ask(args.question, k=args.k, show_sources=args.show_sources)
    except openai.AuthenticationError:
        raise SystemExit("Authentication failed. Check OPENAI_API_KEY.")
    except openai.RateLimitError as e:
        raise SystemExit(f"Rate limited or out of quota: {e}")
    except FileNotFoundError:
        raise SystemExit("No index found. Run: python langchain_rag.py index docs/")

It isn't much shorter than Article 7's rag.py.

And that's honest.

For a tiny app, LangChain doesn't save many lines.

What it gives you is standard parts you can swap: another vector store, another model provider, a PDF loader, or an agent, without rewriting the rest.

The important pattern is:

Index:     Documents → splitter → embeddings → vector store
Retrieve:  question → similarity search → top-k Documents
Generate:  prompt | model | parser → streamed answer with sources
Extend:    wrap the search in @tool → create_agent for agentic RAG


12. Run the LangChain RAG App

Build the index:

python langchain_rag.py index docs/

Expected output:

Indexed 2 chunks from 2 files into lc_index.json

Each sample file is shorter than 1,000 characters, so each becomes one chunk.

Ask a question with sources:

python langchain_rag.py ask "Can I get a refund after 30 days?" --show-sources

Expected output (your wording will differ):

source: docs/refunds.md (start 0)
After 30 days, refunds are issued as store credit only [docs/refunds.md].

You may also see docs/shipping.md listed if it scores above min_score.

Now try the agent:

python langchain_rag.py ask "Can I get a refund after 30 days?" --agent

Expected output:

After 30 days, refunds are issued as store credit only [docs/refunds.md].

Same answer.

But this time, the model chose to call search_docs first.

See inside with LangSmith

Want to see every step, including the prompt that was actually sent?

LangChain apps can log traces to LangSmith, LangChain's tracing platform.

Set two environment variables before you run the app:

export LANGSMITH_TRACING="true"

export LANGSMITH_API_KEY="..."

Each run then shows up as a trace with retrieval, prompts, tool calls and model responses.

The LangSmith tracing guide covers the setup.

It's optional, but it makes debugging a RAG chain far less painful.


13. Common LangChain RAG Errors

ModuleNotFoundError: No module named 'langchain.chains'

You're following an old tutorial.

In LangChain 1.x, the old chains like RetrievalQA and create_retrieval_chain are no longer in the langchain package.

They live in the separate langchain-classic package for legacy code.

For new code, use a chain like prompt | model | StrOutputParser() or create_agent, as in this tutorial.

ImportError: cosine_similarity requires numpy to be installed

InMemoryVectorStore needs NumPy for search.

Install it:

pip install numpy

openai.OpenAIError: Missing credentials

OPENAI_API_KEY isn't set in this terminal.

Set it and run again:

export OPENAI_API_KEY="sk-..."

DeprecationWarning: langchain-community is being sunset

You imported something from langchain_community, such as an old document loader or vector store.

The package itself now warns that it is no longer actively maintained.

Look for the standalone integration package instead, like langchain-openai, langchain-chroma or langchain-postgres.

KeyError when invoking the prompt

The dictionary you pass to the chain is missing a placeholder.

Our prompt needs both context and question.

no_answer is already filled by .partial().

No index found

You haven't built the index yet.

Run:

python langchain_rag.py index docs/


14. Common LangChain RAG Mistakes

1. Copying Pre-1.0 Tutorials

Most LangChain RAG examples online still use RetrievalQA, langchain.vectorstores or langchain_community imports.

Check the date and the version before you copy anything.

2. Re-Embedding on Every Run

InMemoryVectorStore lives in memory.

Without dump() and load(), every run embeds every chunk again and costs you money.

3. Losing Metadata

If you build chunks without a source in metadata, you can't cite anything.

Set metadata when you create each Document, and the splitter copies it to every chunk.

4. Treating the Chain as a Black Box

When answers are wrong, the cause is almost always retrieval, not the model.

Print the retrieved chunks with --show-sources, or turn on LangSmith tracing, before you change the prompt.

5. Jumping Straight to Agents

Agents are exciting, but they add model calls and unpredictability.

Most document Q&A apps work better as 2-step RAG.

6. Mixing Embedding Models

The index and the questions must use the same embedding model.

Change EMBED_MODEL, and rebuild the index.


Do You Need LangChain for RAG?

No.

Article 7 proved that a working RAG app needs about 120 lines of plain Python.

So when does LangChain make sense?

Use Plain Python When

· You have one data source and one model

· You want full control and no extra dependencies

· You're learning how RAG works

Use LangChain When

· You want to swap vector stores, models or providers easily

· You need loaders for PDFs, web pages or other formats

· You're adding agents, tools or multi-step workflows

· You want tracing and evaluation with LangSmith

A practical approach:

Learn RAG with plain Python
        ↓
Use LangChain components where they save real work
        ↓
Keep the parts you need to control in your own code

You don't have to go all in.

Using LangChain just for the splitter and vector store is perfectly fine.


15. Try It Yourself

Don't just copy the code.

Add one feature:

--mmr

When the flag is set, use store.as_retriever(search_type="mmr", search_kwargs={"k": k}) instead of plain similarity search.

Ask a question that matches several near-identical chunks, and compare the sources with and without --mmr.

Bonus

Swap InMemoryVectorStore for Chroma using the langchain-chroma package.

If only the store lines change, you've seen the real value of LangChain's shared interfaces.


FAQs

What is LangChain used for in RAG?

LangChain provides standard building blocks for each RAG step: document objects, text splitters, embedding models, vector stores, retrievers, prompt templates and chat models.

You connect them into a pipeline instead of writing each step by hand.

Is LangChain good for RAG?

Yes, especially when you want to swap models or vector stores, load many file types, or add agents and tools later.

For a very small app with one data source, plain Python can be simpler.

Do I need LangChain to build a RAG application?

No.

A RAG app only needs an embedding model, a way to search vectors and a chat model.

LangChain makes the parts easier to swap and extend, but it isn't required.

Is RetrievalQA deprecated in LangChain?

Yes.

In LangChain 1.x, old chains like RetrievalQA and create_retrieval_chain are no longer in the main langchain package and only remain in langchain-classic for legacy code.

For new projects, build the chain with prompt | model | StrOutputParser() or use create_agent with a retrieval tool.

What is the difference between 2-step RAG and agentic RAG in LangChain?

In 2-step RAG, retrieval always runs before the model answers, so it's fast and predictable.

In agentic RAG, the model gets a search tool and decides when and how often to search, which is more flexible but uses more calls.

Which vector store should I use with LangChain?

Start with InMemoryVectorStore while learning, because it needs no server.

For real projects, choose a persistent store like Chroma, Qdrant or pgvector, and switch by changing only the store setup.


LangChain vs LlamaIndex: which is better for RAG?

Both can build the same RAG app.

LangChain is a general framework for chains, tools and agents, while LlamaIndex focuses on indexing and querying your data.

We'll build the same app with LlamaIndex in the next article so you can compare.


Key Takeaways

You've now built the same RAG app twice: once by hand, and once with LangChain.

Remember these points:

  • LangChain represents text as Document objects with page_content and metadata.

  • RecursiveCharacterTextSplitter is the recommended splitter for generic text.

  • Vector stores share one interface, so you can start in memory and switch later.

  • Save your index with dump() so you don't re-embed on every run.

  • prompt | model | StrOutputParser() is a complete, streamable RAG chain.

  • 2-step RAG is the right default; agentic RAG is for questions that need tools.

  • Old imports like langchain.chains break in LangChain 1.x.

  • When answers are wrong, inspect retrieval first.

  • The biggest lesson is simple:

LangChain doesn't change how RAG works. It gives each step a standard shape, so you can swap the parts without rewriting the app.


Conclusion

In Article 7, we built RAG from scratch.

In Article 8, we made the OpenAI calls production-ready.

In this article, we rebuilt the same pipeline with LangChain.

Documents, splitters, vector stores, prompts and models became standard parts.

A single pipe expression became a streaming RAG chain.

And one decorator turned our search into a tool for an agent.

Because you built it by hand first, none of it is magic.

Next, we'll try the other big framework, LlamaIndex, and see how it approaches the same problem from the data side.

That's where things get more interesting.


Continue Learning

You're currently at Article 9 in the RAG series.

Previous Articles

1. What Is RAG? Retrieval-Augmented Generation Explained

Learn the basic RAG architecture and understand how retrieval and generation work together.

2. What Are Embeddings?

Understand how text is converted into numerical representations for semantic retrieval.

3. Vector Databases Explained

Learn how vector databases store and search embeddings.

4. Semantic Search vs Keyword Search

Understand the difference between exact-term matching and meaning-based retrieval.

5. How Document Chunking Works in RAG

Learn why documents are split into chunks and how chunking affects retrieval quality.

7. Build a Simple RAG Application Using Python

Build a complete RAG app from scratch with OpenAI embeddings and NumPy.

8. How to Use the OpenAI API in a RAG Pipeline

Add streaming, structured outputs, chat history and cost tracking to your RAG app.

Next Articles

10. LlamaIndex Explained: Build LLM Applications with Your Data

Build the same document Q&A app with LlamaIndex and compare it with LangChain.

11. Build a Chat with PDF Application Using RAG

Handle real PDFs, page numbers, and citations in a chat interface.

12. PostgreSQL and pgvector: Build a RAG System with Vector Search

Move the index into a real vector database.

13. RAG vs. Fine-Tuning: Which Approach Should You Choose?

Learn when retrieval beats training, and when it doesn't.


Have a Question?

If something isn't clear or your LangChain RAG app isn't working as expected, leave a comment below.

We'll do our best to explain it with simple words, practical examples, and code you can actually understand.

Happy Coding! 🚀


Follow TheTechLane

Keep learning AI, development, and modern developer technologies with TheTechLane.

Learn • Build • Share

Follow @thetechlanehq for more developer guides, tutorials, and practical technology content.