Auswahl und Anpassung von Embedding-Modellen für produktive RAG-Systeme
Erfahren Sie, wie Embedding-Modelle Text in suchbare Vektoren umwandeln, warum das Domänenwörterbuch die semantische Suche stört und wie Modelle für produktive RAG-Systeme ausgewählt, komprimiert und feinabgestimmt werden.
Dies ist der fünfte Teil einer Reihe über den Aufbau von für die Produktion geeigneten Systemen zur generativen Erweiterung durch Suchfunktionen, die den Weg von Rohdokumenten zu einem System beschreibt, das tatsächliche Fragen beantworten kann. Frühere Schritte dieser Reihe behandelten die Reinigung und Normalisierung des extrahierten Inhalts sowie dessen Aufteilung in Sucheinheiten durch Chunking. Mit den Chunks in der Hand besteht die nächste Aufgabe darin, sie in etwas umzuwandeln, das ein Suchindex tatsächlich vergleichen kann.
Stellen Sie sich denselben Relationship Manager vor, der bereits erwähnt wurde und immer noch mit einer Kreditlinie in Höhe von 12 Millionen Euro für einen hochriskanten Unternehmenskunden konfrontiert ist. Um den Antrag zu bearbeiten, muss er ein in einer Vertragsklausel verstecktes Anforderungskriterium für eine erweiterte Due-Diligence-Prüfung, einen in einer Tabellenzeile festgelegten Genehmigungsschwellenwert sowie eine in einem Prozessdiagramm beschriebene Kontrollsequenz finden. Die Chunking-Phase hat diese bereits als separate, nachvollziehbare Inhaltsbestandteile herausgearbeitet.
Trotzdem kann all das noch nicht über einen Vektorindex gesucht werden.
Ein Embedding-Modell wandelt jeden Textabschnitt in einen numerischen Vektor fester Länge um. Dasselbe Modell konvertiert anschließend eine eingehende Abfrage in einen Vektor, und der Index ruft diejenigen Abschnitte ab, die sich in diesem Vektorraum am nächsten dazu befinden. Dass Ausdrücke wie „Group Credit Committee approval“ in einem Richtliniedokument und „GCC sign-off threshold“ in der Frage eines Benutzers tatsächlich nahe beieinander liegen, ist nicht garantiert – es hängt vollständig davon ab, welches Modell man verwendet hat und was es während des Trainings gelernt hat.
Diese Abhängigkeit ist Gegenstand dieses Teils der Serie.
Die Wahl eines Embedding-Modells bedeutet, darauf zu wetten, wie viel Vokabular und Formulierung Ihre Dokumente mit den Abfragen Ihrer Nutzer teilen. Wählen Sie das falsche Modell für Ihr Anwendungsfeld, werden Sie wochenlang damit verbringen, nach einem scheinbaren Suchfehler zu suchen, der in Wirklichkeit ein Repräsentationsproblem ist – die Vektoren selbst sind falsch platziert, weshalb keine Anpassung des Indexes dies beheben kann.
Was ein Embedding-Modell tatsächlich berechnet
In seinem Kern nimmt ein Embedding-Modell eine Tokenfolge auf und gibt einen dichten Vektor aus, der in der Regel zwischen 384 und 3072 Dimensionen hat, je nach spezifischem Modell. Dieser Vektor soll eine komprimierte Kodierung dessen sein, was die Eingabe bedeutet.
Die zugrunde liegende Annahme bei der Extraktion ist, dass Eingaben mit ähnlicher Bedeutung Vektoren erhalten, die in diesem Raum nah beieinander liegen. Die Nähe wird am häufigsten mit der Kosinusähnlichkeit gemessen, wobei der Winkel zwischen zwei Vektoren berücksichtigt wird und nicht ihre reine Entfernung – eine Eigenschaft, die sie unempfindlich gegenüber Unterschieden in der Textlänge macht.
Betrachten wir eine Konformitätsregel, die vorschreibt, dass als hochriskant eingestufte Unternehmenskunden vor der Einreichung eines Kreditvorschlags zur Prüfung einer erweiterten Sorgfaltspflichtprüfung unterzogen werden müssen. Ein leistungsstarkes Allzweckmodell würde voraussichtlich seinen Vektor in der Nähe vergleichbarer Formulierungen zu Konformitätsanforderungen anderer Banken, in der Nähe rechtlicher Materialien zu Sorgfaltspflichten sowie in der Nähe regulatorischer Richtlinien zur Betreuung hochriskanter Kunden platzieren.
Ein Relationship Manager könnte jedoch denselben zugrundeliegenden Bedarf ganz anders formulieren und beispielsweise fragen, welche Schritte vor dem Einreichen eines Kreditantrags erforderlich sind. Ob diese alltägliche Formulierung dem formalen Richtlinienrahmen nahekommt, hängt letztlich davon ab, inwieweit Trainingsdaten informelle operative Fragen mit formeller Compliance-Sprache verknüpft haben. Modelle, die hauptsächlich mit allgemeinem Webtext trainiert wurden, erfassen oft nie diese spezifische Verknüpfung, wenn es um spezialisierte Bereiche geht.
Dieser Unterschied zwischen der Formulierung alltäglicher Anfragen und der Sprache in spezialisierten Dokumenten wird als Wortschatzunstimmigkeit bezeichnet und ist die Hauptursache für Probleme bei der Qualität der Informationsabrufung in Unternehmens-RAG-Systemen.
Tokenisierung und das Kontextfenster
Vor dem Berechnen von Inhalten zerlegt ein Embedding-Modell zunächst die Eingabe mithilfe seines eigenen internen Wortschatzes in Tokens. Die Anzahl der Tokens entspricht nicht immer direkt der Anzahl der Wörter oder Zeichen. Ein Block von 500 Tokens auf Englisch kann etwa 350–400 Wörter entsprechen, während derselbe Token-Budget in Deutsch – wo Wörter häufig zusammengesetzt sind – weniger unterschiedliche Ideen darstellen kann.
Jedes Embedding-Modell legt eine maximale Kontextlänge fest; alles Längere wird entweder abgeschnitten oder erfordert eine spezielle Behandlung. Sentence-Transformer-Modelle haben in der Regel eine Obergrenze von etwa 256 bis 512 Tokens. OpenAI’s text-embedding-3-large kann bis zu 8.191 Tokens verarbeiten. BGE-M3 erreicht sogar 8.192 Tokens. Auch Jina Embeddings v3 unterstützt bis zu 8.192 Tokens.
Die praktische Erkenntnis für die Produktion von RAG ist, dass die zuvor im Pipeline-Prozess festgelegten Chunk-Grenzen innerhalb der vom gewählten Embedding-Modell vorgegebenen Kontextgrenze bleiben müssen. Jeder Chunk, der diese Grenze überschreitet, wird stillschweigend abgeschnitten, und der resultierende Vektor spiegelt nur einen Teil des ursprünglichen Textes wider – ein Fehlermodus, der in den Logs Ihrer Pipeline nicht erscheinen wird.
Der semantische Raum und wann er versagt
Die meisten aktuellen Embedding-Modelle sind Transformer-Encoder, die mit einem kontrastiven Ziel trainiert wurden: Textpaare, die ähnliche Bedeutungen haben, werden im Vektorraum zusammengebracht, während unähnliche Paare voneinander entfernt werden. Nach ausreichendem Training entsteht so eine geometrische Anordnung, bei der Nähe als Indikator für semantische Verwandtschaft dient.
Diese Struktur funktioniert gut, solange Abfragen und Dokumente die gleiche Terminologie, Schreibweise sowie konzeptionelle Struktur wie das Material aufweisen, mit dem das Modell trainiert wurde. Bei unternehmensintegrierten RAG-Lösungen versagt sie in der Regel auf einige vorhersehbare Weise:
- Bereichsspezifische Terminologie führt zu Fehlern, die leicht übersehen werden können. Ein Analyst, der nach dem „SAR-Abgabenschwellenwert für die Strukturierung“ fragt, erhält möglicherweise kein Ergebnis, wenn ein Policy-Auszug mit den Worten „Kriterien für die Einreichung von Berichten über verdächtige Aktivitäten bei der Strukturierung von Transaktionen“ formuliert ist – vorausgesetzt, das Modell hat nie gelernt, dass diese beiden Formulierungen dasselbe bedeuten.
Das genaue Verständnis dafür, an welchen Stellen allgemeine Embedding-Modelle versagen, ist genauso wichtig wie die Kenntnis darüber, welches Modell an der Spitze der öffentlichen Benchmark-Listen steht.
Die Auswahl eines Embedding-Modells im Jahr 2025
Das Feld der Embedding-Modelle hat sich erheblich eingegrenzt. Der folgende Vergleich umfasst die Modelle, die Mitte 2025 für RAG-Systeme in der Banken- und Finanzbranche von größter Bedeutung sind.
Es gibt keinen universellen Gewinner für jedes Unternehmensszenario. Ihre Entscheidung hängt von der Menge an Sprachen ab, die Sie unterstützen müssen, vom verfügbaren Latenzbudget und der Infrastruktur, davon, ob lokale Inferenz überhaupt eine Option für Sie ist, sowie von der Größe des Terminologiefehlers zwischen Ihrem Bereich und dem, worauf allgemeine Modelle trainiert wurden – ein Unterschied, der entscheidet, ob eine Feinabstimmung die Mühe lohnt.
Asymmetrische Abrufung und Mitteilung an das Modell, welche Art von Eingabe es erhält
Eine Unterscheidung, die viele Teams übersehen, ist die asymmetrische Abrufung. Bei der Abrufung von Abschnitten sind die Abfrage und der mit ihr abgeglichenen Textabschnitt strukturell sehr unterschiedlich. Abfragen sind in der Regel kurz, als Fragen formuliert und enthalten oft einen großen Teil des Wortschatzes nicht, der in einer richtigen Antwort vorkommt. Im Gegensatz dazu sind Abschnitte länger, als Fakten dargestellt und reich an Fachbegriffen aus dem jeweiligen Bereich.
Bei einigen Modellen ist es so konzipiert, dass sie diese Asymmetrie direkt erkennen. E5-Modelle fügen dem Eingabetext das Präfix „query:“ oder „passage:“ hinzu, damit das Modell weiß, welche Rolle es einbetten soll. Cohere’s Embed v3 stellt dies über einen input_type-Parameter bereit, wobei zulässige Werte „search_query“, „search_document“, „classification“ und „clustering“ sind.
Die Angabe des falschen Eingabetyps beim Indexieren oder bei der Abfrage schadet heimlich den Ähnlichkeitswerten auf eine Weise, bei der die Ursache schwer nachzuvollziehen ist. Wenn man ein Dokument so einbettet, als wäre es eine Abfrage, erhält man einen Vektor, der für die Geometrie einer Abfrage und nicht für die eines Textabschnitts konzipiert ist. Die Suche scheitert zwar nicht vollständig – doch die Genauigkeit nimmt ab, was bei gelegentlichen Tests leicht übersehen werden kann.
In der Produktion sollte man sich nicht darauf verlassen, dass Entwickler daran denken, dies korrekt einzustellen – erzwingen Sie es durch Konfiguration. Sowohl der Aufruf zur Embedding-Erstellung zum Indexierungszeitpunkt als auch der Aufruf zum Zeitpunkt der Abfrage sollten ihren Eingabetyp explizit angeben, sofern das von Ihnen verwendete Modell diese Option unterstützt.
import cohere
from typing import List
co = cohere.Client(api_key="your_api_key")
def embed_documents(chunks: List[str]) -> List[List[float]]:
"""Embed document chunks for indexing with explicit document input type."""
response = co.embed(
texts=chunks,
model="embed-english-v3.0",
input_type="search_document",
embedding_types=["float"]
)
return response.embeddings.float
def embed_query(query: str) -> List[float]:
"""Embed a search query with explicit query input type."""
response = co.embed(
texts=[query],
model="embed-english-v3.0",
input_type="search_query",
embedding_types=["float"]
)
return response.embeddings.float[0]
Dünne Vektoren: Wo Schlüsselwortabgleich semantischen Suchen überlegen ist
Dichte Embeddings erfassen Bedeutung. Dünne Repräsentationen hingegen geben an, welche Begriffe vorhanden sind und wie stark sie gewichtet werden sollten. Für einen bedeutenden Anteil an Abfragearten in Unternehmens-RAG-Systemen übertrifft die dünne Suche die dichte Suche deutlich, und in den meisten Produktionsystemen ist eine Kombination beider besser als die Verwendung nur einer von ihnen.
BM25 als zuverlässige Grundlage
BM25 bleibt der Standardansatz für die durch Schlüsselwörter gesteuerte Suche. Es bewertet die Relevanz anhand der Häufigkeit, mit der ein Begriff in einem Dokument vorkommt, der Seltenheit dieses Begriffs im gesamten Korpus sowie eines Normalisierungsfaktors, der die Länge des Dokuments berücksichtigt. Es muss kein Modell trainiert werden, es gibt keine Anforderungen an GPU-Hardware und es sind keine Embedding-API-Aufrufe erforderlich.
Betrachten wir eine Abfrage wie „CRD-EU-047 approval authority threshold“. BM25 wird alle Abschnitte, die diese exakten Begriffe enthalten, hoch bewerten. Ein dichtes Modell hingegen könnte einen solchen Abschnitt möglicherweise nicht anzeigen, es sei denn, sein Trainingskorpus hat zufällig eine starke Verbindung zwischen diesem spezifischen Richtliniencode und dem Konzept der Genehmigungsbehörde hergestellt.
from rank_bm25 import BM25Okapi
import re
from typing import List, Tuple
def tokenise(text: str) -> List[str]:
"""Simple whitespace and punctuation tokeniser for BM25."""
return re.findall(r'\b\w+\b', text.lower())
class BM25Index:
def __init__(self, documents: List[str]):
self.documents = documents
tokenised = [tokenise(doc) for doc in documents]
self.bm25 = BM25Okapi(tokenised)
def search(self, query: str, top_k: int = 10) -> List[Tuple[int, float]]:
"""Return (doc_index, score) pairs for the top_k results."""
tokens = tokenise(query)
scores = self.bm25.get_scores(tokens)
ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
return ranked[:top_k]
Dichte Suchverfahren sind gut darin, konzeptionelle Ähnlichkeiten zu erfassen, während spärliche Suchverfahren präzise Übereinstimmungen bei Begriffen und Identifikatoren erkennen. Die Kombination beider – hybride Suchverfahren – bringt insbesondere bei Bankenkorpora, in denen die regulatorische Sprache präzise und stark auf Identifikatoren ausgerichtet ist, gute Ergebnisse.
In Corpora, die auf stabiler, präzise definierten Richtliniensprache basieren, erreicht BM25 allein oft die Trefferquote dichter Suchverfahren bei eng gefassten faktischen Abfragen – und das mit einem Bruchteil der Infrastrukturkosten. Sein größtes Manko ist die Synonymie: Eine Abfrage mit „EDD-Anforderungen“ wird keinen Abschnitt finden, der nur „Erweiterte Due-Diligence-Anforderungen“ besagt, es sei denn, der genaue Ausdruck kommt irgendwo überein.
SPLADE: Spärliche Vektoren, die das Wortschatzverständnis erweitern
SPLADE (Sparse Lexical and Expansion Model) befindet sich auf dem Mittelweg zwischen einfacher Schlüsselwortabgleich und vollständiger, dichter Suche. Bei der Indizierung wird ein maskiertes Sprachmodell verwendet, um sowohl Dokumente als auch Abfragen mit semantisch verwandtem Vokabular zu bereichern, das nicht unbedingt im ursprünglichen Text enthalten ist. Das Ergebnis ist ein spärlicher Vektor, dessen Dimensionen jeweils einem bestimmten Vokabular-Token entsprechen und je nach Bedeutung dieses Tokens für die Eingabe gewichtet sind.
Eine mit SPLADE kodierte Passage, die Anforderungen an EDD behandelt, kann daher mehr Gewicht auf Begriffe wie „Kundenprüfung“, „Risikobewertung“ und „Identitätsüberprüfung“ legen – selbst wenn keine dieser genauen Ausdrücke im Quelltext vorkommen. Diese Erweiterung verbessert die Auffindbarkeit bei Abfragen, die auf Synonymen beruhen, während gleichzeitig die Effizienz und Interpretierbarkeit beibehalten werden, die spärliche, für invertierte Indizes geeignete Repräsentationen bieten.
Der Kompromiss besteht in höheren Inferenzkosten beim Erstellen des Indexes sowie einem größeren Speicherbedarf im Vergleich zu herkömmlichem BM25. Für Korpora aus dem Finanzsektor, in denen derselbe Politikbegriff in verschiedenen Rechtsgebieten und Dokumentversionen unterschiedlich beschrieben wird, kann diese Erweiterung die Abdeckung der Suchergebnisse erheblich verbessern.
Matryoshka-Embeddings: Anpassbare Vektorgröße zur Kostenkontrolle
Matryoshka Representation Learning (MRL) erstellt Embeddings, bei denen die ersten N Dimensionen bereits eine vollständige, eigenständige Repräsentation der Eingabe bilden – zusätzliche Dimensionen fügen feinere Details hinzu, anstatt das Vorherige zu überschreiben.
Die Technik leitet ihren Namen von russischen Matrjoschka-Puppen ab: Ein 1536-dimensionaler Matryoshka-Vektor enthält in seinen ersten 256 Slots eine voll funktionsfähige 256-dimensionalen Repräsentation, in den ersten 512 Slots eine funktionsfähige 512-dimensionalen Repräsentation und so weiter.
OpenAI’s Text-Embedding-3-Modellfamilie unterstützt dies direkt über einen Dimensionsparameter.
from openai import OpenAI
from typing import List
client = OpenAI()
def embed_with_matryoshka(
texts: List[str],
dimensions: int = 256,
model: str = "text-embedding-3-large"
) -> List[List[float]]:
"""
Embed texts at a specified sub-dimension.
Lower dimensions reduce storage and index cost.
Measure retrieval quality drop before committing to a dimension.
"""
response = client.embeddings.create(
input=texts,
model=model,
dimensions=dimensions
)
return [item.embedding for item in response.data]
Matryoshka-Embeddings nesten immer detailliertere Repräsentationen innerhalb eines Vektors: Die ersten 256 Dimensionen liefern bereits eine brauchbare Repräsentation für die Suche, und jede weitere Ebene erhöht die semantische Präzision zu einem proportionalen Speicherbedarf.
Der eigentliche Produktivitätsvorteil besteht darin, das Verhältnis zwischen Speicherbedarf und Qualität nach Bedarf anpassen zu können, ohne ein Modell neu trainieren oder den Index von Grund auf neu erstellen zu müssen. Für ein Korpus bankbezogener Richtlinien könnte man die Suchleistung bei 256, 512, 1024 und 3072 Dimensionen vergleichen und feststellen, dass 512 Dimensionen zu 97 % der vollständigen Aufrufrate führen, während nur 17 % des Speicherbedarfs benötigt werden, der für vollständige Vektoren erforderlich wäre.
In der Praxis ist dieses Abwägen selten so eindeutig wie in einem Vergleichsdiagramm. Feine Unterschiede im jeweiligen Bereich – insbesondere zwischen eng miteinander verbundenen regulatorischen Konzepten – liegen oft gerade im hochdimensionalen Teil des Vektors. Testen Sie vor der endgültigen Festlegung einer reduzierten Dimensionalität für den Produktivbetrieb Ihr eigenes Korpus sowie tatsächliche Abfragemuster.
Embeddings komprimieren, ohne viel Genauigkeit zu opfern
Standard-Embeddings speichern jede Dimension als 32-Bit-Float-Wert. Wenn man dies auf eine Million Dokumentenblöcke mit jeweils 1536 Dimensionen erweitert, erhält man etwa 6 GB an Rohvektoren, bevor noch zusätzliche Indexierungskosten hinzukommen. Im Unternehmensumfeld werden diese Speicherkapazitäten sowie die damit verbundenen Speicherkosten nicht mehr als Rundungsfehler ignoriert werden können.
Quantisierung löst dieses Problem, indem sie die Anzahl der Bit reduziert, die zur Darstellung jeder Dimension benötigt werden. In der Praxis dominieren drei Techniken: Skalarquantisierung (Umwandlung von float32 in int8), Binärquantisierung (Umwandlung von float32 in einen einzigen Bit) sowie Produktquantisierung (Komprimierung jedes Vektors in einen kürzeren Code).
Skalarquantisierung: int8
Die skalarische Quantisierung überträgt den kontinuierlichen Bereich von float32 auf 256 diskrete Ganzzahlwerte. Jede Dimension verringert sich dabei von 4 Bytes auf 1, wodurch der Speicherbedarf um 75 % gesenkt wird. Da hochdimensionale Embedding-Modelle Informationen über viele Dimensionen verteilen, trägt keine einzelne Dimension für sich genommen viel Gewicht – daher ist die durch diese Rundung verlorene Genauigkeit in der Regel gering.
import numpy as np
from typing import Tuple
def quantise_to_int8(
embeddings: np.ndarray
) -> Tuple[np.ndarray, float, float]:
"""
Scalar quantisation to int8.
Returns quantised array plus the scale and zero_point needed for dequantisation.
"""
min_val = embeddings.min()
max_val = embeddings.max()
scale = (max_val - min_val) / 255.0
zero_point = -round(min_val / scale)
quantised = np.clip(
np.round(embeddings / scale) + zero_point,
0, 255
).astype(np.uint8)
return quantised, scale, zero_point
def dequantise_from_int8(
quantised: np.ndarray,
scale: float,
zero_point: float
) -> np.ndarray:
"""Reconstruct approximate float32 embeddings from int8."""
return ((quantised.astype(np.float32) - zero_point) * scale)
Binäre Quantisierung
Bei der binären Quantisierung geht es noch weiter: Jede Dimension wird auf einen einzigen Bit reduziert, der lediglich anzeigt, ob der ursprüngliche float-Wert positiv oder negativ war. Dadurch wird der Speicherbedarf im Vergleich zu float32 um etwa 97 % gesenkt. Da die Darstellung nicht mehr kontinuierlich ist, wird die Ähnlichkeit anstelle der Kosinusähnlichkeit mit der Hamming-Distanz gemessen.
Diese Technik funktioniert am besten bei Modellen, deren Ausgabeverteilungen von Natur aus gut zentriert sind, sodass bei jeder gegebenen Eingabe etwa die Hälfte der Dimensionen auf jeder Seite von Null liegt. Wenn die Dimensionen eines Modells eher verzerrt als ausgewogen sind, führt binäre Quantisierung zu einem deutlich stärkeren Qualitätsverlust. Cohere entwickelte Embed v3 unter Berücksichtigung dieser Einschränkung, und die von Anthropic veröffentlichte Bewertung dieses Modells weist eine Degradierung der Trefferquote von unter 1 Prozent sowie eine Einsparung von 97 Prozent bei der Speicherung auf ihren Testdatensätzen aus. Betrachten Sie diese Werte eher als Ausgangspunkt und nicht als Garantie, und überprüfen Sie sie anhand Ihres eigenen Korpus, bevor Sie sich darauf verlassen.
import numpy as np
def quantise_to_binary(embeddings: np.ndarray) -> np.ndarray:
"""
Binary quantisation: positive dimensions become 1, negative become 0.
Packs 8 dimensions per byte using numpy packbits.
"""
binary_matrix = (embeddings > 0).astype(np.uint8)
return np.packbits(binary_matrix, axis=1)
def hamming_similarity(
query_binary: np.ndarray,
corpus_binary: np.ndarray
) -> np.ndarray:
"""Compute normalised Hamming similarity for binary embeddings."""
n_bits = corpus_binary.shape[1] * 8
xor = np.bitwise_xor(
query_binary,
corpus_binary
)
hamming_distances = np.unpackbits(xor, axis=1).sum(axis=1)
return 1.0 - (hamming_distances / n_bits)
In der Produktion wird die binäre Quantisierung in der Regel als erste Stufe eines zweistufigen Abrufprozesses eingesetzt: Ein binärer Index ermöglicht eine schnelle und umfassende Auswertung möglicher Ergebnisse, während anschließend eine Berechnung in voller Präzision die besten Ergebnisse erneut bewertet. Diese Konfiguration spart den größten Teil der Speicherressourcen, während die Präzision an dem entscheidenden Punkt – der endgültigen Rangliste – wiederhergestellt wird.
Fine-Tuning für domänenspezifisches RAG
Fine-Tuning ist die richtige Lösung, sobald festgestellt wurde, dass allgemein einsetzbare Embeddings für Ihre Daten nicht ausreichen. Ziel ist es, dem Modell beizubringen, dass das Vokabular, die Abkürzungen sowie die konzeptionellen Verbindungen, die für Ihre Domäne spezifisch sind, im semantischen Raum eng beieinander liegen.
Feintunen ist nicht immer notwendig und stellt auch nicht immer die richtige Lösung dar. Wenn Ihre Probleme bei der Informationsabrufung auf eine mangelhafte Aufteilung in Abschnitte zurückzuführen sind, wie bereits früher in dieser Serie erörtert, hilft das Anpassen des Embedding-Modells nicht. Wenn die Ursache darin liegt, wie das Neuranking konfiguriert ist oder wie Anfragen erstellt werden, zielt Feintunen völlig falsch auf eine Schicht des Systems ab. Bevor Sie Ressourcen dafür einsetzen, analysieren Sie Ihre Fehlschläge beim Informationsabruf nach Anfrageart, um herauszufinden, wo das eigentliche Problem liegt.
Wenn allgemeine Embeddings versagen
Spezifisch im Bereich Banking RAG rechtfertigen einige wiederkehrende Fehlermuster die Investition in Feintunen:
Domänenspezifische Abkürzungen werden falsch interpretiert. Ein allgemein einsetzbares Modell könnte „NPA“ mit der National Parks Association in Verbindung bringen anstelle von Non-Performing Asset, und es könnte „KYC“ nur schwach mit den Konzepten der Compliance und Onboarding verknüpfen, die in Bankanfragen tatsächlich dominieren.
Verbindungen zwischen verwandten Konzepten in verschiedenen Dokumenten gehen verloren. Eine Suche nach „Facility Restructuring Provisions“ sollte Texte zu „Loan Modification Frameworks“ anzeigen, doch ein Modell, das hauptsächlich auf allgemeinem Webinhalt trainiert wurde, hat möglicherweise nie gesehen, dass diese Ausdrücke eng genug miteinander verbunden sind, um eine solche Verbindung herzustellen.
Regulatorische Codes und Identifikatoren erhalten nicht die nötige Bedeutung. Policy-Versionen, Regulierungscodes sowie Zuständigkeitsangaben sollten die Rangfolge maßgeblich beeinflussen, doch allgemeine Embedding-Modelle neigen dazu, sie als weniger wertvolle Token zu betrachten, die nur geringe semantische Signale tragen.
Zahlreiche Schwellenwerte verlieren ihren Kontext innerhalb der Regulierung. Eine Angabe wie „10 Millionen EUR“, die isoliert verwendet wird, sollte nicht automatisch mit einer Anfrage nach der „Genehmigungsbehörde für große Exponate“ übereinstimmen – eine solche Verbindung entsteht nur, wenn das Modell mit Domänendaten trainiert wurde, die die Zahl mit ihrer regulatorischen Bedeutung verknüpfen.
Trainingspaare aus domänenspezifischen Daten erstellen
Die Feinabstimmung von Sentence-Transformer-Modellen unter einem kontrastiven Ziel hängt von positiven Paaren ab: Beispiele, die eine Abfrage mit einem Abschnitt verknüpfen, den das Modell lernen soll als verwandt zu betrachten. Negative Beispiele können entweder manuell ausgewählt oder automatisch aus dem umliegenden Korpus generiert werden.
In einem bankbezogenen RAG-Kontext können diese positiven Paare aus einer Reihe praktischer Quellen zusammengestellt werden:
Bereits von Compliance- und Kreditteams erstellte Frage-Antwort-Sätze, bei denen jede Frage mit ihrem Quellabschnitt verknüpft ist.
Die natürliche Struktur von Richtliniedokumenten, bei der ein Überschriftentext in Kombination mit dem darunterliegenden Absatz ein fertiges positives Beispiel bildet.
Eingetragene Anfragen von Analysten in Kombination mit den Abschnitten, die tatsächlich abgerufen wurden, als die Antwort korrekt war.
Von einem LLM für jeden Textabschnitt erzeugte maschinenergene Anfragen, wobei dieser Abschnitt selbst als passender positiver Abschnitt verwendet wird.
Darunter ist die Erstellung synthetischer Anfragen in der Regel der praktikabelste Ansatz, wenn bereits nur wenig gelabelte Daten vorhanden sind.
from openai import OpenAI
import json
from typing import List, Dict
client = OpenAI()
def generate_training_queries(
chunk: str,
chunk_metadata: Dict,
n_queries: int = 3
) -> List[Dict]:
"""
Generate synthetic query-passage pairs for fine-tuning.
The chunk itself is the positive passage for each generated query.
"""
prompt = f"""You are generating training data for a banking RAG system.
Given the following policy passage, generate {n_queries} realistic questions
that a credit analyst, compliance officer, or relationship manager might ask
that this passage directly answers. Each question should use natural language
and may use different terminology than the passage itself.
Passage:
{chunk}
Return a JSON array of objects with keys "query" and "difficulty".
Difficulty should be "narrow" (single fact) or "synthesis" (multiple facts).
Return only the JSON array, no other text."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
try:
result = json.loads(response.choices[0].message.content)
queries = result.get("queries", result) if isinstance(result, dict) else result
return [
{
"query": q["query"],
"passage": chunk,
"document_id": chunk_metadata.get("document_id"),
"chunk_id": chunk_metadata.get("chunk_id"),
"difficulty": q.get("difficulty", "narrow")
}
for q in queries
]
except (json.JSONDecodeError, KeyError):
return []
Kontrastives Training mit tripletbasiertem Verlustfunktion
Das stärkste Trainingsziel für auf die Recherche ausgerichtete Embedding-Modelle ist das kontrastive Lernen, wobei entweder negative Beispiele im selben Batch oder absichtlich ausgewählte schwierige Negative verwendet werden. Sentence-transformers unterstützt dieses Verfahren über MultipleNegativesRankingLoss, welches jedes andere Beispiel in einem Trainingsbatch als impliziten Negativwert für ein bestimmtes Anker-Positive-Paar verwendet.
from sentence_transformers import SentenceTransformer, InputExample
from sentence_transformers.losses import MultipleNegativesRankingLoss
from torch.utils.data import DataLoader
from typing import List, Dict
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def build_training_examples(
pairs: List[Dict]
) -> List[InputExample]:
"""
Convert query-passage pairs into InputExample objects.
MultipleNegativesRankingLoss expects (anchor, positive) pairs.
Negatives are sampled automatically from other items in the batch.
"""
return [
InputExample(texts=[pair["query"], pair["passage"]])
for pair in pairs
if pair.get("query") and pair.get("passage")
]
def fine_tune_embedding_model(
base_model_name: str,
training_pairs: List[Dict],
output_path: str,
epochs: int = 3,
batch_size: int = 16,
warmup_steps: int = 100
) -> SentenceTransformer:
"""
Fine-tune a sentence-transformers model on domain-specific query-passage pairs.
base_model_name: HuggingFace model identifier or local path.
training_pairs: List of dicts with "query" and "passage" keys.
output_path: Directory to save the fine-tuned model.
"""
model = SentenceTransformer(base_model_name)
logger.info(f"Loaded base model: {base_model_name}")
logger.info(f"Training on {len(training_pairs)} query-passage pairs")
examples = build_training_examples(training_pairs)
loader = DataLoader(examples, shuffle=True, batch_size=batch_size)
loss = MultipleNegativesRankingLoss(model)
total_steps = len(loader) * epochs
logger.info(f"Training for {epochs} epochs, {total_steps} total steps")
model.fit(
train_objectives=[(loader, loss)],
epochs=epochs,
warmup_steps=warmup_steps,
output_path=output_path,
show_progress_bar=True,
checkpoint_path=output_path,
checkpoint_save_steps=len(loader)
)
logger.info(f"Fine-tuned model saved to: {output_path}")
return model
Benchmark-Ergebnisse: Ein Bankkorpus vor und nach Feintuning
Betrachten Sie einen Abruf-Benchmark-Test anhand eines Korpus zur Unternehmenskreditpolitik, das aus 847 Abschnitten besteht, die aus Richtliniedokumenten, Genehmigungsmatrizen, erweiterten Due-Diligence-Verfahren sowie AML-Richtlinien entnommen wurden. Die Bewertungsgruppe umfasst 120 Abfragen, die sich in vier Kategorien einteilen lassen: enge faktenbasierte Abfragen, Schwellenwertfragen, mehrere Beweismittel erfordernnde Fragen sowie Synthesefragen.
Der Nutzen des Domain-Fine-Tunings ist am ausgeprägtesten bei abfragetypischen Anfragen, wie zum Beispiel der Nachfrage nach dem Genehmigungsschwellenwert für hochriskante Unternehmen in der EU. Genau hier weichen Bankabkürzungen und regulatorisches Vokabular am stärksten von dem ab, was ein allgemein einsetzbares Modell während des Vortrainings gesehen hat; dadurch, dass diese Wissenslücke geschlossen wird, ergibt sich der größte Anstieg bei der Trefferquote. Mehrere-Evidenz- und Syntheseanfragen profitieren allein vom Fine-Tuning weniger, erzielen aber einen deutlichen Gewinn, sobald eine hybride Suchmethode hinzugefügt wird.
Betrachten Sie diese Zahlen als Beispiele dafür, was ein gut durchgeführtes Feinabstimmungsprojekt an einem ordnungsgemäß beschrifteten Bankdatensatz erreichen kann, und nicht als Versprechen. Die Zusammensetzung Ihres eigenen Korpus, die Mischung der Abfragen sowie die Genauigkeit der Beschriftung werden die Ergebnisse verändern. Am wichtigsten ist es, die Qualität der Informationsabrufung nach Abfragetypen aufzuteilen anstelle eines zusammengefassten Scores zu berichten, da jeder Abfragetyp aus unterschiedlichen Gründen versagen kann.
Leistungsaspekte bei der Batch-Embedding-Verarbeitung in großem Maßstab
100.000 Datenblöcke nacheinander über eine Embedding-API zu senden, ist sowohl langsam als auch kostspielig. In der Produktion genutzte Embedding-Pipelines verarbeiten hingegen ihre Eingaben in Batches, um eine höhere Durchsatzrate zu erreichen, die Ratelimits einzuhalten, nach Fehlern sauber wiederherzustellen und eine deterministische Ausgabeerstellung zu gewährleisten.
Batch-Verarbeitung über Anbieter-APIs
OpenAIs Embedding-Endpunkt ermöglicht bis zu 2.048 Eingaben pro Anruf. Coheres Embed-Endpunkt begrenzt sich auf 96 Texte pro Anfrage, es sei denn, man verwendet ihre spezielle Batch-API für größere Aufgaben. Die lokale Ausführung von Inferenz mit sentence-transformers bietet konfigurierbare Batch-Größen, die allein durch den verfügbaren GPU-Speicher begrenzt sind.
import time
import logging
from typing import List, Optional
from openai import OpenAI, RateLimitError, APIError
logger = logging.getLogger(__name__)
client = OpenAI()
def embed_in_batches(
texts: List[str],
model: str = "text-embedding-3-large",
batch_size: int = 512,
max_retries: int = 3,
retry_delay: float = 2.0,
dimensions: Optional[int] = None
) -> List[List[float]]:
"""
Embed a large list of texts using batched API calls with retry logic.
texts: Pre-chunked text strings. Caller is responsible for ensuring
no text exceeds the model's token limit.
batch_size: Number of texts per API call. Stay well below the API limit
to avoid hitting per-request token limits.
dimensions: Optional Matryoshka dimension reduction for supported models.
"""
all_embeddings: List[List[float]] = []
total_batches = (len(texts) + batch_size - 1) // batch_size
for batch_idx in range(0, len(texts), batch_size):
batch = texts[batch_idx: batch_idx + batch_size]
current_batch = batch_idx // batch_size + 1
logger.info(f"Embedding batch {current_batch}/{total_batches} "
f"({len(batch)} texts)")
kwargs = {
"input": batch,
"model": model
}
if dimensions is not None:
kwargs["dimensions"] = dimensions
attempt = 0
while attempt < max_retries:
try:
response = client.embeddings.create(**kwargs)
# Preserve input order: API returns items sorted by index
sorted_items = sorted(response.data, key=lambda x: x.index)
all_embeddings.extend([item.embedding for item in sorted_items])
break
except RateLimitError:
attempt += 1
wait = retry_delay * (2 ** attempt)
logger.warning(f"Rate limit hit on batch {current_batch}. "
f"Waiting {wait:.1f}s before retry {attempt}/{max_retries}")
time.sleep(wait)
except APIError as e:
attempt += 1
logger.error(f"API error on batch {current_batch}: {e}. "
f"Retry {attempt}/{max_retries}")
if attempt >= max_retries:
raise
time.sleep(retry_delay)
logger.info(f"Embedding complete. Total vectors: {len(all_embeddings)}")
return all_embeddings
Lokale Inferenz mit sentence-transformers durchführen
Einige Organisationen sind aufgrund von Datenstandort-Vorgaben gezwungen, Richtliniedokumente nicht an eine externe API zu senden. In solchen Fällen ist die lokale Inferenz mit sentence-transformers die geeignete Lösung.
from sentence_transformers import SentenceTransformer
import numpy as np
from typing import List, Optional
import logging
logger = logging.getLogger(__name__)
class LocalEmbeddingPipeline:
"""
Production-ready local embedding pipeline using sentence-transformers.
Suitable for data-residency-constrained banking environments.
"""
def __init__(
self,
model_name_or_path: str,
device: str = "cpu",
batch_size: int = 64,
normalise: bool = True
):
self.model = SentenceTransformer(model_name_or_path, device=device)
self.batch_size = batch_size
self.normalise = normalise
self.device = device
logger.info(f"Loaded model: {model_name_or_path} on {device}")
def embed(
self,
texts: List[str],
show_progress: bool = True
) -> np.ndarray:
"""
Embed a list of texts. Returns an (N, D) numpy array.
Normalises to unit length if normalise=True (required for cosine similarity).
"""
embeddings = self.model.encode(
texts,
batch_size=self.batch_size,
show_progress_bar=show_progress,
normalize_embeddings=self.normalise,
convert_to_numpy=True
)
logger.info(f"Embedded {len(texts)} texts. "
f"Output shape: {embeddings.shape}")
return embeddings
def embed_query(self, query: str) -> np.ndarray:
"""Embed a single query. Returns a 1D array."""
return self.embed([query], show_progress=False)[0]
Prüfung der Tokenlänge vor dem Einbetten
Falls ein Datenblock länger ist als die Token-Limite eines Modells, wird er ohne Warnung abgeschnitten. In einem Policy-Korpus kann diese stille Abschneidung genau jenen Satz oder die numerische Schwellenwert ausschließen, der den Datenblock ursprünglich zur Abrufung berechtigt hat. Die Überprüfung der Token-Zahlen vor dem Einbetten verhindert dieses Problem, bevor es Ihren Index erreicht.
from transformers import AutoTokenizer
from typing import List, Tuple
import logging
logger = logging.getLogger(__name__)
def validate_chunk_lengths(
chunks: List[str],
model_name: str,
max_tokens: int,
truncation_strategy: str = "warn"
) -> Tuple[List[str], List[int]]:
"""
Validate that all chunks are within the model's token limit.
truncation_strategy:
"warn" - Log a warning for oversized chunks and include them (will be truncated by model).
"skip" - Remove oversized chunks and return only valid ones.
"raise" - Raise ValueError on the first oversized chunk.
Returns (validated_chunks, oversized_indices).
"""
tokeniser = AutoTokenizer.from_pretrained(model_name)
oversized = []
for idx, chunk in enumerate(chunks):
token_count = len(tokeniser.encode(chunk, add_special_tokens=True))
if token_count > max_tokens:
oversized.append(idx)
msg = (f"Chunk {idx} has {token_count} tokens, "
f"exceeds model limit of {max_tokens}. "
f"First 80 chars: {chunk[:80]!r}")
if truncation_strategy == "raise":
raise ValueError(msg)
else:
logger.warning(msg)
if truncation_strategy == "skip" and oversized:
valid = [c for i, c in enumerate(chunks) if i not in set(oversized)]
logger.info(f"Removed {len(oversized)} oversized chunks. "
f"{len(valid)} chunks remain.")
return valid, oversized
return chunks, oversized
Hinzufügen von Metadaten und Herkunftsangaben zu Embeddings
Ein reiner Embedding-Vektor allein reicht nicht aus, damit ein reguliertes RAG-System ordnungsgemäß funktioniert. Jeder Vektor benötigt strukturierte Metadaten, damit die nachgelagerten Schritte der Abrufung, Neubewertung und Erstellung sicherstellen können, woher der Inhalt stammt, Zugriffsrechte durchsetzen, Ergebnisse nach Rechtsgebieten einschränken und auf ein autoritatives Quelldokument verweisen können.
Zurück zum Szenario des Vorschlags für einen Kredit in Höhe von 12 Millionen EUR: Jeder eingebettete Datensatz sollte mindestens die hier angezeigten Felder enthalten:
from dataclasses import dataclass, field
from typing import Optional, List
import uuid
@dataclass
class EmbeddedChunk:
"""
Production embedding record for a banking policy RAG system.
The vector enables retrieval. The metadata enables everything else.
"""
# Vector
vector: List[float]
vector_dimensions: int
embedding_model: str
embedding_model_version: str
# Content
text: str
content_type: str # "narrative", "table_row", "proposition", "image_description"
# Provenance
document_id: str
document_version: str # e.g. "7.2"
policy_id: Optional[str] # e.g. "CRD-EU-047"
jurisdiction: Optional[str] # e.g. "EU"
effective_date: Optional[str]
# Chunk structure
chunk_id: str = field(default_factory=lambda: str(uuid.uuid4()))
parent_id: Optional[str] = None
section: Optional[str] = None
page_number: Optional[int] = None
source_artifact_path: Optional[str] = None # path to original image/table
# Access control
classification: str = "INTERNAL" # "PUBLIC", "INTERNAL", "CONFIDENTIAL"
permitted_roles: List[str] = field(default_factory=list)
# Indexing
indexed_at: Optional[str] = None
indexing_pipeline_version: Optional[str] = None
Die konsequente Verwendung dieser Metadaten in allen Phasen des Prozesses – von der Aufteilung in Datensätze über die Einbettung bis hin zur Vektorindizierung – ist nicht nur eine gute Praxis. Im regulierten Bankwesen gilt die Bereitstellung einer technisch korrekten Antwort, die auf einer veralteten Version der Richtlinien beruht, als Compliance-Verstoß. Die Aufgabe des Vektors besteht darin, den entsprechenden Datensatz zu finden; die Aufgabe der Metadaten ist es, sicherzustellen, dass dieser Datensatz aus der korrekten, aktuellen Version der Quelle stammt.
Bewertung der Qualität der Einbettung
Öffentliche Leaderboards wie MTEB geben allgemeine Bewertungswerte für die Informationsabrufleistung in einer breiten Palette von akademischen Datensätzen an. Diese Zahlen helfen dabei, Modelle auszusortieren, die offensichtlich schlecht abschneiden. Sie reichen jedoch nicht aus, wenn es darum geht, das beste Modell für ein spezialisiertes Korpus wie die interne Richtlinienbibliothek einer Bank auszuwählen.
Die einzige wirklich wichtige Messgröße ist die Leistung eines Modells bei der Verarbeitung eigener Dokumente mit eigenen Abfragen, bewertet anhand von Relevanzurteilen, die man selbst vorgenommen hat.
Erstellung eines Evaluationssets für Informationsabrufsysteme
Ein für einen RAG-Embedding-Prozess in der Bankenbranche erstelltes Testset muss verschiedene Abfruchtypen abdecken:
Einschränkte faktenbasierte Abfragen, die auf einen einzigen autoritativen Datensatz verweisen, wie zum Beispiel die Frage, wie oft hochriskante Unternehmenkunden mindestens eine jährliche Überprüfung durchführen müssen.
Schwellenwertbasierte Fragen, die eine bestimmte numerische Bedingung mit der dazugehörigen Verwaltungsregel verbinden, wie zum Beispiel die Frage, welche Genehmigungsstelle erforderlich ist, sobald eine EU-Unternehmensanlage einen Betrag von 10 Millionen Euro überschreitet.
Mehrfachbelegte Fragen, bei denen eine vollständige Antwort davon abhängt, dass mehr als ein Teilinformationen zusammengeführt werden, wie zum Beispiel die Frage, welche Überprüfungen vor der Einreichung eines Kreditantrags für ein Hochrisikounternehmen durchgeführt werden müssen.
Synthesefragen, die Inhalte aus mehreren Abschnitten gleichzeitig heranziehen, wie zum Beispiel die Anfrage nach einer vollständigen Beschreibung des AML-Kontrollrahmens für Kredite an Hochrisikounternehmen.
Querdokumentfragen, die relevant sind, wenn Richtlinien in verschiedenen Dokumenten aufeinander verweisen.
import numpy as np
from typing import List, Dict, Set
def recall_at_k(
retrieved_ids: List[str],
relevant_ids: Set[str],
k: int
) -> float:
"""
Compute Recall@k for a single query.
relevant_ids is the ground truth set of chunk identifiers.
retrieved_ids is the ordered list of retrieved chunk identifiers.
"""
if not relevant_ids:
return 0.0
top_k_retrieved = set(retrieved_ids[:k])
return len(top_k_retrieved & relevant_ids) / len(relevant_ids)
def mean_reciprocal_rank(
retrieved_ids: List[str],
relevant_ids: Set[str]
) -> float:
"""Compute MRR for a single query."""
for rank, chunk_id in enumerate(retrieved_ids, start=1):
if chunk_id in relevant_ids:
return 1.0 / rank
return 0.0
def evaluate_embedding_model(
model_name: str,
evaluation_queries: List[Dict],
corpus_chunks: List[Dict],
k_values: List[int] = [1, 5, 10, 20]
) -> Dict:
"""
Evaluate an embedding model on a labelled retrieval dataset.
evaluation_queries: List of dicts with "query" and "relevant_chunk_ids" keys.
corpus_chunks: List of dicts with "chunk_id" and "text" keys.
Returns per-query-type and aggregate retrieval metrics.
"""
from sentence_transformers import SentenceTransformer
model = SentenceTransformer(model_name)
corpus_texts = [c["text"] for c in corpus_chunks]
corpus_ids = [c["chunk_id"] for c in corpus_chunks]
corpus_embeddings = model.encode(corpus_texts, normalize_embeddings=True)
results_by_type: Dict[str, List] = {}
all_recall: Dict[int, List[float]] = {k: [] for k in k_values}
all_mrr: List[float] = []
for query_item in evaluation_queries:
query = query_item["query"]
relevant = set(query_item["relevant_chunk_ids"])
query_type = query_item.get("query_type", "unspecified")
query_embedding = model.encode(query, normalize_embeddings=True)
scores = corpus_embeddings @ query_embedding
ranked_indices = np.argsort(scores)[::-1]
retrieved = [corpus_ids[i] for i in ranked_indices]
mrr = mean_reciprocal_rank(retrieved, relevant)
all_mrr.append(mrr)
for k in k_values:
r = recall_at_k(retrieved, relevant, k)
all_recall[k].append(r)
if query_type not in results_by_type:
results_by_type[query_type] = {"mrr": [], "recall": {k: [] for k in k_values}}
results_by_type[query_type]["mrr"].append(mrr)
for k in k_values:
results_by_type[query_type]["recall"][k].append(
recall_at_k(retrieved, relevant, k)
)
aggregate = {
"model": model_name,
"n_queries": len(evaluation_queries),
"mrr": float(np.mean(all_mrr)),
"recall": {k: float(np.mean(all_recall[k])) for k in k_values}
}
per_type = {
qt: {
"mrr": float(np.mean(data["mrr"])),
"recall": {k: float(np.mean(data["recall"][k])) for k in k_values},
"n_queries": len(data["mrr"])
}
for qt, data in results_by_type.items()
}
return {"aggregate": aggregate, "by_query_type": per_type}
Die Bewertung von Abrufmetriken sollte nicht isoliert vorgenommen werden, ohne zu berücksichtigen, wie sie die Qualität der endgültigen Antwort beeinflussen. Angenommen, Recall@5 verbessert sich um drei Punkte, doch der Anteil an nahezu identischen Abschnitten, die abgerufen werden, steigt um 30 Prozent – dieser Kompromiss hilft dem LLM möglicherweise gar nicht, da das Bereitstellen von drei fast identischen Textabschnitten anstelle eines nützlichen Abschnitts keine echten Belege hinzufügt.
Der richtige Ansatz besteht darin, die gesamte Kette zu bewerten – von der ursprünglichen Anfrage bis zur endgültigen Antwort, die dem Benutzer präsentiert wird. Die Rolle eines Embedding-Modells beschränkt sich darauf, die richtigen Belege in das Kontextfenster zu bringen. Ob diese Belege anschließend zu einer Antwort führen, die sowohl genau als auch konform ist, wird in jeder nachfolgenden Phase entschieden.
Das Embedding-Pipeline als Infrastruktur
Sobald ein Datenblock den Embedding-Prozess abgeschlossen hat, muss er mit einem Vector versehen, die Metadaten vollständig ausgefüllt sowie mit einer stabilen Identifikationsnummer herauskommen, die es ermöglicht, ihn später abzurufen, zu aktualisieren oder zu löschen, ohne die daneben liegenden Einträge im Index zu beschädigen.
Es handelt sich dabei um eine Infrastrukturkomponente, nicht um einen einmaligen Script. Für produktionstaugliche Embedding-Pipelines in regulierten Bankumgebungen sind folgende Eigenschaften erforderlich:
- Idempotenz. Wenn ein Datenblock aufgrund eines Wechsels der Modellversionen erneut eingebettet wird, sollte dies den bestehenden Eintrag überschreiben und kein Duplikat erzeugen.
Nichts davon ist in der Produktion optional. Das sind die Eigenschaften, die eine Pipeline, die nur in einer Demo funktioniert, von einer unterscheiden, die man über einen längeren Zeitraum in einem regulierten Umfeld betreiben, prüfen und warten kann.
Zurück zum Vorschlag für einen Kredit in Höhe von 12 Millionen EUR
Die ursprüngliche Frage des Relationship Managers hat sich nicht geändert: Welche Genehmigungsstufe benötigt dieser Deal, und welche Kontrollen müssen vor der Einreichung abgeschlossen werden?
- Teil 4 erläuterte, wie man Abrufeinheiten entwerfen kann, die diese Antworten in ihrer ursprünglichen Form erhalten: die EDD-Richtlinienklausel, die Spalte der Genehmigungsmatrix, die die Befugnisse von GCC angibt, sowie das Workflow-Diagramm, das die Kontrollmechanismen vor der Einreichung darstellt.
- In diesem Teil haben wir diese Abrufeinheiten in suchbare Vektoren umgewandelt, wobei ein Modell verwendet wurde, das anhand von bankenspezifischer Terminologie benchmarkt und daraufhin feinjustiert wurde, um zu verstehen, wie regulatorische Abkürzungen mit ihrem Compliance-Kontext zusammenhängen. Zudem wurden vollständige Herkunftsmetadaten eingefügt, die es ermöglichen, später die Richtlinienversion sowie den Zuständigkeitsbereich zu überprüfen.
Sobald die Anfrage eintrifft, findet der Vektorindex die Zeile der Genehmigungsmatrix, die hochriskante Exposure-Werte über 10 Millionen Euro, die EDD-Klausel sowie das Diagramm mit der Kontrollsequenz enthält – und gibt diese zusammen mit Metadaten zurück, die bestätigen, dass alle drei auf die Police CRD-EU-047, Version 7.2, aus dem EU-Rechtgebiet und gültig ab dem 15. Januar 2026 verweisen.
Was zur Erstellungsstufe gelangt, sind präzise, vollständige und nachvollziehbare Beweismittel.
Das ist der Unterschied zwischen einem Embedding-Pipeline, das lediglich Datenblöcke in eine Vektordatenbank lädt, und einem, das alles beibehält, was für zuverlässige und prüfbare Antworten erforderlich ist.
Vor dem Übergang zur Vektorindexierung: Eine Checkliste
Vor dem Einbringen von eingebetteten Datenblöcken in den Vektorindex sollten Sie Folgendes überprüfen:
- Ist der Eingabetyp-Parameter beim Embedding-Modell für beide Indexierungs- und Abfragenanrufe korrekt eingestellt? Ein solcher Mangel schmälert heimlich die Genauigkeit der Suche.
- Wurde die Token-Länge jedes Teils vor dem Embedding überprüft? Eine stillschweigende Kürzung verändert, was der eingebettete Text tatsächlich darstellt, ohne dass irgendwo im Prozess ein Fehler auftritt.
- Enthält jede Vektoraufzeichnung die vollständigen Herkunftsangaben – Dokumentversion, Richtlinien-ID, Zuständigkeitsbereich und Gültigkeitsdatum?
- Wurde das Embedding-Modell tatsächlich anhand Ihres eigenen Domänenkorpus sowie Abfragemustern getestet, anstatt ausschließlich aufgrund von öffentlichen Benchmark-Ergebnissen ausgewählt?
- Falls Sie das Modell feinabgestimmt haben, verfügen Sie über vorherige und nachherige Suchergebnisse, die anhand eines getrennten Abfragesatzes gemessen wurden?
Fehlt eines dieser Kriterien, wird der Vektorindex dies nicht für Sie erkennen – er speichert einfach alles, was Sie ihm zuführen. In der Datenbank wird nichts darauf hinweisen, dass ein Vektor aus einem abgeschnittenen Datensatz erstellt wurde, eine Abfrage mit dem falschen Eingabetyp verwendet wird oder ein Datensatzteil mit einer Modellversion gespeichert wurde, die nicht mit dem Rest des Index synchron ist. Solche Mängel machen sich nicht selbst bemerkbar; sie treten später als Probleme bei der Abfragequalität auf, die von außen wie Probleme mit dem LLM aussehen.
In einem früheren Teil dieser Serie zeigte Teil 4, wie reine Dokumente in Sucheinheiten umgewandelt werden können, ohne dass ihre Struktur beeinträchtigt wird. In diesem Teil wurde gezeigt, wie aus diesen Sucheinheiten suchbare Vektoren erstellt werden können, wobei ihre Herkunft erhalten bleibt.
Danach kommt der Punkt, an dem diese Vektoren tatsächlich mit dem Index in Berührung kommen: dichte Vektorabfragen, spärliche Suche, hybride Ansätze, annähernde Algorithmen für den nächsten Nachbarn sowie das Metadatenfiltern, das entscheidet, welche Vektoren überhaupt für die Suche in Betracht kommen.
Verwandte Literatur
- Referenzarchitektur für Produktionsreife Unternehmens-Agentic-AI-Systeme — Erfahren Sie die grundlegenden Schichten, Speichermethoden, Abrufmechanismen sowie Sicherheitsmaßnahmen, die erforderlich sind, um Agentic AI von Prototypen zu zuverlässigen Unternehmensproduktionsystemen zu entwickeln.
- Erklärung von Vektordatenbanken: Der Motor hinter RAG und KI-Suchen — Lernen Sie, wie Vektordatenbanken Text in Embeddings umwandeln, semantische Suchfunktionen sowie RAG-Pipelines unterstützen und so reale KI-Anwendungen wie Empfehlungssysteme antreiben.