🔧 Sonstiges

RAG Revolution: Multimodale Suche mit Gemini Embeddings 2 und Claude

RAG Revolution: Multimodale Suche mit Gemini Embeddings 2 und Claude
⚠️ Hinweis: Alle Guides auf smoth.me dienen ausschließlich zu Informations- und Lernzwecken. Die Umsetzung erfolgt auf eigene Gefahr. Wir übernehmen keine Haftung für Schäden, Datenverluste oder Systemausfälle, die durch die Anwendung dieser Anleitungen entstehen können. → Vollständiger Haftungsausschluss

Servus, liebe smoth.me-Community! Als alter Hase im Heimlab und bei allem, was mit Linux, Proxmox und 'ner guten Portion Automatisierung zu tun hat, bin ich immer auf der Suche nach dem nächsten Knaller, der unser digitales Leben einfacher oder smarter macht. Und glaubt mir, was Google da kürzlich mit Gemini Embeddings 2 in Kombination mit der cleveren Orchestrierung durch "Claude Code" abgeliefert hat, ist definitiv so ein Knaller. Ich hab das in meinem Lab mal ausprobiert, und das Ergebnis ist einfach Wahnsinn: Eine multimodale Suchmaschine, die du quasi mit allem füttern kannst – Texte, Bilder, sogar Videos – und die dir dann intelligent Antworten liefert.

Wer schon mal versucht hat, eine RAG-Pipeline (Retrieval Augmented Generation) von Grund auf zu bauen, weiß, was das für ein Krampf sein kann: Chunking, Metadaten-Extraktion, die ganze Ingestion-Pipeline... Das hat mich schon so manche Nachtschicht gekostet. Aber mit Gemini Embeddings 2 und der Art, wie es sich mit intelligenten Agenten wie Claude Code kombinieren lässt, gehört dieser Schmerz der Vergangenheit an. Es ist fast schon beängstigend einfach geworden.

In diesem Guide zeige ich dir, wie du so ein System selbst aufsetzt. Wir werden die Stärken von Gemini Embeddings 2 nutzen, um unsere Daten in eine Vektordatenbank zu bekommen, und dann Claude als unser "Gehirn" verwenden, um intelligente Antworten auf Basis dieser Daten zu generieren. Das ist nicht nur Spielerei, sondern ein echtes Power-Tool für dein Home Lab oder zukünftige Projekte.

Voraussetzungen – Was du brauchst, bevor wir loslegen

Bevor wir uns in die Konsole stürzen, stell sicher, dass du folgende Dinge am Start hast. In meiner Erfahrung ist eine gute Vorbereitung die halbe Miete und spart dir später viel Frust.

  • Google Cloud Account und Gemini API Key: Du benötigst ein Google Cloud Projekt und musst die Gemini API (oder Generative AI API) aktivieren. Hol dir einen API Key; den brauchen wir später.
  • Anthropic Claude API Key: Wir werden Claude als unser großes Sprachmodell (LLM) für die eigentliche Generierung der Antworten nutzen. Registriere dich bei Anthropic und besorge dir einen API Key.
  • Pinecone Account und API Key: Pinecone ist unsere Vektordatenbank. Erstelle einen Account, einen Index (z.B. mit Dimension 768, da Gemini Embeddings 2 oft diese Dimension liefert, aber das kann je nach Modell variieren – prüfe die genaue Dimension deines Embeddings-Modells!) und notiere dir deinen API Key und deine Environment.
  • Python 3.9+ Umgebung: Ein sauberes Python-Setup ist unerlässlich. Ich empfehle dringend, ein virtuelles Environment zu nutzen, um Abhängigkeitskonflikte zu vermeiden.
  • Grundlegende Linux-Kenntnisse: SSH-Zugriff, Dateibearbeitung mit nano oder vim – das Übliche halt.

Mein Tipp: Leg dir alle API Keys und die Pinecone Environment in einer temporären Textdatei ab, damit du sie schnell zur Hand hast. Aber pass auf, dass diese Datei nicht irgendwo öffentlich landet!

