Kit enthält ein vollständiges RAG-System (Retrieval-Augmented Generation), das Fragen mithilfe einer Wissensdatenbank anstelle der Trainingsdaten des LLMs beantwortet. Das System verwendet OpenAI-Embeddings und pgvector-Ähnlichkeitssuche, um relevante Wissens-Chunks zu finden, und übergibt nur diese Chunks als Kontext an das LLM — dadurch wird der Token-Verbrauch im Vergleich zum Senden der gesamten Wissensdatenbank um ca. 95 % reduziert.
Diese Seite behandelt die RAG-Pipeline, Vektorsuche, Query-Rewriting, API-Routen und die Einrichtung der Wissensdatenbank. Für das Streaming-Protokoll und die Hooks siehe Chat-System.
Ohne RAG könnte eine typische Wissensdatenbank ~90K Tokens pro Anfrage verbrauchen (vollständige FAQ wird gesendet). Mit RAG werden nur die 3–5 relevantesten Chunks gesendet — typischerweise 3–5K Tokens. Das entspricht einem Unterschied zwischen $0,072 und $0,003 pro Anfrage mit Claude Haiku 4.5.
RAG-Pipeline
Jede RAG-Frage durchläuft eine sechsstufige Pipeline:
Benutzer fragt: "How do I configure authentication?"
|
v
Schritt 1: Query-Erweiterung
|--- If conversation history exists:
| LLM rewrites vague follow-up → self-contained query
| Example: "What else?" → "What other Clerk auth features are available?"
|--- If standalone question: use as-is
|
v
Schritt 2: Embedding-Generierung
|--- OpenAI text-embedding-3-small
|--- Query → 1536-dimensional vector
|
v
Schritt 3: pgvector-Ähnlichkeitssuche
|--- Cosine similarity: 1 - (embedding <=> query_vector)
|--- Filter by similarity threshold (default: 0.35)
|--- Optional category filter
|--- Returns top 5 chunks (configurable)
|
v
Schritt 4: Kontext-Zusammenstellung
|--- Format chunks: [Source 1: Category]\nQ: ...\nA: ...
|--- Estimated ~3-5K tokens (vs ~90K for full FAQ)
|
v
Schritt 5: LLM-Generierung
|--- System prompt (tier-customized)
|--- Context from relevant chunks
|--- Conversation history
|--- Answer with source references
|
v
Schritt 6: Antwort mit Quellenangaben
|--- answer: "To configure authentication, ..."
|--- sources: [{ question, category, similarity }]
|--- chunksUsed: 3
|--- tokensEstimated: 4200
|--- usage: { promptTokens, completionTokens }
RAG-Service
Die
RAGService-Klasse koordiniert die gesamte Pipeline. Die Kernmethode ist answerQuestionWithRAG:src/lib/ai/rag-service.ts — answerQuestionWithRAG
async answerQuestionWithRAG(
question: string,
options: AnswerWithRAGOptions = {}
): Promise<RAGAnswer> {
const {
userId: _userId,
userTier,
categories,
limit = 5,
similarityThreshold = 0.35, // Adjusted for cross-lingual queries and semantic variation
conversationHistory = [],
model: _model,
temperature: _temperature,
} = options
Der Service akzeptiert folgende Optionen:
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
userId | string | — | Benutzer-ID für Tier-Anpassung |
userTier | 'free' | 'pro' | 'enterprise' | — | Tier für System-Prompt-Anpassung |
categories | string[] | Alle | Suche auf bestimmte Kategorien filtern |
limit | number | 5 | Anzahl der abzurufenden Chunks |
similarityThreshold | number | 0,35 | Minimaler Ähnlichkeitsscore (0–1) |
conversationHistory | Message[] | [] | Vorherige Nachrichten als Kontext |
model | string | Provider-Standard | KI-Modell überschreiben |
temperature | number | 0,7 | Antwort-Kreativität |
Die Antwort enthält vollständige Quellenangaben:
typescript
interface RAGAnswer {
answer: string // Generated answer text
sources: Array<{
question: string // Original FAQ question
category: string // Knowledge base category
similarity: number // Cosine similarity score (0-1)
}>
chunksUsed: number // Number of chunks used
tokensEstimated: number // Estimated context tokens
usage?: { // Actual token usage (if available)
promptTokens: number
completionTokens: number
totalTokens: number
}
model?: string // Model used for generation
}
Vektorsuche
Die
searchRAG-Funktion führt eine semantische Ähnlichkeitssuche mit OpenAI-Embeddings und pgvector durch:src/lib/ai/rag-search.ts — Similarity Search
export async function searchRAG(
query: string,
options: SearchOptions = {}
): Promise<SearchResult[]> {
const {
limit = 5,
similarityThreshold = 0.35, // Adjusted for cross-lingual queries and semantic variation
categories,
includeMetadata = true,
} = options
// 1. Generate embedding for user query
const apiKey = getEmbeddingApiKey()
const embeddingModelId = getEmbeddingModel()
const openai = createOpenAI({ apiKey })
const { embedding: queryEmbedding } = await embed({
model: openai.embedding(embeddingModelId),
value: query,
})
// 2. Format embedding as PostgreSQL array string
// Prisma doesn't automatically convert arrays to pgvector format
const embeddingStr = `[${queryEmbedding.join(',')}]`
// 3. Build SQL query with pgvector similarity
// Cosine similarity: 1 - (embedding <=> query_embedding)
// Higher score = more similar (1.0 = identical, 0.0 = unrelated)
// Build category filter SQL
const categoryFilterSQL =
categories && categories.length > 0
? `AND category = ANY(ARRAY[${categories.map((c) => `'${c}'`).join(',')}])`
: ''
const metadataSQL = includeMetadata ? 'metadata,' : ''
// Use $queryRawUnsafe to avoid template literal interpolation limits
const sql = `
SELECT
id,
content,
question,
answer,
category,
${metadataSQL}
1 - (embedding <=> '${embeddingStr}'::vector) as similarity
FROM faq_chunks
WHERE 1 - (embedding <=> '${embeddingStr}'::vector) > ${similarityThreshold}
${categoryFilterSQL}
ORDER BY embedding <=> '${embeddingStr}'::vector
LIMIT ${limit}
`
const results = await prisma.$queryRawUnsafe<SearchResult[]>(sql)
return results
}
Der Suchprozess:
- Embedding generieren — Die Anfrage mithilfe von
text-embedding-3-smallin einen 1536-dimensionalen Vektor umwandeln - Für pgvector formatieren — Das Float-Array in das PostgreSQL-Vektorformat konvertieren
- Cosine-Similarity-Abfrage —
1 - (embedding <=> query_vector)ergibt einen Ähnlichkeitsscore von 0 bis 1 - Filtern und sortieren — Ähnlichkeitsschwellenwert und optionalen Kategoriefilter anwenden, Top-N-Ergebnisse zurückgeben
Die RAG-Suche verwendet OpenAIs Embedding-Modell zur Vektorgenerierung, auch wenn du Anthropic oder Google für den Chat verwendest. Stelle sicher, dass
OPENAI_API_KEY in deiner Umgebung gesetzt ist (oder AI_API_KEY als Fallback).Das Embedding-Modell ist über
AI_EMBEDDING_MODEL konfigurierbar (Standard: text-embedding-3-small, 1536 Dimensionen). Ein Wechsel des Modells erfordert die Neugenerierung aller Embeddings.Ähnlichkeitsschwellenwerte
Der Ähnlichkeitsscore reicht von 0 (völlig unverwandt) bis 1 (identisch). Kit verwendet einen Standard-Schwellenwert von 0,35, um Recall und Precision auszubalancieren:
| Bereich | Interpretation | Maßnahme |
|---|---|---|
| 0,90+ | Fast identisch | Exakte Übereinstimmung gefunden |
| 0,70–0,89 | Sehr relevant | Antwort mit hoher Konfidenz |
| 0,50–0,69 | Gute semantische Übereinstimmung | Zuverlässige Antwort mit Kontext |
| 0,35–0,49 | Lose verwandt | Einbeziehen, ggf. mit Einschränkungen |
| Unter 0,35 | Nicht relevant | Ausgeschlossen (unter Schwellenwert) |
Wenn Benutzer zu viele irrelevante Ergebnisse erhalten, erhöhe
similarityThreshold auf 0,5. Wenn sie zu häufig „keine Ergebnisse gefunden" sehen, senke ihn auf 0,3. Der Standard von 0,35 ist für sprachübergreifende Abfragen optimiert (englische Fragen gegen deutschsprachige FAQ-Inhalte).Hilfsfunktionen
Das Such-Modul bietet zusätzliche Hilfsprogramme:
| Funktion | Zweck |
|---|---|
searchRAG(query, options) | Primäre Suche — Embeddings + pgvector-Ähnlichkeit |
getRelatedQuestions(chunkId, limit) | Ähnliche Fragen in derselben Kategorie finden (Schwellenwert: 0,8) |
searchInCategory(category, query, limit) | Innerhalb einer bestimmten Kategorie suchen (Schwellenwert: 0,4) |
getCategories() | Alle eindeutigen Kategorienamen abrufen |
getRAGStats() | Statistiken: Gesamt-Chunks, Kategorien, Token-Anzahlen |
Query-Rewriting
Wenn Benutzer Folgefragen wie „Was noch?" oder „Erzähl mir mehr" senden, würde die RAG-Suche fehlschlagen, weil die Anfrage keinen Kontext enthält. Die
rewriteQueryWithContext-Methode verwendet das LLM, um vage Folgefragen in eigenständige Anfragen umzuwandeln:src/lib/ai/rag-service.ts — Query Rewriting
/**
* Rewrite vague or follow-up questions using conversation context
* This makes RAG searches work better for multi-turn conversations
*
* @param question - Current user question (may be vague or a follow-up)
* @param history - Previous conversation messages
* @returns Enhanced, self-contained question for RAG search
*
* @example
* // Original: "Und was noch?"
* // With context about Clerk auth
* // Enhanced: "What other Clerk authentication features are available in the boilerplate?"
*/
private async rewriteQueryWithContext(
question: string,
history: Message[]
): Promise<string> {
// Take last 3 messages for context (avoid token bloat)
const recentHistory = history.slice(-3)
// Build prompt for query rewriting
const contextPrompt = `You are a query rewriting assistant for a technical FAQ system about a Next.js SaaS Boilerplate.
Rewrite the user's follow-up question to be self-contained and specific for vector database search.
Conversation History:
${recentHistory.map((m) => `${m.role}: ${m.content}`).join('\n')}
Current Question: ${question}
Instructions:
1. If the question is already clear and specific, return it unchanged
2. If it's a follow-up (e.g., "Und was noch?", "What about X?"), incorporate context from history
3. Make it technical and specific to the Next.js boilerplate
4. Keep it concise (1-2 sentences max)
5. Output ONLY the rewritten question, nothing else
Rewritten Question:`
try {
const response = await this.aiService.answerQuestion(question, {
context: contextPrompt,
systemPrompt:
'You are a query rewriting assistant. Output ONLY the rewritten question, nothing else.',
stream: false,
})
const rewrittenQuery =
response?.choices[0]?.message?.content?.trim() || question
// Log for debugging (helps tune the system)
console.log('[RAG Service] Query Rewriting:', {
original: question,
enhanced: rewrittenQuery,
historyLength: recentHistory.length,
changed: rewrittenQuery !== question,
})
return rewrittenQuery
} catch (error) {
// If rewriting fails, fall back to original question
console.warn(
'[RAG Service] Query rewriting failed, using original:',
error
)
return question
}
}
Beispieltransformationen:
| Ursprüngliche Frage | Gesprächsthema | Umgeschriebene Anfrage |
|---|---|---|
| „Was noch?" | Clerk-Authentifizierung | „Welche weiteren Clerk-Authentifizierungs-Features gibt es?" |
| „Und wie ist das damit?" | Datenbankmigrationen | „Wie funktionieren Prisma-Datenbankmigrationen im Boilerplate?" |
| „Mehr Details bitte" | Payment-Webhooks | „Was sind die Details der Payment-Webhook-Verarbeitung?" |
Das Rewriting:
- Nimmt die letzten 3 Nachrichten als Kontext (vermeidet Token-Aufblähung)
- Gibt die ursprüngliche Frage unverändert zurück, wenn sie bereits spezifisch ist
- Fällt auf die ursprüngliche Frage zurück, wenn der LLM-Aufruf fehlschlägt
- Protokolliert Transformationen für Debugging (
[RAG Service] Query Rewriting: { original, enhanced })
No-Match-Fallback
Wenn keine Wissens-Chunks den Ähnlichkeitsschwellenwert erreichen, gibt der RAG-Service keine leere Antwort zurück. Stattdessen generiert er eine hilfreiche, geführte Antwort basierend auf dem Tech-Stack des Boilerplates:
No chunks found for: "How do I deploy to AWS?"
|
v
Fallback-Prompt eingefügt:
|--- Acknowledge limitation: "I don't have specific FAQ content..."
|--- Provide boilerplate guidance based on tech stack
|--- Reference relevant files/folders
|--- Suggest rephrasing: "Could you ask more specifically?"
|
v
LLM generiert hilfreiche Antwort OHNE zu halluzinieren
Der Fallback-Prompt weist das LLM explizit an:
- Niemals allgemeine SaaS-Geschäftsratschläge zu geben
- Niemals Features oder Pricing-Tiers zu erfinden
- Niemals vorzuschlagen, „den Provider zu konsultieren" (Kit IST der Provider)
- Tatsächliche Dateipfade aus dem Boilerplate zu referenzieren
API-Routen
POST /api/ai/rag/ask
Authentifizierter RAG-Endpunkt für Dashboard-Benutzer. Unterstützt sowohl Streaming- als auch Nicht-Streaming-Antworten.
Der RAG-Endpunkt akzeptiert zwei Anfrageformate zur Kompatibilität:
Objektformat:
{ question: "How do I...?", conversationHistory: [...] }Messages-Format:
{ messages: [{ role: "user", content: "How do I...?" }] }Beide werden intern normalisiert, bevor sie verarbeitet werden.
Anfrage:
json
{
"question": "How do I configure Clerk authentication?",
"conversationHistory": [
{ "role": "user", "content": "Tell me about auth" },
{ "role": "assistant", "content": "Kit uses Clerk for..." }
],
"categories": ["Authentication"],
"stream": true
}
Antwort (ohne Streaming):
json
{
"answer": "To configure Clerk authentication...",
"sources": [
{
"question": "How is Clerk integrated?",
"category": "Authentication",
"similarity": 0.87
}
],
"chunksUsed": 3,
"tokensEstimated": 4200
}
Gesprächsverwaltung
Kit speichert den RAG-Gesprächsverlauf für mehrstufige Gespräche:
| Endpunkt | Methode | Zweck |
|---|---|---|
/api/ai/rag/conversations | GET | Gespräche des Benutzers auflisten |
/api/ai/rag/conversations | POST | Neues Gespräch erstellen |
/api/ai/rag/conversations/[id] | GET | Gespräch mit Nachrichten abrufen |
/api/ai/rag/conversations/[id] | DELETE | Gespräch löschen |
Einrichtung der Wissensdatenbank
Das RAG-System verwendet eine
faq_chunks-Tabelle in PostgreSQL mit pgvector für die Vektorspeicherung:Datenbankschema
sql
CREATE TABLE faq_chunks (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
question TEXT NOT NULL,
answer TEXT NOT NULL,
content TEXT NOT NULL, -- Combined searchable content
category TEXT NOT NULL,
embedding vector(1536), -- OpenAI text-embedding-3-small
metadata JSONB, -- Tags, related topics, keywords
token_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- pgvector index for fast similarity search
CREATE INDEX idx_faq_chunks_embedding
ON faq_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
Wissensdatenbank befüllen
Kit enthält ein Seed-Skript, das Embeddings generiert und die
faq_chunks-Tabelle befüllt:bash
# Embeddings generieren und Wissensdatenbank befüllen
cd apps/boilerplate && npx prisma db seed
# Oder den FAQ-Seeder direkt ausführen
cd apps/boilerplate && npx tsx prisma/seed-faq.ts
Der Seed-Prozess:
- Liest FAQ-Inhalte aus den Markdown-Dateien in
apps/boilerplate/src/content/faq/ - Teilt Inhalte in Chunks auf (Frage-Antwort-Paare)
- Generiert Embeddings über OpenAI
text-embedding-3-small - Fügt Chunks mit Embeddings in
faq_chunksein
Eigene Inhalte hinzufügen
So fügst du eigene Wissensdatenbank-Inhalte hinzu:
- Erstelle Markdown-Dateien in
apps/boilerplate/src/content/faq/mit kategoriebasierter Organisation - Jeder FAQ-Eintrag benötigt einen
question- und einenanswer-Abschnitt - Führe das Seed-Skript aus, um Embeddings zu generieren:
cd apps/boilerplate && npx tsx prisma/seed-faq.ts - Mit dem Stats-Endpunkt überprüfen:
GET /api/ai/rag/stats
Wichtige Dateien
| Datei | Zweck |
|---|---|
apps/boilerplate/src/lib/ai/rag-service.ts | RAG-Pipeline-Koordinator — Query-Rewriting, Suche, Generierung |
apps/boilerplate/src/lib/ai/rag-search.ts | pgvector-Ähnlichkeitssuche, verwandte Fragen, Statistiken |
apps/boilerplate/src/lib/ai/ai-service.ts | KI-Service-Wrapper, der von RAG für LLM-Aufrufe verwendet wird |
apps/boilerplate/src/app/api/ai/rag/ask/route.ts | Authentifizierter RAG-Endpunkt |
apps/boilerplate/src/app/api/ai/rag/conversations/ | Gespräch-CRUD-Endpunkte |
apps/boilerplate/src/lib/ai/sse-parser.ts | Gemeinsamer SSE-Parser mit SSEStreamError und SSELineBuffer |
apps/boilerplate/src/hooks/use-rag-chat.ts | useRAGChat-Hook für RAG-Streaming-Chat |
apps/boilerplate/src/content/faq/ | Quellinhalt der Wissensdatenbank |
apps/boilerplate/prisma/seed-faq.ts | Embedding-Generierung und Datenbank-Seeding |