Pokročilé RAG pipeline v roce 2026: Hybridní vyhledávání, GraphRAG a agentní architektury

Kompletní průvodce pokročilými RAG technikami v roce 2026: hybridní retrieval s BM25 a vektory, reranking cross-encodery, sémantický chunking, GraphRAG, agentní architektury (CRAG) a evaluace s RAGAS. S praktickými příklady v Pythonu.

Pokročilé RAG pipeline: Od hybridního vyhledávání po agentní architektury (2026)

Pokud jste si někdy v roce 2024 nasadili svůj první RAG systém — vektorová databáze, základní chunking, jednoduchý prompt — a od té doby jste na něm nic neměnili, tak mám pro vás zprávu: váš pipeline je s největší pravděpodobností zastaralý. Ne že by přestal fungovat. Ale celý ekosystém se mezitím posunul o několik generací dopředu. A vaši uživatelé to poznají.

V roce 2026 prostě nestačí rozsekat dokument na kusy, embedovat je a doufat, že cosine similarity vrátí něco relevantního. Požadavky na přesnost, komplexnost dotazů a škálovatelnost vzrostly natolik, že naivní RAG architektury nedokážou držet krok. Gartner ve své prognóze předpovídá, že 70 % podniků nasadí AI-řízenou automatizaci promptů do konce roku 2026 — a většina z těchto nasazení bude zahrnovat nějakou formu pokročilého RAG pipeline.

V tomhle článku projdeme celou cestu od hybridního vyhledávání přes reranking, pokročilé chunking strategie, GraphRAG, agentní a samoopravné architektury až po produkční evaluaci. Každou techniku si ukážeme na praktických příkladech v Pythonu. Žádný akademický přehled — cíl je, abyste si z toho odnesli konkrétní postupy, které můžete nasadit příští týden.

Hybridní vyhledávání: Když vektor sám nestačí

Problém čistě vektorového vyhledávání

Dense vector embeddings jsou skvělé v zachycení sémantické podobnosti. Dotaz „jak snížit latenci API" spolehlivě najde chunk o „optimalizaci doby odezvy webových služeb". Jenže mají slepá místa.

Pokud uživatel hledá konkrétní identifikátor — číslo faktury, název funkce, kód chyby — vektorové vyhledávání často selhává. Embedding model prostě nemá důvod přiřadit vysokou podobnost přesnému textovému řetězci, pokud nebyl explicitně trénován na tento typ shody. Je to logické, ale v praxi to občas pořádně zamotá hlavu.

Naopak klasické klíčové vyhledávání (BM25, TF-IDF) exceluje v přesných shodách a termínové relevanci, ale úplně selhává u sémantických dotazů. Takže řešení? Kombinace obou přístupů — hybridní retrieval — je v roce 2026 de facto standard.

Reciprocal Rank Fusion (RRF)

Hlavní otázka hybridního vyhledávání zní: jak sloučit výsledky ze dvou fundamentálně odlišných systémů? Skóre z BM25 a cosine similarity nejsou na stejné škále a nelze je přímo porovnávat. Reciprocal Rank Fusion (RRF) řeší tento problém elegantně — místo porovnávání absolutních skóre pracuje s pořadím (rankem) výsledků.

Vzorec je překvapivě jednoduchý:

RRF_score(d) = Σ 1 / (k + rank_i(d))

Kde k je konstanta (typicky 60), rank_i(d) je pozice dokumentu d v i-tém vyhledávacím systému, a sčítáme přes všechny systémy. Dokument, který se umístí vysoko v obou systémech, získá výrazně vyšší kombinované skóre.

from typing import List, Dict, Tuple

def reciprocal_rank_fusion(
    results_lists: List[List[Tuple[str, float]]],
    k: int = 60
) -> List[Tuple[str, float]]:
    """
    Sloučí výsledky z více vyhledávacích systémů pomocí RRF.

    Args:
        results_lists: Seznam seznamů (doc_id, score) z každého systému
        k: Vyhlazovací konstanta (default 60)

    Returns:
        Seřazený seznam (doc_id, rrf_score)
    """
    rrf_scores: Dict[str, float] = {}

    for results in results_lists:
        for rank, (doc_id, _score) in enumerate(results, start=1):
            if doc_id not in rrf_scores:
                rrf_scores[doc_id] = 0.0
            rrf_scores[doc_id] += 1.0 / (k + rank)

    sorted_results = sorted(
        rrf_scores.items(),
        key=lambda x: x[1],
        reverse=True
    )
    return sorted_results


# Příklad použití s Qdrant (hybridní vyhledávání)
from qdrant_client import QdrantClient
from qdrant_client.models import SparseVector

client = QdrantClient(url="http://localhost:6333")

# Dense vektorové vyhledávání
dense_results = client.search(
    collection_name="documents",
    query_vector=embedding_model.encode(query).tolist(),
    limit=20
)

