Die Herausforderung
Kürzlich war ich intensiv in Tech-Diligence- und Strategieprojekte involviert, bei denen es darum geht, schnell auf die richtigen Informationen zuzugreifen. Die Herausforderung liegt darin, dass diese Informationen über mehrere Quellen verstreut sind, wie Pre-Due-Diligence-Berichte, Post-Buy-Due-Diligence-Dokumente und verschiedene detaillierte Berichte in unterschiedlichen Teilen des Systems.
Und es geht nicht nur um Technologie. Das Verständnis, welche Produkte Unternehmen verkaufen, ihre Produkt-Roadmaps und aktuelle Kundensegmente ist ebenso entscheidend. Dies führt zu einem riesigen Informationsberg, der überwältigend sein kann. Die Konsolidierung dieser Informationen in einem einzigen Dokument oder Wiki ist ein guter Ausgangspunkt, aber oft reicht eine einfache Suchfunktion nicht aus.
Hier kommen Large Language Models (LLMs) als fantastische Lösung ins Spiel. Anstatt mühsam nach Begriffen wie “Produkte” zu suchen, können Sie einfach fragen: “Welche Produkte verkauft Unternehmen A?”
Nach einigen guten Gesprächen mit AI/ML-Experte Robin habe ich eine Pipeline implementiert, die potenziell sensible Daten aufnimmt und Fragen umfassend beantworten kann. Dieser Beitrag basiert stark auf dem, was Hervé Ishimye von Timescale präsentiert hat (Schauen Sie es sich an!).
Dieser Beitrag ist Teil einer Serie von Beiträgen über LLMs und RAGs. Lesen Sie auch die anderen Artikel:
Table Of Contents
Ziele meines eigenen RAG-LLM-Experiments
- Mein eigenes LLM auf meinem eigenen Rechner betreiben, da wir potenziell mit sensiblen Daten arbeiten (Verteidigungsindustrie).
- Die Pipeline als Proof of Concept zum Laufen bringen. Kein Webservice, kein Fine-Tuning.
Der grundlegende Ablauf
Der grundlegende Ablauf ist einfach und besteht aus zwei Schritten:
- Vorbereitung unserer benutzerdefinierten Daten, damit wir sie mit einem LLM abfragen können
- Retrieval - also die Nutzung eines LLMs, um gute Antworten basierend auf unseren sensiblen Daten zu erhalten
Indexierung
Um passende Dokumente und Informationen effizient abzurufen, ist es wichtig, Ihre Daten zu indexieren. Diese Indexierung muss nur einmal durchgeführt werden, oder immer wenn sich Ihre Daten ändern. Wir verwenden ein Large Language Model (LLM), um Embeddings zu generieren.
Embeddings sind mathematische Repräsentationen in einem mehrdimensionalen Vektorraum, um relevante Informationen in Rohtexten zu finden.
Dieser Prozess ermöglicht ein schnelles Auffinden ähnlicher Dokumente, ähnlich wie eine traditionelle Suchmaschine, aber mit einem tieferen Verständnis des indexierten Inhalts. Die Vektoren werden als spezielle Datenbankspalten gespeichert, und obwohl spezialisierte Vektor- Datenbanken wie QDrant verfügbar sind, konzentrieren wir uns der Einfachheit halber auf die Verwendung von PostgreSQL mit pgai von Timescale.
Wir verwenden PostgreSQL + pgai und nomic-embed-text, um die Vektorrepräsentation zu generieren.
Retrieval-Ablauf
Der Retrieval-Ablauf besteht aus zwei Teilen:
- Relevante Inhalte aus den indexierten sensiblen Daten in der Vektordatenbank abrufen
- Die Abfrage (z.B. “Liste alle Produkte von Unternehmen B auf”) zusammen mit einem Kontext (relevante Inhalte, die wir in Schritt 1 abgerufen haben) an ein LLM übergeben.
Dies ermöglicht es dem LLM, allgemeines Wissen mit spezialisiertem Wissen aus dem Kontext zu kombinieren. So kann das LLM Fragen beantworten, die nicht im “allgemeinen Wissen” vorhanden sind.
Wir verwenden Ollama mit dem Modell llama3.2, um die Ergebnisse zu erhalten.
Die gesamte RAG-Magie (Retrieval Augmented Generation) besteht darin, relevante Daten im Kontext Ihrer Modellabfrage bereitzustellen.
Wie groß kann der Kontext sein, den wir zusammen mit der Abfrage bereitstellen?
Dies hängt vom verwendeten Modell ab. Für ein Modell, das 128k Tokens unterstützt (wie llama3.2):
Als Faustregel können wir verwenden:
- 128.000 Tokens / 300 Tokens pro Seite = ungefähr 427 Buchseiten
- 128.000 Tokens / 400 Tokens pro Seite = ungefähr 320 Buchseiten
Es ist also nicht unendlich, aber Sie können viele Informationen bereitstellen.
Schritt-für-Schritt-Anleitung für Ihre eigene private RAG-LLM-Pipeline
Voraussetzungen
Hinweis: Stellen Sie sicher, dass Docker installiert ist und Docker-Container ca. 5 GB RAM nutzen können. Andernfalls erhalten Sie die Meldung “model requires more system memory (3.5 GiB) than is available” Mehr: https://stackoverflow.com/questions/44533319/how-to-assign-more-memory-to-docker-container
OLLama und Metas Modell llama3.2 installieren
Ollama ermöglicht es, LLM-Modelle einfach lokal auszuführen. Es übernimmt die gesamte schwere Arbeit, bietet einfache Möglichkeiten, verschiedene Modelle auszuprobieren, und führt sie gekapselt und einsatzbereit in einem Webserver aus.
## Create a network so that all systems can talk to each other
docker network create rag-net
## Start Ollama - this manages and runs your models
docker run -d --network rag-net -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
## Download model llama 3.2 using ollama (around 2GB)
docker exec -it ollama ollama pull llama3.2
Das Embedding-Modell von Nomic installieren
## Download the Nomic model (can translate your content to vectors)
docker exec -it ollama ollama pull nomic-embed-text
## List models in Ollama
docker exec -it ollama ollama list
NAME ID SIZE MODIFIED
nomic-embed-text:latest 0a109f422b47 274 MB 38 seconds ago
llama3.2:latest a80c4f17acd5 2.0 GB 3 minutes ago
## Check containers that are running
docker ps -a
70dcc4e8535c ollama/ollama "/bin/ollama serve" 9 minutes ago Up 9 minutes 0.0.0.0:11434->11434/tcp ollama
Die Vektordatenbank installieren
## Install the vector database PgAI (postgres + timescale extension)
docker run -d --network rag-net -p 5432:5432 --name timescaledb -e POSTGRES_PASSWORD=password timescale/timescaledb-ha:pg16
## Run psql (to manage postgresql) inside the container
docker exec -it timescaledb psql -d postgres
## Install the pgai extension in the postgres database (happens to be the default database)
CREATE EXTENSION IF NOT EXISTS ai CASCADE;
NOTICE: installing required extension "vector"
NOTICE: installing required extension "plpython3u"
CREATE EXTENSION
## Then we can verify that the extension got installed
postgres=# \dx
List of installed extensions
Name | Version | Schema | Description
---------------------+---------+------------+---------------------------------------------------------------------------------------
ai | 0.3.0 | public | helper functions for ai workflows
plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language
plpython3u | 1.0 | pg_catalog | PL/Python3U untrusted procedural language
timescaledb | 2.17.0 | public | Enables scalable inserts and complex queries for time-series data (Community Edition)
timescaledb_toolkit | 1.18.0 | public | Library of analytical hyperfunctions, time-series pipelining, and other SQL utilities
vector | 0.7.4 | public | vector data type and ivfflat and hnsw access methods
(6 rows)
Jupyter Lab in einer virtuellen Umgebung installieren
Jupyter Lab ist unsere Python-IDE, um die Pipeline auszuführen. Ehrlich gesagt - ich war einige Jahre nicht mehr im Python-Ökosystem unterwegs. Es stellte sich heraus, dass es deutlich schwieriger war, Python sauber zu installieren und auszuführen als erwartet. Schließlich habe ich es mit virtuellen Umgebungen zum Laufen gebracht.
# Let's create e virtual environment to encapsulate all libraries from the global installation
python3 -m venv llm-pipeline
source llm-pipeline/bin/activate
# To connect with our database
pip install psycopg2
# To parse our hugo markdown files
pip install markdown python-frontmatter
# Our IDE
pip install jupyterlab
# This starts the IDE and you can access it in your browser
jupyter lab
Der Code
Kopieren Sie diesen Code einfach in Ihre Jupyter-IDE und führen Sie ihn aus. Wichtig: Dieser Code basiert sehr stark auf Hervé Ishimyes Präsentation hier. Ihm gebührt alle Anerkennung!
Unsere Markdown-Dateien parsen
… Ich verwende den Inhalt meines Blogs als Quelle für “sensible” Daten für das LLM.
import sys
import psycopg2
import os
import frontmatter
def parse_markdown_files(directory):
markdown_data = []
# Use os.walk to traverse the directory tree
for root, _, files in os.walk(directory):
for filename in files:
if filename.endswith('.md'):
# Construct the full file path
filepath = os.path.join(root, filename)
# Open and read the markdown file
with open(filepath, 'r', encoding='utf-8') as file:
# Parse front matter and content using frontmatter library
post = frontmatter.load(file)
# Extract title from front matter
title = post.get('title', 'No Title')
# Extract content (the markdown content itself)
content = post.content
# Append to markdown_data list as a dictionary
markdown_data.append({
"title": title,
"content": content
})
return markdown_data
directory_path = '/Users/I/workspace/raphaelbauer.com/content'
markdown_data = parse_markdown_files(directory_path)
Tabelle zur Datenspeicherung erstellen
def connect_db():
return psycopg2.connect( # use the credentials of your postgresql database
host = 'localhost',
database = 'postgres',
user = 'postgres',
password = 'password',
port = '5432'
)
conn = connect_db()
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
title TEXT,
content TEXT,
embedding VECTOR(768)
);
""")
conn.commit()
cur.close()
conn.close()
Daten in Vektoren übersetzen und speichern
conn = connect_db()
cur = conn.cursor()
# use the port at which your ollama service is running.
for doc in markdown_data:
cur.execute("""
INSERT INTO documents (title, content, embedding)
VALUES (
%(title)s,
%(content)s,
ollama_embed('nomic-embed-text', concat(%(title)s, ' - ', %(content)s), _host=>'http://ollama:11434')
)
""", doc)
conn.commit()
cur.close()
conn.close()
Überprüfen, dass das Retrieval funktioniert
conn = connect_db()
cur = conn.cursor()
cur.execute("""
SELECT title, content, vector_dims(embedding)
FROM documents LIMIT 10;
""")
rows = cur.fetchall()
for row in rows:
print(f"Title: {row[0]}, Content: {row[1]}, Embedding Dimensions: {row[2]}")
cur.close()
conn.close()
Abfrage definieren…
query = "Can you describe how modern QA should look like?"
Benutzerdefinierte Daten aus der Vektordatenbank basierend auf der Abfrage abrufen
conn = connect_db()
cur = conn.cursor()
# Embed the query using the ollama_embed function
cur.execute("""
SELECT ollama_embed('nomic-embed-text', %s, _host=>'http://ollama:11434');
""", (query,))
query_embedding = cur.fetchone()[0]
# Retrieve relevant documents based on cosine distance
cur.execute("""
SELECT title, content, 1 - (embedding <=> %s) AS similarity
FROM documents
ORDER BY similarity DESC
LIMIT 1;
""", (query_embedding,))
rows = cur.fetchall()
# Prepare the context for generating the response
context = "\n\n".join([f"Title: {row[0]}\nContent: {row[1]}" for row in rows])
print(context)
cur.close()
conn.close()
Die Abfrage + Kontext gegen Metas llama 3.2 ausführen
conn = connect_db()
cur = conn.cursor()
# Generate the response using the ollama_generate function
cur.execute("""
SELECT ollama_generate('llama3.2', %s, _host=>'http://ollama:11434');
""", (f"""
DOCUMENT:
{context}
QUESTION:
{query}
INSTRUCTIONS:
Answer the users QUESTION using the DOCUMENT text above.
Keep your answer ground in the facts of the DOCUMENT.
If the DOCUMENT doesn't contain the facts to answer the QUESTION then please say so.
""",))
model_response = cur.fetchone()[0]
print(model_response['response'])
cur.close()
conn.close()
Diskussion
Das Finden relevanter Dokumente basierend auf Ähnlichkeit funktioniert im Allgemeinen gut, aber die Feinabstimmung der Kontexteingabe ist noch notwendig. Das einfache Hinzufügen relevanter Dokumente führt manchmal zu unerwarteten Ergebnissen, oder der Kontext wird nicht effektiv genutzt, was teilweise unbefriedigend war.
Das Betreiben eines eigenen LLMs fühlt sich mächtig an, ist aber langsam. Das Speichern von etwa 2 MB Text- Informationen mit Nomic dauert beispielsweise Minuten auf meinem M1 Mac, und das Abrufen einer Antwort, obwohl unkompliziert, dauert mit mehr Kontext ebenfalls Minuten. Dieser Prozess könnte mit dedizierter Hardware oder durch den Betrieb in der Cloud schneller sein.
Die Rückkehr zum Python-Ökosystem nach zehn Jahren war eine seltsame Erfahrung. Python ist nach wie vor großartig und einfach zu verwenden, aber die vielen kleinen Details, wie die Installation von Bibliotheken, die Verwendung von pip, die Verwaltung virtueller Umgebungen und die Nutzung von Jupyter, fügen eine überraschende Komplexität hinzu. Dennoch macht die Möglichkeit, auf verschiedene Quellen zu verweisen, die Reise lohnenswert.
Nächste Schritte
- Dies zu einem Webservice machen, um meine Gedanken von meiner Homepage als LLM bereitzustellen. Ich stelle mir einen “Sprich mit Raphael Chatbot” vor. Das würde auch bedeuten, daraus eine richtige Anwendung zu machen und nicht nur einen Python-Hack.
- Feinabstimmung des Kontexts und der bereitgestellten Daten. Es ist noch nicht gut genug.
- Glaubwürdigkeit verbessern durch Hinzufügen von Referenzen und Reduzierung von Halluzinationen.
- Geschwindigkeit verbessern. Andere Modelle wie OpenAIs 4o ausprobieren, um es schneller zu machen. Der Nachteil ist natürlich, dass es dann nicht mehr auf meinen lokalen Rechner beschränkt ist.
Mehr
- Quellcode des Jupyter-Notebooks: https://gist.github.com/raphaelbauer/c9e6cc2c95d218cf5fe5e576ff5fa69e
- Über das Nomic-Embedding-Modell: https://www.nomic.ai/blog/posts/nomic-embed-text-v1
- https://help.openai.com/en/articles/8868588-retrieval-augmented-generation-rag-and-semantic-search-for-gpts
- Die großartige Präsentation, die die Grundlage für diesen Artikel bildet: https://www.youtube.com/watch?v=-ikCYKcPoqU
