Vom technischen Anspruch und der Architektur ist diese Lösung nicht sehr komplex und daher ideal für den Einstieg in das Thema der großen Sprachenmodelle geeignet. Auch stellt sich garantiert ein erstes Erfolgserlebnis sehr schnell ein. Alle verwendeten Komponenten lassen sich leicht installieren und miteinander kombinieren. Ich bin der nachfolgenden Beschreibung gefolgt und habe diese noch um ein paar Informationen ergänzt die mir wichtig waren.

Quelle: https://blog.duy-huynh.com/build-your-own-rag-and-run-them-locally/

Im Beitrag „Ollama Ubuntu Installation und Konfiguration“ haben wir ja bereits Ollama installiert. Jetzt möchte ich ganz kurz und knapp auf die Architektur der Anwendung eingehen die wir jetzt zusammen bauen werden. Das folgende Bild zeigt kurz den Aufbau. Wir haben einmal den LLM Server Ollama und die virtuelle Umgebung in der alle Komponenten installiert sind die unsere RAG-Anwendung benötigt damit wir mit einer PDF-Datei chatten können. Streamlit stellt die Benutzeroberfläche zur Verfügung. Auch haben wir den Anwender der eine PDF-Datei hochladen muss um mit dieser dann interagieren zu können.

RAG Chat-PDF app architecture

RAG Chat-PDF app architecture

Video Einführungskurs

Um sich mit den Grundprinzipien einer Retrieval-augmented generation (RAG) Applikation vertraut zu machen, empfehle ich, die nachfolgenden Videos einmal in Ruhe anzuschauen.

Mein Beitrag hier steigt direkt in die Entwicklung der Anwendung ein und geht nicht weiter darauf ein warum etwas wie gemacht wird. Erwähnen möchte ich noch einmal, dass bei dieser RAG-Anwendung die Daten nicht in das Internet übertragen werden sondern alles lokal auf ihrem Rechner läuft.

Software Installation

Ich setze ja immer eine virtuelle Anaconda Umgebung für meine Projekte auf. So mache ich das auch hier und wenn ihr Anaconda unter eurem Ubuntu noch nicht installiert habt könnt ihr hier nachlesen wie ihr Anaconda einrichtet.

URL: https://ai-box.eu/software/installation-von-anaconda-auf-ubuntu-lts-version/1170/

Mit dem folgenden Befehl wird eine Anaconda Umgebung mit dem Namen ollama_rag angelegt.

Befehl: conda create --name ollama_rag

Die frisch angelegte Umgebung müsst ihr noch aktivieren. Dazu bitte den folgenden befehl ausführen.

Befehl: conda activate ollama_rag

Jetzt installiert ihr bitte mit dem folgenden Befehl in die virtuelle Umgebung ollama_rag die Erweiterungen mit dem folgenden Befehl.

Befehl: pip install langchain langchain-community chromadb fastembed streamlit streamlit_chat

Fehlermeldungen:

Nach der Installation der Pakete wie oben beschrieben habe ich die folgenden Fehlermeldungen erhalten. Ich werde jetzt weiter machen um zu sehen wie relevant diese sind. Denn nur als Beispiel, dass openai>=0.26.4 nicht installiert ist sollte jetzt kein Problem sein für den weiteren Fortschritt des Projektes.

ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
llama-index 0.6.12 requires openai>=0.26.4, which is not installed.
auto-gptq 0.3.0+cu117 requires datasets, which is not installed.
transformers 4.26.1 requires tokenizers!=0.11.3,<0.14,>=0.11.1, but you have tokenizers 0.15.1 which is incompatible.
llama-index 0.6.12 requires typing-extensions==4.5.0, but you have typing-extensions 4.9.0 which is incompatible.
clip-interrogator 0.6.0 requires transformers>=4.27.1, but you have transformers 4.26.1 which is incompatible.
auto-gptq 0.3.0+cu117 requires transformers>=4.29.0, but you have transformers 4.26.1 which is incompatible.

Der Programmcode

Diese kleine Anwendung besteht aus zwei Python Dateien. Eine bildet die Logik ab für die Interaktion mit dem großen Sprachenmodell mistral über den Ollama Server. Die andere Python-Datei verkörpert das User-Interface und setzt auf dem Python Programm mit der Logik auf. Ich habe mir einen Ordner mit dem Namen rag angelegt und in diesen die beiden jetzt folgenden Python-Programme abgelegt.

Das original Programm findet ihr hier auf GitHub.

URL: https://gist.github.com/vndee/7776debe50b5e6c2b174add8646a4625

Den jetzt folgenden Quellcode kopiert ihr in eine Python-Datei mit dem Namen rag.py.