# Sparse BM25 vyhledávání
sparse_results = client.search(
    collection_name="documents",
    query_vector=qdrant_models.NamedSparseVector(
        name="bm25",
        vector=bm25_encoder.encode(query)
    ),
    limit=20
)

# Fúze výsledků
dense_list = [(hit.id, hit.score) for hit in dense_results]
sparse_list = [(hit.id, hit.score) for hit in sparse_results]
fused = reciprocal_rank_fusion([dense_list, sparse_list])

Kdy použít jaký přístup

V praxi záleží hlavně na typu dat a dotazů. Čistě vektorové vyhledávání funguje dobře pro sémantické dotazy nad homogenním textem — třeba vyhledávání v článcích nebo dokumentaci. Čistě BM25 je vhodné pro přesné vyhledávání v strukturovaných datech, log souborech nebo kódu. Hybridní přístup je ideální pro produkční systémy, kde nemáte kontrolu nad typem dotazů — a to je upřímně většina reálných případů.

Režijní náklady hybridního přístupu jsou minimální ve srovnání se zlepšením kvality výsledků. Takže pokud váháte — zvolte hybridní.

Reranking s Cross-Encodery: Druhý průchod, který dělá rozdíl

Proč bi-encodery nestačí

Bi-encodery (modely, které nezávisle embeddují dotaz a dokument) jsou rychlé a škálovatelné — proto je používáme v první fázi vyhledávání. Ale mají fundamentální omezení: dotaz a dokument nikdy přímo neinteragují uvnitř modelu. Model nemůže zachytit jemné vztahy mezi konkrétními slovy v dotazu a odpovídajícími pasážemi v dokumentu.

Cross-encodery tento problém řeší. Přijímají dotaz a dokument současně jako jeden vstup a můžou modelovat vzájemné interakce na úrovni tokenů. Výsledkem je výrazně přesnější ohodnocení relevance — za cenu vyšší výpočetní náročnosti. Proto je používáme jako druhý průchod (reranking) nad menší množinou kandidátů z prvního průchodu.

Praktický příklad rerankingu

from sentence_transformers import CrossEncoder
import numpy as np

# Inicializace cross-encoder modelu
reranker = CrossEncoder(
    "BAAI/bge-reranker-v2.5-gemma2-9b",
    max_length=1024,
    device="cuda"
)

def rerank_documents(
    query: str,
    documents: list[dict],
    top_k: int = 5
) -> list[dict]:
    """
    Přeřadí dokumenty pomocí cross-encoder modelu.

    Args:
        query: Uživatelský dotaz
        documents: Seznam dokumentů s klíčem 'text'
        top_k: Počet vrácených dokumentů

    Returns:
        Top-k dokumenty seřazené podle relevance
    """
    if not documents:
        return []

    # Vytvoření párů (query, document) pro cross-encoder
    pairs = [(query, doc["text"]) for doc in documents]

    # Skórování všech párů najednou
    scores = reranker.predict(pairs, batch_size=32)

    # Přiřazení skóre a seřazení
    for doc, score in zip(documents, scores):
        doc["rerank_score"] = float(score)

    reranked = sorted(
        documents,
        key=lambda x: x["rerank_score"],
        reverse=True
    )

    return reranked[:top_k]


# Použití v pipeline
query = "Jaké jsou best practices pro retry logiku v microservices?"

# Fáze 1: Rychlý retrieval (bi-encoder + BM25)
candidates = hybrid_search(query, top_k=50)

# Fáze 2: Přesný reranking (cross-encoder)
final_docs = rerank_documents(query, candidates, top_k=5)

# Fáze 3: Generování odpovědi s top-5 dokumenty
context = "\n\n".join([doc["text"] for doc in final_docs])
response = llm.generate(
    f"Na základě kontextu odpověz na dotaz.\n\n"
    f"Kontext:\n{context}\n\n"
    f"Dotaz: {query}"
)

Dvoustupňový retrieval (rychlý bi-encoder → přesný cross-encoder) je v roce 2026 považovaný za minimum pro produkční RAG systémy. A čísla mluví jasně: Anthropic ve svém výzkumu kontextuálního retrievalu zjistil, že kombinace reranked kontextuálních embeddingů s kontextuálním BM25 snížila míru selhání vyhledávání v top-20 chuncích o 67 % — z 5,7 % na pouhých 1,9 %. To je obrovský rozdíl. V produkčním prostředí každé selhání retrievalu znamená nekvalitní odpověď pro uživatele — a to si nemůžete dovolit.

Strategie chunkingu: Základ, na kterém stojí vše ostatní

Chunking je často podceňovaná součást RAG pipeline, a přitom má obrovský vliv na kvalitu výsledků. Špatně rozsekaný dokument může způsobit, že relevantní informace skončí rozdělená mezi dva chunky, z nichž ani jeden sám o sobě nedává smysl. Z vlastní zkušenosti — tohle je přesně to místo, kde většina RAG projektů ztrácí nejvíc kvality, aniž by si to uvědomovala.

