Salta el contingut

3.2 RAG — Retrieval Augmented Generation

Definició

RAG (Retrieval Augmented Generation) és una tècnica que combina la generació de text dels LLM amb la recuperació de documents rellevants d'una base de coneixement externa. Permet als agents respondre basant-se en informació verificable i actualitzable, eliminant al·lucinacions i superant el knowledge cutoff.


🎯 El Problema que Resol RAG

Imagina que vols crear un assistent que respongui preguntes sobre la documentació interna de la teva empresa. La documentació té 10,000 pàgines i s'actualitza setmanalment.

Sense RAG: Fine-tuning

Entrenar el model amb tota la documentació és car (milers €), lent (dies/setmanes) i cada actualització requereix re-entrenar. A més, el model pot seguir al·lucinant.

Sense RAG: Tot al Context

Posar 10,000 pàgines al context window és impossible (no hi caben). I si hi cabés, seria extremadament car (milions de tokens per petició) i lent.

Amb RAG

El sistema recupera només els 3-5 fragments més rellevants per a cada pregunta i els inclou al context. El model respon basant-se en ells. Actualitzable, econòmic i verificable.


🔄 El Pipeline RAG: Pas a Pas

🔬 Pipeline RAG Complet

El pipeline RAG es divideix en dues fases ben diferenciades:

Fase 1: Indexació (Offline — Es fa una vegada)

📄

Carrega

Carregar els documents originals (PDF, Word, web, etc.)

✂️

Chunking

Dividir en fragments petits i solapats (~512 tokens)

🔢

Embedding

Convertir cada fragment en un vector numèric

🗄️

Emmagatzemar

Guardar els vectors en una base de dades vectorial

Fase 2: Recuperació i Generació (Online — Per cada consulta)

Pregunta

L'usuari fa una pregunta en llenguatge natural

🔍

Cerca

Convertir la pregunta en vector i cercar fragments similars

📋

Context

Construir un prompt amb pregunta + fragments recuperats

💬

Resposta

El LLM genera una resposta basada en el context proveït


💻 Implementació Pràctica amb LangChain

Part 1: Indexació de Documents

# pip install langchain langchain-openai langchain-community chromadb --break-system-packages
# Versions: langchain>=0.3.0, chromadb>=0.5.0, langchain-openai>=0.2.0

from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

# ── FASE 1: CARREGA DE DOCUMENTS ──────────────────────────────

# Opció A: Cargar un sol PDF
loader = PyPDFLoader("docs/manual_empresa.pdf")

# Opció B: Carregar tots els PDFs d'un directori
loader = DirectoryLoader(
    path="./documents/",
    glob="**/*.pdf",
    loader_cls=PyPDFLoader
)

documents = loader.load()
print(f"✅ Carregats {len(documents)} documents")

# ── FASE 2: CHUNKING ──────────────────────────────────────────

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,          # Màxim de caràcters per fragment
    chunk_overlap=50,        # Solapament per mantenir context
    length_function=len,
    separators=["\n\n", "\n", ".", " ", ""]  # Prioritat de divisió
)

chunks = splitter.split_documents(documents)
print(f"✅ Dividit en {len(chunks)} fragments")

# Inspecció d'un fragment
print(f"\n📄 Fragment exemple:")
print(f"  Contingut: {chunks[0].page_content[:200]}...")
print(f"  Metadades: {chunks[0].metadata}")

# ── FASE 3: EMBEDDINGS ────────────────────────────────────────

embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",  # Model econòmic d'embeddings
    # Alternativa: "text-embedding-3-large" (més potent, més car)
)

# ── FASE 4: EMMAGATZEMAR AL VECTOR STORE ─────────────────────

vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db",  # Persistir en disc
    collection_name="docs_empresa"
)

print(f"✅ Index creat! {vectorstore._collection.count()} vectors emmagatzemats")

Part 2: Recuperació i Generació (RAG Chain)

from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# Carregar el vector store existent
vectorstore = Chroma(
    persist_directory="./chroma_db",
    embedding_function=embeddings,
    collection_name="docs_empresa"
)

# Configurar el recuperador
retriever = vectorstore.as_retriever(
    search_type="similarity",   # Cerca per similitud cosinus
    search_kwargs={
        "k": 4,                 # Recuperar els 4 fragments més rellevants
        # "score_threshold": 0.7  # Opcional: filtre de puntuació mínima
    }
)

