601 lines
14 KiB
Markdown
601 lines
14 KiB
Markdown
# LangChain RAG Guide
|
|
|
|
Complete guide to Retrieval-Augmented Generation with LangChain.
|
|
|
|
## What is RAG?
|
|
|
|
**RAG (Retrieval-Augmented Generation)** combines:
|
|
1. **Retrieval**: Find relevant documents from knowledge base
|
|
2. **Generation**: LLM generates answer using retrieved context
|
|
|
|
**Benefits**:
|
|
- Reduce hallucinations
|
|
- Up-to-date information
|
|
- Domain-specific knowledge
|
|
- Source citations
|
|
|
|
## RAG pipeline components
|
|
|
|
### 1. Document loading
|
|
|
|
```python
|
|
from langchain_community.document_loaders import (
|
|
WebBaseLoader,
|
|
PyPDFLoader,
|
|
TextLoader,
|
|
DirectoryLoader,
|
|
CSVLoader,
|
|
UnstructuredMarkdownLoader
|
|
)
|
|
|
|
# Web pages
|
|
loader = WebBaseLoader("https://docs.python.org/3/tutorial/")
|
|
docs = loader.load()
|
|
|
|
# PDF files
|
|
loader = PyPDFLoader("paper.pdf")
|
|
docs = loader.load()
|
|
|
|
# Multiple PDFs
|
|
loader = DirectoryLoader("./papers/", glob="**/*.pdf", loader_cls=PyPDFLoader)
|
|
docs = loader.load()
|
|
|
|
# Text files
|
|
loader = TextLoader("data.txt")
|
|
docs = loader.load()
|
|
|
|
# CSV
|
|
loader = CSVLoader("data.csv")
|
|
docs = loader.load()
|
|
|
|
# Markdown
|
|
loader = UnstructuredMarkdownLoader("README.md")
|
|
docs = loader.load()
|
|
```
|
|
|
|
### 2. Text splitting
|
|
|
|
```python
|
|
from langchain.text_splitter import (
|
|
RecursiveCharacterTextSplitter,
|
|
CharacterTextSplitter,
|
|
TokenTextSplitter
|
|
)
|
|
|
|
# Recommended: Recursive (tries multiple separators)
|
|
text_splitter = RecursiveCharacterTextSplitter(
|
|
chunk_size=1000, # Characters per chunk
|
|
chunk_overlap=200, # Overlap between chunks
|
|
length_function=len,
|
|
separators=["\n\n", "\n", " ", ""]
|
|
)
|
|
|
|
splits = text_splitter.split_documents(docs)
|
|
|
|
# Token-based (for precise token limits)
|
|
text_splitter = TokenTextSplitter(
|
|
chunk_size=512, # Tokens per chunk
|
|
chunk_overlap=50
|
|
)
|
|
|
|
# Character-based (simple)
|
|
text_splitter = CharacterTextSplitter(
|
|
chunk_size=1000,
|
|
chunk_overlap=200,
|
|
separator="\n\n"
|
|
)
|
|
```
|
|
|
|
**Chunk size recommendations**:
|
|
- **Short answers**: 256-512 tokens
|
|
- **General Q&A**: 512-1024 tokens (recommended)
|
|
- **Long context**: 1024-2048 tokens
|
|
- **Overlap**: 10-20% of chunk_size
|
|
|
|
### 3. Embeddings
|
|
|
|
```python
|
|
from langchain_openai import OpenAIEmbeddings
|
|
from langchain_community.embeddings import (
|
|
HuggingFaceEmbeddings,
|
|
CohereEmbeddings
|
|
)
|
|
|
|
# OpenAI (fast, high quality)
|
|
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
|
|
|
|
# HuggingFace (free, local)
|
|
embeddings = HuggingFaceEmbeddings(
|
|
model_name="sentence-transformers/all-mpnet-base-v2"
|
|
)
|
|
|
|
# Cohere
|
|
embeddings = CohereEmbeddings(model="embed-english-v3.0")
|
|
```
|
|
|
|
### 4. Vector stores
|
|
|
|
```python
|
|
from langchain_chroma import Chroma
|
|
from langchain_community.vectorstores import FAISS
|
|
from langchain_pinecone import PineconeVectorStore
|
|
|
|
# Chroma (local, persistent)
|
|
vectorstore = Chroma.from_documents(
|
|
documents=splits,
|
|
embedding=embeddings,
|
|
persist_directory="./chroma_db"
|
|
)
|
|
|
|
# FAISS (fast similarity search)
|
|
vectorstore = FAISS.from_documents(splits, embeddings)
|
|
vectorstore.save_local("./faiss_index")
|
|
|
|
# Pinecone (cloud, scalable)
|
|
vectorstore = PineconeVectorStore.from_documents(
|
|
documents=splits,
|
|
embedding=embeddings,
|
|
index_name="my-index"
|
|
)
|
|
```
|
|
|
|
### 5. Retrieval
|
|
|
|
```python
|
|
# Basic retriever (top-k similarity)
|
|
retriever = vectorstore.as_retriever(
|
|
search_type="similarity",
|
|
search_kwargs={"k": 4} # Return top 4 documents
|
|
)
|
|
|
|
# MMR (Maximal Marginal Relevance) - diverse results
|
|
retriever = vectorstore.as_retriever(
|
|
search_type="mmr",
|
|
search_kwargs={
|
|
"k": 4,
|
|
"fetch_k": 20, # Fetch 20, return diverse 4
|
|
"lambda_mult": 0.5 # Diversity (0=diverse, 1=similar)
|
|
}
|
|
)
|
|
|
|
# Similarity score threshold
|
|
retriever = vectorstore.as_retriever(
|
|
search_type="similarity_score_threshold",
|
|
search_kwargs={
|
|
"score_threshold": 0.5 # Minimum similarity score
|
|
}
|
|
)
|
|
|
|
# Query documents directly
|
|
docs = retriever.get_relevant_documents("What is Python?")
|
|
```
|
|
|
|
### 6. QA chain
|
|
|
|
```python
|
|
from langchain.chains import RetrievalQA
|
|
from langchain_anthropic import ChatAnthropic
|
|
|
|
llm = ChatAnthropic(model="claude-sonnet-4-5-20250929")
|
|
|
|
# Basic QA chain
|
|
qa_chain = RetrievalQA.from_chain_type(
|
|
llm=llm,
|
|
retriever=retriever,
|
|
return_source_documents=True
|
|
)
|
|
|
|
# Query
|
|
result = qa_chain({"query": "What are Python decorators?"})
|
|
print(result["result"])
|
|
print(f"Sources: {len(result['source_documents'])}")
|
|
```
|
|
|
|
## Advanced RAG patterns
|
|
|
|
### Conversational RAG
|
|
|
|
```python
|
|
from langchain.chains import ConversationalRetrievalChain
|
|
from langchain.memory import ConversationBufferMemory
|
|
|
|
# Add memory
|
|
memory = ConversationBufferMemory(
|
|
memory_key="chat_history",
|
|
return_messages=True,
|
|
output_key="answer"
|
|
)
|
|
|
|
# Conversational RAG chain
|
|
qa = ConversationalRetrievalChain.from_llm(
|
|
llm=llm,
|
|
retriever=retriever,
|
|
memory=memory,
|
|
return_source_documents=True
|
|
)
|
|
|
|
# Multi-turn conversation
|
|
result1 = qa({"question": "What is Python used for?"})
|
|
result2 = qa({"question": "Can you give examples?"}) # Remembers context
|
|
result3 = qa({"question": "What about web development?"})
|
|
```
|
|
|
|
### Custom prompt template
|
|
|
|
```python
|
|
from langchain.prompts import PromptTemplate
|
|
|
|
# Custom QA prompt
|
|
template = """Use the following pieces of context to answer the question.
|
|
If you don't know the answer, say so - don't make it up.
|
|
Always cite your sources using [Source N] notation.
|
|
|
|
Context: {context}
|
|
|
|
Question: {question}
|
|
|
|
Helpful Answer:"""
|
|
|
|
prompt = PromptTemplate(
|
|
template=template,
|
|
input_variables=["context", "question"]
|
|
)
|
|
|
|
qa_chain = RetrievalQA.from_chain_type(
|
|
llm=llm,
|
|
retriever=retriever,
|
|
chain_type_kwargs={"prompt": prompt}
|
|
)
|
|
```
|
|
|
|
### Chain types
|
|
|
|
```python
|
|
# 1. Stuff (default) - Put all docs in context
|
|
qa_chain = RetrievalQA.from_chain_type(
|
|
llm=llm,
|
|
retriever=retriever,
|
|
chain_type="stuff" # Fast, works if docs fit in context
|
|
)
|
|
|
|
# 2. Map-reduce - Summarize each doc, then combine
|
|
qa_chain = RetrievalQA.from_chain_type(
|
|
llm=llm,
|
|
retriever=retriever,
|
|
chain_type="map_reduce" # For many documents
|
|
)
|
|
|
|
# 3. Refine - Iteratively refine answer
|
|
qa_chain = RetrievalQA.from_chain_type(
|
|
llm=llm,
|
|
retriever=retriever,
|
|
chain_type="refine" # Most thorough, slowest
|
|
)
|
|
|
|
# 4. Map-rerank - Score answers, return best
|
|
qa_chain = RetrievalQA.from_chain_type(
|
|
llm=llm,
|
|
retriever=retriever,
|
|
chain_type="map_rerank" # Good for multiple perspectives
|
|
)
|
|
```
|
|
|
|
### Multi-query retrieval
|
|
|
|
```python
|
|
from langchain.retrievers import MultiQueryRetriever
|
|
|
|
# Generate multiple queries for better recall
|
|
retriever = MultiQueryRetriever.from_llm(
|
|
retriever=vectorstore.as_retriever(),
|
|
llm=llm
|
|
)
|
|
|
|
# "What is Python?" becomes:
|
|
# - "What is Python programming language?"
|
|
# - "Python language definition"
|
|
# - "Overview of Python"
|
|
docs = retriever.get_relevant_documents("What is Python?")
|
|
```
|
|
|
|
### Contextual compression
|
|
|
|
```python
|
|
from langchain.retrievers import ContextualCompressionRetriever
|
|
from langchain.retrievers.document_compressors import LLMChainExtractor
|
|
|
|
# Compress retrieved docs to relevant parts only
|
|
compressor = LLMChainExtractor.from_llm(llm)
|
|
|
|
compression_retriever = ContextualCompressionRetriever(
|
|
base_compressor=compressor,
|
|
base_retriever=vectorstore.as_retriever()
|
|
)
|
|
|
|
# Returns only relevant excerpts
|
|
compressed_docs = compression_retriever.get_relevant_documents("Python decorators")
|
|
```
|
|
|
|
### Ensemble retrieval (hybrid search)
|
|
|
|
```python
|
|
from langchain.retrievers import EnsembleRetriever
|
|
from langchain.retrievers import BM25Retriever
|
|
|
|
# Vector search (semantic)
|
|
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
|
|
|
|
# Keyword search (BM25)
|
|
keyword_retriever = BM25Retriever.from_documents(splits)
|
|
keyword_retriever.k = 5
|
|
|
|
# Combine both
|
|
ensemble_retriever = EnsembleRetriever(
|
|
retrievers=[vector_retriever, keyword_retriever],
|
|
weights=[0.5, 0.5] # Equal weight
|
|
)
|
|
|
|
docs = ensemble_retriever.get_relevant_documents("Python async")
|
|
```
|
|
|
|
## RAG with agents
|
|
|
|
### Agent-based RAG
|
|
|
|
```python
|
|
from langchain.agents import create_tool_calling_agent
|
|
from langchain.tools.retriever import create_retriever_tool
|
|
|
|
# Create retriever tool
|
|
retriever_tool = create_retriever_tool(
|
|
retriever=retriever,
|
|
name="python_docs",
|
|
description="Searches Python documentation for answers about Python programming"
|
|
)
|
|
|
|
# Create agent with retriever tool
|
|
agent = create_tool_calling_agent(
|
|
llm=llm,
|
|
tools=[retriever_tool, calculator, search],
|
|
system_prompt="Use python_docs tool for Python questions"
|
|
)
|
|
|
|
# Agent decides when to retrieve
|
|
from langchain.agents import AgentExecutor
|
|
agent_executor = AgentExecutor(agent=agent, tools=[retriever_tool])
|
|
|
|
result = agent_executor.invoke({"input": "What are Python generators?"})
|
|
```
|
|
|
|
### Multi-document agents
|
|
|
|
```python
|
|
# Multiple knowledge bases
|
|
python_retriever = create_retriever_tool(
|
|
retriever=python_vectorstore.as_retriever(),
|
|
name="python_docs",
|
|
description="Python programming documentation"
|
|
)
|
|
|
|
numpy_retriever = create_retriever_tool(
|
|
retriever=numpy_vectorstore.as_retriever(),
|
|
name="numpy_docs",
|
|
description="NumPy library documentation"
|
|
)
|
|
|
|
# Agent chooses which knowledge base to query
|
|
agent = create_agent(
|
|
model=llm,
|
|
tools=[python_retriever, numpy_retriever, search]
|
|
)
|
|
|
|
result = agent.invoke({"input": "How do I create numpy arrays?"})
|
|
```
|
|
|
|
## Metadata filtering
|
|
|
|
### Add metadata to documents
|
|
|
|
```python
|
|
from langchain.schema import Document
|
|
|
|
# Documents with metadata
|
|
docs = [
|
|
Document(
|
|
page_content="Python is a programming language",
|
|
metadata={"source": "tutorial.pdf", "page": 1, "category": "intro"}
|
|
),
|
|
Document(
|
|
page_content="Python decorators modify functions",
|
|
metadata={"source": "advanced.pdf", "page": 42, "category": "advanced"}
|
|
)
|
|
]
|
|
|
|
vectorstore = Chroma.from_documents(docs, embeddings)
|
|
```
|
|
|
|
### Filter by metadata
|
|
|
|
```python
|
|
# Retrieve only from specific source
|
|
retriever = vectorstore.as_retriever(
|
|
search_kwargs={
|
|
"k": 4,
|
|
"filter": {"category": "intro"} # Only intro documents
|
|
}
|
|
)
|
|
|
|
# Multiple filters
|
|
retriever = vectorstore.as_retriever(
|
|
search_kwargs={
|
|
"k": 4,
|
|
"filter": {
|
|
"category": "advanced",
|
|
"source": "advanced.pdf"
|
|
}
|
|
}
|
|
)
|
|
```
|
|
|
|
## Document preprocessing
|
|
|
|
### Clean documents
|
|
|
|
```python
|
|
def preprocess_doc(doc):
|
|
"""Clean and normalize document."""
|
|
# Remove extra whitespace
|
|
doc.page_content = " ".join(doc.page_content.split())
|
|
|
|
# Remove special characters
|
|
doc.page_content = re.sub(r'[^\w\s]', '', doc.page_content)
|
|
|
|
# Lowercase (optional)
|
|
doc.page_content = doc.page_content.lower()
|
|
|
|
return doc
|
|
|
|
# Apply preprocessing
|
|
clean_docs = [preprocess_doc(doc) for doc in docs]
|
|
```
|
|
|
|
### Extract structured data
|
|
|
|
```python
|
|
from langchain.document_transformers import Html2TextTransformer
|
|
|
|
# HTML to clean text
|
|
transformer = Html2TextTransformer()
|
|
clean_docs = transformer.transform_documents(html_docs)
|
|
|
|
# Extract tables
|
|
from langchain.document_loaders import UnstructuredHTMLLoader
|
|
|
|
loader = UnstructuredHTMLLoader("data.html")
|
|
docs = loader.load() # Extracts tables as structured data
|
|
```
|
|
|
|
## Evaluation & monitoring
|
|
|
|
### Evaluate retrieval quality
|
|
|
|
```python
|
|
from langchain.evaluation import load_evaluator
|
|
|
|
# Relevance evaluator
|
|
evaluator = load_evaluator("relevance", llm=llm)
|
|
|
|
# Test retrieval
|
|
query = "What are Python decorators?"
|
|
retrieved_docs = retriever.get_relevant_documents(query)
|
|
|
|
for doc in retrieved_docs:
|
|
result = evaluator.evaluate_strings(
|
|
input=query,
|
|
prediction=doc.page_content
|
|
)
|
|
print(f"Relevance score: {result['score']}")
|
|
```
|
|
|
|
### Track sources
|
|
|
|
```python
|
|
# Always return sources
|
|
qa_chain = RetrievalQA.from_chain_type(
|
|
llm=llm,
|
|
retriever=retriever,
|
|
return_source_documents=True
|
|
)
|
|
|
|
result = qa_chain({"query": "What is Python?"})
|
|
|
|
# Show sources to user
|
|
print(result["result"])
|
|
print("\nSources:")
|
|
for i, doc in enumerate(result["source_documents"]):
|
|
print(f"[{i+1}] {doc.metadata.get('source', 'Unknown')}")
|
|
print(f" {doc.page_content[:100]}...")
|
|
```
|
|
|
|
## Best practices
|
|
|
|
1. **Chunk size matters** - 512-1024 tokens is usually optimal
|
|
2. **Add overlap** - 10-20% overlap prevents context loss
|
|
3. **Use metadata** - Track sources for citations
|
|
4. **Test retrieval quality** - Evaluate before using in production
|
|
5. **Hybrid search** - Combine vector + keyword for best results
|
|
6. **Compress context** - Remove irrelevant parts before LLM
|
|
7. **Cache embeddings** - Expensive, cache when possible
|
|
8. **Version your index** - Track changes to knowledge base
|
|
9. **Monitor failures** - Log when retrieval doesn't find answers
|
|
10. **Update regularly** - Keep knowledge base current
|
|
|
|
## Common pitfalls
|
|
|
|
1. **Chunks too large** - Won't fit in context
|
|
2. **No overlap** - Important context lost at boundaries
|
|
3. **No metadata** - Can't cite sources
|
|
4. **Poor splitting** - Breaks mid-sentence or mid-paragraph
|
|
5. **Wrong embedding model** - Domain mismatch hurts retrieval
|
|
6. **No reranking** - Lower quality results
|
|
7. **Ignoring failures** - No handling when retrieval fails
|
|
|
|
## Performance optimization
|
|
|
|
### Caching
|
|
|
|
```python
|
|
from langchain.cache import InMemoryCache, SQLiteCache
|
|
from langchain.globals import set_llm_cache
|
|
|
|
# In-memory cache
|
|
set_llm_cache(InMemoryCache())
|
|
|
|
# Persistent cache
|
|
set_llm_cache(SQLiteCache(database_path=".langchain.db"))
|
|
|
|
# Same query uses cache (faster + cheaper)
|
|
result1 = qa_chain({"query": "What is Python?"})
|
|
result2 = qa_chain({"query": "What is Python?"}) # Cached
|
|
```
|
|
|
|
### Batch processing
|
|
|
|
```python
|
|
# Process multiple queries efficiently
|
|
queries = [
|
|
"What is Python?",
|
|
"What are decorators?",
|
|
"How do I use async?"
|
|
]
|
|
|
|
# Batch retrieval
|
|
all_docs = vectorstore.similarity_search_batch(queries)
|
|
|
|
# Batch QA
|
|
results = qa_chain.batch([{"query": q} for q in queries])
|
|
```
|
|
|
|
### Async operations
|
|
|
|
```python
|
|
# Async RAG for concurrent queries
|
|
import asyncio
|
|
|
|
async def async_qa(query):
|
|
return await qa_chain.ainvoke({"query": query})
|
|
|
|
# Run multiple queries concurrently
|
|
results = await asyncio.gather(
|
|
async_qa("What is Python?"),
|
|
async_qa("What are decorators?")
|
|
)
|
|
```
|
|
|
|
## Resources
|
|
|
|
- **LangChain RAG Docs**: https://docs.langchain.com/oss/python/langchain/rag
|
|
- **Vector Stores**: https://python.langchain.com/docs/integrations/vectorstores
|
|
- **Document Loaders**: https://python.langchain.com/docs/integrations/document_loaders
|
|
- **Retrievers**: https://python.langchain.com/docs/modules/data_connection/retrievers
|