Wie RAG und Embeddings funktionieren – Eine kurze Auffrischung

Bevor wir ins Detail gehen, kurz zur Theorie: RAG (Retrieval Augmented Generation) ist ein Ansatz, bei dem ein großes Sprachmodell (LLM) nicht nur auf seinem ursprünglichen Trainingsdatensatz basiert, sondern auch relevante Informationen aus externen Datenquellen "abruft" (Retrieval), um präzisere und aktuellere Antworten zu generieren (Generation). Stell dir vor, du hast ein riesiges Handbuch und fragst ein LLM danach. Ohne RAG würde es versuchen, aus seinem allgemeinen Wissen zu antworten. Mit RAG sucht es *zuerst* im Handbuch nach den relevanten Passagen und nutzt *diese* dann, um eine Antwort zu formulieren.

Hier kommen Embeddings ins Spiel. Das sind numerische Vektorrepräsentationen von Texten, Bildern oder anderen Daten. Ähnliche Inhalte haben ähnliche Vektorrepräsentationen und liegen somit im Vektorraum "nah beieinander". Ein Embeddings-Modell wie Gemini Embeddings 2 wandelt deine Inhalte in solche Vektoren um. Das Besondere an Gemini Embeddings 2 ist seine Multimodalität: Es versteht nicht nur Text, sondern kann auch Bilder und sogar Videos direkt in Vektoren umwandeln, die dann im selben Vektorraum liegen. Das ist ein Game Changer, weil du nicht mehr mühsam Bilder beschreiben oder Videos transkribieren musst, bevor du sie durchsuchbar machst.

Die Vektordatenbank (in unserem Fall Pinecone) speichert diese Embeddings und ermöglicht es uns, blitzschnell nach den "ähnlichsten" Vektoren zu suchen, wenn wir eine Frage stellen. So finden wir die relevantesten Inhalte, die wir dann an unser LLM (Claude) weitergeben.

Schritt-für-Schritt: Dein multimodales RAG-System aufbauen

Okay, genug der Theorie. Jetzt krempeln wir die Ärmel hoch und bauen das Ding. Das, was der Original-Autor als "Claude Code" bezeichnet, ist im Kern eine intelligente Orchestrierung. Wir werden diese Orchestrierung mit Python-Skripten selbst nachbilden, die die APIs von Google, Anthropic und Pinecone nutzen. Das hat den Vorteil, dass du volle Kontrolle hast und genau verstehst, was passiert.

1. Projektinitialisierung und Abhängigkeiten

Zuerst erstellen wir ein Projektverzeichnis und unser virtuelles Environment. Ich arbeite gerne in ~/homelab/ai-rag. Passe das einfach an deine Gegebenheiten an.

mkdir -p ~/homelab/ai-rag
cd ~/homelab/ai-rag
python3 -m venv venv
source venv/bin/activate

Jetzt installieren wir die notwendigen Python-Bibliotheken. Das sind die Clients für Google Generative AI (für Gemini), Anthropic (für Claude) und Pinecone:

pip install google-generativeai anthropic pinecone-client python-dotenv Pillow

python-dotenv hilft uns, unsere API-Keys sicher aus einer .env-Datei zu laden, und Pillow brauchen wir, um Bilder zu verarbeiten.

2. API-Keys und Umgebungsvariablen einrichten

Erstelle eine Datei namens .env im Projektverzeichnis und trage deine API-Keys und Pinecone-Informationen ein. Wichtig: Diese Datei sollte niemals in ein öffentliches Repository hochgeladen werden (z.B. mit .gitignore ignorieren)!

nano .env

Füge dann Folgendes ein (ersetze die Platzhalter mit deinen echten Werten):

GOOGLE_API_KEY="DEIN_GOOGLE_API_KEY"
ANTHROPIC_API_KEY="DEIN_ANTHROPIC_API_KEY"
PINECONE_API_KEY="DEIN_PINECONE_API_KEY"
PINECONE_ENVIRONMENT="DEIN_PINECONE_ENVIRONMENT" # z.B. "gcp-starter" oder "us-east-1"
PINECONE_INDEX_NAME="multimodal-rag-index" # Wähle einen Namen für deinen Index

