Vissza a tudásbázisba
WhitepaperRAGVector databaseEmbeddingChunkingHybrid searchRe-rankingEvaluationEnterprise AI

RAG a gyakorlatban — Retrieval Augmented Generation éles használata

ÁZ&A
Ádám Zsolt & Airon
||12 perc

A RAG demo 30 perc alatt összerakható. A production RAG-rendszer 3 hónap. A különbség: chunking, re-ranking, evaluation és monitoring.

Bevezetés: miért nem elég a „LangChain quickstart"?

Ha valaha építettél RAG-rendszert, ismered a forgatókönyvet:

  • Letöltöd a LangChain (vagy LlamaIndex) quickstartot
  • Beemelsz 10 PDF-et, vector DB-be töltöd
  • Megkérdezel valamit → kapsz választ → működik
  • Bemutatod a vezetőségnek → tetszik
  • Rákapcsolsz 10.000 dokumentumot → minden szétesik

A gond: a quickstart RAG toy problem-en működik. A production RAG más állat: irreleváns chunkok, hosszú kontextus-leromlás, multi-tenant izoláció, frissítés, monitoring, költségrobbanás.

Ez az anyag a production RAG-rendszer építéséről szól — konkrét kóddal, mérhető best practice-ekkel, és azokról a hibákról, amelyeket mi (és mások) már elkövettünk.

A RAG architektúra rétegei

A naiv RAG így néz ki:

Kérdés → Embedding → Vector search → Top-K → LLM → Válasz

A production RAG így:

Kérdés
  ↓
[Query rewriting / decomposition]
  ↓
[Hybrid search: vector + BM25]
  ↓
[Re-ranking (cross-encoder)]
  ↓
[Context assembly + deduplication]
  ↓
[LLM with structured prompt + citations]
  ↓
[Response validation + citation check]
  ↓
[Logging + evaluation]

Mindegyik réteg kihagyható — de mindegyik kihagyása rontja a minőséget. A kérdés nem az, hogy kell-e, hanem hogy melyik fázisban vezeted be.

Ingestion — a leggyakrabban alábecsült rész

Chunking — a legfontosabb döntés

A chunk-méret eldönti a rendszer minőségét. Túl kicsi → kontextus elveszik. Túl nagy → relevancia híg.

Tapasztalati arányok:

Use case Chunk size Overlap
FAQ / rövid policy 200-300 token 30 token
Műszaki dokumentáció 400-600 token 50 token
Hosszú prózai szöveg 600-800 token 100 token
Kód szemantikus (függvény / osztály) 0

Rossz chunking: fix karakterszám-vágás (pl. 1000 char). A mondatokat és a szakaszokat félbevágja.

Jó chunking: szemantikus határokon vág (bekezdés, szakasz, header).

import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 500,
  chunkOverlap: 50,
  separators: [
    "\n## ",      // markdown header
    "\n### ",     // markdown subheader
    "\n\n",       // bekezdés
    "\n",         // sor
    ". ",         // mondat
    " ",          // szó (utolsó esély)
  ],
});

const chunks = await splitter.splitDocuments(documents);

Metadata — a titkos fegyver

A chunk önmagában kontextus-mentes. Ha a vector DB csak a tartalmat tárolja → a modell nem tudja, honnan jött, mikor, kihez tartozik.

Minimum metadata:

type ChunkMetadata = {
  documentId: string;
  documentTitle: string;
  source: string;          // URL vagy filename
  sectionPath: string[];   // ["Chapter 3", "3.2 Pricing"]
  pageNumber?: number;
  createdAt: Date;
  updatedAt: Date;
  tenantId: string;        // multi-tenant esetén KÖTELEZŐ
  permissions?: string[];  // ki láthatja?
  language: string;
  documentType: "policy" | "faq" | "contract" | "email" | "other";
};

A metadata lehetővé teszi:

  • Filterezést keresés előtt (pl. csak az adott tenant dokumentumai)
  • Forrás-citálást a válaszban
  • Frissesség-szűrést (pl. csak elmúlt 6 hónap)
  • Permission-checket (kit szabad látnia)

Embedding modell választás

Modell Dimenzió Költség (1M token) Mikor?
text-embedding-3-small 1536 $0.02 Default, jó ár / érték
text-embedding-3-large 3072 $0.13 Magas pontosság kell
bge-m3 (open source) 1024 self-host Multilingual, on-prem
voyage-3 1024 $0.06 Specifikus domainek (kód, jog)

