RAG From Scratch
repository·main·Indexed 27 days ago
https://github.com/langchain-ai/rag-from-scratchAn educational repository of Jupyter notebooks teaching the implementation of Retrieval Augmented Generation (RAG) from the ground up. It covers core pipeline components including indexing, retrieval, and generation, featuring practical examples with LangChain, Chroma, OpenAI embeddings, MultiVectorRetriever, and ColBERT via RAGatouille.
What's inside rag-from-scratch
- RAG From Scratch is a collection of notebooks designed to teach the fundamentals of Retrieval Augmented Generation (RAG). The project covers the core components of a RAG pipeline: indexing, retrieval, and generation. These notebooks are intended to be used alongside a companion video playlist to build a deep understanding of how to expand an LLM's knowledge base using external data sources.
Install RAG dependencies
mainTo follow the RAG indexing tutorials, install the following Python packages:
! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain youtube-transcript-api pytubeUse ColBERT with RAGatouille
mainColBERT (Contextualized Late Interaction over BERT) generates contextually influenced vectors for both tokens in passages and tokens in queries, allowing for more granular similarity matching. You can use the
ragatouillelibrary to manage ColBERT indexing and retrieval.- Install:
pip install -U ragatouille - Load Model: Use
RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0"). - Index: Use
.index()on your collection of documents. - Search: Use
.search()for direct queries or.as_langchain_retriever()to integrate with LangChain.
- Install:
Configure LangSmith and OpenAI environment variables
mainSet up your environment variables for LangSmith tracing and OpenAI API access:
import os # LangSmith configuration os.environ['LANGCHAIN_TRACING_V2'] = 'true' os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com' os.environ['LANGCHAIN_API_KEY'] = '<your-api-key>' # OpenAI configuration os.environ['OPENAI_API_KEY'] = '<your-api-key>'import os os.environ['LANGCHAIN_TRACING_V2'] = 'true' os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com' os.environ['LANGCHAIN_API_KEY'] = <your-api-key> os.environ['OPENAI_API_KEY'] = <your-api-key>Implement Multi-representation Indexing with MultiVectorRetriever
mainMulti-representation indexing allows you to index small summaries (child chunks) in a vectorstore while storing the full original documents (parent documents) in a separate byte store. This improves retrieval accuracy by matching queries to concise summaries but returning rich context.
- Initialize the Vectorstore: Use a vectorstore (e.g.,
Chroma) to store embeddings of the summaries. - Initialize the Byte Store: Use a storage layer (e.g.,
InMemoryByteStore) for the full parent documents. - Setup MultiVectorRetriever: Link the vectorstore and byte store using a unique
id_key. - Populate: Add summary documents to the vectorstore and map the original documents to the byte store using unique IDs.
from langchain_community.document_loaders import WebBaseLoader from langchain_text_splitters import RecursiveCharacterTextSplitter import uuid from langchain_core.documents import Document from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain.storage import InMemoryByteStore from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain.retrievers.multi_vector import MultiVectorRetriever # 1. Load documents loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/") docs = loader.load() # 2. Generate summaries using a chain chain = ( {"doc": lambda x: x.page_content} | ChatPromptTemplate.from_template("Summarize the following document:\n\n{doc}") | ChatOpenAI(model="gpt-3.5-turbo",max_retries=0) | StrOutputParser() ) summaries = chain.batch(docs, {"max_concurrency": 5}) # 3. Setup Retriever components vectorstore = Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings()) store = InMemoryByteStore() id_key = "doc_id" retriever = MultiVectorRetriever( vectorstore=vectorstore, byte_store=store, id_key=id_key, ) # 4. Link summaries to parent docs via UUIDs doc_ids = [str(uuid.uuid4()) for _ in docs] summary_docs = [ Document(page_content=s, metadata={id_key: doc_ids[i]}) for i, s in enumerate(summaries) ] # 5. Add to storage retriever.vectorstore.add_documents(summary_docs) retriever.docstore.mset(list(zip(doc_ids, docs))) # 6. Retrieve query = "Memory in agents" retrieved_docs = retriever.get_relevant_documents(query, n_results=1)- Initialize the Vectorstore: Use a vectorstore (e.g.,
Retrieve Wikipedia content via API
mainUse the Wikipedia API to fetch the full text content of a specific page. This is useful for populating RAG pipelines with factual data.
Endpoint:
https://en.wikipedia.org/w/api.phpRequired Parameters:action:queryformat:jsontitles: The page titleprop:extractsexplaintext:True(to get raw text instead of HTML)
import requests def get_wikipedia_page(title: str): URL = "https://en.wikipedia.org/w/api.php" params = { "action": "query", "format": "json", "titles": title, "prop": "extracts", "explaintext": True, } headers = {"User-Agent": "RAGatouille_tutorial/0.0.1 (ben@clavie.eu)"} response = requests.get(URL, params=params, headers=headers) data = response.json() page = next(iter(data["query"]["pages"].values())) return page["extract"] if "extract" in page else None full_document = get_wikipedia_page("Hayao_Miyazaki")Calculate cosine similarity for embeddings
mainWhen using OpenAI embeddings, cosine similarity is the recommended method to measure the distance between two vectors. A value of 1 indicates identical vectors.
import numpy as np def cosine_similarity(vec1, vec2): dot_product = np.dot(vec1, vec2) norm_vec1 = np.linalg.norm(vec1) norm_vec2 = np.linalg.norm(vec2) return dot_product / (norm_vec1 * norm_vec2) # similarity = cosine_similarity(query_result, document_result)Build a complete RAG chain
mainYou can implement a full RAG (Retrieval-Augmented Generation) pipeline by combining document loading, splitting, embedding, and a LangChain expression language (LCEL) chain. This example uses
WebBaseLoaderfor ingestion,RecursiveCharacterTextSplitterfor chunking,Chromaas the vectorstore, andChatOpenAIfor generation.import bs4 from langchain import hub from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.document_loaders import WebBaseLoader from langchain_community.vectorstores import Chroma from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough from langchain_openai import ChatOpenAI, OpenAIEmbeddings #### INDEXING #### # Load Documents loader = WebBaseLoader( web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",), bs_kwargs=dict( parse_only=bs4.SoupStrainer( class_=("post-content", "post-title", "post-header") ) ), ) docs = loader.load() # Split text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) splits = text_splitter.split_documents(docs) # Embed vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings()) retriever = vectorstore.as_retriever() #### RETRIEVAL and GENERATION #### # Prompt prompt = hub.pull("rlm/rag-prompt") # LLM llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0) # Post-processing def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) # Chain rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) # Question rag_chain.invoke("What is Task Decomposition?")Count tokens using tiktoken
mainUse
tiktokento calculate the number of tokens in a string, which is useful for managing LLM context limits.import tiktoken def num_tokens_from_string(string: str, encoding_name: str) -> int: """Returns the number of tokens in a text string.""" encoding = tiktoken.get_encoding(encoding_name) num_tokens = len(encoding.encode(string)) return num_tokens # Example usage num_tokens_from_string("your text here", "cl100k_base")Configure retriever search parameters
mainWhen creating a retriever from a vectorstore, you can pass
search_kwargsto control retrieval behavior, such as the number of documents returned (k).# Retrieve top 1 relevant document retriever = vectorstore.as_retriever(search_kwargs={"k": 1}) docs = retriever.get_relevant_documents("What is Task Decomposition?")Split text using RecursiveCharacterTextSplitter
mainThe
RecursiveCharacterTextSplitteris the recommended splitter for generic text. It attempts to split on a list of characters (defaulting to["\n\n", "\n", " ", ""]) to keep semantically related pieces like paragraphs and sentences together.from langchain.text_splitter import RecursiveCharacterTextSplitter # Example using tiktoken encoder for chunking text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( chunk_size=300, chunk_overlap=50 ) splits = text_splitter.split_documents(blog_docs)Create a custom RAG prompt template
mainYou can define a custom prompt using
ChatPromptTemplateto control how the LLM uses the retrieved context.from langchain.prompts import ChatPromptTemplate template = """Answer the question based only on the following context: {context} Question: {question} """ prompt = ChatPromptTemplate.from_template(template)