Fixed-size chunking

Nejjednodušší přístup — rozdělení textu na bloky o fixní velikosti (typicky 256–1024 tokenů) s překryvem. Je to rychlé, jednoduché a deterministické. Problém nastává, když hranice chunku spadne doprostřed logické myšlenky, odstavce nebo tabulky. A to se stane častěji, než byste čekali.

Sémantický chunking

Sémantický chunking analyzuje text a umisťuje hranice tam, kde dochází ke změně tématu. Typicky to funguje tak, že se pro každou větu nebo odstavec spočítá embedding a hranice se umístí tam, kde cosine similarity mezi sousedními segmenty klesne pod práh.

from sentence_transformers import SentenceTransformer
import numpy as np

def semantic_chunking(
    text: str,
    model: SentenceTransformer,
    similarity_threshold: float = 0.75,
    max_chunk_size: int = 1000
) -> list[str]:
    """
    Rozdělí text na sémanticky koherentní chunky.
    """
    # Rozdělení na věty
    sentences = [s.strip() for s in text.split('.') if s.strip()]

    if len(sentences) <= 1:
        return [text]

    # Embeddování všech vět
    embeddings = model.encode(sentences)

    # Výpočet podobnosti mezi sousedními větami
    chunks = []
    current_chunk = [sentences[0]]

    for i in range(1, len(sentences)):
        similarity = np.dot(embeddings[i-1], embeddings[i]) / (
            np.linalg.norm(embeddings[i-1]) * np.linalg.norm(embeddings[i])
        )

        current_text = '. '.join(current_chunk)

        if similarity < similarity_threshold or len(current_text) > max_chunk_size:
            chunks.append(current_text + '.')
            current_chunk = [sentences[i]]
        else:
            current_chunk.append(sentences[i])

    if current_chunk:
        chunks.append('. '.join(current_chunk) + '.')

    return chunks

Late Chunking

Late chunking je koncept představený v roce 2024, který řeší fundamentální problém — ztrátu kontextu při chunkingu. Místo toho, aby se text nejprve rozsekal a pak embeddoval, late chunking nejprve zpracuje celý dokument transformerovým modelem (získá kontextualizované token embeddings) a teprve poté provede pooling na úrovni chunků.

Výsledek? Každý chunk si zachovává kontextovou informaci z celého dokumentu. Lokální zájmena a reference jsou správně rozřešeny, protože model „viděl" celý text. Jednoduše řečeno — chunk ví, odkud přišel.

Kontextuální retrieval (přístup Anthropic)

Anthropic přišel s elegantním řešením problému ztráty kontextu: před embedováním každého chunku použije LLM k vygenerování krátkého kontextu, který chunk zasadí do širšího rámce dokumentu. Tento kontext se připojí na začátek chunku před embedováním.

import anthropic

client = anthropic.Anthropic()

def add_contextual_header(
    chunk: str,
    full_document: str,
    model: str = "claude-sonnet-4-20250514"
) -> str:
    """
    Přidá kontextuální hlavičku ke chunku pomocí Claude.
    """
    prompt = f"""Zde je celý dokument:
<document>
{full_document}
</document>

Zde je konkrétní chunk z tohoto dokumentu:
<chunk>
{chunk}
</chunk>

Napiš stručný kontext (1-2 věty), který zasadí tento chunk do
kontextu celého dokumentu. Kontext by měl pomoci vyhledávacímu
systému pochopit, o čem chunk pojednává.

Odpověz POUZE kontextem, nic dalšího."""

    response = client.messages.create(
        model=model,
        max_tokens=200,
        messages=[{"role": "user", "content": prompt}]
    )

    context = response.content[0].text
    return f"{context}\n\n{chunk}"


# Příklad pipeline
document = load_document("technical_spec.pdf")
raw_chunks = split_into_chunks(document, chunk_size=512, overlap=50)

# Obohacení chunků kontextem
contextualized_chunks = []
for chunk in raw_chunks:
    enriched = add_contextual_header(chunk, document)
    contextualized_chunks.append(enriched)

# Embedování obohacených chunků
embeddings = embedding_model.encode(contextualized_chunks)

Tenhle přístup je samozřejmě dražší (vyžaduje LLM volání pro každý chunk), ale výsledky jsou opravdu přesvědčivé. Anthropic doporučuje kombinovat kontextuální retrieval s prompt cachingem, který při opakovaném předávání stejného dokumentu výrazně snižuje náklady. Takže v praxi to nemusí být tak bolestivé, jak to na první pohled vypadá.