Magyar nyelvre: a multilingual modellek (text-embedding-3-*, bge-m3) jól működnek. Kerüld az angol-only modelleket (all-MiniLM-L6-v2 magyar szövegen ~30%-kal rosszabb).

Fontos: ha váltasz modellt, újra kell embedelni az egész corpust. Tervezz erre.

Retrieval — több, mint cosine similarity

Hybrid search: vector + keyword

A pure vector search rossz ott, ahol:

  • Pontos kifejezésre van szükség (pl. termékkód, hibakód, név)
  • Ritka szakszavak vannak (a modell nem ismeri őket jól)
  • A kérdés rövid és specifikus

Megoldás: hybrid search = vector + BM25 (keyword). Az eredményeket Reciprocal Rank Fusion (RRF) algoritmussal kombinálod:

function reciprocalRankFusion(
  results: { id: string; rank: number }[][],
  k = 60
): { id: string; score: number }[] {
  const scores = new Map<string, number>();

  for (const resultList of results) {
    for (const item of resultList) {
      const current = scores.get(item.id) || 0;
      scores.set(item.id, current + 1 / (k + item.rank));
    }
  }

  return [...scores.entries()]
    .map(([id, score]) => ({ id, score }))
    .sort((a, b) => b.score - a.score);
}

// Használat:
const vectorResults = await vectorDb.search(queryEmbedding, { topK: 20 });
const keywordResults = await elasticsearch.search(query, { size: 20 });

const merged = reciprocalRankFusion([
  vectorResults.map((r, i) => ({ id: r.id, rank: i })),
  keywordResults.map((r, i) => ({ id: r.id, rank: i })),
]);

A pgvector + tsvector kombinációval Postgres-en belül megoldható — nem kell külön Elasticsearch.

Re-ranking — a kvalitás-ugrás

A vector search gyors, de zajos. A top-20 eredmény között gyakran van 5-10 irreleváns chunk. A megoldás: re-ranker model.

A re-ranker egy cross-encoder: a kérdést és minden chunkot együtt dolgoz fel, és relevancia-pontszámot ad. Lassabb (50-200ms), de jelentősen pontosabb.

import { CohereClient } from "cohere-ai";

const cohere = new CohereClient({ token: process.env.COHERE_API_KEY });

async function rerank(query: string, candidates: Chunk[]): Promise<Chunk[]> {
  const response = await cohere.rerank({
    model: "rerank-multilingual-v3.0",
    query,
    documents: candidates.map((c) => c.content),
    topN: 5,
  });

  return response.results.map((r) => candidates[r.index]);
}

// Pipeline:
const top20 = await hybridSearch(query);          // gyors, zajos
const top5 = await rerank(query, top20);          // lassú, pontos
const answer = await llm(query, top5);            // csak a top-5 megy az LLM-be

Mérési tapasztalat: re-ranking nélkül a relevancia-ratio top-5-ben ~60%, re-ranking-gel ~85-90%. Ez közvetlenül csökkenti a hallucinációt.

Query rewriting

A felhasználói kérdések rosszul formáltak: rövidek, beszélt nyelvi, kontextus-függőek.

"Mi a status?"             ← nem kereshető
"Hogy van a Kovács projekt jelenleg?" ← kereshető

Megoldás: LLM-mel rewrite-old a kérdést a beszélgetés-előzmény alapján:

async function rewriteQuery(
  history: Message[],
  userQuery: string
): Promise<string[]> {
  const response = await openai.chat.completions.create({
    model: "gpt-4o-mini", // olcsó model elég
    messages: [{
      role: "system",
      content: `Generálj 2-3 független keresési kérdést az alábbi
        beszélgetés és a felhasználó utolsó üzenete alapján.
        A kérdések legyenek önállóan értelmezhetőek, kontextus nélkül.
        JSON tömbként add vissza: {"queries": ["...", "..."]}`
    }, {
      role: "user",
      content: `Előzmény:\n${formatHistory(history)}\n\nÚj üzenet: ${userQuery}`
    }],
    response_format: { type: "json_object" }
  });

  return JSON.parse(response.choices[0].message.content).queries;
}

// Multi-query retrieval
const queries = await rewriteQuery(history, userInput);
const allResults = await Promise.all(queries.map(q => hybridSearch(q)));
const merged = reciprocalRankFusion(
  allResults.map(r => r.map((c, i) => ({ id: c.id, rank: i })))
);

Generation — a prompt mint kontraktus

A retrieval-ből kapott chunkokat nem elég a prompt végéhez ragasztani. A prompt kontraktus a modellel.