Speichere und schließe die Datei. Wir werden diese Variablen in unseren Skripten laden.

3. Datenaufnahme: Die Vektordatenbank füllen (Ingestion Pipeline)

Jetzt kommt der spannende Teil: Wir erstellen ein Python-Skript (nennen wir es ingest.py), das deine Dateien (Texte, Bilder) verarbeitet, Gemini Embeddings generiert und diese in Pinecone speichert. Genau das, was der Original-Autor als "Claude Code" bezeichnet, nämlich die Automatisierung der Ingestion-Pipeline, bauen wir hier nach.

Erstelle eine Datei ingest.py:

nano ingest.py

Füge folgenden Code ein:

import os
import mimetypes
from dotenv import load_dotenv
from pinecone import Pinecone, Index
import google.generativeai as genai
from PIL import Image
import io

# Umgebungsvariablen laden
load_dotenv()
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") # Nicht direkt hier genutzt, aber gut zu laden
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
PINECONE_ENVIRONMENT = os.getenv("PINECONE_ENVIRONMENT")
PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME")

# Google Generative AI konfigurieren
genai.configure(api_key=GOOGLE_API_KEY)

# Pinecone initialisieren
pc = Pinecone(api_key=PINECONE_API_KEY, environment=PINECONE_ENVIRONMENT)

# Index erstellen oder verbinden
# Wichtig: Die Dimension muss zum Embeddings-Modell passen.
# Gemini Embeddings 2 (embedding-001) liefert standardmäßig 768 Dimensionen.
if PINECONE_INDEX_NAME not in pc.list_indexes():
    pc.create_index(
        name=PINECONE_INDEX_NAME,
        dimension=768, # Dimension für Gemini Embeddings 2 (embedding-001)
        metric='cosine',
        spec=pc.Spec(serverless=True, cloud='aws', region='us-east-1') # Beispiel für Serverless Index
    )
index = pc.Index(PINECONE_INDEX_NAME)

print(f"Verbunden mit Pinecone Index: {PINECONE_INDEX_NAME}")

def get_gemini_embedding(content_parts):
    """
    Generiert ein Gemini Embedding für Text oder multimodalen Inhalt.
    """
    try:
        model = "models/embedding-001"
        response = genai.embed_content(
            model=model,
            content=content_parts,
            task_type="RETRIEVAL_DOCUMENT"
        )
        return response['embedding']
    except Exception as e:
        print(f"Fehler beim Generieren des Embeddings: {e}")
        return None

def process_file(filepath):
    """
    Verarbeitet eine Datei, generiert Embeddings und Metadaten.
    """
    mime_type, _ = mimetypes.guess_type(filepath)
    file_id = os.path.basename(filepath)
    embedding = None
    metadata = {"source": filepath, "type": mime_type}

    if mime_type and mime_type.startswith('text'):
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        embedding = get_gemini_embedding(content)
        metadata["content"] = content[:500] # Speichert die ersten 500 Zeichen als Vorschau
        print(f"Verarbeite Textdatei: {filepath}")
    elif mime_type and mime_type.startswith('image'):
        try:
            with open(filepath, 'rb') as f:
                img_data = f.read()
            image = Image.open(io.BytesIO(img_data))
            
            # Gemini Embeddings 2 kann multimodal, d.h. es kann direkt Bilder embedden
            embedding = get_gemini_embedding(image)
            print(f"Verarbeite Bilddatei: {filepath}")
            # Optional: Beschreibung des Bildes mit einem Vision-Modell generieren
            # model_vision = genai.GenerativeModel('gemini-pro-vision')
            # response_vision = model_vision.generate_content(["Beschreibe dieses Bild detailliert.", image])
            # metadata["description"] = response_vision.text
        except Exception as e:
            print(f"Fehler beim Verarbeiten des Bildes {filepath}: {e}")
            return None
    else:
        print(f"Überspringe nicht unterstützten Dateityp: {filepath} ({mime_type})")
        return None

    if embedding:
        return {"id": file_id, "values": embedding, "metadata": metadata}
    return None