Praktické srovnání strategií

  • Fixed-size: Rychlé, jednoduché, vhodné pro prototypy a homogenní texty. Nevhodné pro strukturované dokumenty.
  • Sémantický chunking: Lepší koherence chunků, vyšší kvalita retrievalu. Vyžaduje embedding model a ladění prahu.
  • Late chunking: Zachovává kontext celého dokumentu, žádné dodatečné náklady za LLM. Vyžaduje specifický embedding model s podporou long-context.
  • Kontextuální retrieval: Nejlepší kvalita, ale nejvyšší náklady. Ideální pro menší, vysoce hodnotné datasety (interní dokumentace, právní texty, smlouvy).

V praxi většina týmů v roce 2026 začíná sémantickým chunkingem a přechází na kontextuální retrieval tam, kde je kvalita kritická. Můj tip: než se pustíte do složitějších strategií, zkuste si nejdřív pořádně vyladit práh u sémantického chunkingu — občas to stačí.

GraphRAG: Když vztahy mezi entitami rozhodují

Limity čistě vektorového přístupu

Vektorové vyhledávání exceluje v nalezení sémanticky podobných pasáží, ale selhává u dotazů vyžadujících multi-hop reasoning — tedy propojení informací z více zdrojů přes řetězec vztahů. Zkuste si položit dotaz: „Kteří zaměstnanci pracují na projektech financovaných klientem X?" To vyžaduje traversal přes entity (klient → projekt → zaměstnanec) a žádný embedding tuhle strukturu nezachytí.

GraphRAG kombinuje sílu knowledge grafů se sémantickým vyhledáváním — zachycuje jak sémantický význam textu, tak strukturované vztahy mezi entitami. Díky tomu dokáže odpovídat na komplexní dotazy, které by čistě vektorový přístup nikdy nezvládl.

Architektura GraphRAG

Typická GraphRAG pipeline má několik fází: extrakce entit a vztahů z textu (pomocí LLM), uložení do grafové databáze a při dotazu kombinace grafového traversalu s vektorovým vyhledáváním. Výsledný kontext je bohatší a strukturovanější než u klasického RAG.

from neo4j import GraphDatabase
from openai import OpenAI
import json

# Připojení k Neo4j
driver = GraphDatabase.driver(
    "bolt://localhost:7687",
    auth=("neo4j", "password")
)

def extract_entities_and_relations(
    text: str,
    llm_client: OpenAI
) -> dict:
    """
    Extrahuje entity a vztahy z textu pomocí LLM.
    """
    response = llm_client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "system",
            "content": """Extrahuj entity a vztahy z textu.
Vrať JSON ve formátu:
{
  "entities": [{"name": "...", "type": "...", "description": "..."}],
  "relations": [{"source": "...", "target": "...", "type": "...", "description": "..."}]
}"""
        }, {
            "role": "user",
            "content": text
        }],
        response_format={"type": "json_object"}
    )
    return json.loads(response.choices[0].message.content)


def store_in_graph(entities_relations: dict, session):
    """Uloží entity a vztahy do Neo4j."""
    for entity in entities_relations["entities"]:
        session.run(
            """
            MERGE (e:Entity {name: $name})
            SET e.type = $type, e.description = $description
            """,
            name=entity["name"],
            type=entity["type"],
            description=entity.get("description", "")
        )

    for rel in entities_relations["relations"]:
        session.run(
            """
            MATCH (s:Entity {name: $source})
            MATCH (t:Entity {name: $target})
            MERGE (s)-[r:RELATES_TO {type: $rel_type}]->(t)
            SET r.description = $description
            """,
            source=rel["source"],
            target=rel["target"],
            rel_type=rel["type"],
            description=rel.get("description", "")
        )


def graph_enhanced_retrieval(
    query: str,
    session,
    embedding_model,
    vector_store,
    top_k: int = 5
) -> str:
    """
    Kombinuje grafový traversal s vektorovým vyhledáváním.
    """
    # Krok 1: Extrahuj entity z dotazu
    query_entities = extract_entities_from_query(query)

    # Krok 2: Grafový traversal — najdi relevantní subgraf
    graph_context = []
    for entity in query_entities:
        result = session.run(
            """
            MATCH (e:Entity {name: $name})-[r*1..3]-(related)
            RETURN e.name, type(r[0]) as rel_type,
                   related.name, related.description
            LIMIT 20
            """,
            name=entity
        )
        for record in result:
            graph_context.append(
                f"{record['e.name']} --[{record['rel_type']}]--> "
                f"{record['related.name']}: {record['related.description']}"
            )

    # Krok 3: Vektorové vyhledávání pro textový kontext
    vector_results = vector_store.similarity_search(query, k=top_k)

    # Krok 4: Kombinace obou kontextů
    combined_context = (
        "### Strukturované vztahy (z knowledge grafu):\n"
        + "\n".join(graph_context)
        + "\n\n### Relevantní textové pasáže:\n"
        + "\n\n".join([doc.page_content for doc in vector_results])
    )

    return combined_context

