In Teil 1 dieses Zweiteilers haben wir gesehen, welche Aspekte berücksichtigt werden sollten, bevor man sich für eine RAG-Lösung (RAG = Retrieval-Augmented Generation) entscheidet. Der Aufwand ist nicht unerheblich, und die Vorbereitung der Dokumente erfordert ein hohes Maß an Anpassung.
Welche Prozesse wir in der Vorbereitung eines Dokuments genau durchlaufen müssen, darum geht es in diesem 2. Teil der Serie. Dafür sehen wir uns eine RAG-Pipeline genauer an, testen und bewerten unterschiedliche Ansätze.
Was gehört zur RAG-Pipeline?
– Drei zentrale Schritte
Für die Aufnahme von Dokumenten in die Vektordatenbank müssen diese vorbereitet werden. Sie werden in ein Textformat konvertiert, in möglichst semantisch zusammenhängende Abschnitte aufgeteilt und dann vektorisiert:
Abbildung 1: Vorbereitung eines Dokuments für eine Vektordatenbank
Schritt 1: Konvertierung
Die Konvertierung aus dem zumeist vorliegenden Dokumentformat PDF stellt die erste Hürde dar. Hierzu gibt es zwar einfache Tools (PdfBox, Apache Tika, …), die jedoch einige Schwierigkeiten beim „Druckformat“ PDF ausblenden, wie z. B. Kopf-/Fußzeilen. Diese werden einfach in den laufenden Text mit übernommen, was den Zusammenhang zerreißt. Weiterhin können Textstrukturierungen nicht erkannt und übernommen werden.
Bei unseren Tests haben wir festgestellt, dass es sinnvoll ist, Kopf- und Fußzeilen zunächst zu entfernen. Dazu wurde ein Algorithmus erstellt, der die ersten Zeilen jeder Seite miteinander vergleicht (übrigens auf Basis eines Embeddings und dann unter Verwendung des Cosinus-Abstands) und sie bei relativer Gleichheit als Kopf-/Fußzeile klassifiziert und entfernt.
Eine weitere Möglichkeit – die wir getestet haben – ist, den Text zunächst in das Markdown-Format umzuwandeln. Es gibt eine gute Python Bibliothek (pymupdf, pymupdf4llm), die dies kann. Die Umwandlung dauert länger, aber das Ergebnis lässt sich einfacher in Abschnitte mit Zusammenhang aufteilen.
Es gibt weitere Konzepte zur Umwandlung von Dokumenten in strukturierten Text, die auf OCR basieren, und zunächst versuchen, die Textblöcke eines Dokuments durch optische Analyse zu ermitteln, eine Reihenfolge herzustellen (z. B. bei mehrspaltiger Anordnung) und dann erst die Konvertierung vorzunehmen.
Vgl. RagFlow https://ragflow.io (open source),
oder Docupanda https://docupanda.readme.io (kommerziell)
Schritt 2: Splitting & Chunking
Wenn der Text keine Struktur mehr hat, wird es schwierig sinnvolle Gruppen von Text zu bilden. Wir haben unterschiedliche Ansätze getestet, die im Folgenden erläutert werden sollen.
SentenceBasedTextSplitter
Dieser Splitter zerlegt den Dokument-Text in einzelne Sätze zur Weiterverarbeitung. Dabei wird eine NLP Bibliothek verwendet, z.B. „opennlp-de-ud-gsd-sentence-1.1-2.4.0.bin“. Diese ist für deutsche Texte optimiert.
Beispiel:
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
# | Satz |
1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. |
2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. |
3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. |
4 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. |
5 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
Dieser Splitter bildet die Basis der folgenden Splitter und wird nicht selbst direkt verwendet.
Group
Der SimpleGroupTextsplitter bildet Gruppen von Sätzen einstellbarer Anzahl (3,5…?).
z. B. groupSize=3
Gruppe | Sätze |
1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. |
2 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
Die Idee ist eine größere, zusammenhängende Informationseinheit zu gewinnen. Der Nachteil ist jedoch, dass dieser Zusammenhang praktisch willkürlich gewählt wurde, allein durch die groupSize.
Overlapped Group
Der OverlapGroupTextSplitter läßt diese Gruppen noch um eine bestimmte Anzahl an Sätzen (1,2…?) überlappen.
z.B. groupSize=3, overlap=1
Gruppe | Sätze |
1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. |
2 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
Die Intention dabei ist, den Kontextzusammenhang zwischen Gruppen von Sätzen nicht zu verlieren.
Similarity
Der SimilarityTextSplitter versucht Gruppen von Sätzen anhand des Abstands ihrer Vektordarstellung (cosinus-distance) zu ermitteln. Die einstellbare Similarity bestimmt die Größe der Satzgruppen. Die Gruppen haben dann keine einheitliche Anzahl von Sätzen.
# | Sätze | Cosinus Distanz | Gruppe |
1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. | 0 | 1 |
2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. | 0.5551 | 2 |
3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. | 0.4579 | 2 |
4 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. | 0.5669 | 3 |
5 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. | 0.6313 | 4 |
Wenn man nun einen Schwellwert (similarity) von 0.5 annimmt, würde Satz 2 zu weit weg vom ersten Satz stehen, und damit dort eine neue Satzgruppe beginnen. Der nächste Satz (#3) liegt unterhalb des Schwellwertes, und wird daher der gleichen Gruppe zugeschlagen, usw.
Hier erhofft man sich durch die Ähnlichkeitssuche unter den Sätzen, einen realen Bruch im Zusammenhang zu ermitteln. Es wird also nicht mehr willkürlich eine Informationseinheit erstellt, sondern gezielt anhand von Brüchen im Kontext.
Grouped Similarity
Der SentenceGroupSimilarityTextSplitter gruppiert Sätze, und bestimmt den Abstand der jeweiligen Satzgruppe zur nächsten. Darauf folgend wird der Splitting-Abstand (breakpointThreshold) aus der 95ten Percentile ermittelt. Der Ablauf ist also ähnlich wie beim SimilarityTextSplitter außer, dass der Splitting Schwellwert dynamisch ermittelt wird.
Von diesem Ansatz erhofft man sich, die Grenzen der Informationseinheiten im Text noch genauer zu erkennen.
Vergleiche auch verschiedene Splittling Algorithmen hier (5 Levels of Text Splitting).
Markdown
Der MarkdownTextsplitter benötigt als Eingabedateiformat wie der Name schon sagt Markdown Text.
Er verwendet als Trennzeichen die wichtigsten Strukturtags für Markdown. In unseren Test haben wir uns auf „#“, „##“, „###“ und „**“ geeinigt. Man könnte sich in dem Fall auch Gedanken über weitere Merkmale zur Abgrenzung von Texteinheiten machen. Letztendlich geht es immer darum, Kontext zusammenzuhalten.
Das doppelte Sternchen wurde übrigens aufgenommen, weil die Umsetzung von Pdf zu Markdown oft fälschlicherweise die Klassifizierung ‚fett‘ für Absätze der vierten Ebene erkennt.
Des Weiteren wird eine maximale Chunk-Größe vorgegeben. (Derzeit fest auf 1024 Zeichen eingestellt.) Auch hier gibt es Optimierungs- bzw. Anpassungsbedarf.
Schritt 3: Embedding
Nun erst erfolgt das Embedding, d. h. das Umwandeln der Textabschnitte in Vektoren und die Speicherung in der Vektordatenbank. Es gibt spezielle Modell für das Embedding, die wesentlich kleiner sind als die Modelle für die Generierung.
In diesem Schritt ist es entscheidend, wie gut das gewählte Embedding-Modell mit den Dokumenten zurechtkommt. Ausschlaggebend sind oft eine oder mehrere Sprachen, für die ein Modell optimiert wurde.
Wir verwenden gerne das Modell „jina/jina-embeddings-v2-base-de“, weil dieses Modell für Deutsch und Englisch trainiert wurde.
Beim Speichern in der Vektordatenbank werden noch weitere Metadaten zu den einzelnen Dokument-Abschnitten hinzugefügt, z. B. der originale Dateiname. Diese Metadaten erlauben die Ähnlichkeitssuche einzugrenzen, so dass diese effizienter wird. So können auch große Datenmengen mit gleichem Metadaten Schema sehr schnell durchsucht werden.
Fazit
Für die Befüllung eines RAG müssen wor also eine Kette von Verarbeitungsschritten mit einer großen Menge an Stellschrauben durchlaufen.
Um in einem bestimmten Anwendungsfall das bestmögliche Ergebnis zu erhalten, ist es nötig,
- alle Parameter im Auge zu behalten, die Ergebnisse zu bewerten und ggf. immer wieder nachzujustieren.
- Im besten Fall lassen sich Zwischenergebnisse messbar miteinander vergleichen, um für die Gesamtmenge an Dokumenten durch Stichproben einen optimierten Parametersatz zu erstellen.
- Hierzu ist es dann notwendig, der Anwendung auch ein Tool zur Ermittlung und Verbesserung der Parameter hinzuzufügen. In unserem Testszenario haben wir diesem den Namen „parameter-tuning-service“ gegeben.
In Teil 1 haben wir bei der Betrachtung der Daten und deren Mengen gesehen, welche Aspekte für die Entscheidung für oder gegen RAG wichtig sind.
Teil 2 zeigte uns wie hoch der Entwicklungaufwand für die Vorverarbeitung der Dokumente sein kann, denn es gibt nicht „die“ Methode, die für alle Dokumentformate das beste Ergebnis liefert.
Weitere Methoden werden sicherlich in Zukunft noch entwickelt werden. Komplexe Dokumente mit Hilfe von OCR Technologien zu analysieren ist sicherlich nur der Anfang. Bereits heute sehen wir, dass KI gestützte Analyse ebenfalls möglich ist, jedoch unter Kostenaspekten noch viel zu teuer für große Mengen.
Unter all diesen Bedingungen ist die Frage „To RAG or not to RAG“ nicht so einfach zu beantworten. Schon gar nicht allgemein. Sie hängt von vielen Faktoren ab, und die Entscheidung dafür oder dagegen hat große Auswirkung auf die Gestaltung der Anwendung und deren Erstellungs- und Betriebskosten.
RAG or not to RAG, Teil 1: Wann braucht es einen RAG-Ansatz
RAG or not to RAG, Teil 2: Prozesse und Aufwand in der Praxis
Was steckt hinter RAG und Semantischer Suche?
RAG und Semantische Suche in der Praxis