I Large Language Model (LLM) come GPT-4 e Claude sono straordinariamente potenti, ma soffrono di un limite fondamentale: la loro conoscenza è congelata al momento del training. Non possono accedere ai tuoi documenti interni, al tuo database o a informazioni aggiornate in tempo reale. La Retrieval-Augmented Generation (RAG) risolve esattamente questo problema, combinando la potenza generativa degli LLM con la capacità di recuperare informazioni da fonti esterne.
Il Problema: I Limiti degli LLM
Prima di parlare di RAG, è importante capire perché ne abbiamo bisogno.
- Conoscenza statica: Un LLM sa solo quello che ha visto durante il training. Se gli chiedi informazioni su un evento avvenuto dopo il suo cutoff, non può rispondere.
- Allucinazioni: Quando un LLM non conosce la risposta, tende a inventarla, generando informazioni plausibili ma completamente false.
- Nessun accesso a dati privati: Un LLM generico non ha accesso alla documentazione interna della tua azienda, ai ticket, o al tuo codebase.
La RAG affronta tutti e tre questi problemi fornendo al modello un contesto rilevante recuperato da fonti esterne al momento della query.
Cos'è la RAG?
La Retrieval-Augmented Generation è un'architettura che arricchisce il prompt inviato a un LLM con informazioni recuperate da un knowledge base esterno. Invece di affidarsi esclusivamente alla conoscenza parametrica del modello, la RAG cerca prima le informazioni rilevanti e poi le inietta nel prompt, permettendo al modello di generare risposte accurate e fondate.
Come Funziona la RAG in Dettaglio
L'architettura RAG si compone di due fasi principali: Indexing (offline) e Retrieval + Generation (online).
Fase 1: Indexing (Ingestione dei Documenti)
La fase di indexing prepara i tuoi documenti per la ricerca semantica. Si compone di quattro passi.
1. Document Loading
I documenti possono provenire da qualsiasi fonte: file PDF, pagine web, database, file Markdown, API. Il Document Loader si occupa di leggere questi documenti e convertirli in testo strutturato.
2. Text Splitting (Chunking)
Gli LLM hanno una finestra di contesto limitata, e i documenti possono essere molto lunghi. Il Text Splitter divide i documenti in frammenti più piccoli chiamati chunks. La qualità del chunking è critica: chunk troppo piccoli perdono contesto, chunk troppo grandi diluiscono la rilevanza.
Le strategie più comuni sono:
- Recursive Character Splitting: Divide il testo ricorsivamente usando separatori come
\n\n,\n,., rispettando la struttura del documento. - Semantic Splitting: Usa gli embedding per trovare i punti di rottura naturali nel testo.
- Chunk Overlap: Include una sovrapposizione tra chunk consecutivi per preservare il contesto ai confini.
3. Embedding
Ogni chunk viene trasformato in un vettore numerico (embedding) tramite un modello di embedding (come text-embedding-3-small di OpenAI). Questi vettori catturano il significato semantico del testo: frasi con significati simili avranno vettori vicini nello spazio multidimensionale.
4. Vector Store
I vettori vengono salvati in un Vector Store (o database vettoriale), come ChromaDB, Pinecone, Weaviate o FAISS. Questo database è ottimizzato per la ricerca di similarità: data una query, trova i vettori (e quindi i chunk di testo) più simili.
Fase 2: Retrieval + Generation
Quando l'utente fa una domanda:
- La domanda viene trasformata in un embedding usando lo stesso modello di embedding.
- Il Vector Store cerca i chunk più simili tramite ricerca di similarità (tipicamente cosine similarity o distanza euclidea).
- I chunk recuperati vengono inseriti nel prompt come contesto.
- L'LLM genera la risposta basandosi sul contesto fornito.
Costruire una Pipeline RAG con LangChain
LangChain è il framework Python (e JavaScript) più popolare per costruire applicazioni basate su LLM. Offre astrazioni di alto livello per ogni componente della pipeline RAG.
Installazione
pip install langchain langchain-openai langchain-community chromadb
Step 1: Caricare i Documenti
LangChain fornisce decine di Document Loader per diverse fonti dati.
from langchain_community.document_loaders import ( PyPDFLoader, WebBaseLoader, DirectoryLoader, TextLoader, ) # Caricare un PDF pdf_loader = PyPDFLoader("docs/manuale.pdf") pdf_docs = pdf_loader.load() # Caricare una pagina web web_loader = WebBaseLoader("https://docs.example.com/guide") web_docs = web_loader.load() # Caricare tutti i file .md da una directory dir_loader = DirectoryLoader("./knowledge_base", glob="**/*.md", loader_cls=TextLoader) md_docs = dir_loader.load() all_docs = pdf_docs + web_docs + md_docs
Step 2: Dividere i Documenti in Chunk
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", ". ", " ", ""], ) chunks = text_splitter.split_documents(all_docs) print(f"Documenti originali: {len(all_docs)}, Chunks: {len(chunks)}")
Il parametro chunk_overlap è fondamentale: crea una sovrapposizione tra chunk consecutivi in modo che il contesto non venga perso ai confini.
Step 3: Creare gli Embedding e il Vector Store
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embedding_model = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )
Step 4: Creare il Retriever
Il retriever è il componente che, data una query, recupera i chunk più rilevanti dal vector store.
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4}, ) relevant_docs = retriever.invoke("Come funziona l'autenticazione?") for doc in relevant_docs: print(doc.page_content[:200]) print("---")
Step 5: Costruire la Chain RAG
Ora mettiamo tutto insieme con un LLM e un prompt template.
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI(model="gpt-4o", temperature=0) prompt = ChatPromptTemplate.from_template(""" Rispondi alla domanda basandoti esclusivamente sul contesto fornito. Se il contesto non contiene informazioni sufficienti, dì che non lo sai. Contesto: {context} Domanda: {question} Risposta: """) def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) response = rag_chain.invoke("Come funziona l'autenticazione nel sistema?") print(response)
Tecniche RAG Avanzate
La pipeline di base funziona bene, ma ci sono diverse tecniche per migliorare significativamente la qualità delle risposte.
Multi-Query Retrieval
A volte la query dell'utente è ambigua o non allineata con il linguaggio usato nei documenti. Il Multi-Query Retriever genera automaticamente varianti della domanda originale per catturare più prospettive.
from langchain.retrievers import MultiQueryRetriever multi_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), llm=llm, ) docs = multi_retriever.invoke("Quali sono le best practice di sicurezza?")
Contextual Compression
Non tutto il contenuto di un chunk è rilevante per la query. Il Contextual Compression Retriever usa un LLM per estrarre solo le parti pertinenti da ogni chunk recuperato.
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever, )
Hybrid Search
La ricerca puramente semantica non è sempre ottimale. L'Hybrid Search combina la ricerca semantica (embedding) con la ricerca lessicale (BM25, keyword matching) per ottenere risultati migliori.
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 4 semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) hybrid_retriever = EnsembleRetriever( retrievers=[bm25_retriever, semantic_retriever], weights=[0.4, 0.6], )
Conversational RAG (con Memoria)
Per costruire un chatbot RAG che ricordi il contesto della conversazione, è necessario aggiungere una memoria che riformuli le domande dell'utente tenendo conto della cronologia.
from langchain.chains import create_history_aware_retriever from langchain_core.prompts import MessagesPlaceholder contextualize_prompt = ChatPromptTemplate.from_messages([ ("system", "Data la cronologia della chat e l'ultima domanda dell'utente, " "riformula la domanda in modo che sia comprensibile senza la cronologia."), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) history_aware_retriever = create_history_aware_retriever( llm, retriever, contextualize_prompt )
Best Practice
- Scegli la giusta dimensione dei chunk: Sperimenta con diverse dimensioni (500-1500 token). Chunk più piccoli per risposte precise, più grandi per contesto ampio.
- Usa metadata nei documenti: Aggiungi fonte, data, categoria come metadata ai chunk. Questo permette di filtrare i risultati in fase di retrieval.
- Valuta la qualità: Usa framework come RAGAS per misurare metriche come faithfulness, relevancy e context precision.
- Gestisci i documenti aggiornati: Implementa una pipeline di re-ingestione per mantenere il vector store sincronizzato con le fonti dati.
- Aggiungi un re-ranker: Dopo il retrieval iniziale, usa un modello di re-ranking (come Cohere Rerank) per riordinare i risultati in base alla rilevanza reale.
Conclusione
La RAG è diventata l'architettura standard per costruire applicazioni AI che necessitano di accedere a conoscenze specifiche e aggiornate. LangChain semplifica enormemente l'implementazione, fornendo astrazioni per ogni componente della pipeline.
Prossimi passi:
- Sperimenta in locale: Inizia con ChromaDB e pochi documenti per prendere confidenza con la pipeline.
- Esplora LangSmith: Usa LangSmith per monitorare e debuggare le tue chain in produzione.
- Prova diversi modelli di embedding: Confronta modelli come
text-embedding-3-small,text-embedding-3-largee modelli open-source come quelli di Sentence Transformers. - Consulta la documentazione: La documentazione di LangChain è una risorsa eccellente e in costante aggiornamento.