GraphRAG je obzvlášť cenný v doménách, kde jsou vztahy mezi entitami klíčové — finanční analýza (propojení firem, transakcí, osob), zdravotnictví (léky, diagnózy, interakce), právní dokumenty (smlouvy, strany, povinnosti) nebo interní firemní knowledge base (zaměstnanci, projekty, kompetence).

Jo, implementační režie je vyšší než u klasického RAG. Ale v roce 2026 nástroje jako Microsoft GraphRAG, LlamaIndex Property Graph Index nebo Neo4j GenAI plugin výrazně zjednodušují nasazení. Už to není rocket science.

Agentní RAG a Corrective RAG (CRAG)

Od pipeline k smyčce

Klasický RAG je lineární pipeline: dotaz → retrieval → generování. Jednoduché a přímočaré. Ale co se stane, když retrieval vrátí irelevantní dokumenty? Nebo když je dotaz příliš vágní na to, aby retrieval fungoval správně?

V lineárním pipeline nemáte šanci tyto problémy zachytit a opravit — výsledek je prostě špatný. A vy se o tom ani nedozvíte, dokud si uživatel nestěžuje.

Agentní RAG tento problém řeší tím, že mění pipeline na smyčku. Agent může:

  • Vyhodnotit kvalitu retrievnutých dokumentů
  • Přeformulovat dotaz, pokud první retrieval nebyl úspěšný
  • Rozhodnout, zda potřebuje další informace z jiného zdroje
  • Ověřit, zda vygenerovaná odpověď skutečně odpovídá na dotaz
  • Iterovat celý proces, dokud není kvalita dostatečná

Corrective RAG (CRAG)

CRAG je konkrétní architektura, která zavádí evaluační krok po retrievalu. Každý retrievnutý dokument je ohodnocen jako Correct (relevantní), Incorrect (irelevantní) nebo Ambiguous (nejasný). Na základě tohoto hodnocení se pipeline rozhodne, co dál:

  • Correct: Pokračuj ke generování s tímto dokumentem
  • Incorrect: Zahoď dokument, přeformuluj dotaz, zkus znovu (nebo použij webové vyhledávání jako fallback)
  • Ambiguous: Extrahuj relevantní části, doplň dalšími zdroji

Elegantní, že?

Implementace s LangGraph

from langgraph.graph import StateGraph, END
from typing import TypedDict, Literal
from langchain_anthropic import ChatAnthropic
from langchain_core.documents import Document

class RAGState(TypedDict):
    query: str
    documents: list[Document]
    generation: str
    relevance_scores: list[str]
    retry_count: int
    refined_query: str

llm = ChatAnthropic(model="claude-sonnet-4-20250514")

def retrieve(state: RAGState) -> RAGState:
    """Retrieval krok — vyhledání dokumentů."""
    query = state.get("refined_query") or state["query"]
    docs = vector_store.similarity_search(query, k=5)
    return {"documents": docs, "retry_count": state.get("retry_count", 0)}

def grade_documents(state: RAGState) -> RAGState:
    """CRAG evaluace — ohodnocení relevance každého dokumentu."""
    query = state["query"]
    scores = []
    filtered_docs = []

    for doc in state["documents"]:
        grade_prompt = f"""Ohodnoť relevanci dokumentu vzhledem k dotazu.

Dotaz: {query}
Dokument: {doc.page_content[:500]}

Odpověz JEDNÍM slovem: correct, incorrect, nebo ambiguous."""

        response = llm.invoke(grade_prompt)
        grade = response.content.strip().lower()
        scores.append(grade)

        if grade in ("correct", "ambiguous"):
            filtered_docs.append(doc)

    return {
        "documents": filtered_docs,
        "relevance_scores": scores
    }

def decide_next_step(state: RAGState) -> Literal["generate", "refine_query", "web_search"]:
    """Rozhodovací uzel — co dál na základě evaluace."""
    scores = state["relevance_scores"]
    correct_count = scores.count("correct")
    retry_count = state.get("retry_count", 0)

    if correct_count >= 2:
        return "generate"
    elif retry_count >= 2:
        return "web_search"
    else:
        return "refine_query"

def refine_query(state: RAGState) -> RAGState:
    """Přeformulování dotazu pro lepší retrieval."""
    refine_prompt = f"""Původní dotaz nevedl k relevantním výsledkům.

Původní dotaz: {state['query']}
Nalezené dokumenty nebyly dostatečně relevantní.

Přeformuluj dotaz tak, aby lépe zachytil informační potřebu.
Odpověz POUZE přeformulovaným dotazem."""

    response = llm.invoke(refine_prompt)
    return {
        "refined_query": response.content.strip(),
        "retry_count": state.get("retry_count", 0) + 1
    }

