Blog

RAG (Retrieval Augmented Generation) mit LlamaIndex, Elasticsearch und Mistral

Lernen Sie, wie Sie ein RAG-System (Retrieval Augmented Generation) mit LlamaIndex, Elasticsearch und lokal ausgeführtem Mistral implementieren.

In diesem Blogbeitrag werden wir erläutern, wie man ein Q&A-Erlebnis mithilfe einer RAG-Technik (Retrieval Augmented Generation) und Elasticsearch als Vektordatenbank implementiert. Wir werden LlamaIndex und ein lokal laufendes Mistral LLM verwenden.

Bevor wir anfangen, werden wir uns einige Fachbegriffe ansehen.

Terminologie

LlamaIndex ist ein führendes Datenframework für die Entwicklung von LLM-Anwendungen (Large Language Model). LlamaIndex bietet Abstraktionen für verschiedene Phasen der Entwicklung einer RAG-Anwendung (Retrieval Augmented Generation). Frameworks wie LlamaIndex und LangChain bieten Abstraktionen, damit Anwendungen nicht eng an die APIs eines bestimmten LLM gekoppelt werden.

Elasticsearch wird von Elastic angeboten. Elastic ist ein Branchenführer hinter Elasticsearch, einer Such- und Analyse-Engine, die Volltextsuche für Präzision, Vektorsuche für semantisches Verständnis und Hybridsuche für das Beste aus beiden Welten unterstützt. Elasticsearch ist ein skalierbarer Datenspeicher und eine Vektordatenbank. Die in diesem Blog verwendeten Elasticsearch-Funktionen sind in der kostenlosen Open-Source-Version von Elasticsearch verfügbar.

Retrieval Augment Generation (RAG) ist eine KI-Technik bzw. ein KI-Muster, bei dem LLMs mit externem Wissen ausgestattet werden, um Antworten auf Benutzeranfragen zu generieren. Dadurch können die LLM-Antworten auf den jeweiligen Kontext zugeschnitten werden und sind spezifischer.

Mistral bietet sowohl Open-Source- als auch optimierte LLM-Modelle für Unternehmen an. In diesem Tutorial verwenden wir deren Open-Source-Modell Mistral-7b , das auf Ihrem Laptop läuft. Falls Sie das Modell nicht auf Ihrem Laptop ausführen möchten, können Sie alternativ die Cloud-Version nutzen. In diesem Fall müssen Sie den Code in diesem Blog anpassen, um die richtigen API-Schlüssel und Pakete zu verwenden.

Ollama hilft dabei, LLMs lokal auf Ihrem Laptop auszuführen. Wir werden Ollama verwenden, um das Open-Source-Modell Mistral-7b lokal auszuführen.

Einbettungen sind numerische Repräsentationen der Bedeutung von Texten/Medien. Es handelt sich um niedrigdimensionale Darstellungen hochdimensionaler Informationen.

Erstellung einer RAG-Anwendung mit LlamaIndex, Elasticsearch und Mistral: Szenarioübersicht

Szenario:

Wir verfügen über einen Beispieldatensatz (als JSON-Datei) mit Callcenter-Gesprächen zwischen Agenten und Kunden eines fiktiven Hausratversicherungsunternehmens. Wir werden eine einfache RAG-Anwendung entwickeln, die Fragen wie diese beantworten kann:

Give me summary of water related issues.

Hoher Durchfluss

RAG Flow

Wir betreiben Mistral LLM lokal mit Ollama.

Als nächstes laden wir Konversationen aus der JSON-Datei als Documents in den ElasticsearchStore (einen VectorStore, der von Elasticsearch unterstützt wird). Beim Laden der Dokumente erstellen wir Einbettungen mithilfe des lokal laufenden Mistral-Modells. Wir speichern diese Einbettungen zusammen mit den Konversationen im LlamaIndex Elasticsearch Vector Store (ElasticsearchStore).

Wir konfigurieren eine LlamaIndex IngestionPipeline und stellen ihr den lokal verwendeten LLM zur Verfügung, in diesem Fall Mistral, das über Ollama läuft.

Wenn wir eine Frage stellen wie „Geben Sie mir eine Zusammenfassung der wichtigsten wasserbezogenen Probleme.“ Elasticsearch führt eine semantische Suche durch und liefert Konversationen zum Thema Wasser. Diese Konversationen werden zusammen mit der ursprünglichen Frage an das lokal laufende LLM gesendet, um eine Antwort zu generieren.

