Pràctica 2 — Agent amb Memòria RAG¶
📋 Fitxa de la Pràctica
🎯 Objectius¶
- Construir un pipeline RAG complet: ingestió → indexació → recuperació → generació
- Integrar el RAG en un agent amb conversa multi-torn
- Avaluar i millorar la qualitat del chunking
- Implementar citació de fonts en les respostes
🔧 Configuració¶
mkdir practica2-agent-rag && cd practica2-agent-rag
python -m venv .venv && source .venv/bin/activate
pip install langchain==0.3.7 langchain-openai==0.2.6 \
langchain-community==0.3.7 chromadb==0.5.18 \
pypdf==4.3.1 python-dotenv==1.0.1
💻 Part 1: Pipeline RAG Bàsic (90 minuts)¶
"""
Pràctica 2 — Part 1: Pipeline RAG Complet
"""
import os
from pathlib import Path
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
load_dotenv()
# ── CONFIGURABLE ──────────────────────────────────────────────
DOCS_DIR = "./documents" # Directori amb els PDFs/TXTs
CHROMA_DIR = "./chroma_db"
CHUNK_SIZE = 512
CHUNK_OVERLAP = 50
# ── FASE 1: INGESTIÓ ──────────────────────────────────────────
def ingerir_documents(docs_dir: str) -> list:
"""Carrega tots els documents del directori."""
Path(docs_dir).mkdir(exist_ok=True)
# Comprovar si hi ha documents
files = list(Path(docs_dir).glob("**/*.pdf")) + \
list(Path(docs_dir).glob("**/*.txt")) + \
list(Path(docs_dir).glob("**/*.md"))
if not files:
print(f"⚠️ No s'han trobat documents a '{docs_dir}'")
print(" Crea algun fitxer .pdf, .txt o .md per continuar")
return []
loader = DirectoryLoader(
docs_dir,
glob="**/*.{pdf,txt,md}",
show_progress=True
)
documents = loader.load()
print(f"✅ Carregats {len(documents)} documents ({len(files)} fitxers)")
return documents
# ── FASE 2: CHUNKING ──────────────────────────────────────────
def fer_chunking(documents: list) -> list:
"""Divideix els documents en fragments."""
splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(documents)
# Estadístiques
sizes = [len(c.page_content) for c in chunks]
print(f"✅ {len(chunks)} chunks — "
f"Mida mín/màx/mitja: {min(sizes)}/{max(sizes)}/{sum(sizes)//len(sizes)}")
return chunks
# ── FASE 3 i 4: EMBEDDINGS + VECTOR STORE ─────────────────────
def crear_o_carregar_index(chunks: list = None) -> Chroma:
"""Crea un nou índex o en carrega un d'existent."""
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
if Path(CHROMA_DIR).exists() and chunks is None:
print("📂 Carregant índex existent...")
vs = Chroma(persist_directory=CHROMA_DIR, embedding_function=embeddings)
print(f"✅ Índex carregat: {vs._collection.count()} vectors")
return vs
if not chunks:
raise ValueError("Cal proporcionar chunks per crear un nou índex")
print("🔧 Creant nou índex...")
vs = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=CHROMA_DIR,
)
print(f"✅ Índex creat: {vs._collection.count()} vectors")
return vs
# ── FASE 5: RAG CHAIN ─────────────────────────────────────────
RAG_PROMPT = PromptTemplate(
template="""Ets un assistent expert. Respon la pregunta basant-te ÚNICAMENT
en el context proporcionat. Si la informació no hi és, respon
"No ho trobo a la documentació disponible."
Context:
{context}
Pregunta: {question}
Resposta (en català, amb detall i citant la font quan escau):""",
input_variables=["context", "question"]
)
def crear_rag_chain(vectorstore: Chroma) -> RetrievalQA:
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4}
)
return RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": RAG_PROMPT}
)
def preguntar(chain: RetrievalQA, pregunta: str):
"""Fa una pregunta i mostra resposta + fonts."""
print(f"\n🔍 Pregunta: {pregunta}")
result = chain.invoke({"query": pregunta})
print(f"\n💬 Resposta:\n{result['result']}")
print("\n📚 Fonts:")
seen = set()
for doc in result['source_documents']:
src = doc.metadata.get('source', 'desconegut')
page = doc.metadata.get('page', '?')
key = f"{src}:{page}"
if key not in seen:
seen.add(key)
print(f" • {Path(src).name}, pàg. {page}: {doc.page_content[:80]}...")
print("-" * 60)
# ── MAIN ──────────────────────────────────────────────────────
if __name__ == "__main__":
# Ingestió
docs = ingerir_documents(DOCS_DIR)
if not docs:
print("\nCrea alguns documents a ./documents/ i torna a executar")
exit(0)
chunks = fer_chunking(docs)
vs = crear_o_carregar_index(chunks)
chain = crear_rag_chain(vs)
# Preguntes de prova
preguntes = [
"Quins temes cobreix aquest document?",
"Quin és el concepte més important explicat?",
]
for p in preguntes:
preguntar(chain, p)
💻 Part 2: Agent RAG amb Memòria (90 minuts)¶
"""
Pràctica 2 — Part 2: Agent conversacional amb RAG + Memòria
"""
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain.tools import tool
from langchain.memory import ConversationSummaryBufferMemory
from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# Assumint que vs i chain estan definits de la Part 1
@tool
def buscar_documentacio(pregunta: str) -> str:
"""
Busca informació a la base de documents indexada.
Usa aquesta eina per a qualsevol pregunta sobre el contingut
dels documents. Retorna la resposta amb les fonts.
"""
result = chain.invoke({"query": pregunta})
fonts = [doc.metadata.get('source', '?') for doc in result['source_documents']]
return f"Resposta: {result['result']}\nFonts: {set(fonts)}"
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=1000,
memory_key="chat_history",
return_messages=True
)
prompt = ChatPromptTemplate.from_messages([
("system", "Ets un assistent que respon preguntes basant-se en documentació indexada. "
"Usa SEMPRE l'eina buscar_documentacio per a preguntes de contingut. "
"Respon en català."),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, [buscar_documentacio], prompt)
executor = AgentExecutor(
agent=agent, tools=[buscar_documentacio],
memory=memory, verbose=True, max_iterations=5
)
# Conversa multi-torn
print(executor.invoke({"input": "Quins temes cobreix la documentació?"})["output"])
print(executor.invoke({"input": "Explica el primer tema amb més detall"})["output"])
print(executor.invoke({"input": "Hi ha exemples pràctics sobre això?"})["output"])
🏆 Part 3: Comparació de Configuracions (60 minuts)¶
Experimenta amb les variables de chunking i documenta els resultats:
"""
Pràctica 2 — Part 3: Experiment de Chunking
"""
CONFIGURACIONS = [
{"chunk_size": 256, "overlap": 0},
{"chunk_size": 512, "overlap": 50}, # ← Referència
{"chunk_size": 1024, "overlap": 100},
{"chunk_size": 2048, "overlap": 200},
]
PREGUNTES_TEST = [
"Explica el concepte principal del document",
"Quins passos cal seguir per...", # Adaptar al teu document
]
resultats = {}
for config in CONFIGURACIONS:
splitter = RecursiveCharacterTextSplitter(
chunk_size=config["chunk_size"],
chunk_overlap=config["overlap"]
)
chunks = splitter.split_documents(docs)
vs_test = Chroma.from_documents(chunks, embeddings,
collection_name=f"test_{config['chunk_size']}")
chain_test = crear_rag_chain(vs_test)
key = f"chunk={config['chunk_size']},overlap={config['overlap']}"
resultats[key] = []
for p in PREGUNTES_TEST:
r = chain_test.invoke({"query": p})
resultats[key].append({
"pregunta": p,
"resposta": r["result"][:200],
"num_fonts": len(r["source_documents"])
})
# Guardar resultats per a l'informe
import json
with open("resultats_chunking.json", "w", encoding="utf-8") as f:
json.dump(resultats, f, ensure_ascii=False, indent=2)
print("✅ Resultats guardats a resultats_chunking.json")
📊 Criteris d'Avaluació¶
| Criteri | Pes | Indicadors |
|---|---|---|
| Pipeline RAG complet i funcional | 30% | Ingestió, chunking, embeddings, vector store |
| Qualitat de les respostes | 25% | Rellevància, citació de fonts, no al·lucinar |
| Agent conversacional | 20% | Memòria entre torns, ús correcte de l'eina RAG |
| Experiment de chunking | 15% | Compara ≥3 configuracions, conclusions documentades |
| Codi i documentació | 10% | README, comentaris, gestió d'errors |
Entrega
ZIP sense .venv ni .env amb nom P2_NomCognom_AgentsIA.zip.
Inclou els documents usats (o un README indicant d'on obtenir-los).