RAG From Scratch

repository·main·Indexed 27 days ago

https://github.com/langchain-ai/rag-from-scratch

An 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.

Tokens
3.7K
Snippets
10
Records
12
Agent score
94%

What's inside rag-from-scratch

  1. Overview of RAG From Scratch

    main
    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.
  2. Use ColBERT with RAGatouille

    main

    ColBERT (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 ragatouille library to manage ColBERT indexing and retrieval.

    1. Install: pip install -U ragatouille
    2. Load Model: Use RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0").
    3. Index: Use .index() on your collection of documents.
    4. Search: Use .search() for direct queries or .as_langchain_retriever() to integrate with LangChain.
  3. Configure LangSmith and OpenAI environment variables

    main

    Set 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>
  4. Implement Multi-representation Indexing with MultiVectorRetriever

    main

    Multi-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.

    1. Initialize the Vectorstore: Use a vectorstore (e.g., Chroma) to store embeddings of the summaries.
    2. Initialize the Byte Store: Use a storage layer (e.g., InMemoryByteStore) for the full parent documents.
    3. Setup MultiVectorRetriever: Link the vectorstore and byte store using a unique id_key.
    4. 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)
  5. Retrieve Wikipedia content via API

    main

    Use 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.php Required Parameters:

    • action: query
    • format: json
    • titles: The page title
    • prop: extracts
    • explaintext: 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")
  6. Calculate cosine similarity for embeddings

    main

    When 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)
  7. Build a complete RAG chain

    main

    You 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 WebBaseLoader for ingestion, RecursiveCharacterTextSplitter for chunking, Chroma as the vectorstore, and ChatOpenAI for 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?")
  8. Count tokens using tiktoken

    main

    Use tiktoken to 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")
  9. Configure retriever search parameters

    main

    When creating a retriever from a vectorstore, you can pass search_kwargs to 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?")
  10. Split text using RecursiveCharacterTextSplitter

    main

    The RecursiveCharacterTextSplitter is 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)