Anwendung von Embeddings und RAG

KI: Anwendung von Embeddings und RAG

In den bisherigen AI‑Experimenten auf serdg.net habe ich hauptsächlich große Sprachmodelle als Dienste zur Textverarbeitung verwendet: Artikel bearbeiten, Text in HTML umwandeln, übersetzen und zusätzliche Informationen generieren.

Als nächsten Schritt habe ich beschlossen, eine andere Aufgabe zu untersuchen — die Arbeit mit Embeddings, semantischer Suche und RAG (Retrieval‑Augmented Generation).

Das Hauptziel der Arbeit ist weniger die Erweiterung der Websitefunktionalität als vielmehr ein praktisches Kennenlernen der Prinzipien beim Aufbau solcher Systeme.

Im Projekt sind zwei miteinander verbundene, aber eigenständige Mechanismen entstanden:

  1. Suche semantisch ähnlicher Artikel mithilfe von Embeddings;
  2. ein RAG‑Service, der erlaubt, Fragen zu den auf serdg.net veröffentlichten Materialien zu stellen.

Obwohl beide Mechanismen Embeddings verwenden, lösen sie unterschiedliche Aufgaben.


Was sind Embeddings

Bei der üblichen Textsuche ist die Grundeinheit das Wort. Zum Beispiel, wenn ein Nutzer sucht:

Azure Container Apps

sucht die normale Suche nach Dokumenten, die die Wörter Azure, Container oder Apps enthalten. Zwei Texte können jedoch dasselbe Thema beschreiben und unterschiedliche Formulierungen verwenden. Zum Beispiel:

Deploying several containers into Azure

Hosting frontend and backend services in Azure Container Apps

Aus Sicht von String‑Matching sind diese Sätze verschieden, inhaltlich sind sie jedoch nah beieinander. Für solche Aufgaben werden Embeddings verwendet — Repräsentationen von Text als numerischer Vektor.

"Azure Container Apps deployment"
        ↓
Embedding model
        ↓
[0.012, -0.083, 0.041, ...]

Ein echter Vektor enthält Hunderte oder Tausende Zahlen. Die wichtigste Eigenschaft von Embeddings ist, dass Texte mit ähnlicher Bedeutung im Vektorraum nahe beieinander liegen. Nachdem die Embeddings berechnet sind, lässt sich die Bedeutung von Texten mathematisch vergleichen. Eine häufig verwendete Vergleichsmethode ist die Cosine‑Similarity. Wenn die Embeddings zweier Texte als A und B bezeichnet werden, kann man die kosinussimilarität zwischen ihnen berechnen: je höher die Similarity, desto näher der Inhalt der Texte beieinanderliegt.

Wofür Embeddings genutzt werden können

Für einen Blog ergeben sich daraus mehrere nützliche Szenarien.

  • Related articles. Zum Beispiel öffnet ein Leser einen Artikel über das Starten von Angular SSR und .NET API in Azure Container Apps. Das System kann weitere Artikel finden, die mit Docker, Azure, Deployment, Multi‑Container‑Anwendungen und CI/CD zu tun haben. Das funktioniert ohne manuelles Pflegen gleicher Tags — die Verbindung wird über den Inhalt bestimmt.
  • Semantische Suche. Ein Nutzer kann nach "how to deploy frontend and backend together" suchen, selbst wenn dieser Ausdruck nicht in den Artikeln vorkommt. Das Embedding der Anfrage wird mit den Embeddings der Artikel verglichen und es werden Dokumente mit ähnlicher Bedeutung zurückgegeben.
  • RAG. Embeddings sind einer der Hauptbestandteile von Retrieval‑Augmented Generation.

Implementierung von Embeddings

Ich habe versucht, die Geschäftslogik der Anwendung nicht an ein konkretes Azure OpenAI‑API zu binden. Zur Erzeugung von Embeddings verwende ich die Abstraktion IEmbeddingGenerator<string, Embedding<float>> aus Microsoft.Extensions.AI. Dadurch arbeitet der restliche Code über das Interface:

Article
   ↓
Embedding service
   ↓
IEmbeddingGenerator
   ↓
