Retrieval-Augmented Generation with LangChain

Nov 12 2024 · Python 3.12, LangChain 0.3.x, JupyterLab 4.2.4

Lesson 04: Advanced RAG Techniques

Enhancing a Basic RAG App Demo

Episode complete

Play next episode

Next
Transcript

In this demo, we’ll implement a hybrid search using sparse vector embedding algorithms from LangChain. Start a notebook and add the following code:

# Set up a User Agent for this session
import os
from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_community.document_loaders import WikipediaLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

os.environ['USER_AGENT'] = 'sports-buddy-advanced'

llm = ChatOpenAI(model="gpt-4o-mini")

loader = WikipediaLoader("2024_Summer_Olympics",)
docs = loader.load()

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, 
  chunk_overlap=0)
splits = text_splitter.split_documents(docs)

database = Chroma.from_documents(documents=splits, 
  embedding=OpenAIEmbeddings())
retriever = database.as_retriever()

There’s nothing new here. It’s the same initial setup for a basic RAG.

Create a cell. In this cell, create a retriever based on the Best Match 25 algorithm. This will allow you to do a sparse-vector search:

from langchain.retrievers import BM25Retriever, EnsembleRetriever

keyword_retriever = BM25Retriever.from_documents(splits)
keyword_retriever.k =  3

ensemble_retriever = EnsembleRetriever(retrievers=[retriever, 
  keyword_retriever], weights=[0.3, 0.7])

In the next cell, create simple chains for both the dense (semantic) and sparse (keyword) retrievers:

from langchain.chains import RetrievalQA

dense_chain = RetrievalQA.from_chain_type(
  llm=llm, chain_type="stuff", retriever=retriever
)

sparse_chain = RetrievalQA.from_chain_type(
  llm=llm, chain_type="stuff", retriever=ensemble_retriever
)

The dense_chain is the same familiar chain you’ve been using with the Chroma database retriever. The sparse_chain uses the sparse retriever for keyword-based search.

Run the dense_chain in the next cell:

normal_response = normal_chain.invoke("What happened at the opening 
  ceremony of the 2024 Summer Olympics")
print(normal_response['result'])

Observe the output:

The opening ceremony of the 2024 Summer Olympics was held outside of a stadium for the first time in modern Olympic history.

  Athletes were paraded by boat along the Seine River in Paris.

Finally, run the sparse_chain with:

sparse_response = sparse_chain.invoke("What happened at the 
  opening ceremony of the 2024 Summer Olympics")
print(hybrid_response['result'])

And note its output:

The opening ceremony of the 2024 Summer Olympics took place outside of a stadium for the first time in modern Olympic history, with athletes being paraded by boat along the Seine River in Paris. This unique setting was part of the ceremony, making it a significant and memorable event in

  Olympic history.

Notice how the keywords in the query contribute to a more elaborate response in the hybrid search.

Citing in RAG

Citations add extra information to your responses, so you know where they came from. Open a new notebook to learn how to add citations to SportsBuddy. In the notebook, start with the following code:

from langchain_community.retrievers import WikipediaRetriever
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
import os

llm = ChatOpenAI(model="gpt-4o-mini")

system_prompt = (
  "You're a helpful AI assistant. Given a user question "
  "and some Wikipedia article snippets, answer the user "
  "question. If none of the articles answer the question, "
  "just say you don't know."
  "\n\nHere are the Wikipedia articles: "
  "{context}"
)

retriever = WikipediaRetriever(top_k_results=6, doc_content_chars_max=2000)
prompt = ChatPromptTemplate.from_messages(
  [
    ("system", system_prompt),
    ("human", "{input}"),
  ]
)

This prompt instructs the WikipediaRetriever to fetch relevant articles based on the given context. Heads up: It can get things wildly wrong. Because it’s limited to Wikipedia articles, it will fetch articles it thinks best answer the semantic understanding it has for your query. In the next cell, create a chain:

from typing import List

from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough


def format_docs(docs: List[Document]):
  return "\n\n".join(doc.page_content for doc in docs)


rag_chain = (
  RunnablePassthrough.assign(context=(lambda x: format_docs(x["context"])))
  | prompt
  | llm
  | StrOutputParser()
)

retrieve_docs = (lambda x: x["input"]) | retriever

chain = RunnablePassthrough.assign(context=retrieve_docs).assign(
  answer=rag_chain
)

Execute a simple query and examine the structure of the response object:

result = chain.invoke({"input": "How did the USA fair at the 2024 
  Summer Olympics"})
print(result.keys())
dict_keys(['input', 'context', 'answer'])

The response contains input (the query), context (the reference document), and answer. This information is accessible due to OpenAI’s tool-calling support. We can format this neatly into a citation model.

Let’s use the CitedAnswer model:

from typing import List
from langchain_core.pydantic_v1 import BaseModel, Field


class CitedAnswer(BaseModel):
  """Answer the user question based only on the given sources, and cite 
    the sources used."""

  answer: str = Field(
    ...,
    description="The answer to the user question, which is based only on 
      the given sources.",
  )
  citations: List[int] = Field(
    ...,
    description="The integer IDs of the SPECIFIC sources which justify 
      the answer.",
  )

To use the citation model, search with the following:

structured_llm = llm.with_structured_output(CitedAnswer)

query = """How did the USA fair at the 2024 Summer Olympics"""
result = structured_llm.invoke(query)

result

The model assigns the values to answer and citations by interpreting the description. The response is wrapped in a CitedAnswer class.

You could modify the citation to reference source URLs instead of integer IDs:

citations: List[str] = Field(
  ...,
  description="The string URLs of the SPECIFIC sources which justify 
    the answer.",
)

However, take note that because the documents aren’t retrieved verbatim, these URLs might be generated and lead to 404 errors. If you instead want to cite a portion of the retrieved document, consider using a model like this:

class Citation(BaseModel):
  source_id: int = Field(
    ...,
    description="The integer ID of a SPECIFIC source which 
      justifies the answer.",
  )
  quote: str = Field(
    ...,
    description="The VERBATIM quote from the specified source that 
      justifies the answer.",
  )


class QuotedAnswer(BaseModel):
  """Answer the user question based only on the given sources, and 
    cite the sources used."""

  answer: str = Field(
    ...,
    description="The answer to the user question, which is based 
      only on the given sources.",
  )
  citations: List[Citation] = Field(
    ..., description="Citations from the given sources that 
      justify the answer."
  )

You can use it similarly:

rag_chain = (
  RunnablePassthrough.assign(context=(lambda x: 
    format_docs_with_id(x["context"])))
  | prompt
  | llm.with_structured_output(QuotedAnswer)
)

retrieve_docs = (lambda x: x["input"]) | retriever

chain = RunnablePassthrough.assign(context=retrieve_docs).assign(
  answer=rag_chain
)

chain.invoke({"input": "How did the USA fair at the 2024 Summer
  Olympics"})

And that’s it! You’ve now added citations to your RAG’s responses.

See forum comments
Cinema mode Download course materials from Github
Previous: Enhancing a Basic RAG App Next: Conclusion