def generate(state: RAGState) -> RAGState:
    """Generování odpovědi z relevantních dokumentů."""
    context = "\n\n".join([doc.page_content for doc in state["documents"]])

    gen_prompt = f"""Na základě kontextu odpověz na dotaz.
Pokud kontext neobsahuje dostatečné informace, řekni to.

Kontext:
{context}

Dotaz: {state['query']}"""

    response = llm.invoke(gen_prompt)
    return {"generation": response.content}

def web_search_fallback(state: RAGState) -> RAGState:
    """Fallback na webové vyhledávání."""
    from langchain_community.tools import TavilySearchResults
    search = TavilySearchResults(max_results=3)
    results = search.invoke(state["query"])

    web_docs = [
        Document(page_content=r["content"])
        for r in results
    ]
    return {"documents": web_docs}


# Sestavení grafu
workflow = StateGraph(RAGState)

workflow.add_node("retrieve", retrieve)
workflow.add_node("grade_documents", grade_documents)
workflow.add_node("refine_query", refine_query)
workflow.add_node("generate", generate)
workflow.add_node("web_search", web_search_fallback)

workflow.set_entry_point("retrieve")
workflow.add_edge("retrieve", "grade_documents")
workflow.add_conditional_edges(
    "grade_documents",
    decide_next_step,
    {
        "generate": "generate",
        "refine_query": "refine_query",
        "web_search": "web_search"
    }
)
workflow.add_edge("refine_query", "retrieve")
workflow.add_edge("web_search", "generate")
workflow.add_edge("generate", END)

# Kompilace a spuštění
app = workflow.compile()
result = app.invoke({
    "query": "Jaké jsou aktuální regulační požadavky na AI v EU?",
    "retry_count": 0
})
print(result["generation"])

Tenhle přístup dramaticky zlepšuje robustnost RAG systému. V produkčním prostředí se podíl nekvalitních odpovědí typicky sníží o 40–60 %, protože systém dokáže rozpoznat a opravit špatný retrieval ještě před generováním. Jasně, stojí to víc výpočetního času a LLM volání — je třeba najít rovnováhu mezi kvalitou a latencí. Ale ten rozdíl v kvalitě odpovědí stojí za to.

Evaluace s frameworkem RAGAS

Proč potřebujeme systematickou evaluaci

Bez měření nemůžete zlepšovat. Tečka.

A ruční hodnocení RAG odpovědí je neškálovatelné — po stém přečteném výstupu ztratíte objektivitu (a trochu i zdravý rozum). Framework RAGAS (Retrieval-Augmented Generation Assessment) řeší tento problém tím, že poskytuje automatickou, referenčně nezávislou evaluaci bez potřeby ground-truth labelů. To je klíčové, protože vytváření referenčních odpovědí je nákladné a časově náročné.

Klíčové metriky RAGAS

  • Context Precision: Měří, jak velký podíl retrievnutých dokumentů je skutečně relevantní pro odpověď. Vysoká precision = málo šumu v kontextu.
  • Context Recall: Měří, zda retrieval zachytil všechny informace potřebné pro kompletní odpověď. Vysoký recall = žádné chybějící informace.
  • Faithfulness: Měří, zda jsou tvrzení v odpovědi podložena retrievnutým kontextem. Jinými slovy — nehalucinuje model? Toto je obvykle nejdůležitější metrika (a osobně bych s tím souhlasil).
  • Answer Relevancy: Měří, zda odpověď skutečně adresuje původní dotaz. Model může generovat fakticky správnou, ale tématicky irelevantní odpověď — a to je skoro horší než žádná odpověď.

Praktická evaluace

from ragas import evaluate
from ragas.metrics import (
    context_precision,
    context_recall,
    faithfulness,
    answer_relevancy
)
from ragas.llms import LangchainLLMWrapper
from ragas.embeddings import LangchainEmbeddingsWrapper
from langchain_anthropic import ChatAnthropic
from langchain_openai import OpenAIEmbeddings
from datasets import Dataset

# Příprava evaluačního datasetu
eval_data = {
    "question": [
        "Jaké jsou výhody hybridního vyhledávání?",
        "Jak funguje Corrective RAG?",
        "Co je GraphRAG a kdy ho použít?"
    ],
    "answer": [
        # Odpovědi vygenerované vaším RAG systémem
        rag_pipeline.answer("Jaké jsou výhody hybridního vyhledávání?"),
        rag_pipeline.answer("Jak funguje Corrective RAG?"),
        rag_pipeline.answer("Co je GraphRAG a kdy ho použít?")
    ],
    "contexts": [
        # Kontexty retrievnuté vaším systémem
        rag_pipeline.retrieve("Jaké jsou výhody hybridního vyhledávání?"),
        rag_pipeline.retrieve("Jak funguje Corrective RAG?"),
        rag_pipeline.retrieve("Co je GraphRAG a kdy ho použít?")
    ],
    "ground_truth": [
        # Volitelné — RAGAS funguje i bez nich
        "Hybridní vyhledávání kombinuje BM25 a vektory...",
        "CRAG evaluuje dokumenty jako correct/incorrect/ambiguous...",
        "GraphRAG kombinuje knowledge grafy s vektorovým vyhledáváním..."
    ]
}