from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOllama
from langchain.embeddings import FastEmbedEmbeddings
from langchain.schema.output_parser import StrOutputParser
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import PromptTemplate
from langchain.vectorstores.utils import filter_complex_metadata
class ChatPDF:
    vector_store = None
    retriever = None
    chain = None
    def __init__(self):
        self.model = ChatOllama(model="mistral")
        self.text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=100)
        self.prompt = PromptTemplate.from_template(
            """
            <s> [INST] You are an assistant for question-answering tasks. Use the following pieces of retrieved context
            to answer the question. If you don't know the answer, just say that you don't know. Use three sentences
             maximum and keep the answer concise. [/INST] </s>
            [INST] Question: {question}
            Context: {context}
            Answer: [/INST]
            """
        )
    def ingest(self, pdf_file_path: str):
        docs = PyPDFLoader(file_path=pdf_file_path).load()
        chunks = self.text_splitter.split_documents(docs)
        chunks = filter_complex_metadata(chunks)
        vector_store = Chroma.from_documents(documents=chunks, embedding=FastEmbedEmbeddings())
        self.retriever = vector_store.as_retriever(
            search_type="similarity_score_threshold",
            search_kwargs={
                "k": 3,
                "score_threshold": 0.5,
            },
        )
        self.chain = ({"context": self.retriever, "question": RunnablePassthrough()}
                      | self.prompt
                      | self.model
                      | StrOutputParser())
    def ask(self, query: str):
        if not self.chain:
            return "Please, add a PDF document first."
        return self.chain.invoke(query)
    def clear(self):
        self.vector_store = None
        self.retriever = None
        self.chain = None

Jetzt benötigt ihr noch die zweite Datei die das User-Interface beinhaltet und die rag.py Datei einbindet. Auch diese habe ich mir aus dem folgenden Projekt auf GitHub kopiert.
Den jetzt folgenden Quellcode kopiert ihr in eine Python-Datei mit dem Namen rag-app.py.

import os
import tempfile
import streamlit as st
from streamlit_chat import message
from rag import ChatPDF
st.set_page_config(page_title="ChatPDF")
def display_messages():
    st.subheader("Chat")
    for i, (msg, is_user) in enumerate(st.session_state["messages"]):
        message(msg, is_user=is_user, key=str(i))
    st.session_state["thinking_spinner"] = st.empty()
def process_input():
    if st.session_state["user_input"] and len(st.session_state["user_input"].strip()) > 0:
        user_text = st.session_state["user_input"].strip()
        with st.session_state["thinking_spinner"], st.spinner(f"Thinking"):
            agent_text = st.session_state["assistant"].ask(user_text)
        st.session_state["messages"].append((user_text, True))
        st.session_state["messages"].append((agent_text, False))
def read_and_save_file():
    st.session_state["assistant"].clear()
    st.session_state["messages"] = []
    st.session_state["user_input"] = ""
    for file in st.session_state["file_uploader"]:
        with tempfile.NamedTemporaryFile(delete=False) as tf:
            tf.write(file.getbuffer())
            file_path = tf.name
        with st.session_state["ingestion_spinner"], st.spinner(f"Ingesting {file.name}"):
            st.session_state["assistant"].ingest(file_path)
        os.remove(file_path)
def page():
    if len(st.session_state) == 0:
        st.session_state["messages"] = []
        st.session_state["assistant"] = ChatPDF()
    st.header("ChatPDF")
    st.subheader("Upload a document")
    st.file_uploader(
        "Upload document",
        type=["pdf"],
        key="file_uploader",
        on_change=read_and_save_file,
        label_visibility="collapsed",
        accept_multiple_files=True,
    )
    st.session_state["ingestion_spinner"] = st.empty()
    display_messages()
    st.text_input("Message", key="user_input", on_change=process_input)
if __name__ == "__main__":
    page()

Wenn ihr beide Dateien gespeichert habt dann geht es jetzt hier im nachfolgenden Absatz weiter.

Programm RAG Chat-Applikation ausführen

Wechselt jetzt in der Konsole in den Ordner rag auf eurem Rechner und führ die Python-Datei rag-app.py mit dem folgenden Befehl aus.

Befehl: streamlit run rag-app.py

Wenn ihr jetzt im Browser die IP Adresse mit dem Port 8501 aufruft dann sollte sich die Web-Oberfläche der kleinen Anwendung aufbauen.

URL: <Eure IP-Adresse>:8501

Bei mir sieht die Web-Oberfläche jetzt wie folgt aus.

simple chat pdf app

simple chat pdf app

Jetzt müsst ihr eine PDF Datei hochladen.

Erste Tests bzw. Chat-Versuche

Für den ersten Versuch habe ich einen Reiseführer über NewYork herunter geladen. Diesen findet ihr hier.

URL: https://guides.tripomatic.com/download/tripomatic-free-city-guide-new-york-city.pdf

Jetzt ladet ihr die Datei in die kleine App und wartet kurz bis die Vektor DB aufgebaut ist. Anschließend habe ich die folgende Frage gestellt die aus der PDF Datei heraus beantwortet werden können müsste.

Frage: „I need your help as an travel guide for NewYork. I woul like to visit NewYork in March. Please tell what going on in NewYork in March.“

Die Antwort die zurück kam war richtig und findet sich auch exakt so wieder in der PDF-Datei.

RAG Chat app example

RAG Chat app example

Text-Embeddings erstellen – Hintergrundwissen