AI provider

Derzeit ist der reale Provider Azure OpenAI, aber die Anwendungslogik sollte die Details des HTTP‑APIs eines bestimmten Modells nicht kennen. Das ist ein zentrales Architekturprinzip der gesamten AI‑Integration: Der AI‑Provider befindet sich an der Grenze der Anwendung, und die Geschäftslogik arbeitet über Abstraktionen.

Separates Embedding für jede Lokalisierung

serdg.net unterstützt Artikel auf Russisch, Englisch und Deutsch. Deswegen wird das Embedding nicht einfach für einen Article erstellt, sondern für die konkrete Sprachversion:

Article 25
 ├── EN → embedding
 ├── DE → embedding
 └── RU → embedding

Das ermöglicht semantische Suche innerhalb der gewünschten Sprache und verhindert das Vermischen von Lokalisierungen. Für das Embedding wird der Inhalt der jeweiligen Sprachversion des Artikels genutzt, ausreichend, um seinen allgemeinen Sinn darzustellen.

Vector‑Storage direkt in SQL Server

Für das Experiment wollte ich keine separate spezialisierte Vektordatenbank hinzufügen. Das Datenvolumen von serdg.net ist klein, und nach dem Umstieg auf SQL Server 2025 lassen sich Embeddings direkt in der bestehenden Datenbank speichern und vergleichen.

Für die Suche nach ähnlichen Dokumenten wird die eingebaute Unterstützung für Vektordaten in SQL Server und EF.Functions.VectorDistance verwendet.

Das Schema ist einfach:

Article
   ↓
Embedding
   ↓
SqlServer VECTOR
   ↓
cosine distance
   ↓
Related articles

Für ein Projekt dieser Größe ist das eine bequeme Lösung: kein zusätzlicher Dienst wie Qdrant, Pinecone oder Weaviate ist nötig. Bei wachsendem Datenvolumen können sich die Anforderungen ändern, aber für die aktuelle Aufgabe deckt SQL Server das Szenario ab.

Von Embeddings zu RAG

Die semantische Suche beantwortet die Frage „Welche Artikel sind dem Suchbegriff inhaltlich ähnlich?“, aber der Nutzer will normalerweise keine Liste von Vektoren, sondern eine konkrete Antwort. Zum Beispiel:

Как в проекте использовались Azure Container Apps?

Man kann ähnliche Artikel finden oder die gefundenen Auszüge an ein Sprachmodell übergeben und es bitten, eine Antwort zu formulieren. So entsteht Retrieval‑Augmented Generation.

Question
   ↓
Retrieve relevant information
   ↓
Add retrieved information to prompt
   ↓
LLM
   ↓
Grounded answer

Der Schlüsselpunkt ist das Retrieval. Das Modell darf sich nicht auf ein eigenes „Gedächtnis“ über den Inhalt von serdg.net verlassen. Die Anwendung findet zuerst die relevanten Ausschnitte und übergibt diese dann an das Modell.

Warum RAG nötig ist

Ein gewöhnliches LLM antwortet basierend auf Wissen aus dem Training, aber es kennt keine Details meines Projekts, sofern diese nicht explizit übergeben wurden. Zum Beispiel: „Wie wird auf serdg.net das Aktualisieren von Embeddings umgesetzt?“ Ein generelles Modell kann typische Ansätze beschreiben, aber das garantiert nicht die Übereinstimmung mit der tatsächlichen Implementierung auf der Seite.

RAG verändert die Aufgabe: Das Modell erhält die Frage und konkrete Fragmente aus Artikeln mit der Anweisung, ausschließlich darauf basierend zu antworten und anzugeben, wenn die Daten nicht ausreichen. In dieser Rolle dient das LLM als Werkzeug zur Synthese und Formulierung einer Antwort basierend auf den gefundenen Quellen.

Chunking

Für Related Articles reicht oft das Embedding des gesamten Artikels. Für RAG ist das nicht ausreichend. Wenn ein Artikel 3–5 Tausend Wörter und mehrere Themen enthält, mittelt das Embedding des gesamten Artikels die Bedeutung und hilft bei einer speziellen Frage möglicherweise nicht weiter. Deshalb werden Artikel in kleine Teile — Chunks — zerlegt.