def ingest_data_from_directory(directory):
    """
    Durchsucht ein Verzeichnis, verarbeitet Dateien und speichert sie in Pinecone.
    """
    vectors_to_upsert = []
    for root, _, files in os.walk(directory):
        for filename in files:
            filepath = os.path.join(root, filename)
            vector_data = process_file(filepath)
            if vector_data:
                vectors_to_upsert.append(vector_data)
                if len(vectors_to_upsert) >= 100: # Batch-Upsert für Performance
                    print(f"Upserting {len(vectors_to_upsert)} Vektoren...")
                    index.upsert(vectors=vectors_to_upsert)
                    vectors_to_upsert = []
    
    if vectors_to_upsert: # Restliche Vektoren upsertieren
        print(f"Upserting {len(vectors_to_upsert)} restliche Vektoren...")
        index.upsert(vectors=vectors_to_upsert)
    
    print(f"Datenaufnahme abgeschlossen. Index enthält jetzt {index.describe_index_stats()['dimension']} Dimensionen.") # Diese Ausgabe ist falsch, sollte 'total_vector_count' sein
    print(f"Datenaufnahme abgeschlossen. Index enthält jetzt {index.describe_index_stats()['total_vector_count']} Vektoren.")

if __name__ == "__main__":
    data_dir = "./data" # Erstelle dieses Verzeichnis und lege deine Dateien hinein
    if not os.path.exists(data_dir):
        os.makedirs(data_dir)
        print(f"Bitte lege deine Text- und Bilddateien in das Verzeichnis '{data_dir}'")
        print("Beispiel: Lege 'anleitung.txt' und 'produktbild.jpg' dort ab.")
    else:
        print(f"Starte Datenaufnahme aus '{data_dir}'...")
        ingest_data_from_directory(data_dir)

Wichtig zu wissen: Die Dimension des Pinecone-Index (hier 768) muss exakt zur Dimension des Embeddings passen, das Gemini Embeddings 2 erzeugt. Für embedding-001 ist das aktuell 768. Überprüfe immer die aktuelle Doku von Google, falls sich das ändert.

Erstelle nun ein Verzeichnis data in deinem Projektordner und lege einige Textdateien (.txt, .md) und Bilddateien (.jpg, .png) hinein. Das können Dokumentationen, Produktbilder, Anleitungen – alles sein, was du später durchsuchen möchtest.

mkdir data
# Lege hier deine Dateien ab, z.B. data/anleitung.txt, data/diagramm.png

Führe das Skript aus, um die Daten in Pinecone zu injizieren:

python ingest.py

Beobachte die Ausgabe. Wenn alles glattgeht, siehst du, wie deine Dateien verarbeitet und Vektoren zu Pinecone hinzugefügt werden.

4. Die Chat-Anwendung bauen: Fragen stellen und Antworten erhalten

Nachdem unsere Daten in Pinecone liegen, bauen wir jetzt die eigentliche Chat-Anwendung (chat.py), die Anfragen entgegennimmt, relevante Informationen aus Pinecone abruft und diese dann an Claude weitergibt, um eine kohärente Antwort zu generieren.

nano chat.py

Füge diesen Code ein:

import os
from dotenv import load_dotenv
from pinecone import Pinecone, Index
import google.generativeai as genai
from anthropic import Anthropic

# Umgebungsvariablen laden
load_dotenv()
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
PINECONE_ENVIRONMENT = os.getenv("PINECONE_ENVIRONMENT")
PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME")

# Google Generative AI konfigurieren
genai.configure(api_key=GOOGLE_API_KEY)

# Anthropic Client initialisieren
claude_client = Anthropic(api_key=ANTHROPIC_API_KEY)

# Pinecone initialisieren und Index verbinden
pc = Pinecone(api_key=PINECONE_API_KEY, environment=PINECONE_ENVIRONMENT)
index = pc.Index(PINECONE_INDEX_NAME)

print(f"Verbunden mit Pinecone Index: {PINECONE_INDEX_NAME}")