Jetzt möchte ich noch mit euch kurz auf einen sehr wichtigen Punkt bei dieser RAG Chat-Applikation eingehen. Die PDF-Datei wird ja in Text-Embeddings zerlegt und als Vektoren in der Chroma Vektor-DB gespeichert. Jetzt ist das Erstellen der Vektoren gar nicht so leicht da diese idealerweise Abschnitte des Textes enthalten sollten die geschlossen zusammenhängen. Zerpflückt man jetzt den Text recht ungünstig kann es sein, dass die RAG-Anwendung keine guten Ergebnisse liefert. Damit man sich das besser vorstellen kann gibt es von Greg Kamradt die folgende Mini-Anwendung mit der man sich das schneiden des Textes in Text-Embeddings bildlich ganz gut vorstellen kann.

URL: https://chunkviz.up.railway.app/

Im Programm rag.py wird der Text nach der Methode RecursiveCharacterTextSplitter  zerlegt und als Text-Embedding mit einer Länge von 1024 Zeichen mit einer Überschneidung von 100 Zeichen als Vektoren abgelegt.

self.text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=100)

Ich habe die Übersicht mit den Events in NewYork aus der PDF-Datei heraus kopiert und in ChunkViz eingefügt. ChunkViz erzeugt dann je nach Einstellung eine Übersicht wie die Chunks erstellt werden würden und diese sieht dann wie folgt gezeigt aus. Interessant zu sehen ist auch, dass wohl der Parameter chunk_overlap=100 bei der Methode RecursiveCharacterTextSplitter keine Anwendung findet.

ChunkViz version 0.1

ChunkViz version 0.1

Aber probiert hier einfach am besten selber einmal aus wie ihr euren Text am besten in die Vektor DB überführt.

Hierarchical Contextual Augmentation

Für alle die diesen Ausflug interessant fanden empfehle ich noch die folgende Arbeit „A Hierarchical Contextual Augmentation RAG for Massive Documents QA“ zu lesen.

Der Text diskutiert die Einschränkungen des traditionellen RAG (Retrieval-Augmented Generation)-Ansatzes bei der genauen Informationssuche in großen Dokumenten mit Texten, Tabellen und Bildern wie z. B. einem HomeDepot Produktkatalog oder eben dem Makita Werkzeugktalog. Um diese Herausforderungen zu bewältigen, stellt das Papier einen hierarchischen kontextuellen Augmentierungsansatz (HCA) vor und führt das MasQA-Datenset zur Bewertung von Multi-Document Question Answering (MDQA)-Systemen ein.

Der HCA-Ansatz besteht aus drei Hauptschritten:

  1. Markdown-Formatierer: Verwendet Language Model (LLM) zur Analyse von Dokumenten im Markdown-Format und behandelt jedes Kapitel als Überschrift der ersten Ebene mit einer numerischen Kennung. Es generiert auch Tabellen und extrahiert Bilder mithilfe von PDFImageSearcher.
  2. Hierarchischer kontextueller Augmentor (HCA): Verarbeitet strukturelle Metadaten, wandelt Segmente in Einbettungsvektoren um und bettet Bildunterschriften ein, die von Very Large Models (VLMs) generiert wurden. Dabei werden Datenfelder innerhalb von Tabellen beim Einbetten ausgelassen.
  3. Mehrwegesuche: Kombiniert Vektorsuche, Elastic Search und Schlüsselwortübereinstimmung, um die Präzision der Informationssuche zu verbessern.

Die Bewertung des Ansatzes führt das Log-Rank Index-Maß zur Bewertung der Ranglistenwirksamkeit ein. Das MasQA-Datenset umfasst eine Vielzahl von Materialien, darunter technische Handbücher und Finanzberichte, mit einer vielfältigen Auswahl an Fragetypen wie Einzel- und Mehrfachauswahl, beschreibende, Tabellen- und Berechnungsfragen.

HiQA Framework

HiQA Framework

Summarizing a text with LangChain and Ollama and StableLM 2

Ein weiteres interessantes Projekt das ich noch selber ausprobieren möchte ist dieses hier: ollamalangchainsummary.py

  • Setup:
    • My MacBookPro M3Max with 48 GB GPU.
    • Ollama as Language Model host.
    • Stability AI’s StableLM. 
    • LangChain as the underlying toolbox.

Video Kurs – Advanced QA over a lot of Tabular Data (combine text-to-SQL with RAG)

Zusammenfassung

Mir hat der Bau bzw. die kleine Entwicklung dieser RAG Chat-Applikation mit dem Ollama Server und den beiden Python Programmen sehr viel Freude gemacht. Im Vergleich zu August 2023 und jetzt Februar 2024 ist die Entwicklung schon wieder deutlich weiter und der Bau von solchen kleinen Anwendungen zusammen mit einem der großen Sprachenmodelle macht viel Freude. Auch ist es nicht mehr so schwierig alles zum Laufen zu bekommen. Es gibt sehr viele aktuelle Videos auf YouTube und Anleitungen auf GitHub & Co. So freue ich mich in was für einer tollen Zeit ich leben darf und werde hier noch einiges ausprobieren.