12 Retrieval Augmented Generation (RAG) und Text-to-SQL
Dieses Kapitel hebt sich vom Rest des Buchs etwas ab, da wir hier keine Arbeitstechniken und Werkzeuge vorstellen, die Ihnen das Programmieren erleichtern, sondern ein Programm entwickeln, das KI verwendet, um zu einem Ergebnis zu kommen. (Natürlich gibt es dennoch eine Verbindung: Den Code zu den Beispielen in diesem Kapitel haben wir natürlich wieder mit KI-Hilfe erstellt. So enthält auch dieses Kapitel wieder eine Menge Prompts sowie einige Anmerkungen, warum wir den Prompt genau so formuliert haben und warum der eine oder andere vorangegangene Versuch gescheitert ist.)
Die Problemstellung für die folgende KI-Applikation ist, dass Daten aus unterschiedlichen Quellen mithilfe eines großen Sprachmodells (LLM) analysiert werden sollen. Als Basis-Daten werden wir PDF-Dokumente, Webseiten im HTML-Format und eine SQL-Datenbank verwenden. Um diese Anforderungen zu realisieren, verwenden wir zwei verschiedene Techniken: Retrieval Augmented Generation und Text-to-SQL. Die Idee dabei ist, mit diesen Technologien einen schlummernden Datenschatz suchbar zu machen.
In vielen Projekten gibt es das Problem, dass die Stichwortsuche für archivierte PDFs und andere Dokumente nicht ausreichend ist – von den Daten in einer SQL-Datenbank ganz zu schweigen. Fragen in natürlicher Sprache zu stellen und diese mit dem Inhalt dieser Daten beantwortet zu bekommen, kann wirklich ein Game Changer in gewissen Bereichen sein.
Retrieval Augmented Generation (RAG) ist eines der Buzz-Wörter im aktuellen KI-Hype. Da es mit vertretbarem Aufwand unmöglich ist, ein fertiges LLM um eigene Informationen zu erweitern, wird einem großen, allgemeinen Sprachmodell ein zusätzliches kleines, selbst erzeugtes Sprachmodell vorgeschaltet, das sogenannte Embedding-Modell. Bei einer Abfrage werden die Ergebnisse dieses Modells an das große Sprachmodell weitergeleitet, das in weiterer Folge die Antwort für den Benutzer erzeugt.
Das Embedding-Modell konvertiert unstrukturierte Daten, wie Text oder Bilder, in mehrdimensionale Vektoren. Wie bei einem geografischen Koordinatensystem, das Punkte auf der Erde beschreibt, geben diese Vektoren Auskunft über die Position von Informationen zueinander. Nur sind diese Vektoren nicht wie bei einem geografischen Koordinatensystem auf drei Dimensionen beschränkt, sondern können Hunderte oder Tausende Dimensionen besitzen.
Bei Text-to-SQL erzeugt ein LLM eine SQL-Abfrage aus einer Anfrage in natürlicher Sprache. Damit das funktioniert, werden dem LLM Informationen zur Datenbank-Struktur bei der Anfrage mitübergeben. Sie können sich vorstellen, dass das LLM nur dann eine Chance hat, vernünftige SQL-Abfragen zu formulieren, wenn die Tabellen- und Spaltennamen sinnvolle Bezeichnungen haben.
Für die Entwicklung der KI-Applikation im folgenden Beispiel verwenden wir die Open-Source-Bibliothek LlamaIndex (https://www.llamaindex.ai). Sie bietet sowohl Anbindung für die Programmiersprache Python als auch für TypeScript. Wir haben uns bei diesem Beispiel für Python entschieden.
12.1 Schnellstart RAG
Als Einstieg in die Thematik wollen wir Ihnen zeigen, wie schnell man zu einer lauffähigen KI-Applikation kommen kann, wenn diese die KI selbst schreibt. Wir möchten auf einer HTML-Seite Fragen zu Dokumenten stellen, die wir in einem Ordner pdfs auf der Festplatte ablegen. Es handelt sich um Jahresberichte und weitere Informationen zu einem Forschungsprojekt über Insektenzählungen in Österreich, auf das wir im weiteren Verlauf des Kapitels noch genauer eingehen werden. Unser Prompt an das LLM Claude-3.5-Sonnet lautet:
Prompt: Generate a FastAPI backend for a llamaindex Q&A application and a HTML page to input questions. Data for llamaindex is in a folder pdfs. Do not use templating, but serve the HTML file from server root.
Der erzeugte Python-Code ist leider nicht lauffähig, weil sich die LlamaIndex-Bibliothek seit dem Ende der Trainingsdaten für Claude etwas geändert hat. Ein Problem, dem wir in diesem Buch schon öfters begegnet sind. Die Änderungen sind aber minimal (der GPTSimpleVectorIndex wurde in VectorStoreIndex umbenannt), und mit dem Ändern von drei Zeilen Code läuft die Applikation. Wir zeigen Ihnen hier das ganze Backend-Skript, das den Webserver startet, Fragen mithilfe der KI beantwortet und die Antwort in Form von JSON zurückgibt:
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from llama_index.core import (
VectorStoreIndex, SimpleDirectoryReader
)
app = FastAPI()
documents = SimpleDirectoryReader("pdfs").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
class Question(BaseModel):
text: str
@app.post("/api/ask")
async def ask_question(question: Question):
try:
response = query_engine.query(question.text)
return {"answer": str(response)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
app.mount("/", StaticFiles(directory="static", html=True),
name="static")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Die meisten der wenigen Codezeilen betreffen den Webserver, der mit den Bibliotheken FastAPI und uvicorn sehr komfortabel einzurichten ist. HTML-Dokumente, die im Ordner static liegen, werden regulär über den Webserver ausgeliefert. Bei einer HTTP-POST-Anfrage, die an die URL /api/ask gesendet wird, wird der Inhalt der Anfrage auf die zuvor definierte Klasse Question abgebildet. Der darin enthaltene text-Eintrag wird dem query_engine-Aufruf übergeben.
Hier geschieht die Magie: Mit nur drei Zeilen Code erstellt die LlamaIndex-Bibliothek die query_engine, die den Inhalt der PDF-Dokumente mit Anfragen in natürlicher Sprache durchsuchen kann. Dazu liest der SimpleDirectoryReader einen Ordner mit Dateien, in unserem Fall PDF-Dokumenten, ein. Anschließend wird ein VectorStoreIndex aus diesen Dokumenten erzeugt, aus dem wiederum die query_engine abgeleitet wird.
Was bei diesen Schritten im Detail passiert, werden wir in den nächsten Abschnitten klären. Zuerst werden wir unsere Applikation ausprobieren. Wie Claude in der kurzen Erklärung erläutert, müssen wir dazu die notwendigen Python-Bibliotheken installieren:
pip install fastapi uvicorn llama-index pydantic
Das HTML-Frontend kommt mit wenigen Zeilen Javascript-Code und ohne zusätzliche Bibliotheken aus. An der Datei, die wir im Ordner static als index.html speichern, müssen wir keine Änderungen vornehmen. Der zentrale Teil der HTML-Datei sieht wie folgt aus:
<textarea id="question" placeholder="Enter your question here">
</textarea>
<button onclick="askQuestion()">Ask</button>
<div id="answer"></div>
<script>
async function askQuestion() {
const question = document.getElementById("question").value;
const answerDiv = document.getElementById("answer");
answerDiv.innerHTML = "Loading...";
try {
const response = await fetch("/api/ask", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text: question }),
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
answerDiv.innerHTML = data.answer;
} catch (error) {
answerDiv.innerHTML = "Error: " + error.message;
}
}
</script>
Die Funktion askQuestion, die beim Drücken des Ask-Buttons ausgeführt wird, sendet den Inhalt des Textfeldes mit der ID question mittels einer HTTP-POST-Anfrage als JSON-String an das Backend. Sofern die Anfrage erfolgreich beantwortet wird, wird der Inhalt der JSON-Struktur data.answer in dem entsprechenden HTML-DIV-Bereich eingesetzt.
Was Claude in der Beschreibung nicht erwähnt hat, ist, dass LlamaIndex die Schwerarbeit an OpenAI auslagert. Die Applikation kann also nur funktionieren, wenn Sie einen OpenAI-API-Schlüssel haben und noch Kredit auf Ihrem Konto vorhanden ist. Damit die Applikation Zugang zu Ihrem Konto bekommt, müssen Sie den API-Schlüssel in der Umgebungsvariable OPENAI_API_KEY setzen. In gängigen Linux-Shells rufen Sie dazu folgendes Kommando auf:
export OPENAI_API_KEY="sk-proj-xxxxxxxx"
Jetzt können wir unsere Applikation mit folgendem Kommando starten:
python main.py
Das Ergebnis ist durchaus überzeugend.
Abbildung 12.1 Unser Schnelleinstieg in RAG: Nach 10 Minuten ist die erste Web-Applikation fertig, die Fragen in natürlicher Sprache zu PDF-Dokumenten auf der SSD beantwortet.
Bevor Sie jetzt das Buch zuklappen (oder den E-Reader ausschalten) und an die Arbeit gehen, um dieses Beispiel umzusetzen, möchten wir Sie noch auf einige Defizite dieser Applikation hinweisen:
-
Jedes Mal, wenn Sie den Webserver starten, wird der Index neu erzeugt. Das ist nicht notwendig, denn Sie können einen VectorStoreIndex auch lokal speichern.
-
Mit den Standardeinstellungen müssen Sie einen Zugang bei OpenAI haben und bezahlen. LlamaIndex funktioniert aber mit anderen Cloud-Anbietern und auch mit lokalen LLMs, und das gar nicht schlecht.
-
Welche Typen von Dokumenten Sie indizieren wollen und wie die Indizierung im Detail funktioniert, können Sie sehr genau einstellen. Hier gibt es großes Optimierungspotenzial.
Lesen Sie die nächsten Abschnitte, um einen etwas tieferen Einblick in die Funktionsweise von RAG und in die LlamaIndex-Bibliothek zu bekommen.