Schritte zum Erstellen der RAG-Anwendung

Mistral lokal ausführen

Laden Sie Ollama herunter und installieren Sie es. Nach der Installation von Ollama führen Sie diesen Befehl aus, um Mistralherunterzuladen und auszuführen.

ollama run mistral

Das Herunterladen und erstmalige lokale Ausführen des Modells kann einige Minuten dauern. Prüfen Sie, ob Mistral weht, indem Sie eine Frage wie die folgende stellen: „Schreibe ein Gedicht über Wolken“ und prüfen Sie, ob Ihnen das Gedicht gefällt. Bitte lassen Sie ollama laufen, da wir später über Code mit dem Mistral-Modell interagieren müssen.

Elasticsearch installieren

Elasticsearch lässt sich entweder durch Erstellen einer Cloud-Bereitstellung (Anleitung hier) oder durch Ausführen in Docker (Anleitung hier) in Betrieb nehmen. Sie können auch eine produktionsreife, selbstgehostete Elasticsearch-Bereitstellung erstellen, indem Sie hier beginnen.

Sofern Sie die Cloud-Bereitstellung nutzen, besorgen Sie sich den API-Schlüssel und die Cloud-ID für die Bereitstellung, wie in der Anleitung beschrieben. Wir werden sie später verwenden.

RAG-Anwendung

Der vollständige Code ist als Referenz in diesem GitHub-Repository zu finden. Das Klonen des Repos ist optional, da wir den Code im Folgenden durchgehen werden.

Erstellen Sie in Ihrer bevorzugten IDE eine neue Python-Anwendung mit den folgenden 3 Dateien.

  • index.py Hierhin wird der Code für die Indizierung von Daten eingefügt.

  • query.py Hier wird der Code für Abfragen und die Interaktion mit LLM eingefügt.

  • .env dort, wo Konfigurationseigenschaften wie API-Schlüssel gespeichert werden.

Wir müssen einige Pakete installieren. Wir beginnen damit, eine neue virtuelle Python-Umgebung im Stammverzeichnis Ihrer Anwendung zu erstellen.

python3 -m venv .venv

Aktivieren Sie die virtuelle Umgebung und installieren Sie die unten aufgeführten erforderlichen Pakete.

source .venv/bin/activate
pip install llama-index 
pip install llama-index-embeddings-ollama
pip install llama-index-llms-ollama
pip install llama-index-vector-stores-elasticsearch
pip install sentence-transformers
pip install python-dotenv

Indexierungsdaten

Laden Sie die Datei conversations.json herunter, die Konversationen zwischen Kunden und Callcenter-Mitarbeitern unseres fiktiven Hausversicherungsunternehmens enthält. Platzieren Sie die Datei im Stammverzeichnis der Anwendung zusammen mit den beiden Python-Dateien und der .env-Datei. die Datei, die Sie zuvor erstellt haben. Nachfolgend ein Beispiel für den Inhalt der Datei.

{
    "conversation_id": 103,
    "customer_name": "Sophia Jones",
    "agent_name": "Emily Wilson",
    "policy_number": "JKL0123",
    "conversation": "Customer: Hi, I'm Sophia Jones. My Date of Birth is November 15th, 1985, Address is 303 Cedar St, Miami, FL 33101, and my Policy Number is JKL0123.\nAgent: Hello, Sophia. How may I assist you today?\nCustomer: Hello, Emily. I have a question about my policy.\nCustomer: There's been a break-in at my home, and some valuable items are missing. Are they covered?\nAgent: Let me check your policy for coverage related to theft.\nAgent: Yes, theft of personal belongings is covered under your policy.\nCustomer: That's a relief. I'll need to file a claim for the stolen items.\nAgent: We'll assist you with the claim process, Sophia. Is there anything else I can help you with?\nCustomer: No, that's all for now. Thank you for your assistance, Emily.\nAgent: You're welcome, Sophia. Please feel free to reach out if you have any further questions or concerns.\nCustomer: I will. Have a great day!\nAgent: You too, Sophia. Take care.",
    "summary": "A customer inquires about coverage for stolen items after a break-in at home, and the agent confirms that theft of personal belongings is covered under the policy. The agent offers assistance with the claim process, resulting in the customer expressing relief and gratitude."
}

