Veröffentlicht am

Parent Document Retriever in Aktion: Einrichten von RAG mit Mistral LLM und LangChain

6 min read
Autoren
  • Profile picture of aithemes.net
    Name
    aithemes.net
    Twitter
Post image

Einführung

Dieser Beitrag zeigt, wie man ein Retrieval-Augmented Generation (RAG)-System mit LangChain einrichtet, das einen Parent Document Retriever mit Mistral AI-Modellen integriert. Er bietet Implementierungsdetails, einschließlich Python-Code, um zu zeigen, wie RAG die Qualität der Antworten von Sprachmodellen verbessert. Dies ergänzt den verwandten Beitrag, der den finalen Fall präsentiert.

Schlüsselarchitektur von RAG

Hier sind die Schlüsselkomponenten dieses RAG-Systems, die ihre Rollen und Beiträge zur Gesamtarchitektur beschreiben:

  • Mistral LLM und Embeddings: Verwendet Mistral-Embeddings und Mistral Large LLM, um relevante Antworten basierend auf externem Wissen zu generieren.
  • Parent Document Retriever: Ruft kleinere Informationsbrocken ab, während er auf ihre übergeordneten Dokumente für den Kontext verweist.
  • FAISS Vector Store: Speichert Embeddings und ermöglicht effiziente Ähnlichkeitssuchen.
  • Document Chunking: Teilt PDF-Dokumente in kleinere Teile auf, um die Abrufqualität zu verbessern.
  • Naive RAG Chain: Verbindet den Retriever, den Vector Store und das LLM, um fundierte Antworten zu generieren.

Mit diesen Komponenten im Hinterkopf wollen wir nun untersuchen, wie sie zusammenarbeiten, um ein effektives Retrieval-Augmented Generation-System zu schaffen.

LangChain-Framework

LangChain ist das primäre Framework, das für die Implementierung des Retrieval-Augmented Generation (RAG)-Systems verwendet wird. Es bietet die notwendigen Tools und Komponenten, um die RAG-Anwendung zu erstellen, einschließlich der Integration mit Vector Stores und Retrievern.

Nachdem wir nun ein Framework haben, wollen wir tiefer in die Funktionsweise des ParentDocumentRetrievers eintauchen, der Spezifität und Kontext beim Dokumentenabruf ausbalanciert.

ParentDocumentRetriever: Ausbalancieren von Spezifität und Kontext

Der ParentDocumentRetriever erstellt kleine Brocken für genaue Embeddings, behält aber genug Kontext für eine sinnvolle Abfrage bei. Er ruft präzise Brocken und ihre übergeordneten Dokumente ab, um Spezifität und Kontext zu gewährleisten, ohne wichtige Informationen zu verlieren.

Als Nächstes werfen wir einen Blick darauf, wie PDF-Chunking und FAISS Vector Store-Indexierung zusammenarbeiten, um die Abrufleistung zu verbessern.

PDF-Chunking und FAISS Vector Store-Indexierung

Das folgende Code-Snippet zeigt, wie man PDF-Dokumente für RAG vorbereitet, indem man sie in Brocken aufteilt und in einem FAISS Vector Store indexiert. Dieser Prozess umfasst mehrere Schritte, darunter die Definition der Brockengrößen, das Erstellen von Embeddings, das Einrichten des Speichers für die Dokumente und deren Indexierung für Ähnlichkeitssuchen. Die übergeordneten und untergeordneten Brockengrößen sowie die Überlappungsgrößen sind wichtige Parameter, die die Granularität der Brocken und den beibehaltenen Kontext beeinflussen und somit die Qualität des Abrufs beeinflussen.

Dokumente werden mit einem PDF-Loader geladen, und der ParentDocumentRetriever wird erstellt, um den Abruf von Dokumentenbrocken zusammen mit ihren übergeordneten Dokumenten zu handhaben. Der Code durchläuft dann die geladenen Dokumente, fügt sie in Batches zum Retriever hinzu und speichert schließlich die indexierte Datenbank lokal für die zukünftige Verwendung.

# Constants for chunk overlap
CHILD_CHUNK_SIZE = 1024
CHILD_CHUNK_OVERLAP = 100
PARENT_CHUNK_SIZE = 4096
PARENT_CHUNK_OVERLAP = 400

# Create embeddings instance
embeddings = MistralAIEmbeddings(model="mistral-embed", mistral_api_key=my_api_key)

# Settings
index_name = args.index_name
data_files_path = args.data_files_path
dbstore_path = args.dbstore_path
docstore_path = args.docstore_path

# Create stores
fs = LocalFileStore(docstore_path)
store = create_kv_docstore(fs)

# Create Parent and Child text splitters
child_text_splitter = RecursiveCharacterTextSplitter(chunk_size=CHILD_CHUNK_SIZE, chunk_overlap=CHILD_CHUNK_OVERLAP)
parent_text_splitter = RecursiveCharacterTextSplitter(chunk_size=PARENT_CHUNK_SIZE, chunk_overlap=PARENT_CHUNK_OVERLAP)

# Create FAISS vectorstore
dimensions = len(embeddings.embed_query("dummy"))

db = FAISS(
    embedding_function=embeddings,
    index=IndexFlatL2(dimensions),
    docstore=InMemoryDocstore(),
    index_to_docstore_id={},
    normalize_L2=False
)

# Load documents
logging.info("Loading documents...")
loader = PyPDFDirectoryLoader(data_files_path)
docs = loader.load()
logging.info(f"Number of document blocks loaded: {len(docs)}")

