Hai mai sognato di poter fare “CTRL+F” nella tua documentazione aziendale, ma ottenendo risposte intelligenti invece che semplici corrispondenze di parole? O di chiedere a ChatGPT: “Riassumimi la clausola 4 del contratto PDF che ti ho appena dato” senza dover fare copia-incolla manuale?
La realtà è che i modelli LLM generici (come GPT-4) sono incredibilmente potenti, ma hanno due grandi limiti: non conoscono i tuoi dati privati e la loro conoscenza è ferma alla data del loro ultimo addestramento (Knowledge Cutoff).
Benvenuto nel mondo del RAG (Retrieval-Augmented Generation).
In questo rag langchain tutorial, non perderemo tempo in teoria accademica astratta. Ti guiderò passo dopo passo nella creazione di un’applicazione Node.js reale, capace di leggere un PDF, trasformarlo in vettori matematici e usarlo come “memoria esterna” per rispondere alle tue domande in modo preciso.
Il tutto mantenendo il codice estremamente pulito: meno di 50 righe di JavaScript.
Cos’è il RAG (Spiegato ai Dev)
Per capire il RAG, immagina di dover sostenere un esame universitario complesso a “libro aperto”.
- L’LLM (come GPT-4) è lo studente geniale: sa scrivere bene, ragiona perfettamente e comprende la lingua, ma non ha studiato il libro di testo specifico del corso.
- Il RAG è il meccanismo che gli permette di sfogliare rapidamente il libro di testo (i tuoi dati privati) per trovare la pagina giusta con la risposta, invece di provare a indovinarla o inventarsela (le temute “allucinazioni”).
Senza RAG, l’AI si affida solo alla sua memoria di addestramento. Con il RAG, uniamo la potenza linguistica dell’AI con la precisione dei tuoi dati.
Tecnicamente, il flusso (Pipeline) è composto da 5 step fondamentali:
- Ingestion: Carichiamo il PDF grezzo.
- Chunking: Lo spezziamo in piccoli blocchi logici. Un’intera pagina potrebbe essere troppo lunga per il contesto dell’AI, quindi la dividiamo.
- Embedding: Trasformiamo il testo in lunghe liste di numeri (vettori). Questo permette al computer di capire il significato semantico, non solo le parole chiave.
- Retrieval: Quando fai una domanda, il sistema calcola il vettore della tua domanda e cerca nel database i vettori dei “chunks” più vicini (simili) matematicamente.
- Generation: Passiamo quei pezzi di testo recuperati a GPT-4 come “Contesto” e gli diciamo: “Usa queste informazioni specifiche per rispondere alla domanda dell’utente”.
Setup dell’Ambiente
Per questo tutorial useremo Node.js e la libreria ufficiale di LangChain, che è diventata lo standard de facto per l’orchestrazione di applicazioni LLM. Crea una cartella vuota e installa le dipendenze necessarie:
mkdir rag-tutorial
cd rag-tutorial
npm init -y
npm install langchain @langchain/openai pdf-parse dotenv hnswlib-node
Crea un file .env nella root del progetto. Questo è fondamentale per non esporre le tue chiavi API nel codice sorgente:
OPENAI_API_KEY=sk-LaTuaChiaveSegretaQui...
Nota: Assicurati di inserire nella cartella un file PDF di prova con del testo selezionabile (chiamalo documento.pdf). Se usi una scansione immagine, avrai bisogno di un passaggio OCR aggiuntivo.
Fase 1: Caricamento e Chunking (Divisione Intelligente)
L’errore numero uno nel RAG è passare tutto il documento all’AI in una volta sola. I modelli hanno limiti di contesto (token windows) e costi basati sull’input. Inoltre, passare informazioni irrilevanti confonde il modello. La soluzione è il Chunking.
Non basta “tagliare” il file ogni 1000 caratteri. Se tagliamo una frase a metà, perdiamo il significato. Ecco perché usiamo chunkOverlap:
Ecco il codice per index.js:
import { PDFLoader } from "langchain/document_loaders/fs/pdf";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
// 1. Caricamento del PDF
// PDFLoader gestisce l'estrazione del testo grezzo e dei metadati (es. numero pagina)
const loader = new PDFLoader("documento.pdf");
const docs = await loader.load();
// 2. Splitting (Chunking)
// RecursiveCharacterTextSplitter è intelligente: cerca di dividere il testo
// usando separatori naturali (paragrafi, newlines) prima di tagliare le parole.
// chunkOverlap: 200 assicura che ci sia una sovrapposizione tra i blocchi,
// mantenendo il contesto semantico tra la fine di un blocco e l'inizio del successivo.
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000, // Dimensione target di ogni pezzo
chunkOverlap: 200, // Sovrapposizione per non perdere contesto ai bordi
});
const splitDocs = await splitter.splitDocuments(docs);
console.log(`Documento processato e diviso in ${splitDocs.length} chunks.`);
Fase 2: Vector Store e Embedding
Ora dobbiamo rendere questi chunks “ricercabili” per significato. Se cerchi “canino”, il sistema deve trovare anche “cane”, anche se la parola è diversa. Questo si ottiene con gli Embeddings.
Per questo tutorial useremo HNSWLib, un vector store in-memory velocissimo basato su grafi. È perfetto per lo sviluppo e i test perché non richiede di configurare container Docker o database esterni (come Pinecone, Weaviate o Milvus).
import { OpenAIEmbeddings } from "@langchain/openai";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import dotenv from "dotenv";
dotenv.config();
// 3. Creazione del Vector Store
// OpenAIEmbeddings (modello text-embedding-3-small di default) trasforma
// ogni chunk di testo in un vettore di 1536 numeri.
// HNSWLib indicizza questi vettori in memoria per una ricerca rapidissima.
const vectorStore = await HNSWLib.fromDocuments(
splitDocs,
new OpenAIEmbeddings()
);
console.log("Vector Store creato e indicizzato in memoria!");
Fase 3: La Catena di Recupero (Retrieval Chain)
È il momento di collegare tutto. In LangChain, una “Chain” è una sequenza di operazioni. Qui creiamo una pipeline che:
- Prende la domanda dell’utente.
- Cerca i chunk rilevanti nel Vector Store (il “Retriever”).
- Inserisce quei chunk nel Prompt come
{context}. - Invia tutto all’LLM per la risposta finale.
import { ChatOpenAI } from "@langchain/openai";
import { createRetrievalChain } from "langchain/chains/retrieval";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
// Configuriamo il modello (GPT-3.5-turbo o GPT-4o)
// temperature: 0 rende il modello deterministico e fattuale,
// riducendo drasticamente il rischio di creatività indesiderata (allucinazioni).
const model = new ChatOpenAI({
modelName: "gpt-3.5-turbo",
temperature: 0,
});
// Creiamo un prompt template specifico.
// Questo è il "system instruction" che forza l'AI a comportarsi come un assistente documentale.
const prompt = ChatPromptTemplate.fromTemplate(`
Sei un assistente specializzato nell'analisi di documenti tecnici.
Rispondi alla domanda basandoti ESCLUSIVAMENTE sul contesto fornito qui sotto.
Se la risposta non è presente nel testo fornito, devi rispondere: "Non ho abbastanza informazioni nel documento per rispondere".
Non inventare nulla.
Contesto recuperato dal PDF:
{context}
Domanda dell'utente:
{input}
`);
// "StuffDocumentsChain" è la strategia più semplice: prende tutti i documenti trovati
// e li "ficca" (stuff) dentro la variabile {context} del prompt.
const combineDocsChain = await createStuffDocumentsChain({
llm: model,
prompt,
});
// Creiamo la catena finale di RAG che orchestra il retriever e l'LLM
const chain = await createRetrievalChain({
retriever: vectorStore.asRetriever({ k: 3 }), // Recuperiamo i top 3 chunks più simili
combineDocsChain,
});
// 4. Testiamo il Chatbot!
// Simuliamo una domanda reale che un utente potrebbe fare al documento.
const response = await chain.invoke({
input: "Quali sono i punti principali trattati nel documento?",
});
console.log("\n--- RISPOSTA AI ---");
console.log(response.answer);
// Opzionale: Vediamo quali pezzi di testo ha usato per rispondere
console.log("\n--- FONTI UTILIZZATE ---");
response.context.forEach((doc, i) => {
console.log(`Fonte ${i+1} (Pag. ${doc.metadata.loc.pageNumber}): ${doc.pageContent.slice(0, 50)}...`);
});
Conclusione e Prossimi Step
Congratulazioni! 🎉 In pochi minuti hai costruito un sistema RAG completamente funzionante. Hai appena superato la barriera tra un semplice script e l’AI Engineering. Non stiamo più solo “chattando” con GPT, stiamo costruendo architetture software che sfruttano l’AI come motore di ragionamento sui nostri dati.
Vuoi portare questo sistema in produzione? Questo codice è perfetto per prototipi, ma ha un limite: HNSWLib qui gira in memoria. Se riavvii lo script, perdi l’indice e devi ricalcolare gli embedding (che costano tempo e denaro).
Nel prossimo articolo vedremo come sostituire la memoria volatile con un database vettoriale persistente e scalabile come Pinecone o Supabase (pgvector), permettendoti di gestire milioni di documenti senza doverli ricaricare ogni volta.
Se questo tutorial ti è stato utile e vuoi approfondire l’AI Engineering con JavaScript, condividilo su LinkedIn o iscriviti alla newsletter!