A jó RAG system prompt

const RAG_SYSTEM_PROMPT = `Te egy szakértő asszisztens vagy.
Csak a megadott FORRÁSOK alapján válaszolj. Szabályok:

1. Ha a forrás nem tartalmazza a választ, mondd ki:
   "Nincs erre vonatkozó információm a dokumentációban."
   NE találj ki választ.

2. Minden tényállításhoz csatolj forrás-hivatkozást: [Forrás: <id>]

3. Ha a források ellentmondanak egymásnak, jelezd:
   "A források ellentmondásosak: [Forrás A] szerint X,
    de [Forrás B] szerint Y."

4. NE hivatkozz a tréning-tudásodra. Csak a forrásokra.

5. Ha a kérdés tisztázásra szorul, kérdezz vissza.`;

const userPrompt = `FORRÁSOK:
${chunks.map((c, i) => `
[Forrás ${c.id} | ${c.source} | ${c.sectionPath.join(" > ")}]
${c.content}
`).join("\n---\n")}

KÉRDÉS: ${userQuery}`;

Konkrét trükkök:

  • A forrásokat előre tedd, a kérdést utána (lost-in-the-middle ellen)
  • A forrás-ID legyen gépileg parsolható (pl. [Forrás abc123])
  • A „nincs információm" választ explicit engedd meg — különben hallucinálni fog

Citation validáció

A modell hivatkozhat nem létező forrásra is. Validáld:

async function validateAndStripCitations(
  answer: string,
  validSourceIds: Set<string>
): Promise<{ answer: string; warnings: string[] }> {
  const citationRegex = /\[Forrás:\s*([a-zA-Z0-9_-]+)\]/g;
  const warnings: string[] = [];

  const cleaned = answer.replace(citationRegex, (match, id) => {
    if (!validSourceIds.has(id)) {
      warnings.push(`Hallucinált forrás-ID: ${id}`);
      return ""; // strip vagy retry
    }
    return match;
  });

  return { answer: cleaned, warnings };
}

Ha sok a warning → újraindítsd a generálást szigorúbb prompttal.

Evaluation — anélkül vakon mész

A legtöbb csapat nem méri a RAG minőségét. „Működik" — amíg egy ügyfél nem panaszkodik. Aztán nem tudják, miért romlott el.

Az eval dataset

Készíts 20-100 kérdés-válasz párt kézzel, kategorizálva:

type EvalCase = {
  id: string;
  question: string;
  expectedAnswer: string;
  expectedSources: string[];     // mely chunkoknak kellene visszajönniük
  category: "factual" | "comparative" | "procedural" | "edge_case";
  difficulty: "easy" | "medium" | "hard";
};

Retrieval metrikák

function calculateRecallAtK(
  retrieved: string[],
  expected: string[],
  k: number
): number {
  const top = new Set(retrieved.slice(0, k));
  const hits = expected.filter(id => top.has(id)).length;
  return hits / expected.length;
}

// Per-case mérés:
for (const testCase of evalDataset) {
  const retrieved = await hybridSearch(testCase.question);
  const recall5 = calculateRecallAtK(
    retrieved.map(r => r.id),
    testCase.expectedSources,
    5
  );
  console.log(`${testCase.id}: Recall@5 = ${recall5}`);
}

Cél: Recall@5 > 0.85. Ha ez alatti, nem a generálással van baj — a retrieval nem találja a releváns tartalmat.

Generation metrikák — LLM-as-a-judge

A válasz minőségét LLM-mel értékeled:

async function evaluateAnswer(
  question: string,
  expectedAnswer: string,
  actualAnswer: string,
  sources: string[]
): Promise<EvalScore> {
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [{
      role: "system",
      content: `Értékeld a választ az alábbi szempontok szerint (1-5):
        - faithfulness: csak a forrásokra támaszkodik?
        - relevance: a kérdésre válaszol?
        - completeness: minden szükséges infót tartalmaz?
        - correctness: egyezik a várt válasszal?
        JSON: {faithfulness, relevance, completeness, correctness, reasoning}`
    }, {
      role: "user",
      content: `Kérdés: ${question}
        Várt válasz: ${expectedAnswer}
        Tényleges válasz: ${actualAnswer}
        Források: ${sources.join("\n")}`
    }],
    response_format: { type: "json_object" },
    temperature: 0
  });

  return JSON.parse(response.choices[0].message.content);
}

Industry tools: Ragas, TruLens, DeepEval. Ne építs sajátot, ha nem muszáj.

