Pràctica PR5073/01: Assistent RAG amb LangChain i Docker
Objectius
- Configurar entorn LangChain + Ollama amb Docker
- Carregar i processar documents propis (PDF i pàgines web)
- Crear un vector store amb Chroma DB
- Construir una cadena RAG (Retrieval-Augmented Generation)
- Crear una interfície conversacional amb Gradio
- Personalitzar l'assistent amb el nom de l'alumne
Prerequisits
| Requisit | Detall |
|---|---|
| Temps estimat | 10 hores |
| RAM mínima | 8 GB (16 GB recomanat per a models llama3.1:8b) |
| Espai en disc | 10 GB lliures (model + dependències) |
| Sistema operatiu | Linux, macOS o Windows amb WSL2 |
| Docker Desktop | v4.25+ instal·lat i funcionant |
| Coneixements | Python intermedi, conceptes bàsics de NLP |
Introducció
Qu és RAG?
RAG (Retrieval-Augmented Generation) es una tècnica que combina la recuperació d'informació (Information Retrieval) amb la generació de text dels LLMs. En lloc de dependre únicament del coneixement que el model va aprendre durant l'entrenament, el RAG permet al model consultar una base de coneixement personalitzada en el moment de la inferència.
La diferència clau respecte al fine-tuning:
| Fine-tuning | RAG | |
|---|---|---|
| Quan es fa | Reentrenament del model | En el moment de la consulta |
| Cost | Alt (GPU, temps, dades) | Baix (CPU, indexació prèvia) |
| Actualitzar dades | Reentrenament complet | Reindexació (minuts) |
| Fonts citables | No | Si (cita la font exacta) |
| Alucinacions | Reduïdes, però presents | Molt reduïdes (si hi ha context) |
| Privadesa | Dades integrades al model | Dades separades del model |
El RAG es superior al fine-tuning per a la majoria de casos empresarials on: - Les dades canvien freqüentment (documentació, normatives, preus) - Cal traçabilitat de les fonts - El pressupost per a reentrenament es limitat - Les dades son confidencials i no es vol integrar-les al model
Casos d'ús empresarials 2025
- Assistents de documentació interna: els empleats fan preguntes en llenguatge natural sobre manuals, polítiques i procediments
- Suport tècnic de primera línia: l'assistent RAG resol el 60-70% de tickets amb la base de coneixement existent
- Assistents legals: consulta de jurisprudència, contractes i normatives
- Assistents educatius: tutors que responen preguntes sobre el temari del curs
- Due diligence empresarial: anàlisi de centenars de documents en minuts
Arquitectura RAG
flowchart LR
Documents --> Chunking
Chunking --> Embeddings
Embeddings --> VectorStore[(Vector Store\nChroma)]
Query --> QueryEmbed[Query Embedding]
QueryEmbed --> Retriever
VectorStore --> Retriever
Retriever --> Context
Context --> LLM[LLM Ollama]
Query --> LLM
LLM --> Resposta
Fase d'indexació (es fa una vegada): 1. Carregar documents (PDF, web, text) 2. Dividir en chunks (fragments de text) 3. Convertir cada chunk en un vector (embedding) 4. Guardar els vectors a Chroma DB
Fase de consulta (cada vegada que l'usuari pregunta): 1. Convertir la pregunta en un vector 2. Cercar els chunks més similars (cosine similarity) 3. Enviar els chunks rellevants com a context al LLM 4. El LLM genera la resposta basant-se en el context
Part 1: Configuració de l'entorn
Opció A: Amb Ollama (RECOMANADA - sense cost d'API)
L'opció recomanada usa Ollama, que permet executar models LLM localment de manera gratuïta. No necessiteu cap API key.
# Pas 1: Arrancar Ollama
docker run -d --name ollama-joan-garcia \
-p 11434:11434 \
-v ollama-joan-garcia:/root/.ollama \
ollama/ollama:latest
# Verificar que arrenca correctament
docker logs ollama-joan-garcia
# Pas 2: Descarregar model LLaMA 3.1 8B (5 GB)
# Aquest model necessita ~8 GB de RAM
docker exec ollama-joan-garcia ollama pull llama3.1:8b
# Pas 3: Descarregar model d'embeddings (274 MB)
docker exec ollama-joan-garcia ollama pull nomic-embed-text
# Pas 4: Verificar que els models estan disponibles
docker exec ollama-joan-garcia ollama list
# Prova rapida del model
docker exec -it ollama-joan-garcia ollama run llama3.1:8b "Hola, respon en catala: que es RAG?"
Si teniu poca RAM
Si el vostre ordinador te menys de 12 GB de RAM, useu el model llama3.2:3b (2.0 GB) en lloc de llama3.1:8b. La qualitat es lleugerament inferior però funciona en màquines amb 6-8 GB de RAM.
Opció B: Amb API d'OpenAI o Anthropic
Si preferiu usar un model cloud (requereix API key i te cost per token):
# OpenAI
export OPENAI_API_KEY="sk-proj-..."
# Anthropic
export ANTHROPIC_API_KEY="sk-ant-api03-..."
Les parts del codi que usen Ollama hauran d'adaptar-se:
- OllamaEmbeddings → OpenAIEmbeddings
- Ollama(model="llama3.1:8b") → ChatOpenAI(model="gpt-4o-mini")
Configuració del projecte Python
# Crear directori del projecte
mkdir rag-joan-garcia
cd rag-joan-garcia
# Crear fitxer de dependències
cat > requirements.txt << 'EOF'
langchain==0.3.7
langchain-community==0.3.5
langchain-ollama==0.2.1
chromadb==0.5.15
gradio==5.5.0
pypdf==4.3.1
beautifulsoup4==4.12.3
sentence-transformers==3.2.1
requests==2.32.3
lxml==5.3.0
EOF
# Arrancar contenidor Python per al projecte RAG
docker run -d --name rag-joan-garcia \
-p 7860:7860 \
-v $(pwd):/app \
-w /app \
--network host \
python:3.11-slim \
bash -c "pip install -r requirements.txt && tail -f /dev/null"
# Verificar que el contenidor esta en marxa
docker ps | grep rag-joan-garcia
network host a Windows
Si useu Docker Desktop a Windows, --network host no funciona igual que a Linux. En aquest cas, useu:
docker run -d --name rag-joan-garcia \
-p 7860:7860 \
-v $(pwd):/app \
-w /app \
python:3.11-slim \
bash -c "pip install -r requirements.txt && tail -f /dev/null"
http://host.docker.internal:11434.
Part 2: Càrrega i processament de documents
Creeu el fitxer carrega_documents.py:
# carrega_documents.py
# Pràctica PR5073/01 - RAG amb LangChain
# Alumne: Joan Garcia
from langchain_community.document_loaders import (
PyPDFLoader,
WebBaseLoader,
TextLoader,
DirectoryLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from typing import List
from langchain_core.documents import Document
ALUMNE = "Joan Garcia"
ALUMNE_ID = ALUMNE.replace(" ", "_").lower()
print(f"=== Sistema RAG - {ALUMNE} ===")
def carregar_pdf(ruta: str) -> List[Document]:
"""Carrega un document PDF i retorna una llista de Documents."""
print(f"Carregant PDF: {ruta}")
loader = PyPDFLoader(ruta)
docs = loader.load()
print(f" -> {len(docs)} pagines carregades")
return docs
def carregar_web(url: str) -> List[Document]:
"""Carrega el contingut d'una pagina web."""
print(f"Carregant web: {url}")
loader = WebBaseLoader(url)
docs = loader.load()
print(f" -> {len(docs)} documents carregats")
return docs
def carregar_directori(ruta: str, pattern: str = "**/*.txt") -> List[Document]:
"""Carrega tots els fitxers de text d'un directori."""
print(f"Carregant directori: {ruta}")
loader = DirectoryLoader(ruta, glob=pattern, loader_cls=TextLoader)
docs = loader.load()
print(f" -> {len(docs)} fitxers carregats")
return docs
def crear_chunks(documents: List[Document]) -> List[Document]:
"""
Divideix els documents en chunks optims per a RAG.
chunk_size=1000: equilibri entre context suficient i precisio de cerca
chunk_overlap=200: el 20% de superposicio evita tallar conceptes
separators: ordre de prioritat per on tallar (paragraf > linia > frase)
"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
length_function=len,
separators=["\n\n", "\n", ". ", "? ", "! ", " ", ""]
)
chunks = splitter.split_documents(documents)
# Afegir metadades de l'alumne a cada chunk
for i, chunk in enumerate(chunks):
chunk.metadata["alumne"] = ALUMNE
chunk.metadata["chunk_id"] = f"{ALUMNE_ID}_{i:04d}"
print(f"Total chunks creats: {len(chunks)}")
print(f"Mida mitja del chunk: {sum(len(c.page_content) for c in chunks) // len(chunks)} caracters")
return chunks
# ============================================================
# EXECUCIO PRINCIPAL
# ============================================================
if __name__ == "__main__":
tots_els_docs = []
# Opció 1: Carregar un PDF propi
# (substituiu per un PDF que tingueu o descarregueu un de prova)
try:
pdf_docs = carregar_pdf("document_prova.pdf")
tots_els_docs.extend(pdf_docs)
except FileNotFoundError:
print("AVIS: No s'ha trobat document_prova.pdf, continuant sense ell")
# Opció 2: Carregar pàgines web d'exemple (Viquipèdia en català)
urls_exemple = [
"https://ca.wikipedia.org/wiki/Intel%C2%B7lig%C3%A8ncia_artificial",
"https://ca.wikipedia.org/wiki/Aprenentatge_automatic"
]
for url in urls_exemple:
try:
web_docs = carregar_web(url)
tots_els_docs.extend(web_docs)
except Exception as e:
print(f"AVIS: No s'ha pogut carregar {url}: {e}")
if not tots_els_docs:
# Crear documents de prova si no hi ha res disponible
print("Creant documents de prova per a demostrar el sistema...")
from langchain_core.documents import Document
tots_els_docs = [
Document(
page_content="""La intel·ligència artificial (IA) es la simulació de processos
d'intel·ligència humana per part de sistemes informatiques. Els processos
principals son: aprenentatge (adquisicio d'informacio i regles per usar-la),
raonament (usar regles per arribar a conclusions) i autocorreccio.""",
metadata={"source": "prova", "page": 1}
),
Document(
page_content="""L'aprenentatge automatic (machine learning) es una branca de la IA
que permet als sistemes aprendre i millorar automaticament a partir de l'experiencia.
Els principals paradigmes son: supervisat, no supervisat i per reforç.""",
metadata={"source": "prova", "page": 2}
),
Document(
page_content="""Els LLMs (Large Language Models) com GPT-4, Claude 3.5 i Gemini 1.5
son models de llenguatge entrenats en grans quantitats de text. Usen l'arquitectura
Transformer i el mecanisme d'atencio per generar text coherent.""",
metadata={"source": "prova", "page": 3}
)
]
# Crear chunks
chunks = crear_chunks(tots_els_docs)
print(f"\n✓ Processament completat per a l'alumne: {ALUMNE}")
print(f"✓ Total chunks llestos per a indexar: {len(chunks)}")
Part 3: Vector Store amb Chroma
Creeu el fitxer vector_store.py:
# vector_store.py
# Pràctica PR5073/01 - Vector Store amb Chroma
# Alumne: Joan Garcia
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.documents import Document
from typing import List
import os
ALUMNE = "Joan Garcia"
ALUMNE_ID = ALUMNE.replace(" ", "_").lower()
DIRECTORI_CHROMA = f"./chroma_{ALUMNE_ID}"
URL_OLLAMA = "http://localhost:11434" # Canviar a host.docker.internal si cal
def crear_vector_store(chunks: List[Document]) -> Chroma:
"""
Crea o actualitza el vector store amb els chunks proporcionats.
Usa nomic-embed-text, un model d'embeddings d'alta qualitat i lleuger.
"""
print(f"Creant embeddings amb nomic-embed-text...")
print(f"Model Ollama a: {URL_OLLAMA}")
embeddings = OllamaEmbeddings(
model="nomic-embed-text",
base_url=URL_OLLAMA
)
# Prova de connexio
try:
test_embed = embeddings.embed_query("prova de connexio")
print(f"Connexio OK - dimensio dels embeddings: {len(test_embed)}")
except Exception as e:
raise ConnectionError(f"No es pot connectar a Ollama a {URL_OLLAMA}: {e}")
# Crear vector store (Chroma guarda les dades al disc)
print(f"Indexant {len(chunks)} chunks a Chroma...")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=DIRECTORI_CHROMA,
collection_name=f"rag_{ALUMNE_ID}"
)
print(f"Vector store creat a: {DIRECTORI_CHROMA}")
print(f"Total vectors indexats: {vectorstore._collection.count()}")
return vectorstore
def carregar_vector_store() -> Chroma:
"""Carrega un vector store ja existent."""
if not os.path.exists(DIRECTORI_CHROMA):
raise FileNotFoundError(f"No existeix el vector store a {DIRECTORI_CHROMA}")
embeddings = OllamaEmbeddings(
model="nomic-embed-text",
base_url=URL_OLLAMA
)
vectorstore = Chroma(
persist_directory=DIRECTORI_CHROMA,
embedding_function=embeddings,
collection_name=f"rag_{ALUMNE_ID}"
)
print(f"Vector store carregat: {vectorstore._collection.count()} vectors")
return vectorstore
def crear_retriever(vectorstore: Chroma):
"""
Crea un retriever amb MMR (Maximal Marginal Relevance).
MMR equilibra rellevancia i diversitat dels resultats.
k=5: retornar els 5 chunks mes rellevants
fetch_k=20: considerar 20 candidats i seleccionar els 5 mes diversos
"""
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 5,
"fetch_k": 20,
"lambda_mult": 0.7 # 0=max diversitat, 1=max rellevancia
}
)
return retriever
# Test del retriever
if __name__ == "__main__":
from carrega_documents import crear_chunks, carregar_web
# Carregar documents de prova
from langchain_core.documents import Document
docs_prova = [
Document(page_content="La IA es la simulacio de processos humans per ordinadors.",
metadata={"source": "prova"}),
Document(page_content="LangChain es un framework per a construir aplicacions amb LLMs.",
metadata={"source": "prova"}),
Document(page_content="RAG combina recuperacio d'informacio amb generacio de text.",
metadata={"source": "prova"})
]
chunks = crear_chunks(docs_prova)
vectorstore = crear_vector_store(chunks)
retriever = crear_retriever(vectorstore)
# Prova de cerca
pregunta = "Que es LangChain?"
docs_rellevants = retriever.invoke(pregunta)
print(f"\nCerca: '{pregunta}'")
print(f"Documents recuperats: {len(docs_rellevants)}")
for i, doc in enumerate(docs_rellevants):
print(f" [{i+1}] {doc.page_content[:100]}...")
Part 4: Cadena RAG
Creeu el fitxer cadena_rag.py:
# cadena_rag.py
# Pràctica PR5073/01 - Cadena RAG
# Alumne: Joan Garcia
from langchain_ollama import OllamaLLM
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain_core.vectorstores import VectorStoreRetriever
from typing import Dict, Any
ALUMNE = "Joan Garcia"
ALUMNE_CURT = ALUMNE.split()[0] # "Joan"
URL_OLLAMA = "http://localhost:11434"
def crear_llm(model: str = "llama3.1:8b", temperatura: float = 0.1):
"""
Crea el model LLM.
temperatura=0.1: respostes deterministes i precises (baix per a RAG)
"""
llm = OllamaLLM(
model=model,
base_url=URL_OLLAMA,
temperature=temperatura,
num_ctx=4096 # Context window del model
)
return llm
def crear_prompt_catala() -> PromptTemplate:
"""
Crea el prompt del sistema en catala.
El prompt defineix la personalitat i el comportament de l'assistent.
"""
template = """Ets l'Assistent_{alumne}, un assistent expert creat per {alumne_complet}.
Respon SEMPRE en catala. Ets precis, rigorós i amable.
Usa el context proporcionat per respondre la pregunta.
Si la informacio no esta al context, digues:
"No tinc aquesta informacio a la meva base de coneixement. Et recomano consultar [font alternativa]."
NO inventes ni extrapolis informacio que no esta al context.
Cita sempre la font quan sigui possible.
Context:
{context}
Pregunta: {question}
Resposta detallada en catala:""".format(
alumne=ALUMNE.replace(" ", "_"),
alumne_complet=ALUMNE
)
# Nota: {context} i {question} son les variables que LangChain omple
# No les formatem aqui perque son placeholders de LangChain
template_final = template.replace(
"Resposta detallada en catala:",
""
)
prompt = PromptTemplate(
input_variables=["context", "question"],
template=template
)
return prompt
def crear_cadena_rag(retriever: VectorStoreRetriever) -> RetrievalQA:
"""Crea la cadena RAG completa."""
llm = crear_llm()
prompt = crear_prompt_catala()
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # "stuff": concatena tots els chunks al prompt
retriever=retriever,
chain_type_kwargs={
"prompt": prompt,
"verbose": False
},
return_source_documents=True, # Retorna els documents usats
verbose=False
)
return qa_chain
def fer_pregunta(qa_chain: RetrievalQA, pregunta: str) -> Dict[str, Any]:
"""
Fa una pregunta a la cadena RAG i retorna la resposta amb fonts.
"""
print(f"\nPregunta: {pregunta}")
print("Processant...")
resultat = qa_chain.invoke({"query": pregunta})
resposta = resultat["result"]
docs_font = resultat["source_documents"]
# Extreure les fonts uniques
fonts = list(set(
doc.metadata.get("source", "Font desconeguda")
for doc in docs_font
))
print(f"Resposta: {resposta[:200]}...")
print(f"Fonts: {fonts}")
return {
"pregunta": pregunta,
"resposta": resposta,
"fonts": fonts,
"num_docs_usats": len(docs_font),
"alumne": ALUMNE
}
# Test de la cadena
if __name__ == "__main__":
import os
from vector_store import carregar_vector_store, crear_retriever, crear_vector_store
from carrega_documents import crear_chunks
from langchain_core.documents import Document
DIRECTORI_CHROMA = f"./chroma_{ALUMNE.replace(' ', '_').lower()}"
if os.path.exists(DIRECTORI_CHROMA):
vectorstore = carregar_vector_store()
else:
# Crear base de coneixement de prova
docs_prova = [
Document(page_content="""RAG (Retrieval-Augmented Generation) es una tecnica que combina
la recuperacio d'informacio amb la generacio de text. Permet als LLMs consultar
bases de coneixement externes en el moment de la inferencia.""",
metadata={"source": "teoria_rag.txt", "page": 1}),
Document(page_content="""LangChain es un framework de Python per a construir aplicacions
basades en LLMs. Ofereix abstraccions per a models, prompts, cadenes i agents.
Es compatible amb OpenAI, Anthropic, Ollama i molts altres proveïdors.""",
metadata={"source": "teoria_langchain.txt", "page": 1}),
]
chunks = crear_chunks(docs_prova)
vectorstore = crear_vector_store(chunks)
retriever = crear_retriever(vectorstore)
qa_chain = crear_cadena_rag(retriever)
# Preguntes de prova
preguntes = [
"Que es RAG i per a que serveix?",
"Quins son els avantatges de LangChain?"
]
for pregunta in preguntes:
resultat = fer_pregunta(qa_chain, pregunta)
print(f"\n{'='*60}")
print(f"RESPOSTA COMPLETA:\n{resultat['resposta']}")
print(f"Fonts consultades: {', '.join(resultat['fonts'])}")
Part 5: Interfície Gradio
Creeu el fitxer principal app.py:
# app.py
# Pràctica PR5073/01 - Interficie Gradio per a assistent RAG
# Alumne: Joan Garcia
import gradio as gr
import os
from typing import List, Tuple
from datetime import datetime
from carrega_documents import crear_chunks, carregar_pdf, carregar_web
from vector_store import crear_vector_store, carregar_vector_store, crear_retriever
from cadena_rag import crear_cadena_rag, fer_pregunta
ALUMNE = "Joan Garcia"
ALUMNE_ID = ALUMNE.replace(" ", "_").lower()
DIRECTORI_CHROMA = f"./chroma_{ALUMNE_ID}"
# Variable global per a la cadena RAG
qa_chain = None
retriever = None
def inicialitzar_sistema():
"""Inicialitza el sistema RAG en arrencar l'aplicacio."""
global qa_chain, retriever
print(f"=== Inicialitzant sistema RAG per a {ALUMNE} ===")
if os.path.exists(DIRECTORI_CHROMA):
print("Carregant vector store existent...")
vectorstore = carregar_vector_store()
else:
print("Creant vector store nou amb documents de prova...")
from langchain_core.documents import Document
docs_inicials = [
Document(
page_content="Soc l'Assistent RAG creat per " + ALUMNE + ". Estic especialitzat en IA i Big Data.",
metadata={"source": "sistema", "tipus": "presentacio"}
)
]
chunks = crear_chunks(docs_inicials)
vectorstore = crear_vector_store(chunks)
retriever = crear_retriever(vectorstore)
qa_chain = crear_cadena_rag(retriever)
print(f"Sistema inicialitzat correctament per a {ALUMNE}")
def respondre_pregunta(
missatge: str,
historial: List[Tuple[str, str]]
) -> Tuple[str, List[Tuple[str, str]]]:
"""Funcion principal de resposta per a Gradio ChatInterface."""
global qa_chain
if qa_chain is None:
return "El sistema RAG no esta inicialitzat. Torna-ho a intentar.", historial
if not missatge.strip():
return "Si us plau, escriu una pregunta.", historial
try:
resultat = fer_pregunta(qa_chain, missatge)
resposta_text = resultat["resposta"]
fonts = resultat["fonts"]
# Afegir informacio de fonts al final de la resposta
if fonts and fonts != ["Font desconeguda"]:
fonts_uniques = [f for f in fonts if f != "Font desconeguda"]
if fonts_uniques:
resposta_text += f"\n\n---\n**Fonts consultades:** {', '.join(fonts_uniques)}"
historial.append((missatge, resposta_text))
return "", historial
except Exception as e:
error_msg = f"Error processant la pregunta: {str(e)}"
historial.append((missatge, error_msg))
return "", historial
def afegir_document_web(url: str) -> str:
"""Afegeix una pagina web a la base de coneixement."""
global qa_chain, retriever
if not url.startswith("http"):
return "URL no valida. Ha de comecar per http:// o https://"
try:
docs = carregar_web(url)
chunks = crear_chunks(docs)
# Obtenir vectorstore existent i afegir documents
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = OllamaEmbeddings(model="nomic-embed-text", base_url="http://localhost:11434")
vectorstore = Chroma(
persist_directory=DIRECTORI_CHROMA,
embedding_function=embeddings,
collection_name=f"rag_{ALUMNE_ID}"
)
vectorstore.add_documents(chunks)
# Actualitzar retriever i cadena
retriever = crear_retriever(vectorstore)
qa_chain = crear_cadena_rag(retriever)
return f"Document afegit correctament: {len(chunks)} chunks indexats de {url}"
except Exception as e:
return f"Error afegint document: {str(e)}"
# ============================================================
# INTERFICIE GRADIO
# ============================================================
with gr.Blocks(
title=f"Assistent RAG - {ALUMNE}",
theme=gr.themes.Soft(),
css=".gradio-container { max-width: 900px; margin: auto; }"
) as demo:
gr.Markdown(f"""
# Assistent RAG - {ALUMNE}
**Pràctica PR5073/01** | IABD - Institut Sa Palomera | {datetime.now().year}
Assistent en català sobre documentació personalitzada.
Tots els models s'executen localment via Ollama (sense cost d'API).
""")
with gr.Tab("Assistent"):
chatbot = gr.Chatbot(
label=f"Conversa amb Assistent_{ALUMNE.replace(' ', '_')}",
height=450,
show_label=True
)
with gr.Row():
msg = gr.Textbox(
placeholder="Escriu la teva pregunta en català...",
label="Pregunta",
scale=4
)
submit_btn = gr.Button("Enviar", variant="primary", scale=1)
gr.Examples(
examples=[
"Que es RAG i quins avantatges te respecte al fine-tuning?",
"Explica'm el concepte d'embeddings",
"Com funciona LangChain?",
"Quins models LLM puc usar localment?"
],
inputs=msg,
label="Preguntes d'exemple"
)
clear_btn = gr.Button("Netejar conversa", variant="secondary")
msg.submit(respondre_pregunta, [msg, chatbot], [msg, chatbot])
submit_btn.click(respondre_pregunta, [msg, chatbot], [msg, chatbot])
clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg])
with gr.Tab("Afegir documents"):
gr.Markdown("""
### Afegeix fonts de coneixement
Pots afegir pàgines web per ampliar la base de coneixement de l'assistent.
""")
url_input = gr.Textbox(
placeholder="https://...",
label="URL de la pàgina web",
lines=1
)
url_btn = gr.Button("Afegir pàgina web", variant="primary")
url_status = gr.Textbox(label="Estat", interactive=False)
url_btn.click(afegir_document_web, inputs=url_input, outputs=url_status)
with gr.Tab("Informacio del sistema"):
gr.Markdown(f"""
### Configuracio del sistema
| Parametre | Valor |
|-----------|-------|
| Alumne | {ALUMNE} |
| Model LLM | llama3.1:8b (Ollama) |
| Model Embeddings | nomic-embed-text |
| Vector Store | Chroma DB |
| Directori dades | {DIRECTORI_CHROMA} |
| Estrategia de cerca | MMR (k=5, fetch_k=20) |
| Mida de chunk | 1000 caracters |
| Superposicio de chunk | 200 caracters |
### Com funciona?
1. Els documents es divideixen en chunks de 1000 caracters
2. Cada chunk es converteix en un vector de 768 dimensions (nomic-embed-text)
3. Quan fas una pregunta, es cerca per similitud cosinus als vectors
4. Els 5 chunks mes rellevants s'envien al LLM com a context
5. El LLM genera la resposta en catala basant-se en el context
""")
# Inicialitzar el sistema en arrencar
inicialitzar_sistema()
# Arrancar l'aplicacio
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
show_error=True
)
Executeu l'aplicació:
# Dins del contenidor Docker
docker exec rag-joan-garcia python app.py
# O directament si teniu Python local
python app.py
Accediu a http://localhost:7860 per a veure la interfície.
Part 6: Proves i validació
6.1 Prova de la cadena RAG en terminal
Heu de veure sortides similars a:
=== Sistema RAG - Joan Garcia ===
Carregant vector store existent...
Vector store carregat: 45 vectors
Pregunta: Que es RAG i per a que serveix?
Processant...
Resposta: RAG (Retrieval-Augmented Generation) combina la recuperació...
Fonts: ['teoria_rag.txt', 'ca.wikipedia.org']
6.2 Proves de qualitat de resposta
Prepara un conjunt de preguntes de prova per avaluar la qualitat del sistema:
| Pregunta | Resposta esperada | Resposta obtinguda | Fonts correctes? | Qualitat (1-10) |
|---|---|---|---|---|
| Que es RAG? | Explicació de RAG | ... | Si/No | |
| Avantatges de LangChain | Llista d'avantatges | ... | Si/No | |
| Pregunta sense resposta al context | "No tinc aquesta informació" | ... | Si/No |
6.3 Mesura de rendiment
# benchmark.py - Mesura el temps de resposta
import time
from cadena_rag import crear_cadena_rag, fer_pregunta
from vector_store import carregar_vector_store, crear_retriever
vectorstore = carregar_vector_store()
retriever = crear_retriever(vectorstore)
qa_chain = crear_cadena_rag(retriever)
preguntes_test = [
"Que es RAG?",
"Com funcionen els embeddings?",
"Quins models LLM son millors per a catala?"
]
resultats = []
for pregunta in preguntes_test:
t_inici = time.time()
resultat = fer_pregunta(qa_chain, pregunta)
temps = time.time() - t_inici
resultats.append({
"pregunta": pregunta,
"temps_s": round(temps, 2),
"longitud_resposta": len(resultat["resposta"])
})
print("\n=== RESULTATS DE RENDIMENT ===")
for r in resultats:
print(f"Pregunta: {r['pregunta'][:50]}...")
print(f" Temps: {r['temps_s']}s | Longitud resposta: {r['longitud_resposta']} cars")
temps_mitja = sum(r["temps_s"] for r in resultats) / len(resultats)
print(f"\nTemps mitja de resposta: {temps_mitja:.2f}s")
Preguntes de reflexió
Preguntes de reflexió
Responeu aquestes preguntes al document de memòria de la pràctica:
-
Chunking: Heu provat mides de chunk de 500, 1000 i 2000 caracters. Quina ha donat millors resultats per als vostres documents? Per quin motiu creieu que es aixi?
-
Embeddings: Qué es un vector d'embedding? Per quin motiu dos fragments de text sobre el mateix tema tindran vectors similars?
-
MMR vs. similitud cosinus: L'estrategia MMR intenta maximitzar la rellevancia i la diversitat. En quin escenari la diversitat es important? Poseu un exemple concret.
-
Alucinacions: Heu pogut comprovar que el model respon "No tinc aquesta informació" quan se li pregunta sobre alguna cosa fora del context? Descriviu com ho heu provat.
-
Millora del sistema: Proposa dues millores concretes per al sistema RAG que heu construït (noves funcionalitats, millors estratègies de chunking, models alternatius...).
Lliurament
Que heu de lliurar
- Codi font (fitxers
.py): carrega_documents.pyvector_store.pycadena_rag.pyapp.py-
(opcional)
benchmark.py -
Captures de pantalla (5 obligatòries):
- Interfície Gradio en funcionament
- Almenys 3 converses reals (preguntes i respostes)
-
Resultat del benchmark de rendiment
-
Memòria tècnica (document PDF, 3-5 pàgines):
- Descripció de l'arquitectura implementada
- Documents carregats i criteri de selecció
- Decisions tècniques preses i justificació
- Resultats del benchmark
- Respostes a les preguntes de reflexió
-
Dificultats trobades i com s'han resolt
-
Rúbrica emplenada (vegeu
rubriques/rubrica_langchain.md)
Format de lliurament
- Comprimir tot en un ZIP:
PR5073/01_Joan_Garcia.zip - Lliurar a la plataforma Moodle del curs
- Termini: consultar calendari al Moodle
Consell per a la defensa oral
Prepareu-vos per a explicar: (1) l'arquitectura completa del sistema, (2) per quin motiu heu triat la mida de chunk que heu usat, (3) com funciona MMR i per quin motiu es millor que la similitud pura, i (4) una millora concreta que implementaríeu si tinguéssiu més temps.
Rúbrica de correcció: PR5073/01 - Rúbrica