Wir definieren eine Funktion namens get_documents_from_file in index.py , die die JSON-Datei liest und eine Liste von Dokumenten erstellt. Dokumentobjekte sind die grundlegende Informationseinheit, mit der LlamaIndex arbeitet.

# index.py
import json, os
from llama_index.core import Document, Settings
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.ingestion import IngestionPipeline
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.vector_stores.elasticsearch import ElasticsearchStore
from dotenv import load_dotenv

def get_documents_from_file(file):
   """Reads a json file and returns list of Documents"""

   with open(file=file, mode='rt') as f:
       conversations_dict = json.loads(f.read())
      
   # Build Document objects using fields of interest.
   documents = [Document(text=item['conversation'],
                         metadata={"conversation_id": item['conversation_id']})
                for
                item in conversations_dict]
   return documents

IngestionPipeline erstellen

Fügen Sie zunächst die Elasticsearch CloudID und die API-Schlüssel, die Sie im Abschnitt Install Elasticsearch erhalten haben, in die Datei .env ein. Ihre .env -Datei sollte wie folgt aussehen (mit realen Werten).

ELASTIC_CLOUD_ID=<REPLACE WITH YOUR CLOUD ID>
ELASTIC_API_KEY=<REPLACE WITH YOUR API_KEY>

Mit LlamaIndex IngestionPipeline können Sie eine Pipeline aus mehreren Komponenten zusammensetzen. Fügen Sie den unten stehenden Code in die Datei index.py ein.

# index.py

# Load .env file contents into env
# ELASTIC_CLOUD_ID and ELASTIC_API_KEY are expected to be in the .env file.
load_dotenv('.env')

# ElasticsearchStore is a VectorStore that
# takes care of ES Index and Data management.
es_vector_store = ElasticsearchStore(index_name="calls",
                                     vector_field='conversation_vector',
                                     text_field='conversation',
                                     es_cloud_id=os.getenv("ELASTIC_CLOUD_ID"),
                                     es_api_key=os.getenv("ELASTIC_API_KEY"))


def main():
    # Embedding Model to do local embedding using Ollama.
    ollama_embedding = OllamaEmbedding("mistral")

    # LlamaIndex Pipeline configured to take care of chunking, embedding
    # and storing the embeddings in the vector store.
    pipeline = IngestionPipeline(
        transformations=[
            SentenceSplitter(chunk_size=350, chunk_overlap=50),
            ollama_embedding,
        ],
        vector_store=es_vector_store
    )

    # Load data from a json file into a list of LlamaIndex Documents
    documents = get_documents_from_file(file="conversations.json")

    pipeline.run(documents=documents)
    print(".....Done running pipeline.....\n")


if __name__ == "__main__":
    main()

