Salta el contingut

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.

docker exec ollama-joan-garcia ollama pull llama3.2:3b

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: - OllamaEmbeddingsOpenAIEmbeddings - 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"
I canvieu l'URL d'Ollama a 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

# Executar proves en el contenidor
docker exec -it rag-joan-garcia python cadena_rag.py

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:

  1. 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?

  2. Embeddings: Qué es un vector d'embedding? Per quin motiu dos fragments de text sobre el mateix tema tindran vectors similars?

  3. 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.

  4. 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.

  5. 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

  1. Codi font (fitxers .py):
  2. carrega_documents.py
  3. vector_store.py
  4. cadena_rag.py
  5. app.py
  6. (opcional) benchmark.py

  7. Captures de pantalla (5 obligatòries):

  8. Interfície Gradio en funcionament
  9. Almenys 3 converses reals (preguntes i respostes)
  10. Resultat del benchmark de rendiment

  11. Memòria tècnica (document PDF, 3-5 pàgines):

  12. Descripció de l'arquitectura implementada
  13. Documents carregats i criteri de selecció
  14. Decisions tècniques preses i justificació
  15. Resultats del benchmark
  16. Respostes a les preguntes de reflexió
  17. Dificultats trobades i com s'han resolt

  18. 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