In der aktuellen Implementierung wird aus dem HTML der sichtbare Text extrahiert (ohne script, style, Kommentare, template) und eine Struktur gebildet:

Title

Header

Article text...

Dann wird der Text in Fragmente zerlegt. In der Konfiguration beträgt die Größe eines Chunks ungefähr 500 Token, der Overlap zwischen benachbarten Chunks etwa 75 Token:

Chunk 1
████████████████████

Chunk 2
              ████████████████████

Chunk 3
                            ████████████████████

Der Overlap ist notwendig, damit Informationen an der Grenze zweier Chunks nicht den Kontext verlieren. Wo möglich, werden Absatzgrenzen bewahrt; ein hartes Zerschneiden erfolgt nur bei zu großen Blöcken.

Separater RAG‑Index

Für jeden Chunk wird ein eigenes Embedding erstellt. Konzeptionell sieht der Index so aus:

Article
 ├── Chunk 1 → embedding
 ├── Chunk 2 → embedding
 ├── Chunk 3 → embedding
 └── Chunk 4 → embedding

Bei einer Anfrage:

Question
   ↓
Question embedding
   ↓
Vector search
   ↓
Most similar chunks

erfolgt der Vergleich mit kleinen thematischen Fragmenten, was die Wahrscheinlichkeit erhöht, den relevanten Textabschnitt zu finden.

Index‑Profile

Der RAG‑Index hängt nicht nur vom Text ab: Einflussfaktoren sind Embedding‑Provider, Deployment, Dimensionalität, Chunk‑Größe, Overlap und die Version des Indexierungsalgorithmus. In der Implementierung wird das Konzept des RAG‑Profiles verwendet — ein Kennzeichen der Konfiguration, mit der der Index erstellt wurde. Wenn wichtige Einstellungen geändert werden, ändert sich das Profil. Das verhindert den Vergleich von Daten, die mit inkompatiblen Konfigurationen erzeugt wurden.

Wiederverwendung von Embeddings für Chunks

Beim Bearbeiten eines Artikels wird der RAG‑Index neu aufgebaut, aber nicht unbedingt alle Chunks erneut an das Embedding‑Modell gesendet. Für jedes Fragment wird ein Hash berechnet. Wenn der Text des Chunks unverändert ist und das Profil gleich geblieben ist, wird das vorhandene Embedding wiederverwendet.

Before:

Chunk A → hash1 → embedding1
Chunk B → hash2 → embedding2
Chunk C → hash3 → embedding3

After editing:

Chunk A → hash1 → reuse embedding1
Chunk B → hash4 → generate new embedding
Chunk C → hash3 → reuse embedding3

Das reduziert die Anzahl der Aufrufe beim AI‑Provider.

Index‑Aktualisierung als Teil des Speichervorgangs

In der aktuellen Implementierung gibt es keinen separaten Background‑Worker für RAG. Nach dem Speichern aktualisiert der Artikel den entsprechenden RAG‑Index. Die Pipeline sieht so aus:

Save article
      ↓
Article stored
      ↓
Build chunks
      ↓
Generate/reuse embeddings
      ↓
Replace RAG index

Ich habe dieses Schema gewählt, weil es einfach und für die Projektgröße passend ist. Nachteil ist, dass das Speichern länger dauert (schätzungsweise etwa 500 ms für einen durchschnittlichen Artikel). Dafür wird keine Message Queue, kein Background‑Worker, keine verteilte Koordination, kein separater Indexierungsstatus und keine Retry‑Infrastruktur benötigt. Für ein kleines persönliches Projekt ist dieser Trade‑off akzeptabel. Bei deutlich höherer Last kann die Indexierung in einen asynchronen Hintergrundprozess ausgelagert werden.

Retrieval

Bei einer Frage wird zunächst das Embedding der Anfrage erstellt, dann erfolgt eine Vektor‑Suche unter den Chunks derselben Lokalisierung. In SQL Server wird die Cosine‑Distance verwendet.

question embedding
      ↓
VECTOR_DISTANCE
      ↓