# Create ParentDocumentRetriever
big_chunks_retriever = ParentDocumentRetriever(
    vectorstore=db,
    docstore=store,
    child_splitter=child_text_splitter,
    parent_splitter=parent_text_splitter
)

# Add documents to retriever
MAX_BATCH_SIZE = 100

for i in tqdm(range(0, len(docs), MAX_BATCH_SIZE)):
    logging.info(f"Start: {i}")
    i_end = min(len(docs), i + MAX_BATCH_SIZE)
    logging.info(f"End: {i_end}")
    batch = docs[i:i_end]
    try:
        big_chunks_retriever.add_documents(batch, ids=None)
    except ValueError as e:
        logging.error(e)
        big_chunks_retriever.add_documents(batch[:50], ids=None)
        big_chunks_retriever.add_documents(batch[50:], ids=None)
        continue
    logging.info(f"Number of keys stored in the docstore: {len(list(store.yield_keys()))}")

# Save the database
db.save_local(dbstore_path, index_name)
logging.info("Completed")

Naive RAG Chain Diagramm

Unten ist ein Diagramm dargestellt, das den Workflow des Naive RAG-Systems darstellt und zeigt, wie jede Komponente interagiert, um fundierte Antworten zu generieren.

Workflow chart

Nachdem wir den Workflow veranschaulicht haben, wollen wir uns nun ansehen, wie man die RAG Chain mit der LangChain Expression Language (LCEL) definiert.

Definieren der RAG Chain mit LangChain Expression Language (LCEL)

Dieser Abschnitt erklärt, wie man den Parent Document Retriever mit LangChain einrichtet, einschließlich der Konfiguration des Vector Stores und der Definition von Text-Splittern.

Das erste Code-Snippet zeigt, wie man eine Funktion zum Neuerstellen des Retrievers definiert. Diese Funktion nimmt Pfade für den Dokumentenspeicher und den Datenbankspeicher sowie das Embeddings-Modell und den Indexnamen entgegen. Sie erstellt sowohl untergeordnete als auch übergeordnete Text-Splitter, um sicherzustellen, dass Dokumente angemessen für die Indexierung und den Abruf aufgeteilt werden.

# Constants
NUM_CTX = 32768
RETRIEVED_CHUNKS = 20
CHILD_CHUNK_SIZE = 1024
CHILD_CHUNK_OVERLAP = 100
PARENT_CHUNK_SIZE = 4096
PARENT_CHUNK_OVERLAP = 400

def rebuild_retriever(docstore_path, dbstore_path, embeddings, index_name):
    child_text_splitter = RecursiveCharacterTextSplitter(chunk_size=CHILD_CHUNK_SIZE, chunk_overlap=CHILD_CHUNK_OVERLAP)
    parent_text_splitter = RecursiveCharacterTextSplitter(chunk_size=PARENT_CHUNK_SIZE, chunk_overlap=PARENT_CHUNK_OVERLAP)

    fs = LocalFileStore(docstore_path)
    docstore = create_kv_docstore(fs)

    vectordb = FAISS.load_local(
        folder_path=dbstore_path,
        embeddings=embeddings,
        index_name=index_name,
        allow_dangerous_deserialization=True
    )

    big_chunks_retriever = ParentDocumentRetriever(
        vectorstore=vectordb,
        docstore=docstore,
        child_splitter=child_text_splitter,
        parent_splitter=parent_text_splitter,
        search_type="similarity",
        search_kwargs={
            "k": RETRIEVED_CHUNKS
        }
    )
    return big_chunks_retriever

Als Nächstes definieren wir eine Prompt-Vorlage, die die Antwort des Sprachmodells leitet und sicherstellt, dass es sich nur auf den abgerufenen Kontext stützt.

# Define Prompt Template
def get_prompt_template() -> PromptTemplate:
    """
    Define and return a prompt template for question-answering tasks.

    Returns:
        PromptTemplate: The prompt template for question-answering tasks.
    """
    system_prompt = """
You are an assistant for question-answering tasks related to a knowledge domain based on a context provided to you.
Answer the question only based on the provided context.
If the context does not contain the information, just say that you don't know and don´t give any other response.
Give a response in technical English language and do not translate acronyms in the response.
Include the references at the end of the response, specifying only the name of the document and the page number(s) of the documents in the context used to build the response.
Do not include metadata information in the list of documents used to build the response.
Avoid duplicates in the list of documents used to build the response.

Example of output:

Response goes here

References:
- Document Name: name goes here - Page Number(s): pages go here
    """

    prompt = ChatPromptTemplate.from_messages([
        ("system", system_prompt),
        ("human", "{question}\n Context: {context}\n"),
    ])

    return prompt

Schließlich zeigt das letzte Snippet, wie man die gesamte RAG Chain in LCEL aufbaut, indem man den Retriever, den Prompt und das Sprachmodell integriert.

# Build retriever
big_chunks_retriever = rebuild_retriever(docstore_path, dbstore_path, embeddings, index_name)

# Define prompt template
prompt = get_prompt_template()

llm = ChatMistralAI(model="mistral-large-2407", mistral_api_key=my_api_key, temperature=0.0, num_ctx=NUM_CTX)
naive_chain = (
    {
        "context": big_chunks_retriever, "question": RunnablePassthrough()
    }
    | prompt
    | llm
    | StrOutputParser()
)

Hat Ihnen dieser Beitrag gefallen? Fanden Sie ihn hilfreich? Hinterlassen Sie gerne einen Kommentar unten, um Ihre Gedanken mitzuteilen oder Fragen zu stellen. Ein GitHub-Konto ist erforderlich, um an der Diskussion teilzunehmen.