# Prompt personalitzat per al RAG
# És molt important ser EXPLÍCIT sobre el comportament esperat
rag_prompt = PromptTemplate(
    template="""
Ets un assistent d'empresa expert. Respon les preguntes ÚNICAMENT 
basant-te en el context proporcionat. 

Si la informació no es troba al context, respon: 
"No disposo d'informació sobre això en la documentació disponible."

NO inventes informació que no estigui al context.

Context:
{context}

Pregunta: {question}

Resposta:""",
    input_variables=["context", "question"]
)

# Crear la cadena RAG
rag_chain = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(model="gpt-4o-mini", temperature=0),
    chain_type="stuff",          # Tots els chunks en un sol prompt
    retriever=retriever,
    return_source_documents=True, # Retorna les fonts (per verificació!)
    chain_type_kwargs={"prompt": rag_prompt}
)

# ── TEST DE LA CADENA RAG ─────────────────────────────────────

def ask_with_sources(question: str):
    """Fa una pregunta i mostra la resposta amb les fonts."""
    result = rag_chain.invoke({"query": question})

    print(f"\n🔍 Pregunta: {question}")
    print(f"\n💬 Resposta: {result['result']}")
    print(f"\n📚 Fonts:")
    for i, doc in enumerate(result['source_documents'], 1):
        print(f"  {i}. Pàgina {doc.metadata.get('page', '?')} — "
              f"{doc.page_content[:100]}...")
    print("-" * 60)

# Proves
ask_with_sources("Quin és el procediment per demanar vacances?")
ask_with_sources("Qui és el responsable del departament de vendes?")
ask_with_sources("Quin és el menú del dinar?")  # Fora del context

🔬 Cerca Semàntica vs Cerca per Paraules Clau

Una de les innovacions clau del RAG és usar cerca semàntica en lloc de cerca per paraules clau (BM25, TF-IDF).

Característica Cerca Clàssica (BM25) Cerca Semàntica (Embeddings)
Mecanisme Coincidència de paraules exactes Similitud de significat
"cotxe" vs "vehicle" ❌ No les relaciona ✅ Sap que són similars
Idiomes ❌ Per idioma ✅ Multilingüe
Velocitat ✅ Molt ràpida Ⓜ️ Moderada
Qualitat semàntica ❌ Baixa ✅ Alta
# Demostració de cerca semàntica
query = "com puc demanar permís per absència?"

# La cerca semàntica trobarà documents que parlin de:
# - "vacances", "absències", "baixes", "permisos"
# fins i tot si no contenen la paraula exacta "absència"

docs = vectorstore.similarity_search_with_score(query, k=3)
for doc, score in docs:
    print(f"  Score: {score:.4f} | {doc.page_content[:80]}...")

🔧 Estratègies Avançades de Chunking

El chunking és una de les decisions més crítiques d'un pipeline RAG.

📝

Chunk Fix-Size

512 caràcters, 50 solapament. El mètode més simple. Pot tallar enmig d'una idea. Bé per a documents homogenis.

🔗

Recursive Splitting

Divideix per \n\n → \n → . → espai. Respecta l'estructura del document. Recomanat per defecte. El que hem usat a l'exemple.

📋

Semantic Chunking

Usa embeddings per agrupar frases similars. Els chunks contenen idees coherents. Millor qualitat però més lent i car.

🌲

Parent-Child Chunking

Chunks petits per cercar, chunks grans per contextualitzar. La cerca es fa en chunks de 128 tokens, però es recupera el chunk pare de 512 tokens per tenir més context.


✅ Activitats de Consolidació

Exercici 3.2.1 — Construir un RAG bàsic

Crea un pipeline RAG complet usant com a base de coneixement la documentació oficial de Python 3.12 (o qualsevol altra documentació tècnica).

  1. Descarrega els docs en PDF o HTML
  2. Crea l'índex amb ChromaDB
  3. Implementa el RAG chain
  4. Fes 5 preguntes i verifica que les respostes provenen dels docs

Exercici 3.2.2 — Comparació de Chunking

Usant el mateix document, compara:

  • Chunk size: 256 vs 512 vs 1024 caràcters
  • Overlap: 0 vs 50 vs 100 caràcters

Per a cada combinació, fes la mateixa pregunta i observa com canvia la qualitat de la resposta. Quina combinació dóna millors resultats i per qué?

Exercici 3.2.3 — Detecció de Limitacions

Implementa un RAG i prova de "trencar-lo" deliberadament:

  1. Fes una pregunta sobre quelcom que NO estigui als documents
  2. Fes una pregunta ambigua
  3. Fes una pregunta en un idioma diferent al dels documents

Documenta els resultats i proposa millores al prompt per gestionar millor cada cas.