similarity
      ↓
ORDER BY similarity DESC

Ein einfaches TOP N ist nicht ausreichend: Wenn ein großer Artikel viele ähnliche Chunks enthält, kann er den gesamten Kontext füllen. Deshalb wird zusätzlich die Anzahl der Chunks pro Artikel begrenzt. In der aktuellen Konfiguration wählt das Retrieval bis zu sechs Fragmente, aber nicht mehr als zwei aus einem Artikel — das erhöht die Vielfalt der Quellen. Zusätzlich wird ein minimaler Similarity‑Schwellenwert angewandt: schwach verknüpfte Fragmente werden nicht an das Modell übergeben.

Beschränkung des Kontexts

Gefundene Chunks können nicht einfach an das LLM gesendet werden: Das Modell hat ein Context‑Window und jede Anfrage hat Kosten. Der Kontext wird unter Berücksichtigung des Token‑Budgets zusammengestellt. Einschränkungen umfassen:

  • Länge der Frage;
  • Anzahl der retrieved Chunks;
  • Anzahl der Chunks aus einem einzelnen Artikel;
  • maximale Größe des Kontexts;
  • Gesamter Input;
  • max output.

Fragmente werden dem Prompt hinzugefügt, solange der nächste Fragment‑Eintrag das Budget nicht überschreitet.

Retrieve candidates
       ↓
Rank
       ↓
Filter
       ↓
Apply token budget
       ↓
Build context

Grounded Generation

Nach der Vorbereitung des Kontexts wird eine Anfrage an ein Chat‑Modell gestellt, jedoch wird die Freiheit des Modells durch den Prompt eingeschränkt. Es erhält Anweisungen:

  • nur die übergebenen Auszüge zu verwenden;
  • keine externen Kenntnisse zur Auffüllung von Lücken zu nutzen;
  • die Frage und die Auszüge als Daten zu betrachten, nicht als Anweisungen;
  • bei Informationsknappheit einen speziellen Status zurückzugeben;
  • die IDs der Fragmente anzugeben, auf denen die Antwort basiert.

Die Antwort des Modells wird in einer JSON‑Struktur etwa folgendermaßen zurückgegeben:

{
  "status": "answered",
  "answer": "...",
  "sourceIds": [12, 18]
}

oder:

{
  "status": "insufficient_content",
  "answer": "",
  "sourceIds": []
}

Diese Trennung ist praktisch: Das LLM ist für das Verständnis und die Formulierung des Textes zuständig, die Anwendung kontrolliert den Vertragsinhalt des Ergebnisses.

Dem Modell‑Antworten nicht blind vertrauen

Die Implementierung prüft die Antwort des Modells zusätzlich, selbst wenn sie der JSON‑Schema entspricht. Die Anwendung verifiziert:

  • ob ein gültiger Status angegeben ist;
  • ob eine Antwort vorhanden ist und ob sie das Token‑Limit nicht überschreitet;
  • ob die sourceIds tatsächlich zu den übergebenen Chunks gehören;
  • ob das Modell keine nicht existierende Quelle erfunden hat;
  • ob die verwendeten Chunks noch verfügbar sind;
  • ob die ursprünglichen Artikel veröffentlicht und sichtbar sind.

Erst nach dieser Prüfung werden interne Fragment‑IDs in Links zu realen Artikeln umgewandelt. Das Modell kann nicht selbst eine URL der Quelle zurückgeben: Es meldet beispielsweise „I used chunks 12 and 18“, und die Anwendung bestimmt, zu welchen Artikeln diese Fragmente gehören. Das ist ein wichtiger Architekturpunkt: Das LLM schlägt ein Ergebnis vor, die Anwendung prüft und interpretiert es.

Wann RAG nicht antworten sollte

