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