dataset = Dataset.from_dict(eval_data)

# Konfigurace evaluačního LLM
eval_llm = LangchainLLMWrapper(
    ChatAnthropic(model="claude-sonnet-4-20250514")
)
eval_embeddings = LangchainEmbeddingsWrapper(
    OpenAIEmbeddings(model="text-embedding-3-large")
)

# Spuštění evaluace
results = evaluate(
    dataset=dataset,
    metrics=[
        context_precision,
        context_recall,
        faithfulness,
        answer_relevancy
    ],
    llm=eval_llm,
    embeddings=eval_embeddings
)

# Výsledky
print(f"Context Precision: {results['context_precision']:.3f}")
print(f"Context Recall:    {results['context_recall']:.3f}")
print(f"Faithfulness:      {results['faithfulness']:.3f}")
print(f"Answer Relevancy:  {results['answer_relevancy']:.3f}")

# Export do DataFrame pro detailní analýzu
df = results.to_pandas()
print("\nDetailní výsledky per otázka:")
print(df[["question", "faithfulness", "answer_relevancy"]].to_string())

Doporučuju integrovat RAGAS evaluaci do CI/CD pipeline. Při každé změně chunking strategie, embedding modelu nebo prompt šablony automaticky spusťte evaluaci na testovacím datasetu a sledujte trendy v metrikách. Pokles faithfulness o 5 % po změně promptu je jasný signál, že něco je špatně — a zachytíte ho dřív, než si začnou stěžovat uživatelé.

Produkční best practices

Cachování a optimalizace latence

V produkčním prostředí je latence kritická. Tady je pár praktik, které doporučuju nasadit od prvního dne:

  • Sémantický cache: Cachujte nejen přesné dotazy, ale i sémanticky podobné. Pokud přijde dotaz s cosine similarity > 0,95 vůči cachovanému dotazu, vraťte cachovanou odpověď. Nástroje jako GPTCache nebo Redis s vektorovým rozšířením to umožňují.
  • Prompt caching: Anthropic Claude i další modely podporují prompt caching. Pokud používáte systémový prompt s rozsáhlými instrukcemi, cachování ušetří desítky procent nákladů a latence.
  • Předvýpočet embeddingů: Všechny statické dokumenty embeddujte předem. Embedding model volejte pouze pro uživatelské dotazy.
  • Streaming odpovědí: Nenuťte uživatele čekat na celou odpověď. Streamujte tokeny, jakmile jsou k dispozici — percipovaná latence klesne řádově.

Monitoring a observabilita

RAG systém má víc pohyblivých částí než klasická aplikace a potřebuje odpovídající monitoring. Bez něj se pohybujete poslepu.

  • Retrieval metriky: Sledujte průměrné skóre relevance retrievnutých dokumentů, podíl dotazů s nízkou relevancí, distribuci similarity skóre. Náhlý pokles může signalizovat problém s indexem nebo embedding modelem.
  • Latence per komponent: Měřte čas strávený v každé fázi — retrieval, reranking, generování. Bottleneck často není tam, kde čekáte.
  • Token consumption: Sledujte spotřebu tokenů per dotaz. Pokud náhle vzroste, může to znamenat, že chunking produkuje příliš velké bloky nebo se zvýšil počet retrievnutých dokumentů.
  • Uživatelská zpětná vazba: Implementujte jednoduché thumbs up/down na odpovědi. Korelujte zpětnou vazbu s retrieval skóre a dalšími metrikami pro identifikaci systematických problémů.

Nástroje jako LangSmith, Langfuse, Phoenix (Arize) nebo Weights & Biases Weave poskytují specializovanou observabilitu pro LLM aplikace a RAG pipeline. Upřímně, v roce 2026 provozovat RAG bez tracingu a monitoringu je profesní sebevražda.

Bezpečnost

Bezpečnost RAG systémů má specifické aspekty, které překračují klasické webové zabezpečení. Tohle je oblast, kde se spousta týmů pálí:

  • Prompt injection přes retrieval: Útočník může vložit škodlivé instrukce do dokumentů ve vaší knowledge base. Pokud tyto dokumenty projdou do kontextu, model je může „poslechnout". Implementujte sanitizaci retrievnutého kontextu a oddělte systémové instrukce od uživatelského kontextu jasnými delimitery.
  • Přístupová kontrola na úrovni dokumentů: Pokud vaše knowledge base obsahuje dokumenty s různými úrovněmi oprávnění, musíte zajistit, že retrieval respektuje ACL (access control lists). Uživatel z marketingu nesmí dostat kontext z důvěrných HR dokumentů. To zní samozřejmě, ale je překvapivé, jak často se to v praxi neřeší.
  • Data leakage přes embeddingy: Embedding vektory můžou obsahovat dostatek informací pro částečnou rekonstrukci původního textu. Ošetřete přístup k vektorové databázi stejně pečlivě jako k databázi s originálními dokumenty.
  • Audit logging: Logujte každý dotaz, retrievnuté dokumenty a vygenerovanou odpověď. Nejen pro debugging, ale i pro compliance — zejména v regulovaných odvětvích.