CI/CD integráció

Minden RAG-változtatást (új chunking, új embedder, új prompt) futtass át az eval suite-on:

npm run rag:eval
# Recall@5: 0.87 (was 0.84) ✓
# Faithfulness: 4.6/5 (was 4.5) ✓
# Cost per query: $0.012 (was $0.011) ⚠

A regresszió azonnal kibukik — nem 2 hét múlva egy ügyfél-panaszból.

Production gotchák

Stale data

A vector DB nem frissül magától. Kell:

  • Webhook a forrásrendszerből (CMS, Confluence, Drive) → re-embed
  • Scheduled re-index (napi / heti)
  • Soft delete: töröld a régi chunkokat, ne csak overwrite-old

Multi-tenant izoláció

Soha ne hagyd, hogy A tenant lekérdezze B tenant adatait:

// ROSSZ: filter csak prompt-szinten
const chunks = await vectorDb.search(query, { topK: 10 });
const filtered = chunks.filter(c => c.tenantId === userTenantId);

// JÓ: filter a query-szinten (DB-ben)
const chunks = await vectorDb.search(query, {
  topK: 10,
  filter: { tenantId: userTenantId }
});

A „filter prompt után" megközelítés adatszivárgási kockázat — ha a top-K mind másik tenanté, üres választ kapsz, és nem tudod, miért.

Cost monitoring

RAG-ben a költség könnyen elszáll:

  • Embedding (egyszeri, de tömeges)
  • Vector DB hosting
  • LLM hívások (kontextus-méret szorozza!)
  • Re-ranker hívások

Alapszabály: minden user-query-hez logold a token-számot. Hetente nézd át a top-1% legdrágább queryt — onnan jön a pénz 80%-a.

Latency budget

Lépés Tipikus latency
Query embedding 50-100ms
Vector search (10K chunks) 20-50ms
Vector search (10M chunks) 100-300ms
BM25 30-100ms
Re-ranking (top-20) 100-300ms
LLM generation (streamed) 500ms - 3s

Total: 1-4s P95. Ha ez nem fér bele, streamelj (SSE) a frontendre.

Mikor NE használj RAG-et?

A RAG nem mindenre jó. Ne használd:

  • Kreatív feladatra (marketing copy, brainstorm) — itt nem cél a faktualitás
  • Konzisztens stílusú outputra (pl. brand voice) — ehhez fine-tuning kell
  • Real-time aggregációra (pl. „mennyi a mai bevétel?") — ehhez DB query / SQL agent
  • Komplex multi-step workflowra — ehhez agent, nem RAG
  • Strukturált adatokra (táblázatok, listák) — ehhez text-to-SQL jobb

A RAG azon a sweet spoton működik a legjobban, ahol:

  • Jól strukturált, szöveges dokumentumok vannak
  • A felhasználó kérdez (nem alkot)
  • A válasznak forrás-támogatottnak kell lennie

Összefoglalás: 8 takeaway

  1. A naiv RAG demo-ra jó, production-re nem — chunking, hybrid search, re-ranking, eval mind kell.
  2. A chunking az alap — szemantikus határokon vágj, ne fix karakterszám alapján. 400-600 token a default.
  3. Metadata > tartalom — citation, filtering, permission, frissesség mind ezen múlik.
  4. Hybrid search (vector + BM25) + re-ranker = ~85% top-5 relevancia. Ezt érdemes elérni.
  5. Query rewriting kontextusos beszélgetésekhez kötelező — különben a 3. üzenettől halott a retrieval.
  6. A prompt kontraktus — engedd meg a „nincs információ" választ, kötelezd a citálást, validáld a forrás-ID-ket.
  7. Eval suite kötelező — 20-100 kérdés, Recall@K, LLM-as-a-judge. Anélkül vakon mész.
  8. Multi-tenant izoláció DB-szinten, ne prompt-szinten. Ez nem optimalizáció, hanem biztonság.

A jó RAG-rendszer nem trükkös — csak fegyelmezett. Minden réteget meg kell mérni, minden hibát logolni, minden változtatást evalon átengedni. Ha ezt megteszed, a 80%-os hibás demo-RAG-ből 95-98%-os production RAG lesz.

Az utolsó 2-5% mindig human-in-the-loop. Ezt fogadd el.

Demo-RAG-ed van, production kell?

Egy RAG audit során átnézzük a chunking stratégiádat, a retrieval pipeline-t, az eval lefedettséget és a multi-tenant izolációt — mielőtt élesbe küldenéd.

RAG audit kérése