RAG muss in der Lage sein, nicht zu antworten. Wenn in der Datenbank keine Informationen zur Frage vorhanden sind (z. B. „Как в serdg.net настроен Kubernetes cluster?“), sollte das System keinen plausibel klingenden Leitfaden zu Kubernetes generieren. Wenn das Retrieval nicht genügend Kontext gefunden hat, wird das Chat‑Modell nicht aufgerufen und die API gibt insufficient_content zurück. Dasselbe kann auch das Modell selbst zurückgeben, wenn die gefundenen Fragmente thematisch nahe sind, aber keine Antwort enthalten. Das ist der wesentliche Unterschied zwischen RAG und einer einfachen Anbindung von ChatGPT an eine Website: Ziel ist es, Antworten zu liefern, die an prüfbare Quellen gebunden sind, und nicht, Text nach Schema zu generieren.

Gesamtarchitektur

Indizierung:

Article
   ↓
HTML → plain text
   ↓
Chunking
   ↓
Embedding generation
   ↓
SQL Server vector storage
Anfrage:

User question
      ↓
Question embedding
      ↓
Vector search
      ↓
Relevant chunks
      ↓
Similarity filtering
      ↓
Token budget
      ↓
LLM + grounded prompt
      ↓
Structured result
      ↓
Validation
      ↓
Answer + article sources

Wichtig ist, dass jedes Element der Pipeline seine eigene Aufgabe hat: das Embedding‑Modell generiert keine Antworten, die Vektor‑Datenbank kennt die Sprache des Nutzers nicht, der Retriever formt keinen Text, das LLM hat keinen direkten Zugriff auf die Datenbank, und die API vertraut den Quellen nicht, bevor sie nicht geprüft sind.

Was ich aus der Implementierung mitgenommen habe

Vor dem Projekt erschien RAG vor allem als Kombination von "Vector Database + LLM". In der Praxis sind Embeddings einer der einfachsten Teile. Die Hauptarbeit konzentriert sich auf die anwendungsbezogene Infrastruktur:

  • zu bestimmen, welchen Text man indizieren sollte;
  • Dokumente korrekt in Chunks zu unterteilen;
  • zu entscheiden, wann der Index aktualisiert werden soll;
  • nicht unveränderte Embeddings erneut zu erzeugen;
  • einen Similarity‑Threshold zu wählen;
  • die Anzahl der retrieved Fragments zu begrenzen;
  • das Token‑Budget zu kontrollieren;
  • zu verhindern, dass ein Dokument den gesamten Kontext belegt;
  • das Modell dazu zu bringen, nur die bereitgestellten Daten zu nutzen;
  • die Quellen, die es angibt, zu prüfen;
  • die Situation zu behandeln, wenn keine Antwort in der Wissensdatenbank vorhanden ist.

Diese Infrastruktur verwandelt einen einfachen Aufruf des Embedding‑APIs in eine vollständige RAG‑Pipeline. Für serdg.net ist die Architektur vielleicht komplexer, als es für einen kleinen Blog nötig wäre, aber genau das war der Sinn des Experiments: den gesamten Weg von der Umwandlung eines Artikels in Vektoren bis zur Erzeugung einer grounded Antwort mit Verweisen auf reale Quellen durchzugehen.

Der nächste Schritt ist, RAG mit der normalen Suche und AI‑Tool‑Calling zu verbinden, sodass eine Eingabezeile unterschiedliche Mechanismen je nach Nutzerintention nutzen kann.


AI: Intelligente Suche und Routing von Anfragen mit Tool Calling

Praktische Umsetzung von AI Tool Calling: intelligente Weiterleitung von Anfragen, Suche und Filterung von Artikeln, Integration mit RAG. Überblick über architektonische Lösungen, Einschränkungen und Besonderheiten der Implementierung auf .NET und Angular.

Lesen
Erste Erfahrungen in der Automatisierung von Prozessen mit KI

Integration von OpenAI in eine .NET‑Anwendung am Beispiel eines realen Prozesses zur Erstellung von Artikeln — vom ersten Entwurf bis zur HTML‑Auszeichnung und Übersetzungen in mehrere Sprachen.

Lesen
Lass uns mit dem Coden beginnen

Ein neues Projekt zu starten ist wie der Schritt in eine Welt voller Magie. Wie wird das Projekt aussehen? Wie wird es funktionieren? Sind die technischen Entscheidungen gut genug? Der einzige Weg, diese Fragen zu beantworten, ist einzutauchen und anzufangen!

Lesen