RAG-System

Wissensdatenbank-Suche mit pgvector-Ähnlichkeit, Query-Rewriting, Quellenangaben und öffentlichem Zugriff

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.

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:
OptionTypStandardBeschreibung
userIdstringBenutzer-ID für Tier-Anpassung
userTier'free' | 'pro' | 'enterprise'Tier für System-Prompt-Anpassung
categoriesstring[]AlleSuche auf bestimmte Kategorien filtern
limitnumber5Anzahl der abzurufenden Chunks
similarityThresholdnumber0,35Minimaler Ähnlichkeitsscore (0–1)
conversationHistoryMessage[][]Vorherige Nachrichten als Kontext
modelstringProvider-StandardKI-Modell überschreiben
temperaturenumber0,7Antwort-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:
  1. Embedding generieren — Die Anfrage mithilfe von text-embedding-3-small in einen 1536-dimensionalen Vektor umwandeln
  2. Für pgvector formatieren — Das Float-Array in das PostgreSQL-Vektorformat konvertieren
  3. Cosine-Similarity-Abfrage1 - (embedding <=> query_vector) ergibt einen Ähnlichkeitsscore von 0 bis 1
  4. Filtern und sortieren — Ähnlichkeitsschwellenwert und optionalen Kategoriefilter anwenden, Top-N-Ergebnisse zurückgeben

Ä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:
BereichInterpretationMaßnahme
0,90+Fast identischExakte Übereinstimmung gefunden
0,70–0,89Sehr relevantAntwort mit hoher Konfidenz
0,50–0,69Gute semantische ÜbereinstimmungZuverlässige Antwort mit Kontext
0,35–0,49Lose verwandtEinbeziehen, ggf. mit Einschränkungen
Unter 0,35Nicht relevantAusgeschlossen (unter Schwellenwert)

Hilfsfunktionen

Das Such-Modul bietet zusätzliche Hilfsprogramme:
FunktionZweck
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 FrageGesprächsthemaUmgeschriebene 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.
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:
EndpunktMethodeZweck
/api/ai/rag/conversationsGETGespräche des Benutzers auflisten
/api/ai/rag/conversationsPOSTNeues Gespräch erstellen
/api/ai/rag/conversations/[id]GETGespräch mit Nachrichten abrufen
/api/ai/rag/conversations/[id]DELETEGesprä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:
  1. Liest FAQ-Inhalte aus den Markdown-Dateien in apps/boilerplate/src/content/faq/
  2. Teilt Inhalte in Chunks auf (Frage-Antwort-Paare)
  3. Generiert Embeddings über OpenAI text-embedding-3-small
  4. Fügt Chunks mit Embeddings in faq_chunks ein

Eigene Inhalte hinzufügen

So fügst du eigene Wissensdatenbank-Inhalte hinzu:
  1. Erstelle Markdown-Dateien in apps/boilerplate/src/content/faq/ mit kategoriebasierter Organisation
  2. Jeder FAQ-Eintrag benötigt einen question- und einen answer-Abschnitt
  3. Führe das Seed-Skript aus, um Embeddings zu generieren: cd apps/boilerplate && npx tsx prisma/seed-faq.ts
  4. Mit dem Stats-Endpunkt überprüfen: GET /api/ai/rag/stats

Wichtige Dateien

DateiZweck
apps/boilerplate/src/lib/ai/rag-service.tsRAG-Pipeline-Koordinator — Query-Rewriting, Suche, Generierung
apps/boilerplate/src/lib/ai/rag-search.tspgvector-Ähnlichkeitssuche, verwandte Fragen, Statistiken
apps/boilerplate/src/lib/ai/ai-service.tsKI-Service-Wrapper, der von RAG für LLM-Aufrufe verwendet wird
apps/boilerplate/src/app/api/ai/rag/ask/route.tsAuthentifizierter RAG-Endpunkt
apps/boilerplate/src/app/api/ai/rag/conversations/Gespräch-CRUD-Endpunkte
apps/boilerplate/src/lib/ai/sse-parser.tsGemeinsamer SSE-Parser mit SSEStreamError und SSELineBuffer
apps/boilerplate/src/hooks/use-rag-chat.tsuseRAGChat-Hook für RAG-Streaming-Chat
apps/boilerplate/src/content/faq/Quellinhalt der Wissensdatenbank
apps/boilerplate/prisma/seed-faq.tsEmbedding-Generierung und Datenbank-Seeding