Wie bereits erwähnt, kann die LlamaIndex IngestPipeline aus mehreren Komponenten zusammengesetzt sein. Wir fügen der Pipeline in Zeile pipeline = IngestionPipeline(... 3 Komponenten hinzu.

  • SentenceSplitter: Wie aus der Definition von get_documents_from_file() hervorgeht, verfügt jedes Dokument über ein Textfeld, das die in der JSON-Datei gefundene Konversation enthält. Dieses Textfeld enthält einen langen Text. Damit die semantische Suche gut funktioniert, muss der Text in kleinere Abschnitte unterteilt werden. Die Klasse SentenceSplitter erledigt das für uns. Diese Abschnitte werden in der LlamaIndex-Terminologie als Knoten bezeichnet. Die Knoten enthalten Metadaten, die auf das Dokument verweisen, zu dem sie gehören. Alternativ können Sie auch Elasticsearch Ingestpipeline für das Chunking verwenden, wie in diesem Blog gezeigt.

  • OllamaEmbedding: Embedding-Modelle wandeln einen Textabschnitt in Zahlen (auch Vektoren genannt) um. Die numerische Darstellung ermöglicht uns die semantische Suche, bei der die Suchergebnisse der Bedeutung des Wortes entsprechen, anstatt nur eine Textsuche durchzuführen. Wir stellen der IngestionPipeline OllamaEmbedding("mistral") zur Verfügung. Die mit SentenceSplitter aufgeteilten Chunks werden über Ollama an das auf Ihrem lokalen Rechner laufende Mistral-Modell gesendet. Mistral erstellt dann Einbettungen für die Chunks.

  • ElasticsearchStore: Der LlamaIndex ElasticsearchStore-Vektorspeicher sichert die in einen Elasticsearch-Index erstellten Einbettungen. ElasticsearchStore kümmert sich um das Erstellen und Befüllen des angegebenen Elasticsearch-Index mit Inhalten. Beim Erstellen des ElasticsearchStore (referenziert durch es_vector_store) geben wir den Namen des Elasticsearch-Index an, den wir erstellen möchten (calls in unserem Fall ), das Feld im Index, in dem die Einbettungen gespeichert werden sollen (conversation_vector in unserem Fall ) und das Feld, in dem der Text gespeichert werden soll (conversation in unserem Fall ). Zusammenfassend lässt sich sagen, dass auf Basis unserer Konfiguration ElasticsearchStore ein neuer Index in Elasticsearch mit conversation_vector und conversation als Feldern (neben anderen automatisch erstellten Feldern) erstellt wird.

Um das Ganze zusammenzuführen, starten wir die Pipeline durch den Aufruf von pipeline.run(documents=documents).

Führen Sie das Skript index.py aus, um die Ingest-Pipeline zu starten:

python index.py

Sobald der Pipeline-Lauf abgeschlossen ist, sollte in Elasticsearch ein neuer Index mit dem Namen calls angezeigt werden. Wenn Sie eine einfache Elasticsearch-Abfrage über die Entwicklerkonsole ausführen, sollten Sie die geladenen Daten zusammen mit den Einbettungen sehen können.

GET calls/_search?size=1

Zusammenfassend lässt sich sagen, dass wir bisher Dokumente aus einer JSON-Datei erstellt, diese in Abschnitte unterteilt, Einbettungen für diese Abschnitte erstellt und die Einbettungen (sowie die Textkonversation) in einem Vektorspeicher (ElasticsearchStore) gespeichert haben.

Abfrage

Mit dem llamaIndex VectorStoreIndex können Sie relevante Dokumente abrufen und Daten abfragen. Standardmäßig speichert VectorStoreIndex Einbettungen im Arbeitsspeicher in einem SimpleVectorStore. Alternativ können jedoch externe Vektorspeicher (wie ElasticsearchStore) verwendet werden, um die Einbettungen persistent zu machen.

Öffnen Sie query.py und fügen Sie den folgenden Code ein.

# query.py
from llama_index.core import VectorStoreIndex, QueryBundle, Response, Settings
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from index import es_vector_store

# Local LLM to send user query to
local_llm = Ollama(model="mistral")
Settings.embed_model= OllamaEmbedding("mistral")

index = VectorStoreIndex.from_vector_store(es_vector_store)
query_engine = index.as_query_engine(local_llm, similarity_top_k=10)

query="Give me summary of water related issues"
bundle = QueryBundle(query, embedding=Settings.embed_model.get_query_embedding(query))
result = query_engine.query(bundle)
print(result)

Wir definieren ein lokales LLM (local_llm), das auf das Mistral-Modell verweist, das auf Ollama läuft. Als nächstes erstellen wir einen VectorStoreIndex (index) aus dem zuvor erstellten ElasticsearchStore-Vektorspeicher und erhalten dann eine Abfrage-Engine aus dem Index. Beim Erstellen der Abfrage-Engine verweisen wir auf das lokale LLM, das zur Beantwortung verwendet werden soll. Wir geben außerdem (similarity_top_k=10) an, um die Anzahl der Dokumente zu konfigurieren, die aus dem Vektorspeicher abgerufen und an das LLM gesendet werden sollen, um eine Antwort zu erhalten.

Führen Sie das Skript query.py aus, um den RAG-Ablauf auszuführen:

python query.py

Wir senden die Anfrage Give me summary of water related issues (Sie können query gerne anpassen) und die Antwort des LLM, die mit zugehörigen Dokumenten geliefert wird, sollte in etwa wie folgt aussehen.

Im gegebenen Kontext sehen wir mehrere Fälle, in denen Kunden nach einer Deckung für Wasserschäden gefragt haben. In zwei Fällen verursachten Überschwemmungen Schäden an Kellern, in einem weiteren Fall waren Dachlecks das Problem. Die Agenten bestätigten, dass beide Arten von Wasserschäden durch ihre jeweiligen Versicherungsbedingungen abgedeckt sind. Daher sind Probleme im Zusammenhang mit Wasser, wie Überschwemmungen und Dachlecks, typischerweise durch Wohngebäudeversicherungen abgedeckt.

Einige Einschränkungen:

Dieser Blogbeitrag ist eine Einführung für Anfänger in die RAG-Technik mit Elasticsearch und lässt daher die Konfiguration von Funktionen aus, die es Ihnen ermöglichen, diesen Ausgangspunkt in die Produktion zu überführen. Bei der Entwicklung für Produktionsanwendungen sollten Sie komplexere Aspekte berücksichtigen, wie z. B. die Möglichkeit, Ihre Daten mit Document Level Security zu schützen, Ihre Daten im Rahmen einer Elasticsearch Ingest-Pipeline in Chunking-Aufteilung aufzuteilen oder sogar andere ML-Jobs auf denselben Daten auszuführen, die für GenAI/Chat/Q&A-Anwendungsfälle verwendet werden.

Sie könnten auch in Erwägung ziehen, Daten aus verschiedenen externen Quellen (z. B. Azure Blob Storage, Dropbox, Gmail usw.) zu beziehen und Einbettungen mithilfe von Elastic Connectors zu erstellen.

Elastic macht all das und noch viel mehr möglich und bietet eine umfassende Lösung auf Unternehmensebene für GenAI-Anwendungsfälle und darüber hinaus.

Was kommt als Nächstes?

  • Möglicherweise ist Ihnen aufgefallen, dass wir 10 thematisch zusammenhängende Konversationen zusammen mit der Benutzerfrage an das LLM senden, damit dieses eine Antwort formulieren kann. Diese Gespräche können personenbezogene Daten (PII) wie Name, Geburtsdatum, Adresse usw. enthalten. In unserem Fall ist das LLM lokal, daher ist ein Datenleck kein Problem. Wenn Sie jedoch ein LLM in der Cloud (z. B. OpenAI) verwenden möchten, ist es nicht wünschenswert, Texte zu senden, die personenbezogene Daten enthalten. In einem Folgeblogbeitrag werden wir sehen, wie man personenbezogene Daten maskiert, bevor man sie im RAG-Flow an externe LLMs sendet.

  • In diesem Beitrag haben wir ein lokales LLM verwendet. Im kommenden Beitrag über die Maskierung personenbezogener Daten in RAG werden wir uns ansehen, wie man einfach von einem lokalen LLM zu einem öffentlichen LLM wechseln kann.

Häufige Fragen

Was ist LlamaIndex?

LlamaIndex ist ein führendes Datenframework für die Entwicklung von LLM-Anwendungen (Large Language Model).

Was ist Mistral?

Mistral ist ein Unternehmen, das sowohl Open-Source- als auch optimierte LLM-Modelle für Unternehmen anbietet.

Zugehörige Inhalte

Fortgeschrittene RAG-Techniken Teil 2: Abfragen und Testen

Han Xiang Choong

Entitätsauflösung mit Elasticsearch, Teil 4: Die ultimative Herausforderung

Jessica Moszkowicz

Automatisierung des Log-Parsing in Streams mit ML

Nastia Havriushenko

Entwicklung eines KI-Agenten für die Personalabteilung mit Elastic Agent Builder und GPT-OSS

Tomás Murúa

Erweiterte RAG-Techniken Teil 1: Datenverarbeitung

Han Xiang Choong

Sind Sie bereit, hochmoderne Sucherlebnisse zu schaffen?

Eine ausreichend fortgeschrittene Suche kann nicht durch die Bemühungen einer einzelnen Person erreicht werden. Elasticsearch wird von Datenwissenschaftlern, ML-Ops-Experten, Ingenieuren und vielen anderen unterstützt, die genauso leidenschaftlich an der Suche interessiert sind wie Sie. Lasst uns in Kontakt treten und zusammenarbeiten, um das magische Sucherlebnis zu schaffen, das Ihnen die gewünschten Ergebnisse liefert.

Probieren Sie es selbst aus