Abrufmuster für RAG-Pipelines implementieren

Abgeschlossen

Die durch Abruf erweiterte Generierung (RAG) verbessert die Sprachmodell-Antworten, indem sie relevanten Kontext aus Ihren Daten bereitstellt. Die Retriever-Komponente durchsucht Ihre Wissensbasis und gibt Dokumente zurück, die von der LLM verwendet werden, um genaue, geerdete Antworten zu generieren. Die Azure-Datenbank für PostgreSQL mit pgvector dient als effektives Abfragesystem, das Ihre Dokumenteinbettungen speichert und Ähnlichkeitsabfragen ausführt, die den Generierungsschritt unterstützen.

Diese Einheit befasst sich mit dem Entwerfen von Schemas für RAG-Workloads, der Implementierung von Blockabrufabfragen, dem Erstellen von Dokumentaufnahmepipelines, dem Zurückgeben von Zitatmetadaten und der Auswertung der Abrufqualität.

Grundlegendes zur RAG-Architektur und der Retriever-Rolle

RAG-Systeme folgen einem dreistufigen Muster:

  1. Einbetten von Abfragen: Konvertieren der Frage des Benutzers in einen Vektor mithilfe eines Einbettungsmodells
  2. Abrufen: Durchsuchen des Vektorspeichers nach Dokumenten ähnlich der Abfrageeinbettung
  3. Generation: Übergeben der abgerufenen Dokumente als Kontext an ein LLM, das eine Antwort generiert

PostgreSQL fungiert als Retriever in dieser Architektur. Die Qualität des Abrufs wirkt sich direkt auf die Erzeugungsqualität aus. Wenn der Retriever irrelevante Dokumente zurückgibt, fehlt dem LLM der Kontext, der zur korrekten Antwort erforderlich ist, und generiert möglicherweise falsche Informationen. Wenn der Retriever relevante Dokumente verpasst, ist die Antwort möglicherweise unvollständig.

Für einen Rechtswissenschaftlichen Mitarbeiter bedeutet ein effektiver Abruf, die Fallvorfälle, Statuten und Vertragsklauseln zu finden, die sich auf die Frage des Anwalts beziehen. Das LLM synthetisiert diese Quellen dann in eine kohärente Antwort mit Zitaten.

Entwerfen von Schemas für RAG-Workloads

RAG-Anwendungen arbeiten in der Regel mit Dokumentblöcken und nicht mit ganzen Dokumenten. Lange Dokumente überschreiten LLM-Kontextgrenzwerte und enthalten möglicherweise Abschnitte, die für eine bestimmte Abfrage irrelevant sind. Das Chunking unterteilt Dokumente in kleinere, fokussierte Segmente, die unabhängig voneinander abgerufen werden können.

Trennen von Dokumenten und Blöcken

Verwenden Sie zwei Tabellen: eine für Quelldokumente und eine für Blöcke mit Einbettungen:

CREATE TABLE source_documents (
    id SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    source_url TEXT,
    document_type TEXT,
    ingested_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE document_chunks (
    id SERIAL PRIMARY KEY,
    document_id INTEGER REFERENCES source_documents(id) ON DELETE CASCADE,
    chunk_index INTEGER NOT NULL,
    content TEXT NOT NULL,
    embedding vector(1536),
    token_count INTEGER,
    start_char INTEGER,
    end_char INTEGER,
    UNIQUE (document_id, chunk_index)
);

Dieses Design bietet mehrere Vorteile:

  • Flexibler Abruf: Sie können einzelne Abschnitte abrufen oder Dokumentabschnitte rekonstruieren
  • Quellnachverfolgung: Jeder Block verweist auf seine Quelle für Zitate
  • Effizienter Speicher: Dokumentmetadaten werden einmal gespeichert, nicht pro Block dupliziert
  • Einfache Updates: Das Ersetzen eines Dokuments bedeutet, dass seine Teilstücke gelöscht und erneut eingelesen werden.

Hinzufügen von Metadaten zum Filtern und Kontext

Fügen Sie Metadaten hinzu, die bei der Filter- und Kontextrekonstruktion hilfreich sind:

CREATE TABLE document_chunks (
    id SERIAL PRIMARY KEY,
    document_id INTEGER REFERENCES source_documents(id) ON DELETE CASCADE,
    chunk_index INTEGER NOT NULL,
    content TEXT NOT NULL,
    embedding vector(1536),
    token_count INTEGER,
    section_title TEXT,
    page_number INTEGER,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Abschnittstitel und Seitenzahlen helfen Benutzern, abgerufene Informationen zu überprüfen und dem LLM zu helfen, den Kontext der einzelnen Abschnitte zu verstehen.

Erstellen von Indizes für RAG-Abfragemuster

Vektorverknüpfungsindizes (HNSW oder IVFFlat) ermöglichen schnelle Einbettungssuche, wie weiter oben in diesem Modul beschrieben. RAG-Workloads profitieren auch von B-Tree-Indizes, die Verknüpfungen und Filter gezielt beim Datenblockabruf beschleunigen. Beim Abrufen von Segmenten stellen Sie häufig eine Verbindung zu Quelldokumenten für Zitiermetadaten her und filtern oder sortieren sie nach ihrer Position innerhalb eines Dokuments. Ohne diese Indizes scannt PostgreSQL die gesamte Datenabschnittstabelle für jede Abrufabfrage.

-- B-tree index for document lookups (speeds up JOINs to source_documents)
CREATE INDEX document_chunks_document_id_idx
ON document_chunks (document_id);

-- Composite index for chunk ordering (supports context window queries)
CREATE INDEX document_chunks_doc_chunk_idx
ON document_chunks (document_id, chunk_index);

Der zusammengesetzte Index (document_id, chunk_index) ist besonders wichtig für den Abruf von Kontextfenstern, bei dem Sie benachbarte Blöcke abrufen, um den umgebenden Kontext bereitzustellen. Ohne dies muss PostgreSQL alle Blöcke scannen, um Nachbarn zu finden.

Implementieren des Blockabrufs für die Kontexterstellung

RAG-Abfragen rufen Blöcke ab, die zum Kontext des LLM werden. Das ausgewählte Abrufmuster wirkt sich sowohl auf die Antwortqualität als auch auf die Tokeneffizienz aus. Sie müssen drei konkurrierende Aspekte in Einklang bringen: ausreichend relevante Inhalte abzurufen, innerhalb der Tokengrenzen zu bleiben und ausreichenden Kontext bereitzustellen, damit die LLM jede Einheit verstehen kann.

Der einfachste Ansatz ruft die top-k-ähnlichen Blöcke direkt ab. Dies funktioniert gut, wenn Ihre Blöcke eigenständig sind und Ihre Fragen sauber mit einzelnen Blöcken übereinstimmen. Rechtliche Dokumente, technische Spezifikationen und narrative Inhalte verbreiten jedoch häufig Konzepte über mehrere Absätze hinweg. Ein Block kann auf "die vorstehenden Bedingungen" oder "wie oben beschrieben" verweisen, ohne den referenzierten Text zu enthalten.

In diesen Fällen ruft der Abruf von Kontextfenstern benachbarte Blöcke zusammen mit jeder Übereinstimmung ab. Wenn der Abschnitt 47 der Abfrage am ähnlichsten ist, rufen Sie auch Blöcke 46 und 48 ab. Dies erhöht die Tokennutzung, verringert jedoch das Risiko unvollständiger Antworten. Der Kompromiss hängt von Ihrer Inhaltsstruktur ab: Hochstrukturierte Dokumente mit klaren Abschnittsgrenzen benötigen weniger umgebenden Kontext als fließenden narrativen Text.

Tokenbeschränkungen fügen eine weitere Einschränkung hinzu. LLMs verfügen über feste Kontextfenster, und Sie benötigen Platz für die Systemaufforderung, Benutzerfrage und generierte Antwort. Wenn Sie jeweils 10 Blöcke mit jeweils 500 Token abrufen, haben Sie 5.000 Token verbraucht, bevor die LLM ein einzelnes Wort schreibt. Verfolgen Sie die kumulierte Anzahl der Tokens während des Abrufs, um innerhalb des Budgets zu bleiben.

Die folgende Abfrage kombiniert diese Muster. Es ruft die ähnlichsten Segmente mit ihren Nachbarn ab, erfasst kumulative Token und enthält Zitationsmetadaten.

WITH matched_chunks AS (
    -- Find the most similar chunks
    SELECT id, document_id, chunk_index, embedding <=> $1 AS distance
    FROM document_chunks
    ORDER BY embedding <=> $1
    LIMIT 3
),
context_window AS (
    -- Expand to include adjacent chunks for context
    SELECT DISTINCT dc.id, dc.document_id, dc.chunk_index, dc.content,
           dc.token_count, mc.distance
    FROM matched_chunks mc
    JOIN document_chunks dc ON dc.document_id = mc.document_id
        AND dc.chunk_index BETWEEN mc.chunk_index - 1 AND mc.chunk_index + 1
),
token_limited AS (
    -- Track cumulative tokens to respect LLM context limits
    SELECT cw.*, sd.title AS source_title, sd.source_url,
           SUM(cw.token_count) OVER (ORDER BY cw.distance, cw.chunk_index) AS cumulative_tokens
    FROM context_window cw
    JOIN source_documents sd ON cw.document_id = sd.id
)
SELECT id, content, source_title, source_url, distance
FROM token_limited
WHERE cumulative_tokens <= 3000
ORDER BY distance, chunk_index;

Passen Sie die Parameter basierend auf Ihrem Anwendungsfall an. Für einen juristischen Rechercheassistenten, der spezifische Fragen zu Klauseln beantwortet, könnten Sie beispielsweise fünf Treffer ohne Kontextfenster abrufen. Für einen Kundendienst-Bot, der offene Fragen zur Produktdokumentation beantwortet, können Sie drei Treffer mit jeweils zwei Abschnitten des Kontextes abrufen.

Verwalten von Pipeline für Dokumentenaufnahmen

Neue Dokumente müssen verarbeitet werden, bevor sie durchsucht werden können. Die Aufnahmepipeline teilt Dokumente in Blöcke auf, generiert Einbettungen und speichert alles in PostgreSQL.

Wie Sie Dokumente direkt teilen, wirkt sich auf die Abrufqualität aus. Die richtige Aufteilungsstrategie hängt von Ihrer Inhaltsstruktur und Ihrem Abfragemuster ab.

  • Blöcke mit fester Größe: Teilen Sie alle N-Zeichen oder Token. Dieser Ansatz ist einfach zu implementieren und erzeugt eine voraussagbare Tokenanzahl, jedoch kann er Sätze oder Absätze mitten im Gedanken schneiden. Verwenden Sie Blöcke mit fester Größe, wenn Der Inhalt keine klaren strukturellen Grenzen aufweist oder wenn Sie konsistente Blockgrößen für die Tokenbudgetplanung benötigen.

  • Semantische Blöcke: Teilen Sie an natürlichen Begrenzungen wie Absätzen, Abschnitten oder Sätzen. Dies behält die Bedeutung innerhalb der einzelnen Blöcke bei, erzeugt jedoch variable Größen. Verwenden Sie semantische Blöcke für strukturierte Dokumente, bei denen Abschnitte vollständige Gedanken darstellen, z. B. Rechtliche Klauseln, API-Dokumentation oder HÄUFIG gestellte Fragen.Use semantic chunking for structured documents where sections represent complete thoughts, such as legal clauses, API documentation, or FAQ entries.

  • Überlappende Blöcke: Fügen Sie Text aus benachbarten Blöcken ein, um den Kontext an Grenzen beizubehalten. Beispielsweise stellt eine 200-Zeichen-Überlappung zwischen 1.000-Zeichenblöcken sicher, dass Konzepte, die Blockgrenzen umfassen, in mindestens einem vollständigen Block angezeigt werden. Verwenden Sie überlappende Elemente, wenn Ihre Abfragen möglicherweise mit Inhalten in der Nähe von Blockgrenzen übereinstimmen.

Für das Szenario des rechtswissenschaftlichen Assistenten funktioniert die semantische Segmentierung an Absatz- oder Abschnittsgrenzen gut, da Rechtstexte in einzelne Klauseln und Argumente unterteilt sind. Jeder Teil stellt ein vollständiges rechtsrechtliches Konzept dar, das im Kontext des LLM eigenständig stehen kann.

Nachdem Sie Ihre Dokumente in Abschnitte unterteilt und Einbettungen generiert haben, fügen Sie diese effizient mithilfe von Batchvorgängen ein. Das Zwei-Tabellen-Design erfordert, dass zuerst das Quelldokument eingefügt wird, um seine ID zu erhalten, und anschließend alle Teile in einer einzigen mehrzeiligen INSERT eingefügt werden. Dieser Ansatz minimiert Roundtrips zur Datenbank und behält die Beziehung zwischen Dokumenten und Blöcken intakt. Die meisten Einbettungs-APIs akzeptieren auch mehrere Texte pro Anforderung, sodass Sie Einbettungen für die Blöcke eines gesamten Dokuments in einem API-Aufruf generieren können, bevor Sie sie einfügen.

Dokumentaktualisierungen erfordern eine Entscheidung: Versionieren Sie Dokumente oder ersetzen Sie sie? Für die meisten RAG-Anwendungen ist der Ersatz einfacher. Löschen Sie die vorhandenen Blöcke (die ON DELETE CASCADE Einschränkung behandelt dies automatisch, wenn Sie das Quelldokument löschen), und nehmen Sie dann den aktualisierten Inhalt erneut auf. Mit diesem Ansatz wird sichergestellt, dass Die Abrufergebnisse immer den aktuellen Dokumentstatus widerspiegeln. Wenn Sie den Versionsverlauf benötigen, fügen Sie eine version Spalte hinzu source_documents , und behalten Sie alte Blöcke zusammen mit neuen abschnitten bei, und filtern Sie nach Version zur Abfragezeit.

HNSW-Indizes bewältigen Löschungen problemlos, ohne dass sie neu erstellt werden müssen. IVFFlat-Indizes können nach wesentlichen Änderungen Fragmentierung ansammeln, was eine regelmäßige Neukonfiguration erfordert, um die Abfrageleistung aufrechtzuerhalten.

Implementierung des Abrufs mit Zitaten

Zitate verwandeln RAG aus einer schwarzen Box in ein transparentes Recherchetool. Wenn der Rechtswissenschaftliche Assistent eine Vertragsklausel angibt, muss der Anwalt das Zitat gegen das Originaldokument überprüfen. Ohne Zitatmetadaten müssen Benutzer sich blind auf das Ergebnis des LLM verlassen – ein erhebliches Risiko in rechtlichen, medizinischen oder finanziellen Kontexten, wo Genauigkeit entscheidend ist.

Effektive Zitate erfordern mehr als nur den Blockinhalt. Ihre Abrufabfragen sollten den Quelldokumenttitel, die URL oder die Dokument-ID, die Abschnittsüberschrift und die Seitenzahl zurückgeben, sofern verfügbar. Mit diesen Metadaten kann Ihre Anwendung Zitate formatieren, denen Benutzer tatsächlich bis zur Quelle folgen können. Die Entfernungsbewertung hilft auch: Sie können besonders vertrauenswürdige Zitate anzeigen, während Übereinstimmungen mit niedrigerer Konfidenz für die Benutzerüberprüfung gekennzeichnet werden.

Eine häufige Herausforderung besteht, wenn mehrere Blöcke aus demselben Dokument einer Abfrage entsprechen. Wenn Blöcke 12, 15 und 18 aus der "Arbeitsvertragsvorlage" alle in den Top-Ergebnissen angezeigt werden, führt deren Auflistung als drei separate Zitate dazu, dass die Antwort überladen wird. Gruppieren Sie stattdessen Blöcke nach Quelldokument, und präsentieren Sie sie als einzelnes Zitat mit mehreren relevanten Auszügen. Dieser Ansatz erzeugt eine übersichtlichere Ausgabe und hilft Benutzern, den vollständigen Kontext jeder Quelle zu sehen.

Die folgende Abfrage veranschaulicht den abruf von Dokumenten gruppiert. Er bewertet Blöcke innerhalb jedes Dokuments, beschränkt sich auf drei Blöcke pro Dokument, um zu vermeiden, dass der Kontext überwältigt wird, und aggregiert den Inhalt für eine übersichtlichere Zitatformatierung:

WITH ranked_chunks AS (
    SELECT
        dc.*,
        sd.title AS document_title,
        sd.source_url,
        dc.embedding <=> $1 AS distance,
        ROW_NUMBER() OVER (PARTITION BY dc.document_id ORDER BY dc.embedding <=> $1) AS rank_in_doc
    FROM document_chunks dc
    JOIN source_documents sd ON dc.document_id = sd.id
    WHERE dc.embedding <=> $1 < 0.5
)
SELECT
    document_id,
    document_title,
    source_url,
    array_agg(content ORDER BY chunk_index) AS chunks,
    MIN(distance) AS best_distance
FROM ranked_chunks
WHERE rank_in_doc <= 3
GROUP BY document_id, document_title, source_url
ORDER BY best_distance
LIMIT 5;

Ihre Anwendung kann dies dann in benutzerfreundliche Zitate formatieren:

"Der Arbeitgeber kann diese Vereinbarung mit 30 Tagen Kündigungsfrist kündigen." — Arbeitsvertragsvorlage, Abschnitt 4.2, Seite 3

Bewerten und Verbessern der Abrufqualität

Die Abrufqualität bestimmt die RAG-Effektivität. Schlechter Abruf führt zu einer schlechten Erzeugung, unabhängig davon, wie leistungsfähig Ihr LLM ist. Wenn der Retriever irrelevante Datenblöcke zurückgibt, verschwendet das LLM Kontextfensterkapazität an nutzlosen Text. Wenn der Retriever relevante Blöcke verpasst, fehlt der LLM an den erforderlichen Informationen, um richtig zu antworten. Die Messung der Abrufqualität getrennt von der Erzeugungsqualität hilft Ihnen, zu diagnostizieren, wo Ihre RAG-Pipeline Verbesserung benötigt.

Drei Metriken erfassen unterschiedliche Aspekte der Abrufleistung:

  • Präzision: Der Anteil der abgerufenen Blöcke, die tatsächlich relevant sind. Geringe Genauigkeit bedeutet, dass der LLM irrelevanten Kontext erhält, der ihn verwirren oder dazu führen kann, dass falsche Informationen generiert werden. Verbessern Sie die Genauigkeit, indem Sie Entfernungsschwellenwerte erhöhen oder die Anzahl der abgerufenen Blöcke verringern.

  • Erinnern: Der Anteil der relevanten Blöcke, die abgerufen werden. Geringe Abrufrate bedeutet, dass das LLM wichtige Informationen verpasst, was zu unvollständigen oder falschen Antworten führt. Verbessern Sie den Rückruf durch Lockerung von Entfernungsschwellenwerten, Erhöhen der abgerufenen Blockanzahl oder Anpassen von Indexparametern wie ef_search.

  • Mean Reciprocal Rank (MRR): Wie weit oben das erste relevante Ergebnis in der Rangliste erscheint. MRR ist wichtig, da LLMs früheren Kontext stärker gewichten, und Benutzer, die Zitate scannen, bemerken zuerst die wichtigsten Ergebnisse. Verbessern Sie MRR, indem Sie das Einbettungsmodell oder die Vorverarbeitung von Abfragen verfeinern.

Um diese Metriken zu messen, benötigen Sie ein Auswertungsdatenset: eine Reihe repräsentativer Abfragen, die mit menschlichen Beurteilungen darüber gekoppelt sind, welche Abschnitte relevant sind. Das Erstellen dieses Datasets erfordert Vorabaufwand – jemand muss Beispielabfragen ausführen und die Ergebnisse beschriften – aber es zahlt sich aus, indem Sie datengesteuerte Verbesserungen vornehmen können, anstatt zu erraten. Beginnen Sie mit 20-50 Abfragen, die die Arten von Fragen darstellen, die Ihre Benutzer tatsächlich stellen. Rufen Sie für jede Abfrage die obersten 10-20 Blöcke ab, und lassen Sie einen Domain-Experten ihre Relevanz auf einer einfachen Skala (irrelevant, etwas relevant, hoch relevant) bewerten.

Mit diesem Auswertungssatz können Sie die Genauigkeit und Erinnerung bei verschiedenen Grenzwerten (z. B. Präzision@5, Erinnerung@10) messen und nachverfolgen, wie sich Änderungen an Ihrer Pipeline auf diese Metriken auswirken. Führen Sie Ihre Abrufabfragen mit dem Auswertungssatz aus, vergleichen Sie die Ergebnisse mit den menschlichen Urteilen, und berechnen Sie die Metriken. Die meisten Teams automatisieren dies in einem Bewertungsskript, das ausgeführt wird, wenn sie Chunking-Strategien, Einbettungsmodelle oder Indexparameter ändern.

Wenn Metriken unter das Zielniveau sinken, experimentieren Sie systematisch mit Parametern, die sich auf die Präzisions-Recall-Abwägung auswirken.

  • Blockgröße: Kleinere Blöcke verbessern die Genauigkeit, indem sie gezieltere Inhalte liefern, könnten jedoch die Rückrufrate beeinträchtigen, indem relevante Informationen über mehrere Blöcke verteilt werden. Größere Blöcke verbessern die Erinnerung, verwässern aber die Relevanz.

  • Blocküberlappung: Mehr Überlappung behält den Kontext über Grenzen hinweg bei, sodass Abfragen, die mit Inhalten in der Nähe von Blöcken übereinstimmen, unterstützt werden. Überlappungen erhöhen jedoch die Speicheranforderungen und könnten redundante Inhalte zurückgeben.

  • Einbettungsmodell: Verschiedene Modelle erfassen unterschiedliche semantische Beziehungen. Ein Modell, das auf Rechtstext geschult ist, könnte ein allgemeines Modell für die rechtswissenschaftliche Forschung übertrifft. Ziehen Sie domänenspezifische oder fein abgestimmte Modelle in Betracht, wenn allgemeine Modelle unterperform sind.

  • Indexparameter: Höhere Werte für ef_search (HNSW) oder probes (IVFFlat) verbessern den Recall, indem mehr Kandidaten durchsucht werden, was jedoch mit einer erhöhten Abfragelatenz verbunden ist. Beginnen Sie mit Standardparametern, und erhöhen Sie sie nur, wenn der Rückruf nicht ausreicht.

  • Entfernungsschwellenwerte: Engere Schwellenwerte verbessern die Genauigkeit, indem marginale Übereinstimmungen ausgeschlossen werden. Losere Schwellenwerte verbessern den Rückruf, indem mehr Kandidaten eingeschlossen werden. Verwenden Sie Ihr Auswertungsdatenset, um den Schwellenwert zu finden, der beide Metriken für Ihren Anwendungsfall ausgleicht.