π LangChain Learning Path - Step 4 of 7
- β Step 3: Tool Calling
- Step 4 (this page): RAG Basics
- Step 5: LangGraph & Agents β
π Hands-On Practice
β¬οΈ Download Jupyter Notebook - Build complete RAG systems with document loading, embeddings, and retrieval.
The Problem: LLMs Donβt Know Your Data
LLMs are trained on public internet data, but they donβt know about:
- Your companyβs internal documents
- Your personal notes
- Recent information (after their training cutoff)
- Proprietary data
RAG solves this problem!
What is RAG?
The Simple Explanation
RAG (Retrieval Augmented Generation) is like giving an AI a library card and teaching it to look things up before answering.
Real-World Analogy
Imagine asking a librarian a question:
Without RAG (Bad Librarian):
You: "What does our employee handbook say about vacation days?"
Librarian: "I think it's probably 10-15 days? That seems normal..."
(Making it up!)
With RAG (Good Librarian):
You: "What does our employee handbook say about vacation days?"
Librarian: (pulls out handbook, reads page 23)
Librarian: "According to page 23, employees get 15 vacation days annually."
(Looks it up first!)
How RAG Works: The Four Steps
& Store in Vector DB"] S3["3οΈβ£ Retrieve Relevant Docs
for Question"] S4["4οΈβ£ Generate Answer
Using Retrieved Docs"] S1 --> S2 --> S3 --> S4 style S1 fill:#dbeafe,stroke:#2563eb style S2 fill:#fef3c7,stroke:#d97706 style S3 fill:#fce7f3,stroke:#db2777 style S4 fill:#d1fae5,stroke:#059669
Letβs break down each step!
Step 1: Loading Documents
What is Document Loading?
Reading files and breaking them into manageable chunks.
(100 pages)"] L["Document Loader"] C["π Many Chunks
(Each ~500 words)"] F --> L --> C style F fill:#fecaca,stroke:#dc2626 style L fill:#fef3c7,stroke:#d97706 style C fill:#d1fae5,stroke:#059669
Loading Different File Types
from langchain.document_loaders import (
TextLoader,
PDFLoader,
WebBaseLoader,
DirectoryLoader
)
# Load a text file
text_loader = TextLoader("company_policy.txt")
text_docs = text_loader.load()
# Load a PDF
pdf_loader = PDFLoader("manual.pdf")
pdf_docs = pdf_loader.load()
# Load from a website
web_loader = WebBaseLoader("https://example.com/docs")
web_docs = web_loader.load()
# Load all files in a directory
dir_loader = DirectoryLoader("./documents", glob="**/*.txt")
all_docs = dir_loader.load()
Why Split Documents?
Documents are split into chunks because:
- LLMs have token limits (canβt read entire book at once)
- Smaller chunks = more precise retrieval
- Better performance and cost
100-page document
to LLM"] P1["Expensive"] P2["Slow"] P3["Hits token limit"] end subgraph right["β Smart Chunks"] C["Send only
relevant paragraphs"] B1["Cheap"] B2["Fast"] B3["Focused"] end style wrong fill:#fecaca,stroke:#dc2626 style right fill:#d1fae5,stroke:#059669
Splitting Documents
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Load documents
loader = TextLoader("big_document.txt")
documents = loader.load()
# Split into chunks
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # ~500 characters per chunk
chunk_overlap=50, # 50 character overlap between chunks
)
chunks = text_splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")
Why overlap? Overlap ensures important information at chunk boundaries isnβt lost.
Chunk 1: "...employees get 15 vacation days. This includes..."
β overlap
Chunk 2: "...This includes holidays and sick leave..."
Step 2: Embeddings & Vector Stores
What are Embeddings?
Embeddings convert text into numbers that capture meaning. Similar meanings = similar numbers!
car is far away"] style text fill:#dbeafe,stroke:#2563eb style embed fill:#fef3c7,stroke:#d97706
Real-World Analogy
Think of embeddings like a coordinate system for meanings:
Coordinates on a map:
- New York: (40.7Β°N, 74.0Β°W)
- Boston: (42.4Β°N, 71.1Β°W) β Close to New York!
- Los Angeles: (34.1Β°N, 118.2Β°W) β Far from New York
Embeddings for words:
- "king": [0.8, 0.5, 0.1]
- "queen": [0.75, 0.48, 0.12] β Close to "king"!
- "apple": [0.2, 0.9, 0.7] β Far from "king"
Creating Embeddings
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
# Embed some text
text = "The quick brown fox jumps over the lazy dog"
vector = embeddings.embed_query(text)
print(f"Embedding has {len(vector)} dimensions")
# Output: Embedding has 1536 dimensions
Vector Stores
A vector store is like a library catalog but for embeddings. It stores documents and lets you search by meaning!
Creating a Vector Store
from langchain.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import TextLoader
# 1. Load documents
loader = TextLoader("company_handbook.txt")
documents = loader.load()
# 2. Split into chunks
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
chunks = text_splitter.split_documents(documents)
# 3. Create embeddings
embeddings = OpenAIEmbeddings()
# 4. Create vector store
vectorstore = FAISS.from_documents(chunks, embeddings)
print(f"Created vector store with {len(chunks)} documents")
Step 3: Retrieval
What is a Retriever?
A retriever searches the vector store for documents relevant to a question.
Creating a Retriever
# Turn the vector store into a retriever
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3} # Return top 3 matches
)
# Test it
question = "What's the vacation policy?"
relevant_docs = retriever.invoke(question)
for doc in relevant_docs:
print(doc.page_content)
print("---")
How Similarity Search Works
What's the refund policy?"] QE["Convert to embedding"] subgraph vs["ποΈ All Documents"] D1["Vacation policy
β Not similar"] D2["Refund policy
β Very similar!"] D3["Hiring policy
β Not similar"] D4["Return process
β Similar"] end R["Return top matches"] Q --> QE --> vs --> R style Q fill:#dbeafe,stroke:#2563eb style QE fill:#fef3c7,stroke:#d97706 style D2 fill:#d1fae5,stroke:#059669 style D4 fill:#d1fae5,stroke:#059669 style R fill:#d1fae5,stroke:#059669
Step 4: Generation (Putting It Together)
Creating a RAG Chain
Now we combine everything:
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Create the prompt
template = """Answer the question based only on the following context:
Context:
{context}
Question: {question}
Answer: """
prompt = ChatPromptTemplate.from_template(template)
# Create the chain
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
| prompt
| ChatOpenAI(model="gpt-4")
| StrOutputParser()
)
# Use it!
answer = rag_chain.invoke("What's our vacation policy?")
print(answer)
How the Chain Works
Complete RAG Example
Letβs build a complete Q&A system for company policies:
from langchain.document_loaders import DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Step 1: Load all company policy documents
loader = DirectoryLoader(
"./company_policies",
glob="**/*.txt"
)
documents = loader.load()
print(f"Loaded {len(documents)} documents")
# Step 2: Split into chunks
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=100
)
chunks = text_splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")
# Step 3: Create vector store
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(chunks, embeddings)
# Step 4: Create retriever
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3}
)
# Step 5: Create RAG chain
template = """You are a helpful assistant answering questions about company policies.
Use the following context to answer the question. If you can't find the answer in the context, say "I don't have that information in the company policies."
Context:
{context}
Question: {question}
Answer:"""
prompt = ChatPromptTemplate.from_template(template)
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
| prompt
| ChatOpenAI(model="gpt-4", temperature=0)
| StrOutputParser()
)
# Step 6: Use it!
questions = [
"How many vacation days do employees get?",
"What's the work from home policy?",
"How do I request time off?"
]
for q in questions:
print(f"\nQ: {q}")
print(f"A: {rag_chain.invoke(q)}")
Advanced: Adding Citations
Why Citations Matter
Users should know where answers come from!
(Where from?)"] end subgraph good["β With Citations"] G["Answer: You get 15 days
(Source: Employee Handbook, p.12)"] end style bad fill:#fecaca,stroke:#dc2626 style good fill:#d1fae5,stroke:#059669
Implementing Citations
from langchain_core.runnables import RunnableParallel
# Modified chain that returns both answer and sources
rag_chain_with_sources = RunnableParallel(
{
"context": retriever,
"question": RunnablePassthrough()
}
).assign(
answer=lambda x: (
prompt
| ChatOpenAI(model="gpt-4")
| StrOutputParser()
).invoke({
"context": format_docs(x["context"]),
"question": x["question"]
})
)
# Use it
result = rag_chain_with_sources.invoke("What's the vacation policy?")
print("Answer:", result["answer"])
print("\nSources:")
for doc in result["context"]:
print(f"- {doc.metadata.get('source', 'Unknown')}")
Common RAG Patterns
Pattern 1: Multi-Query
Generate multiple versions of the question for better retrieval:
from langchain.retrievers import MultiQueryRetriever
retriever = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(),
llm=ChatOpenAI(temperature=0)
)
# This will:
# 1. Generate variations of your question
# 2. Retrieve for each variation
# 3. Combine and deduplicate results
Pattern 2: Contextual Compression
Only keep the most relevant parts of retrieved documents:
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
compressor = LLMChainExtractor.from_llm(ChatOpenAI())
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever()
)
# Returns shorter, more focused results
Pattern 3: Conversational RAG
Remember chat history:
from langchain.chains import create_history_aware_retriever
# Reformulate questions based on chat history
history_retriever = create_history_aware_retriever(
llm=ChatOpenAI(),
retriever=vectorstore.as_retriever(),
prompt=history_prompt
)
# User: "What's the vacation policy?"
# Bot: "15 days per year"
# User: "Can I roll them over?" β Uses history to understand "them" = "vacation days"
Best Practices
1. Chunk Size Matters
about vacation policy"] R2["Complete context"] end style small fill:#fecaca,stroke:#dc2626 style large fill:#fecaca,stroke:#dc2626 style right fill:#d1fae5,stroke:#059669
Guidelines:
- General text: 500-1000 characters
- Code: 100-300 lines
- Tables: Keep rows together
2. Test Retrieval Quality
Before building the full chain, test your retriever:
# Test if retrieval works
test_questions = [
"vacation policy",
"remote work",
"sick leave"
]
for q in test_questions:
docs = retriever.invoke(q)
print(f"Question: {q}")
print(f"Retrieved {len(docs)} docs")
print(docs[0].page_content[:200])
print("---")
3. Use Appropriate Search Parameters
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={
"k": 3, # Return top 3 results
"score_threshold": 0.7 # Only return if similarity > 0.7
}
)
4. Keep Prompt Instructions Clear
template = """Rules:
1. Only answer from the provided context
2. If information isn't in context, say "I don't know"
3. Cite your sources
4. Be concise
Context: {context}
Question: {question}
Answer:"""
5. Monitor and Improve
Track:
- Questions that fail to find answers
- Low-quality retrievals
- User feedback
Common Issues & Solutions
Issue 1: Poor Retrieval Quality
Problem: Retriever returns irrelevant documents
Solutions:
- Adjust chunk size and overlap
- Try different embedding models
- Use metadata filtering
- Implement hybrid search (keyword + semantic)
Issue 2: Hallucinations
Problem: AI makes up answers not in documents
Solutions:
- Make prompt more strict
- Lower temperature (0-0.3)
- Add explicit instructions to only use context
- Implement citation requirements
Issue 3: Slow Performance
Problem: Takes too long to answer
Solutions:
- Reduce number of retrieved documents (k parameter)
- Use faster embedding models
- Implement caching
- Use local vector stores
RAG Architecture Overview
What Youβve Learned
β
What RAG is and why itβs useful
β
Loading and splitting documents
β
Understanding embeddings and vector stores
β
Creating retrievers
β
Building complete RAG chains
β
Adding citations and advanced features
β
Best practices and troubleshooting
You can now build AI systems that answer questions from your own documents! Next, weβll learn about LangGraph - building complex agentic workflows.
Whatβs Next?
In the next section, weβll learn about LangGraph & Agents:
- Building workflows with states and nodes
- Creating agents that can plan and adapt
- Using tools in agentic systems
- Managing complex decision flows
β Continue to Step 5: LangGraph & Agents
Quick Reference
Basic RAG Setup
# 1. Load & split
loader = TextLoader("docs.txt")
docs = loader.load()
chunks = text_splitter.split_documents(docs)
# 2. Create vector store
vectorstore = FAISS.from_documents(chunks, embeddings)
# 3. Create retriever
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 4. Create chain
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| ChatOpenAI()
| StrOutputParser()
)
# 5. Use it
answer = rag_chain.invoke("Your question")
Ready to build agents? Letβs go! π