Eine praktische RAG-Architektur auf Firebase und Vertex AI mit Firestore Vectors
RAG auf Firebase mit Vertex AI-Embeddings und Firestore-Vektorabfrage: Batching, Metadatenfilter, Tenant-Isolation, grounded Prompts, Caching und Metriken.
Quellen: How to Implement RAG (Retrieval-Augmented Generation) in Your Web Application
Retrieval-Augmented Generation ist zu einem gängigen Ansatz für KI-Apps geworden, die mit privaten, sich ändernden oder spezialisierten Informationen umgehen müssen. Anstatt von einem LLM zu erwarten, dass es alles aus der Trainingsphase abruft, ermöglicht RAG dem Modell, zum Zeitpunkt der Abfrage eine externe Wissensquelle zu konsultieren. Dadurch lässt sich die Entwicklung von Assistenten für Unternehmensdokumentationen, Kundendaten, Produktwissen, historische Daten oder interne Prozesse realisieren.
RAG ist mehr als nur eine Vektordatenbank, die mit einem LLM verbunden ist. Eine produktionsreife Implementierung umfasst Dokumentverarbeitung, Embeddings, Vektorsuche, Metadatenfilterung, Kontextzusammenstellung, Prompt-Design, Generierung, Caching, Fehlerbehandlung und Überwachung. Im Folgenden wird eine praktische Architektur auf Firebase und Vertex AI vorgestellt, zusammen mit den Implementierungsansätzen, die wichtig sind, wenn ein Prototyp in die Produktion übergeht.
Drei Schichten
Ein RAG-System besteht aus drei Schichten: einem Wissensspeicher, einer Abrufschicht und einer Generierungsschicht. Quelldokumente oder Anwendungsdaten werden in Embeddings umgewandelt und zusammen mit ihrem ursprünglichen Text sowie Metadaten gespeichert. Wenn ein Benutzer eine Frage stellt, wird auch diese Frage in ein Embedding umgewandelt, und eine semantische Ähnlichkeitssuche findet die am nächsten liegenden Informationen. Diese Informationen bilden den Kontext, der an das Sprachmodell übergeben wird, welches anschließend die Antwort auf der Grundlage der gefundenen Daten erstellt.
Die zentrale Idee ist, dass das Modell niemals die gesamte Wissensbasis memorisieren muss. Der Abruf erfolgt für jede Anfrage neu, sodass sich die zugrunde liegenden Daten ändern können, ohne dass ein Neutraining erforderlich ist.
User Question
|
v
Generate Query Embedding
|
v
Metadata Filtering
|
v
Vector Similarity Search
|
v
Top-K Relevant Documents
|
v
Context Construction
|
v
LLM / Gemini
|
v
Grounded Response + Sources
Die Trennung erleichtert außerdem die Wartung. Die Abrufstrategie kann sich ändern, während das Generierungsmodell unverändert bleibt, oder das LLM kann ausgetauscht werden, ohne die Wissensdatenbank neu aufbauen zu müssen.
Embeddings
Embeddings bilden die Grundlage für semantischen Abruf. Anstatt Text als Schlüsselwörter zu behandeln, wandelt ein Embedding-Modell ihn in einen numerischen Vektor um, der beziehungsbasierte Bedeutungsebenen erfassen kann. Die Referenzimplementierung verwendet Vertex AI’s gemini-embedding-001, wobei für Dokumente und Abfragen unterschiedliche Modelltypen genutzt werden, da gespeicherte Dokumente und eingehende Fragen unterschiedliche Rollen spielen.
const { PredictionServiceClient, helpers } =
require('@google-cloud/aiplatform');
const PROJECT_ID = process.env.PROJECT_ID;
const client = new PredictionServiceClient({
apiEndpoint: 'aiplatform.googleapis.com'
});
async function generateEmbedding(
text,
taskType = 'RETRIEVAL_DOCUMENT'
) {
const endpoint =
`projects/${PROJECT_ID}/locations/global/` +
`publishers/google/models/gemini-embedding-001`;
const instance = {
content: text,
task_type: taskType
};
const request = {
endpoint,
instances: [helpers.toValue(instance)]
};
const [response] = await client.predict(request);
return response.predictions[0].embeddings.values;
}
Die Indizierung der Wissensdatenbank erfordert RETRIEVAL_DOCUMENT, während die Verarbeitung einer Benutzerfrage RETRIEVAL_QUERY benötigt. Es ist wichtig, hier Konsistenz zu wahren, denn die Qualität der Informationsabrufung hängt stark davon ab, wie beide Aspekte dargestellt werden.
Batching und Konkurrenz
Frühe RAG-Codebeispiele fügen in der Regel jeden Dokument einzeln hinzu. Das eignet sich für ein kleines Prototyp, ist aber verschwenderisch, wenn Tausende oder Millionen von Teilen verarbeitet werden müssen. Die Gruppierung von Texten in Batches innerhalb der Grenzen von Anfragen und Tokens funktioniert besser.
Der Referenzcode schätzt die Anzahl der Tokens einfach und begrenzt sowohl die Anzahl der Segmente als auch die geschätzte Anzahl der Tokens pro Anfrage. Zudem wird nur begrenzte Konkurrenz genutzt, sodass mehrere Batches parallel ausgeführt werden können, ohne die API zu überlasten.
const EMBEDDING_CONFIG = {
maxSegmentsPerRequest: 100,
maxTokensPerRequest: 18000,
concurrency: 3,
tokenEstimateDivisor: 3
};
function estimateTokens(text) {
return Math.ceil(
text.length / EMBEDDING_CONFIG.tokenEstimateDivisor
);
}
function packIntoBatches(texts) {
const batches = [];
let currentBatch = [];
let currentTokens = 0;
for (const text of texts) {
const tokens = estimateTokens(text);
const exceedsCount =
currentBatch.length >=
EMBEDDING_CONFIG.maxSegmentsPerRequest;
const exceedsTokens =
currentTokens + tokens >
EMBEDDING_CONFIG.maxTokensPerRequest;
if (exceedsCount || exceedsTokens) {
if (currentBatch.length > 0) {
batches.push(currentBatch);
}
currentBatch = [text];
currentTokens = tokens;
} else {
currentBatch.push(text);
currentTokens += tokens;
}
}
if (currentBatch.length > 0) {
batches.push(currentBatch);
}
return batches;
}
Die Grenzwerte sollten in der Konfiguration festgelegt werden und nicht als vorausgesetzte Werte eingebaut sein. In der Produktion sollten sie je nach Umgebung variieren, und vor der Anpassung der Konkurrenzfähigkeit müssen API-Latenz, Rate Limits, Ausfallraten sowie Durchsatz überwacht werden.
Vektoren neben den Anwendungsdaten speichern
Eine praktische Lösung ist es, Vektoren zusammen mit den Anwendungsdaten zu speichern, anstatt sofort eine weitere Datenbank hinzuzufügen. Der Referenzcode speichert den Dokumenttext, die Embeddings sowie Metadaten in Firestore und holt ähnliche Einträge über eine Vektor-Suche nach dem nächsten Nachbarn ab.
const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();
async function storeDocumentWithEmbedding(
collectionPath,
docId,
text,
embedding,
metadata
) {
const docRef =
firestore.doc(`${collectionPath}/${docId}`);
await docRef.set({
text,
embedding,
...metadata,
createdAt: Firestore.FieldValue.serverTimestamp()
});
}
async function findSimilarDocuments(
collectionPath,
queryEmbedding,
limit = 5
) {
const collectionRef =
firestore.collection(collectionPath);
const vectorQuery = collectionRef.findNearest({
vectorField: 'embedding',
queryVector: queryEmbedding,
limit,
distanceMeasure: 'DOT_PRODUCT'
});
const snapshot = await vectorQuery.get();
return snapshot.docs.map(doc => ({
id: doc.id,
data: doc.data()
}));
}
Die Abrufstrategie erfordert sorgfältige Planung. Die Vektorähnlichkeit zeigt an, welche Dokumente semantisch der Anfrage nahekommen, doch eine ähnliche Bedeutung bedeutet nicht immer Geschäftsrelevanz. Mit dem Wachstum einer Anwendung werden Metadaten daher äußerst wichtig.
Filtern und Isolierung der Nutzer
Eine reine Vektorsuche reicht in der Praxis selten aus. Stellen Sie sich eine Wissensdatenbank vor, die mehrere Kunden, Abteilungen, Produkte oder Regionen abdeckt. Eine Anfrage könnte Dokumente mehrerer Nutzer ähneln, doch die Anwendung sollte nur das zurückgeben, was der aktuelle Benutzer sehen darf.
Ein effektiverer Ansatz besteht darin, vor oder zusammen mit der semantischen Suche strukturierte Filter anzuwenden. Der Referenzcode filtert zunächst nach Themen, bevor die Vektoranfrage gestellt wird, wodurch der Suchraum eingeschränkt und die Relevanz verbessert wird.
async function retrieveContext(
queryText,
selectedTopics = [],
limit = 5
) {
const queryEmbedding =
await generateEmbedding(
queryText,
'RETRIEVAL_QUERY'
);
let collectionRef =
firestore.collection('knowledge_base');
if (selectedTopics.length > 0) {
collectionRef = collectionRef.where(
'topics',
'array-contains-any',
selectedTopics
);
}
const vectorQuery =
collectionRef.findNearest({
vectorField: 'embedding',
queryVector: queryEmbedding,
limit,
distanceMeasure: 'DOT_PRODUCT'
});
const snapshot = await vectorQuery.get();
return snapshot.docs.map(doc => {
const data = doc.data();
return {
text: data.text,
source: data.source,
metadata: data.metadata || {}
};
});
}
Dies ist insbesondere in Multi-Tenant-Anwendungen von großer Bedeutung. Die Datenerfassung darf nicht versehentlich zu einer Sicherheitsbarriere werden, die die Daten eines anderen Kunden preisgibt. Autorisierung und Isolierung der Nutzer sollten unabhängig von der semantischen Ähnlichkeit durchgesetzt werden.
Erstellung der Anfrage
Die Datenerfassung löst nur die Hälfte des Problems. Der abgerufte Text muss außerdem in einer Form beim LLM ankommen, die das Quellmaterial von der tatsächlichen Frage des Benutzers unterscheidet. Der Referenzcode erstellt einen speziellen Kontextabschnitt, enthält Quellinformationen und weist das Modell ausdrücklich an, aus dem bereitgestellten Kontext zu antworten und anzugeben, wenn der Kontext das Notwendige fehlt.
async function generateRAGResponse(
userQuery,
contextDocuments
) {
const contextSection =
contextDocuments
.map((doc, index) => {
return `
### Reference ${index + 1}
${doc.text}
Source: ${doc.metadata?.source || 'Unknown'}
`;
})
.join('\n');
const prompt = `
You are an AI assistant with access
to a knowledge base.
Use the provided context to answer
the user's question accurately.
CONTEXT:
${contextSection}
USER QUESTION:
${userQuery}
INSTRUCTIONS:
1. Answer using the provided context.
2. If the context is insufficient, say so clearly.
3. Cite the references used.
4. Do not invent information.
ANSWER:
`;
const result =
await genAI.models.generateContent({
model: 'gemini-2.5-flash-lite',
contents: prompt
});
return result.text.trim();
}
Die Qualität der Informationsabrufung und die Qualität der Anfrage hängen miteinander zusammen. Auch starke Dokumente können zu schlechten Antworten führen, wenn der Kontext schlecht strukturiert ist, dupliziert wird, zu umfangreich ist oder ohne klare Anweisungen dargestellt wird.
Verzögerung, Kosten und Fehler
Verzögerung und Kosten verdienen von Anfang an Aufmerksamkeit. Die Erstellung von Embeddings erfolgt über einen externen API-Aufruf, und derselbe Abfrage kann häufig wiederholt werden; daher reduziert das Caching beliebter Embeddings überflüssige Anfragen. Vorübergehende Fehler sowie Rate-Limits erfordern Wiederholungsversuche, nicht sofortigen Misserfolg der gesamten Anfrage. Der Referenzcode zeigt ein in-Memory-Embedding-Cache sowie ein exponentielles Backoff-Verfahren für Fehler aufgrund von Rate-Limits.
const embeddingCache = new Map();
async function getCachedEmbedding(text, taskType) {
const cacheKey = `${taskType}:${text}`;
if (embeddingCache.has(cacheKey)) {
return embeddingCache.get(cacheKey);
}
const embedding =
await generateEmbedding(text, taskType);
embeddingCache.set(cacheKey, embedding);
return embedding;
}
async function retryWithBackoff(
fn,
maxRetries = 3
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (
error.code === 429 ||
error.message.includes('rate limit')
) {
const delay =
Math.pow(2, attempt) * 1000;
await new Promise(resolve =>
setTimeout(resolve, delay)
);
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
Sobald mehrere Anwendungsinstanzen laufen, reicht ein prozessbezogener Map nicht mehr aus, und ein verteilter Cache gewährleistet eine bessere Konsistenz zwischen den Kopien. Das Prinzip steht über der Technologie: Rechne teure, deterministische Aufgaben nicht erneut aus, wenn das Ergebnis sicher wiederverwendet werden kann.
Mehrere Wissensquellen
Manche Fragen erfordern mehr als nur eine Art von Information. Ein Assistent benötigt möglicherweise eine Produktdefinition, ein praktisches Beispiel sowie einige Hintergrundinformationen, um gut antworten zu können. Wenn alle Dokumente in einer einzigen, generischen Sammlung zusammengefasst werden, wird die Suche weniger präzise.
Der Referenzansatz trennt Definitionen, Beispiele und historische Daten, ruft jeweils unabhängig darauf zu und fügt die Ergebnisse zu einem strukturierten Kontextobjekt zusammen.
async function retrieveMultiContext(
queryText,
options = {}
) {
const {
includeDefinitions = true,
includeExamples = true,
includeHistorical = false,
topics = []
} = options;
const queryEmbedding =
await generateEmbedding(
queryText,
'RETRIEVAL_QUERY'
);
const contextPromises = [];
if (includeDefinitions) {
contextPromises.push(
findSimilarDocuments(
'definitions',
queryEmbedding,
3
).then(documents => ({
type: 'definitions',
documents
}))
);
}
if (includeExamples) {
contextPromises.push(
findSimilarDocuments(
'examples',
queryEmbedding,
5
).then(documents => ({
type: 'examples',
documents
}))
);
}
const contexts =
await Promise.all(contextPromises);
return contexts;
}
So erlangt die Anwendung die Kontrolle. Anstatt von einer einzigen Vektorabfrage alles erledigen zu lassen, wählt sie aus, welche Quellen für jede Abfrage relevant sind, und kombiniert sie vor der Erstellung des Ergebnisses.
Überwachbarkeit
Die Sichtbarkeit ist ein zentraler Punkt, an dem Demos und produktive RAG-Anwendungen auseinandergehen. Eine Antwort des Chatbots ist kein Beweis dafür, dass die Dokumentensuche ordnungsgemäß funktioniert. Teams benötigen Daten zu den Suchzeiten, den pro Abfrage zurückgegebenen Dokumenten, deren Relevanz, dem Cache-Nutzungsgrad sowie der Fehlerhäufigkeit.
Der Referenzcode enthält eine RAGMetrics-Klasse, die das Abfragevolumen, die Latenz, die Genauigkeit der Dokumentensuche, das Cache-Verhalten sowie Fehler erfasst und bei vorhandenen relevanten Dokumenten Präzision und Recall berechnet.
class RAGMetrics {
constructor() {
this.metrics = {
totalQueries: 0,
averageLatency: 0,
retrievalAccuracy: [],
errors: []
};
}
logQuery(
query,
contextCount,
latency,
sources
) {
this.metrics.totalQueries++;
const previousLatency =
this.metrics.averageLatency *
(this.metrics.totalQueries - 1);
this.metrics.averageLatency =
(previousLatency + latency) /
this.metrics.totalQueries;
console.log({
query: query.substring(0, 100),
contextCount,
latency,
sourceCount: sources.length,
timestamp: new Date().toISOString()
});
}
logRetrievalAccuracy(
retrievedDocs,
relevantDocs
) {
const retrievedIds =
new Set(retrievedDocs.map(d => d.id));
const relevantIds =
new Set(relevantDocs.map(d => d.id));
const intersection =
new Set(
[...retrievedIds]
.filter(id => relevantIds.has(id))
);
const precision =
intersection.size / retrievedIds.size;
const recall =
intersection.size / relevantIds.size;
this.metrics.retrievalAccuracy.push({
precision,
recall
});
}
}
Sobald solche Zahlen vorhanden sind, wird die Verbesserung von RAG zu einer ingenieurtechnischen Aufgabe. Variationen wie die Größe der Blöcke, Top-K, Metadatenfilter, das Embedding-Modell, die Struktur der Anfragen sowie das Neuranking können nacheinander getestet werden, wobei Messungen zeigen, ob die Informationsabrufung und die Antworten tatsächlich verbessert wurden.
Der gesamte Prozessablauf
Ein produktionsreifes RAG-System ist ein Prozessablauf und nicht nur ein einzelner Modellaufruf. Dokumente werden gereinigt und in Blöcke aufgeteilt, Embeddings werden effizient erzeugt, Metadaten werden konsistent gespeichert, die Informationsabrufung berücksichtigt Berechtigungen und die Generierungsschicht erhält klare Anweisungen. Eine praktische Struktur sieht wie folgt aus:
DOCUMENT INGESTION
|
v
Clean + Split Documents
|
v
Generate Embeddings
|
v
Firestore + Metadata Storage
|
|
USER QUERY --------+
|
v
Query Embedding
|
v
Authorization + Filters
|
v
Vector Similarity Search
|
v
Relevant Context
|
v
Prompt Construction
|
v
Gemini / LLM
|
v
Answer + Sources + Metrics
Es gibt hier nichts, was das Design festlegt. Spätere Erweiterungen wie die hybride Suchfunktion nach Schlüsselwörtern und Vektoren, Neubewertungsalgorithmen, Umformulierung von Abfragen, Gesprächsmerkmale, Bewertungssätze, fließende Ausgabe, ein gemeinsamer Cache sowie Hintergrundverarbeitung passen alle ohne Veränderung der grundlegenden RAG-Idee.
Lektionen
Das große Sprachmodell ist nur ein Bestandteil. Ein sehr leistungsstarkes Modell kann mangelhaftes Aufteilen in Abschnitte, schwache Suchfunktionen, fehlerhafte Metadaten, fehlende Autorisierungsprüfungen oder irrelevante Kontexte nicht ausgleichen. In der Praxis entscheidet oft die Qualität der Suche darüber, ob das Endsystem intelligent wirkt oder unzuverlässig ist.
Es funktioniert am besten, schrittweise zu entwickeln: Beginnen Sie mit der Dateneingabe, Embeddings, Vektor Suche sowie einem einfachen Prompt und messen Sie die Ergebnisse, bevor Sie Komplexität hinzufügen. Fügen Sie anschließend je nach tatsächlichen Engpässen Filterung von Metadaten, Caching, Wiederholungsversuche, Überwachung, Bewertung sowie fortgeschrittene Suchmethoden hinzu.
RAG verändert die Art und Weise, wie KI-Apps entwickelt werden, denn ein Modell kann mit Wissen arbeiten, das nicht unbedingt in seinen Trainingsdaten enthalten sein muss. Embeddings, semantische Suche, strukturierte Metadaten sowie kontextsensitive Generierung können gemeinsam eine gewöhnliche Anwendung in einen Assistenten verwandeln, der seine eigene Wissensbasis versteht.
Die besten RAG-Systeme sind nicht immer die aufwendigsten. Es sind vielmehr jene, bei denen die Suche messbar ist, der Kontext relevant ist, der Zugriff sicher ist, Fehler behoben werden können und die Antworten auf nützliche Quellen zurückverfolgt werden können. Mit einfachen Anfängen und Verbesserungen anhand echter Metriken ist es besser, Komplexität hinzuzufügen, nachdem das eigentliche Problem verstanden wurde, anstatt vorher Komplexität zu erzeugen.
Verwandte Artikel
- Suchen und Beurteilen bei RAG: Warum die Informationsabrufung eine separate Neubewertungsphase benötigt — Durch Einbettung von Ähnlichkeitsmessungen werden Kandidaten gefunden, doch die Relevanz kann nicht beurteilt werden. Erfahren Sie mehr über das zweistufige Suchen-und-Beurteilen-Verfahren mit RRF, Cross-Encodern, ColBERT sowie LLM-Neubewertungsalgorithmen.
- Praktische Hinweise: Was ist Retrieval-Augmented Generation (RAG)? Ein praktischer Leitfaden — Ein verständlicher Leitfaden mit Schritt-für-Schritt-Anleitungen, Kontrollpunkten sowie Code-Vorlagen für Teams, die dieses Verfahren einsetzen möchten.