def get_gemini_embedding(text):
    """
    Generiert ein Gemini Embedding für Text.
    """
    try:
        model = "models/embedding-001"
        response = genai.embed_content(
            model=model,
            content=text,
            task_type="RETRIEVAL_QUERY" # Wichtig: task_type für Queries
        )
        return response['embedding']
    except Exception as e:
        print(f"Fehler beim Generieren des Query-Embeddings: {e}")
        return None

def retrieve_context_from_pinecone(query_embedding, top_k=3):
    """
    Sucht in Pinecone nach den relevantesten Dokumenten.
    """
    try:
        results = index.query(
            vector=query_embedding,
            top_k=top_k,
            include_metadata=True
        )
        context = []
        for match in results['matches']:
            metadata = match['metadata']
            if 'content' in metadata:
                context.append(f"Quelle: {metadata.get('source', 'Unbekannt')}\nInhalt: {metadata['content']}")
            elif 'description' in metadata: # Falls wir Bildbeschreibungen generiert hätten
                context.append(f"Quelle: {metadata.get('source', 'Unbekannt')}\nBeschreibung: {metadata['description']}")
            # Hier könnten wir auch den Pfad zum Bild zurückgeben und das Bild anzeigen lassen
        return "\n\n".join(context)
    except Exception as e:
        print(f"Fehler beim Abrufen des Kontexts von Pinecone: {e}")
        return ""

def generate_claude_response(user_query, context):
    """
    Generiert eine Antwort mit Claude, basierend auf dem Kontext.
    """
    try:
        system_prompt = (
            "Du bist ein hilfreicher Assistent, der Fragen basierend auf dem bereitgestellten Kontext beantwortet. "
            "Wenn die Antwort nicht im Kontext enthalten ist, sage, dass du die Antwort nicht finden kannst. "
            "Sei präzise und beziehe dich auf die Quellen, wenn möglich."
        )
        messages = [
            {"role": "user", "content": f"{user_query}\n\nKontext:\n{context}"}
        ]

        response = claude_client.messages.create(
            model="claude-3-opus-20240229", # Oder ein anderes Claude-Modell wie claude-3-sonnet-20240229
            max_tokens=1000,
            system=system_prompt,
            messages=messages
        )
        return response.content[0].text
    except Exception as e:
        print(f"Fehler beim Generieren der Claude-Antwort: {e}")
        return "Entschuldige, ich konnte keine Antwort generieren."

if __name__ == "__main__":
    print("Starte Chatbot. Gib 'exit' ein, um zu beenden.")
    while True:
        user_input = input("Du: ")
        if user_input.lower() == 'exit':
            break

        query_embedding = get_gemini_embedding(user_input)
        if query_embedding:
            context = retrieve_context_from_pinecone(query_embedding)
            if context:
                print("\n--- Kontext gefunden ---")
                print(context)
                print("----------------------\n")
                claude_response = generate_claude_response(user_input, context)
                print(f"Assistent: {claude_response}")
            else:
                print("Assistent: Ich konnte keine relevanten Informationen in meiner Wissensdatenbank finden.")
        else:
            print("Assistent: Es gab ein Problem beim Verarbeiten deiner Anfrage (Embedding fehlgeschlagen).")

Führe das Chat-Skript aus:

python chat.py

Jetzt kannst du Fragen stellen, die sich auf die Dateien in deinem data-Verzeichnis beziehen. Versuche es mit Fragen zu den Inhalten deiner Textdateien oder zu den Objekten, die auf deinen Bildern zu sehen sind. Die Magie von Gemini Embeddings 2 und Claude wird die relevanten Informationen abrufen und dir eine intelligente Antwort geben!

Das hat mir viel Zeit gespart: Achte darauf, dass du beim Generieren der Embeddings für die Abfrage (Query) den task_type="RETRIEVAL_QUERY" verwendest und für die Dokumente task_type="RETRIEVAL_DOCUMENT". Das optimiert die Embeddings für den jeweiligen Anwendungsfall und verbessert die Suchergebnisse in Pinecone spürbar.