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¶
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).
- Descarrega els docs en PDF o HTML
- Crea l'índex amb ChromaDB
- Implementa el RAG chain
- 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:
- Fes una pregunta sobre quelcom que NO estigui als documents
- Fes una pregunta ambigua
- Fes una pregunta en un idioma diferent al dels documents
Documenta els resultats i proposa millores al prompt per gestionar millor cada cas.