Hai un LLM che allucina risposte sui tuoi documenti interni? Oppure un chatbot aziendale che risponde solo fino alla data di training del modello? Il problema è sempre lo stesso: la conoscenza del modello è generale, non specifica. Noi, di Meteora Web, affrontiamo questo limite con RAG — Retrieval Augmented Generation. Un approccio che non richiede fine-tuning costosi e tiene i dati sotto il tuo controllo. Ecco come lo implementiamo con LangChain, in produzione.
Perché RAG e non un fine-tuning
Immagina di avere un manuale di prodotto di 500 pagine. Il fine-tuning richiederebbe GPU, ore di training e rischierebbe l'overfitting su domande inviste. RAG invece lascia il modello base intatto e gli fornisce i pezzi giusti di contesto al momento della risposta. Vantaggi: aggiornabile in tempo reale (basta cambiare i documenti), zero training, trasparenza (sai esattamente quali fonti ha usato). E funziona subito.
Architettura di un RAG con LangChain
I componenti sono tre: Indicizzazione (caricare, suddividere, vettorizzare i documenti), Retrieval (cercare per similarità semantica) e Generazione (LLM + contesto recuperato). LangChain li unisce in una pipeline coerente.
1. Indicizzazione dei documenti
Partiamo da un PDF. Noi usiamo PyPDFLoader o UnstructuredPDFLoader per estrarre testo. Poi lo dividiamo in chunk: troppo piccoli perdono contesto, troppo grandi sforano il context window. Noi usiamo RecursiveCharacterTextSplitter con chunk size ~1000 caratteri e overlap 200.
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
loader = PyPDFLoader("manuale_prodotto.pdf")
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", " ", ""]
)
chunks = text_splitter.split_documents(documents)
print(f"{len(chunks)} chunk creati")
Poi generiamo gli embeddings con un modello. Noi usiamo text-embedding-3-small di OpenAI per rapporto qualità/costo, ma funzionano anche modelli locali con HuggingFaceEmbeddings. Per vettori usiamo Chroma (leggero, nessun server) o Qdrant in produzione.
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
vectorstore.persist()
2. Retrieval – trovare i chunk giusti
Il retrieval è il cuore di RAG. Con LangChain usiamo as_retriever e impostatiamo il numero di chunk da restituire (k=4 o 5 di solito). Possiamo aggiungere filtri di metadati (es. solo documenti di una certa categoria).
retriever = vectorstore.as_retriever(
search_kwargs={"k": 4}
)
Un errore comune: affidarsi solo alla similarità coseno. Se i chunk sono di lunghezza diversa, meglio normalizzare. Noi a volte usiamo Maximal Marginal Relevance (MMR) per diversificare i risultati ed evitare ridondanze.
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 4, "lambda_mult": 0.5}
)
3. Generazione con contesto
Qui arriva la magia: creiamo un prompt che istruisce il LLM a usare SOLO il contesto fornito. Niente conoscenze generali se non supportate. In LangChain usiamo ChatPromptTemplate e la chain RAG classica con create_stuff_documents_chain e create_retrieval_chain.
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "Sei un assistente esperto. Usa solo il contesto fornito per rispondere. Se non trovi la risposta, dì che non lo sai."),
("human", "Contesto: \n{context}\n\nDomanda: {input}")
])
combine_docs_chain = create_stuff_documents_chain(llm, prompt)
qa_chain = create_retrieval_chain(retriever, combine_docs_chain)
result = qa_chain.invoke({"input": "Quali istruzioni di sicurezza ci sono per l'uso del prodotto?"})
print(result["answer"])
Attenzione: nel prompt specifichiamo esplicitamente di non allucinare. I modelli più recenti (GPT-4o, Claude 3.5) rispettano meglio questa direttiva.
Ottimizzare un RAG per la produzione
Un RAG funzionante è solo l'inizio. Per portarlo in produzione servono accorgimenti.
Chunking intelligente
Documenti con tabelle, immagini, codice? Il chunking naive può rompere contesti critici. Noi usiamo semantic chunking con modelli come NLTKTextSplitter o SpacyTextSplitter per rispettare confini di frase. Per codice sorgente, chunk per funzione/classe. Mai spezzare in mezzo a un paragrafo che contiene un riferimento a un'altra parte del documento.
Hybrid search
La similarità semantica non basta sempre: termini esatti come “ID cliente 12345” vanno recuperati con BM25 o full-text. Uniamo i due in un hybrid retriever. LangChain lo supporta con `EnsembleRetriever`.
from langchain.retrievers import EnsembleRetriever
from langchain.retrievers import BM25Retriever
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, semantic_retriever],
weights=[0.3, 0.7]
)
Controllo dei contesto – Reranking
Non tutti i chunk recuperati sono ugualmente utili. Un reranker (es. Cohere Rerank) riordina i risultati per pertinenza. LangChain supporta ContextualCompressionRetriever o il wrapper per Cohere Rerank. Noi lo usiamo quando il numero di chunk cresce (k>5).
Errori comuni e come evitarli
- Over-stuffing: passare troppi chunk (più di 8-10) degrada la risposta. Il modello si distrae. K=4/5 è un buon punto di partenza.
- Chunk senza metadati: se non salvi il nome del documento e la pagina, non saprai indicare la fonte. LangChain permette di includere metadati nei document loader.
- Prompt debole: un prompt generico “Usa il contesto” non basta. Specifica cosa fare se il contesto è irrilevante o contraddittorio.
- Embedding vecchi: se la conoscenza del dominio è particolare, usa un modello fine-tuned per il tuo dominio (es. embedding legali).
Strumenti e tecnologie reali che usiamo
Per clienti che richiedono privacy totale (documenti sensibili), usiamo Ollama con modelli locali (Llama 3, Mistral) e Chroma in locale. Per carichi alti, Qdrant su cloud. L'AI generativa va amplificata, non sostituita: ogni output viene verificato da chi conosce il dominio. Noi lo inseriamo in un sistema di feedback: l'utente può votare la risposta, e i log finiscono in un database per migliorare il retrieval.
Cosa fare adesso
1. Prepara un dataset di prova. Prendi 3-5 documenti aziendali (PDF, markdown, pagine web).
2. Installa le dipendenze: pip install langchain langchain-openai langchain-community chromadb pypdf e imposta la variabile d'ambiente OPENAI_API_KEY.
3. Copia e adatta il codice sopra per creare la tua pipeline di RAG. Sperimenta con chunk size e k.
4. Misura la qualità. Chiedi 10 domande di cui conosci la risposta. Controlla quante volte il sistema risponde correttamente e quante allucina. Un buon RAG dovrebbe avere almeno l'80% di correttezza sulle domande coperte dai documenti.
5. Integra in produzione — backend FastAPI, frontend React o Streamlit. Documenta la fonte di ogni risposta con i metadati.
Link utili:
- LangChain RAG Tutorial (ufficiale)
- GitHub Copilot: come lo usiamo per scrivere codice più veloce (Meteora Web)
- Sicurezza degli agenti AI: lezioni dal caso Meta (Meteora Web)
Sponsored Protocol