Každý tým dokáže za odpoledne sestavit RAG prototyp, který na demo působí přesvědčivě. Ale mezi „funguje v Jupyteru" a „běží v produkci 24/7 na reálných datech" leží propast, kterou většina projektů nikdy nepřekročí. Tenhle článek je o tom, jak ji překročit — bez iluzí a bez marketingových zkratek.
Retrieval-Augmented Generation (RAG) je architektonický pattern, kde jazykový model negeneruje odpovědi čistě z parametrické paměti, ale nejdřív si vyhledá relevantní kontext z externích zdrojů — dokumentů, databází, API — a teprve pak formuluje odpověď. Principiálně jednoduchý koncept: embed dotaz, najdi podobné dokumenty, vlož je do promptu, generuj odpověď.
Problém je, že tato jednoduchá verze funguje jen na jednoduchá data. Jakmile máte tisíce dokumentů různých formátů, data, která se mění denně, uživatele, kteří kladou neočekávané otázky, a business požadavek na nulovou toleranci halucinací — naivní RAG se rozpadne. Ne na úrovni modelu, ale na úrovni celého systému.
Za poslední dva roky jsme viděli desítky RAG implementací — vlastních i klientských. Tyhle chyby se opakují s překvapivou pravidelností.
Většina prototypů rozdělí dokumenty na fixní kusy po 512 tokenech a jde dál. V produkci je to katastrofa. Fixní chunking ignoruje strukturu dokumentu — rozsekne tabulku napůl, oddělí nadpis od obsahu, smíchá dva nesouvisející paragrafy do jednoho chunku. Výsledek: retriever vrací kontextově nesmyslné fragmenty a model z nich generuje nesmyslné odpovědi.
Řešení: Hierarchický chunking respektující strukturu dokumentu. Chunky na úrovni sekcí, paragrafů a vět s překryvem. Metadata o hierarchii (ke kterému dokumentu, kapitole a sekci chunk patří) jsou součástí indexu. Pro tabulky a strukturovaná data — separátní pipeline, ne text chunking.
Obecné embedding modely (ada-002, nomic-embed) fungují rozumně na běžný text. Ale pokud vaše knowledge base obsahuje právní smlouvy, technickou dokumentaci a zákaznické emaily, jeden model na to nestačí. Sémantická vzdálenost mezi dotazem „jaká je výpovědní lhůta" a relevantním paragrafem smlouvy může být pro obecný embedding model příliš velká.
Řešení: Fine-tuned embedding modely na doménových datech. Nebo alespoň hybridní retrieval — kombinace vektorového vyhledávání s keyword-based BM25, kde sémantický model zachytí kontext a BM25 zachytí přesné termíny.
Vektorové vyhledávání vrátí top-k výsledků seřazených podle cosine similarity. Problém: cosine similarity na embedding vektorech je přibližný signál, ne přesný relevance score. Dva dokumenty se similarity 0.82 a 0.80 mohou být zásadně odlišné v kvalitě odpovědi na konkrétní dotaz. Bez rerankingu model dostane nesprávně seřazený kontext, což přímo ovlivňuje kvalitu odpovědi.
Řešení: Cross-encoder reranker (Cohere Rerank, bge-reranker, ColBERT) jako druhý stupeň. Retriever vrátí top-50 kandidátů, reranker je přeseřadí na základě skutečné relevance k dotazu, a model dostane top-5 skutečně nejrelevantnějších chunků.
RAG prototyp naplníte jednou a demonstraci uděláte nad statickými daty. V produkci se dokumenty mění denně. Zákaznická dokumentace se aktualizuje, smlouvy se obnovují, produktové specifikace se verzují. Pokud váš index neodráží aktuální stav dat, model odpovídá na základě zastaralých informací — a to je horší než žádná odpověď.
Řešení: Inkrementální indexovací pipeline s change detection. Content hashing pro detekci změn, timestampy pro freshness scoring, verzování embeddings. A hlavně: jasná strategie, kdy se reindexuje a jak se řeší konflikt mezi starými a novými verzemi dokumentu.
„Zeptali jsme se 10 otázek a odpovědi vypadaly OK" není evaluace. V produkci potřebujete systematické měření kvality retrieval i generace. Bez metrik nevíte, jestli změna v chunking strategii zlepšila nebo zhoršila kvalitu odpovědí. Nevíte, na jakých typech dotazů systém selhává. A nevíte, kdy kvalita klesla pod akceptovatelnou úroveň.
Řešení: Evaluační pipeline od prvního dne. Retrieval metriky (recall@k, MRR, nDCG), generační metriky (faithfulness, answer relevancy, hallucination rate), a golden test set s ručně ověřenými páry otázka–odpověď. Automatizované, běží po každé změně.
Po desítkách nasazení se nám vykrystalizovala architektura, která funguje konzistentně napříč doménami. Tři klíčové vrstvy: indexovací pipeline, retrieval strategie a reranking.
Indexovací pipeline je základ, na kterém stojí všechno ostatní. Vstupem jsou surové dokumenty v různých formátech, výstupem je strukturovaný, prohledávatelný index. Klíčové komponenty:
Naivní „embed query → cosine search → top-k" má v produkci dvě zásadní slabiny: jednokrokový retrieval nezvládá složité dotazy a čistě vektorové vyhledávání ztrácí přesné termíny. Proto používáme multi-stage retrieval:
Reranking je nejlevnější způsob, jak výrazně zlepšit kvalitu RAG systému. Bi-encoder (embedding model) je rychlý, ale nepřesný — porovnává query a dokument nezávisle. Cross-encoder je pomalý, ale přesný — zpracovává query a dokument společně a generuje skutečný relevance score.
V praxi to vypadá tak, že bi-encoder vrátí top-50 kandidátů za ~20ms, cross-encoder je přeseřadí za ~100ms, a model dostane top-5 skutečně nejrelevantnějších chunků. Latence se zvýší minimálně, kvalita odpovědí výrazně — typicky vidíme 15–25% nárůst v answer accuracy jen přidáním rerankeru.
Následující ukázka ukazuje skeleton production-ready retrieval pipeline s hybridním vyhledáváním, rerankingem a structured output:
# production_retrieval.py — RAG retrieval pipeline
from dataclasses import dataclass
from typing import list
import hashlib, logging
logger = logging.getLogger(__name__)
@dataclass
class RetrievedChunk:
content: str
source: str
score: float
metadata: dict
class ProductionRetriever:
"""Hybrid retriever s reranking a freshness scoring."""
def __init__(self, vector_store, bm25_index, reranker, llm):
self.vector_store = vector_store
self.bm25 = bm25_index
self.reranker = reranker
self.llm = llm
def retrieve(self, query: str, top_k: int = 5) -> list:
# 1. Query rewriting — LLM generuje varianty
queries = self._rewrite_query(query)
logger.info(f"Rewritten into {len(queries)} variants")
# 2. Hybrid search — vector + BM25
candidates = {}
for q in queries:
vec_results = self.vector_store.search(q, k=30)
bm25_results = self.bm25.search(q, k=30)
merged = self._rrf_merge(vec_results, bm25_results)
for chunk_id, score in merged.items():
candidates[chunk_id] = max(
candidates.get(chunk_id, 0), score
)
# 3. Reranking — cross-encoder přeřadí top-50
top_candidates = sorted(
candidates.items(), key=lambda x: x[1], reverse=True
)[:50]
chunks = [self._load_chunk(cid) for cid, _ in top_candidates]
reranked = self.reranker.rerank(query, chunks)
# 4. Freshness penalty — starší dokumenty skórují méně
for chunk in reranked:
chunk.score *= self._freshness_weight(chunk.metadata)
result = sorted(reranked, key=lambda c: c.score, reverse=True)[:top_k]
logger.info(f"Retrieved {len(result)} chunks, top score: {result[0].score:.3f}")
return result
def _rrf_merge(self, vec_results, bm25_results, k=60):
"""Reciprocal Rank Fusion — spojí dva ranked listy."""
scores = {}
for rank, (doc_id, _) in enumerate(vec_results):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
for rank, (doc_id, _) in enumerate(bm25_results):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return scores
def _rewrite_query(self, query: str) -> list:
prompt = f"Přeformuluj dotaz do 3 variant pro vyhledávání:\n{query}"
variants = self.llm.generate(prompt).split("\n")
return [query] + [v.strip() for v in variants if v.strip()]
Klíčové aspekty této implementace: query rewriting generuje varianty dotazu pro lepší pokrytí, hybrid search kombinuje sémantické i keyword vyhledávání, RRF merge spojuje výsledky bez nutnosti normalizovat skóre z různých systémů, a freshness penalty penalizuje zastaralé dokumenty. V reálném nasazení přidáváme ještě metadata filtering, caching a circuit breaker pro případ výpadku vector store.
RAG systém bez monitoringu je černá skříňka. Nevíte, jestli funguje, dokud si někdo nestěžuje. A stěžovat si začne pozdě — po desítkách špatných odpovědí, které podkopaly důvěru uživatelů. Proto monitoring stavíme od prvního dne.
Automatizované evaluace běží po každé změně — jiný chunking, nový embedding model, úprava promptu. Bez toho střílíte naslepo. Používáme kombinaci:
V CORE SYSTEMS přistupujeme k RAG jako k datovému inženýrskému problému, ne jako k AI experimentu. Model je jen jedna komponenta — a obvykle ne ta nejsložitější. Největší práce je v datovém pipeline, kvalitě indexu a provozní spolehlivosti.
Každý projekt začíná data audit workshopem: zmapujeme datové zdroje, zhodnotíme kvalitu a strukturu dokumentů, identifikujeme edge cases (skenované PDF, tabulky v Excelu, vícejazyčný obsah). Teprve pak navrhujeme architekturu — protože chunking strategie pro právní smlouvy je zásadně odlišná od strategie pro produktovou dokumentaci.
Dodáváme end-to-end: indexovací pipeline, retrieval engine, generační vrstvu, evaluační framework a monitoring dashboard. Vše běží na klientské infrastruktuře — podporujeme Azure, AWS i on-premise, protože v regulovaných odvětvích data nesmí opustit perimetr. Provozujeme to, co stavíme — to nás nutí stavět věci, které v produkci skutečně fungují.
Používáme open-source stack (LlamaIndex, pgvector/Qdrant, RAGAS) doplněný o vlastní komponenty pro governance, security a enterprise integrace. Nejsme locked na jednoho LLM providera — podporujeme OpenAI, Anthropic, Azure OpenAI i lokální modely přes vLLM/Ollama, protože volba modelu je business rozhodnutí, ne technické omezení.
Největší ponaučení z desítek RAG nasazení? Kvalita RAG systému je z 80 % určena kvalitou dat a retrievalu, ne kvalitou modelu. Lepší chunking, lepší embeddingy, reranking a čisté datové pipeline vám přinesou víc než upgrade z GPT-4o na jakýkoliv novější model.
Firmy, které k RAG přistoupí jako k datovému inženýrskému problému — s robustním pipeline, systematickým měřením a provozní disciplínou — budou mít systém, který funguje 24/7. Ostatní budou pořád předvádět demo v Jupyteru a divit se, proč to v produkci nefunguje.