RAG
Co tato dovednost pokrývá
Návrh a ladění retrieval-augmented generation (RAG) pipeline: rozdělení zdrojových dokumentů na chunky, vyhledávání relevantních chunků (dense, sparse nebo hybridní), reranking výsledků před předáním modelu a hodnocení, zda je vygenerovaná odpověď skutečně podložená tím, co bylo vyhledáno. Pro základy embeddingů a vektorového vyhledávání a lokální provoz modelů viz Vývoj AI a Ollama.
Kdy ji použít
- Při návrhu nebo ladění RAG pipeline od začátku do konce
- Při výběru strategie chunkingu a velikosti/překryvu chunků pro sadu zdrojových dokumentů
- Při rozhodování mezi dense, sparse (klíčová slova) nebo hybridním retrievalem pro vyhledávací funkci
- Při přidávání kroku rerankingu, protože top-k retrieval opakovaně míjí správnou pasáž
- Při hodnocení, zda jsou odpovědi RAG systému skutečně podložené vyhledaným kontextem
Architektura RAG
RAG pipeline má dvě fáze:
Ingesce (offline): načtení dokumentů → rozdělení na chunky → embedding každého chunku → uložení vektorů a textu do vektorové databáze.
Dotaz (online): embedding uživatelské otázky → vyhledání top-k nejrelevantnějších chunků → volitelně reranking → předání vyhledaného textu spolu s otázkou LLM → generování odpovědi.
Dokumenty → Chunking → Embedding → Vektorová databáze
│
Dotaz uživatele → Embedding → Retrieval ────┘
│
Reranking (volitelný)
│
Generování LLM → Odpověď
Strategie chunkingu
Způsob dělení zdrojového textu přímo ovlivňuje kvalitu retrievalu — příliš velké chunky ředí relevanční signál, příliš malé chunky ztrácejí kontext.
Pevná velikost s překryvem:
def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start += chunk_size - overlap
return chunks
Rekurzivní dělení (zkouší postupně hranice odstavce → věty → slova, takže se chunky lámou na přirozených hranicích textu, kde je to možné):
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document_text)
Doporučení:
- Začněte kolem 300–800 znaků (přibližně 100–300 tokenů) s překryvem 10–20 %; hodnoty laďte podle vlastní evaluace retrievalu, ne podle pevného pravidla
- Preferujte dělení na přirozených hranicích (odstavce, sekce, nadpisy) před dělením podle libovolného počtu znaků, pokud to formát zdroje umožňuje
- Ke každému chunku připojte metadata (zdrojový dokument, název sekce, číslo stránky) — jsou potřeba pro citace a pro filtrování v čase dotazu
Metody retrievalu
Dense retrieval (podobnost embeddingů)
import numpy as np
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def dense_retrieve(query_embedding, chunk_embeddings, chunks, top_k=5):
scores = [cosine_similarity(query_embedding, emb) for emb in chunk_embeddings]
ranked = sorted(zip(scores, chunks), key=lambda x: x[0], reverse=True)
return ranked[:top_k]
Dobře funguje pro shodu významu a parafráze; slabší na přesné termíny, které nikdy neviděl (produktové kódy, zkratky, vzácná vlastní jména).
Sparse retrieval (klíčová slova / BM25)
from rank_bm25 import BM25Okapi
tokenized_chunks = [chunk.lower().split() for chunk in chunks]
bm25 = BM25Okapi(tokenized_chunks)
query_tokens = query.lower().split()
scores = bm25.get_scores(query_tokens)
top_indices = np.argsort(scores)[::-1][:top_k]
Dobře funguje na přesné shody termínů; míjí sémanticky related text, který nesdílí slovní zásobu.
Hybridní retrieval (reciprocal rank fusion)
Kombinace obou žebříčků místo výběru jednoho:
def reciprocal_rank_fusion(rankings: list[list[str]], k: int = 60) -> list[str]:
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(scores, key=scores.get, reverse=True)
k=60 je běžně používaná výchozí hodnota z originální práce o RRF — tlumí vliv jakéhokoli jednotlivého vysokého pořadí, takže dokument, který je #1 v jednom žebříčku a nezařazený ve druhém, automaticky nedominuje.
Reranking
Cross-encoder hodnotí každou dvojici (dotaz, chunk) společně, což je přesnější než pouhá podobnost embeddingů, ale příliš pomalé na běh nad celým korpusem — proto se aplikuje jen na top-k kandidátů z retrievalu:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
pairs = [(query, chunk) for chunk in retrieved_chunks]
scores = reranker.predict(pairs)
reranked = [chunk for _, chunk in sorted(zip(scores, retrieved_chunks), reverse=True)]
Typický vzor: levně vyhledejte top 20–50 kandidátů (dense/sparse/hybrid), poté rerankingem zúžte na top 3–5, které skutečně jdou do promptu.
End-to-End příklad
Propojení chunkingu, dense retrievalu a generování s lokálním modelem přes Ollamu (nastavení viz Ollama):
import ollama
import numpy as np
def embed(text: str) -> np.ndarray:
result = ollama.embed(model="mxbai-embed-large", input=text)
return np.array(result["embeddings"][0])
def answer(question: str, chunks: list[str], top_k: int = 3) -> str:
chunk_embeddings = [embed(c) for c in chunks]
query_embedding = embed(question)
ranked = dense_retrieve(query_embedding, chunk_embeddings, chunks, top_k=top_k)
context = "\n\n".join(chunk for _, chunk in ranked)
prompt = f"Answer the question using only the context below.\n\nContext:\n{context}\n\nQuestion: {question}"
response = ollama.chat(model="llama3.2:1b", messages=[{"role": "user", "content": prompt}])
return response["message"]["content"]
Evaluace
Kvalita RAG má dva nezávislé druhy selhání — špatný retrieval (správný chunk nebyl nikdy nalezen) a špatné generování (model dobrý kontext ignoroval nebo špatně interpretoval) — proto je měřte odděleně:
- Context precision / recall: z vyhledaných chunků, kolik jich bylo skutečně relevantních, a z relevantních chunků, které existují, kolik jich bylo vyhledáno
- Faithfulness / groundedness: obsahuje vygenerovaná odpověď pouze tvrzení podložená vyhledaným kontextem, nebo přidává nepodložená tvrzení
- Answer relevance: řeší odpověď skutečně danou otázku, nezávisle na tom, zda je podložená
RAGAS je široce používaná open-source knihovna pro výpočet těchto metrik nad označenou evaluační sadou.
Doporučené postupy
- Hodnoťte chunking a retrieval odděleně od generování — špatná odpověď může znamenat, že správný chunk nebyl nikdy vyhledán, ne že model špatně uvažoval
- V produkci logujte vyhledané chunky spolu s každou odpovědí; bez nich nepoznáte, zda šlo o selhání retrievalu, nebo generování
- Velikost chunků a top-k dolaďte podle vlastní evaluační sady — výchozí hodnoty fungující pro jeden korpus se automaticky nepřenáší na jiný
- Cachujte embeddingy pro nezměněné zdrojové dokumenty; přegenerování embeddingů celého korpusu při každém běhu ingesce je zbytečný náklad
- Ke každému chunku připojte zdrojová metadata, aby odpovědi mohly citovat, odkud pocházejí