Škálovatelnost

Při škálování RAG systému na miliony dokumentů a tisíce současných uživatelů se objeví výzvy, na které narazíte překvapivě rychle:

  • Sharding vektorové databáze: Většina moderních vektorových databází (Qdrant, Weaviate, Milvus) podporuje horizontální sharding. Plánujte kapacitu dopředu — migrace sharding strategie za provozu není triviální (a upřímně, je to noční můra).
  • Asynchronní indexování: Nové dokumenty indexujte asynchronně. Neblokujte uživatelské dotazy reindexací. Použijte message queue (Kafka, RabbitMQ) pro pipeline zpracování dokumentů.
  • Tiered retrieval: Pro velké knowledge base implementujte vícestupňový retrieval — nejprve identifikujte relevantní kolekci nebo namespace, pak teprve vyhledávejte uvnitř. Snížíte tím search space a zlepšíte přesnost i latenci.
  • Model serving: Reranking modely a embedding modely servírujte přes dedikované inference servery (vLLM, Triton, TEI). Oddělte compute pro retrieval od compute pro generování — mají odlišné škálovací charakteristiky.

Závěr: Kam směřuje RAG v roce 2026 a dál

Pokud jste dočetli až sem (gratulace k trpělivosti!), máte poměrně kompletní přehled o tom, jak vypadá state-of-the-art RAG pipeline v roce 2026. Pojďme si shrnout klíčové body:

  1. Hybridní retrieval (BM25 + dense vectors + RRF) je základ — ne nadstandard. Pokud stále používáte jen vektorové vyhledávání, přicházíte o přesnost.
  2. Dvoustupňový retrieval s cross-encoder rerankingem dramaticky zlepšuje precision. Anthropic prokázal 67% snížení míry selhání retrievalu.
  3. Chunking strategie mají obrovský vliv na kvalitu. Sémantický chunking a kontextuální retrieval řeší problém ztráty kontextu.
  4. GraphRAG otevírá dveře multi-hop reasoning a je nenahraditelný v doménách s bohatými vztahy mezi entitami.
  5. Agentní a samoopravné architektury (CRAG) přeměňují pipeline na smyčku, která se dokáže zotavit z chybného retrievalu.
  6. Systematická evaluace (RAGAS) je nutná podmínka pro iterativní zlepšování — bez měření se pohybujete poslepu.
  7. Produkční praxe (cachování, monitoring, bezpečnost, škálovatelnost) rozhoduje o tom, zda váš RAG přežije kontakt s reálnými uživateli.

Kam směřuje budoucnost? Vidím pár trendů, které budou definovat RAG v nadcházejících měsících. Zaprvé, multimodální RAG — retrieval nejen přes text, ale i přes obrázky, tabulky, diagramy a video. Modely s nativním multimodálním porozuměním to umožňují, ale tooling pro indexování a vyhledávání stále dohání. Zadruhé, konvergence RAG a fine-tuningu — techniky jako RAPTOR (hierarchické sumarizace) a stále sofistikovanější prompt caching stírají hranici mezi „retrieval at inference time" a „knowledge at training time". Zatřetí, decentralizované a federované RAG — organizace budou chtít sdílet znalostní báze přes oddělení nebo i firmy bez centralizace dat, což přinese nové výzvy v oblasti privacy a access control.

Jedno je jisté: RAG není mrtvý, jak někteří předpovídali s příchodem modelů s obřími kontextovými okny. Spíš naopak. Kontextové okno o milionu tokenů neznamená, že do něj nacpete celou firemní knowledge base. Potřebujete inteligentní retrieval, který vybere ten správný kontext pro každý konkrétní dotaz. A přesně to dělá pokročilý RAG pipeline.

Začněte tím, co vám přinese největší hodnotu — pro většinu týmů to bude hybridní retrieval s rerankingem. Přidejte RAGAS evaluaci, abyste měřili pokrok. A pak iterujte. Testujte chunking strategie, experimentujte s GraphRAG, nasaďte agentní smyčky tam, kde to dává smysl. RAG je inženýrská disciplína, ne magie — a jako každá inženýrská disciplína vyžaduje systematický přístup, měření a neustálé zlepšování.

O Autorovi Editorial Team

Our team of expert writers and editors.