Crea un RAG con LangChainJS, Milvus e Strapi
Fornire risposte accurate e pertinenti alle query degli utenti è fondamentale quando si creano applicazioni di IA. Sebbene siano capaci di conversazioni naturali, i chatbot tradizionali e i modelli di IA spesso non hanno accesso a informazioni specifiche e aggiornate. Le loro risposte provengono da dati di addestramento che potrebbero essere obsoleti o troppo generici per esigenze specializzate.
Retrieval Augmented Generation (RAG) affronta questa limitazione combinando le capacità generative dei modelli di IA con una knowledge base personalizzata alimentata da un database vettoriale come Milvus. Invece di basarsi esclusivamente sulla conoscenza pre-addestrata, RAG cerca attivamente nei tuoi contenuti per trovare informazioni pertinenti prima di generare risposte. Questo approccio garantisce che le risposte siano sia accurate sia contestualmente appropriate.
Comprendere RAG e la ricerca vettoriale
RAG colma il divario tra risposte IA generiche e conoscenza specializzata integrando un meccanismo di recupero con il processo di generazione. A differenza dei modelli linguistici tradizionali che si basano esclusivamente sulla conoscenza pre-addestrata, RAG prima recupera informazioni pertinenti da una knowledge base personalizzata per integrare le risposte dell'IA.
Ad esempio, quando a un assistente IA viene chiesto di una policy o procedura specifica, non si basa solo sulla memoria; invece, cerca tra i documenti pertinenti prima di fornire una risposta. Questo processo di recupero è alimentato da modelli di embedding e database vettoriali.
Ora, vediamo come funziona un RAG standard.
Embedding del testo: Il testo viene convertito in rappresentazioni numeriche, o vettori, che ne catturano il significato semantico tramite un modello di embedding. Ad esempio, le frasi Come posso restituire un articolo? e Qual è la procedura di rimborso? potrebbero usare parole diverse ma condividere un significato simile, riflesso nella loro similarità vettoriale.
Ricerca vettoriale: Quando un utente pone una domanda, anche la query viene trasformata in un vettore. Questo vettore viene quindi confrontato con altri archiviati in un database vettoriale, come Milvus, per trovare le voci semanticamente più simili.
Generazione della risposta: Le informazioni recuperate, insieme alla query dell'utente, vengono passate a un modello linguistico di grandi dimensioni. Il modello genera una risposta basata sul contenuto recuperato, assicurando che la risposta sia pertinente e accurata.
Questa combinazione di recupero e generazione garantisce che le risposte dell'IA siano contestualmente appropriate e basate sui dati più pertinenti della knowledge base.
Nelle sezioni seguenti, creeremo un sistema FAQ basato su RAG in grado di rispondere a domande su Milvus utilizzando una knowledge base gestita in Strapi. Questo sistema integrerà Milvus per la ricerca vettoriale, LangChain.js per il coordinamento del workflow e i modelli linguistici di OpenAI per fornire risposte accurate e contestuali. I contenuti saranno organizzati in Strapi, un headless CMS Node.js open-source, e interrogati in tempo reale per basare le risposte dell'IA sui tuoi dati personalizzati.
Se vuoi dare una rapida occhiata, ecco il codice completo e il backend Strapi.
Configurare il tuo ambiente di sviluppo
Prima di scrivere codice, dobbiamo configurare i tre componenti chiave di cui abbiamo parlato sopra: Milvus per l'archiviazione vettoriale, Strapi per la gestione dei contenuti e una chiave API OpenAI per generare embedding e risposte. Configuriamoli uno per uno.
Installare Milvus
Iniziamo configurando Milvus, il nostro database vettoriale. Puoi scaricare il file Docker Compose preconfigurato manualmente oppure usando wget. Nel tuo terminale, esegui:
wget https://github.com/milvus-io/milvus/releases/download/v2.0.2/milvus-standalone-docker-compose.yml -O docker-compose.yml
Con il file di configurazione pronto, avvia Milvus usando Docker:
sudo docker-compose up -d
Se stai usando Docker Compose V2 invece di V1, usa docker compose invece di docker-compose. Puoi verificare la tua versione eseguendo docker compose version.
Installazione di Strapi
Ora configuriamo Strapi per gestire il contenuto della nostra base di conoscenza. Crea un nuovo progetto Strapi usando il seguente comando:
npx create-strapi-app@latest my-project
Scegli SQLite come database quando richiesto. Dopo l’installazione, Strapi si aprirà nel tuo browser, dove creerai il tuo account amministratore.
Nel pannello di amministrazione di Strapi, crea un nuovo tipo di raccolta chiamato MILVUS-KNOWLEDGEBASE. Aggiungi due campi essenziali: un campo Title usando il tipo Text e un campo Content usando il tipo Rich text.
Figura- Aggiunta di campi a una raccolta Strapi
Figura: Aggiunta di campi a una raccolta Strapi
Questa struttura ci aiuterà a organizzare efficacemente le voci della nostra base di conoscenza.
Una volta configurata la tua raccolta, vai a Content Manager per aggiungere alcuni contenuti di esempio.
Figura- Raccolta Strapi popolata con dati
Figura: Raccolta Strapi popolata con dati
Assicurati di pubblicare le tue voci dopo averle create; le voci non pubblicate non saranno accessibili alla nostra applicazione.
Dopo aver pubblicato le voci, dobbiamo rendere il contenuto disponibile al sistema RAG configurando le autorizzazioni. Vai su Settings, poi Roles, e seleziona il ruolo Public. Trova la tua raccolta MILVUS-KNOWLEDGEBASE e abilita le autorizzazioni find e findOne.
Ottenere la tua chiave API OpenAI
L’ultimo elemento di cui abbiamo bisogno è una chiave API OpenAI per generare embedding e risposte. Visita la piattaforma OpenAI e registrati o accedi. Vai alla sezione delle chiavi API e crea una nuova chiave segreta. Copia e salva la chiave in un luogo sicuro, poiché ne avremo bisogno in seguito e potrai visualizzarla una sola volta.
Creazione dell’applicazione React
Con i nostri servizi principali pronti, configuriamo l’applicazione React che ospiterà il nostro sistema RAG. Useremo lo strumento create-react-app per creare un nuovo progetto React e installare le dipendenze necessarie.
Per prima cosa, crea una nuova applicazione React:
npx create-react-app rag-app
cd rag-app
Questo comando configura un progetto React completo con tutti gli strumenti di build e la configurazione necessari. Successivamente, installa i pacchetti necessari per il nostro sistema RAG:
npm install @langchain/community @langchain/openai @zilliz/milvus2-sdk-node axios cors express langchain react-markdown
Ecco cosa farà ciascun pacchetto nella nostra applicazione:
@langchain/community: Fornisce accesso a integrazioni con vari strumenti e servizi. Lo useremo per l’integrazione con il vector store Milvus.@langchain/openai: Gestisce le interazioni con i modelli di OpenAI. Lo useremo per creare embedding e generare risposte usando GPT-3.5.@zilliz/milvus2-sdk-node: L’SDK ufficiale di Milvus per Node.js. Ci consente di comunicare direttamente con la nostra istanza Milvus per le operazioni vettoriali.axios: Questo è un client HTTP. Lo useremo per recuperare contenuti dalla nostra API Strapi ed effettuare richieste tra il nostro frontend e backend.cors: Middleware per Express che abilita Cross-Origin Resource Sharing. Questo consente al nostro frontend di comunicare in modo sicuro con il nostro server backend.express: Un framework web per Node.js. Lo useremo per creare la nostra API backend che coordina il nostro frontend, Milvus e OpenAI.langchain: La libreria principale LangChain che aiuta a orchestrare il nostro flusso di lavoro RAG, gestendo il flusso tra il recupero dei contenuti e la generazione delle risposte.react-markdown: Esegue il rendering dei contenuti Markdown nei nostri componenti React. Lo useremo per visualizzare risposte formattate dal nostro assistente AI.
Creazione del server backend
Ora che abbiamo completato la configurazione del nostro ambiente di sviluppo, iniziamo a creare il server che alimenterà il nostro sistema RAG. Il server gestirà diverse attività chiave: recuperare contenuti da Strapi, convertire il testo in vector embeddings, archiviarli in Milvus e coordinare il flusso di lavoro RAG per generare risposte.
Crea un file chiamato server.mjs e un altro chiamato .env nella root del tuo progetto. Nel file .env, incolla la tua chiave OpenAI come segue:
OPENAI_API_KEY= your-api-key
Questo è il file che useremo per archiviare le nostre informazioni sensibili. Ora apri il file del server e iniziamo a programmare.
1. Configurazione del server e dell’ambiente
Inizia importando le librerie necessarie e configurando le variabili d’ambiente per garantire che il sistema venga eseguito in modo sicuro.
import express from "express";
import axios from "axios";
import dotenv from "dotenv";
import cors from "cors";
import { ChatOpenAI } from "@langchain/openai";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { OpenAIEmbeddings } from "@langchain/openai";
import { Milvus } from "@langchain/community/vectorstores/milvus";
import { Document } from "langchain/document";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
dotenv.config();
Importare nel nostro codice le librerie che abbiamo installato nel nostro ambiente garantisce che possiamo chiamare e utilizzare le loro funzioni e i loro metodi nel nostro codice. La funzione dotenv.config() carica la nostra chiave API, assicurando che sia accessibile in tutta l’applicazione senza esporla direttamente nel codice.
2. Configurazione dell’applicazione
Successivamente, centralizza le impostazioni della tua applicazione usando un oggetto di configurazione. Questo passaggio aiuta a mantenere tutti i parametri chiave in un unico posto, rendendoli più facili da gestire e modificare secondo necessità.
const CONFIG = {
PORT: parseInt(process.env.PORT || "30080", 10),
STRAPI: {
URL: "http://localhost:1337/api/milvus-knowledgebases",
TIMEOUT: 5000
},
MILVUS: {
URL: "localhost:19530",
COLLECTION: "rag_collection",
PRIMARY_FIELD: "pk",
VECTOR_FIELD: "vector",
TEXT_FIELD: "text",
TEXT_MAX_LENGTH: 4096,
SEARCH_PARAMS: {
nprobe: 16,
offset: 0
},
BATCH_SIZE: 100
},
CHUNKING: {
SIZE: 2000,
OVERLAP: 200
},
TOP_K: 3
};
Questo oggetto di configurazione definisce parametri critici per la nostra applicazione. L’impostazione PORT determina su quale porta il server resta in ascolto. La sezione STRAPI contiene l’URL per il sistema di gestione dei contenuti e un valore di timeout per evitare lunghe attese durante il recupero dei dati.
La sezione MILVUS configura il database vettoriale, specificando dettagli come il nome della collection, i limiti di lunghezza del testo e i parametri di ricerca. nprobe controlla la precisione delle ricerche vettoriali regolando quanti cluster Milvus esamina. Infine, la sezione CHUNKING descrive come i documenti vengono suddivisi in parti più piccole, inclusi la dimensione del chunk e l’overlap, garantendo che nessun contesto venga perso durante l’elaborazione.
3. Inizializzazione del server e dei servizi AI
Con la configurazione in posizione, il passo successivo è inizializzare il server Express e configurare i servizi AI. Questi includono il modello linguistico di grandi dimensioni per generare risposte e il servizio di embedding per convertire il testo in vettori.
const app = express();
app.use(express.json({ limit: '1mb' }));
app.use(cors());
const model = new ChatOpenAI({
modelName: "gpt-3.5-turbo",
temperature: 0.7,
openAIApiKey: process.env.OPENAI_API_KEY,
maxRetries: 3,
timeout: 30000,
});
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
maxRetries: 3,
});
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: CONFIG.CHUNKING.SIZE,
chunkOverlap: CONFIG.CHUNKING.OVERLAP,
separators: ["\n\n", "\n", " ", ""]
});
L'oggetto app viene creato usando Express, con middleware aggiunto per analizzare le richieste JSON in arrivo e gestire le richieste cross-origin usando CORS. Questo garantisce che il server possa accettare payload da domini diversi, il che è essenziale per la comunicazione con il frontend.
Successivamente, creiamo un'istanza ChatOpenAI che useremo per connettere il sistema al modello GPT-3.5 di OpenAI per generare risposte. Il parametro temperature controlla la creatività delle risposte, con valori più bassi che producono output più prevedibili e valori più alti che generano output più vari e creativi.. L'istanza OpenAIEmbeddings convertirà il testo in vettori, abilitando le ricerche semantiche. Infine, l'istanza RecursiveCharacterTextSplitter dividerà il testo in chunk in base alla dimensione e alla sovrapposizione definite nella configurazione.
4. Preparazione dei documenti per l'elaborazione
Prima di poter archiviare o cercare nei documenti usando Milvus, dobbiamo pulire e strutturare il contenuto grezzo in un formato coerente. Questo garantisce che i dati siano pronti per l'embedding e per un recupero efficiente più avanti nel workflow.
let milvusStore = null;
let lastDataHash = null;
const processDocument = (content) => {
if (!content?.Content || !content.Title) return null;
const processedContent = content.Content
.map(section => {
if (section.type === 'paragraph' && section.children) {
return section.children
.map(child => child.text)
.join(' ')
.trim();
}
return null;
})
.filter(Boolean)
.join('\n\n');
return processedContent ? new Document({
pageContent: processedContent,
metadata: {
source: 'milvus_content',
id: content.id,
title: content.Title,
documentId: content.documentId
}
}) : null;
};
Nel codice sopra, le variabili let milvusStore e let lastDataHash agiscono come riferimenti globali. milvusStore contiene la connessione inizializzata alla collection Milvus, e lastDataHash tiene traccia del fatto che i dati dei documenti sottostanti siano cambiati dall'ultimo aggiornamento.
La funzione processDocument ci aiuterà a prendere i dati grezzi da Strapi e a formattarli in un oggetto Document strutturato. Per prima cosa, controlla se i campi contenuto e titolo sono presenti. Se sono validi, scorre le sezioni del contenuto, estraendo il testo dai paragrafi e pulendolo eliminando gli spazi non necessari. Queste sezioni vengono unite in un blocco di testo coeso. La funzione quindi restituisce il testo elaborato racchiuso in un oggetto Document, includendo metadati come il titolo, l'ID e la fonte del documento. Questo formato garantisce la compatibilità con Milvus e LangChain per ulteriori elaborazioni.
5. Connessione a Milvus o inizializzazione di Milvus
Ora che i documenti sono pronti, il passo successivo è integrarli con Milvus. Stabiliremo una connessione a una collection Milvus esistente oppure, se non esiste, ne creeremo una e la popoleremo con i documenti preparati.
async function getMilvusStore() {
if (milvusStore) {
return milvusStore;
}
try {
// First try to connect to existing collection
try {
console.log('Attempting to connect to existing collection');
milvusStore = await Milvus.fromExistingCollection(
embeddings,
{
url: CONFIG.MILVUS.URL,
collectionName: CONFIG.MILVUS.COLLECTION,
primaryField: CONFIG.MILVUS.PRIMARY_FIELD,
vectorField: CONFIG.MILVUS.VECTOR_FIELD,
textField: CONFIG.MILVUS.TEXT_FIELD,
textFieldMaxLength: CONFIG.MILVUS.TEXT_MAX_LENGTH,
}
);
return milvusStore;
} catch (error) {
console.log('Collection does not exist, will create new one');
// If collection doesn't exist, fetch data and create it
const response = await axios.get(CONFIG.STRAPI.URL, {
timeout: CONFIG.STRAPI.TIMEOUT
});
const processPromises = response.data.data.map(processDocument);
const processedDocs = await Promise.all(processPromises);
const uniqueDocs = new Map();
processedDocs
.filter(Boolean)
.forEach(doc => uniqueDocs.set(doc.metadata.documentId, doc));
const docs = Array.from(uniqueDocs.values());
// Split documents
const splitPromises = docs.map(doc => splitter.splitDocuments([doc]));
const splitDocs = await Promise.all(splitPromises);
const allSplitDocs = splitDocs.flat();
console.log(`Creating new collection with ${allSplitDocs.length} documents`);
milvusStore = await Milvus.fromDocuments(
allSplitDocs,
embeddings,
{
url: CONFIG.MILVUS.URL,
collectionName: CONFIG.MILVUS.COLLECTION,
primaryField: CONFIG.MILVUS.PRIMARY_FIELD,
vectorField: CONFIG.MILVUS.VECTOR_FIELD,
textField: CONFIG.MILVUS.TEXT_FIELD,
textFieldMaxLength: CONFIG.MILVUS.TEXT_MAX_LENGTH,
}
);
lastDataHash = Buffer.from(JSON.stringify(response.data)).toString('base64');
return milvusStore;
}
} catch (error) {
console.error('Failed to initialize Milvus:', error);
throw error;
}
}`
Questa funzione gestisce la connessione alla collection Milvus. Se la variabile milvusStore contiene già una connessione attiva, la funzione la restituisce immediatamente per evitare operazioni non necessarie. In caso contrario, tenta di connettersi a una collection esistente in Milvus usando il metodo fromExistingCollection. Se la collection non esiste, la funzione procede a crearne una nuova. Innanzitutto, recupera i dati da Strapi, elabora i documenti usando la funzione processDocument e garantisce che ogni documento sia unico utilizzando il documentId come chiave in una Map. Poi, i documenti vengono suddivisi in chunk più piccoli usando lo splitter, il che garantisce che i documenti non siano troppo grandi per l’embedding o l’indicizzazione in Milvus. I chunk elaborati vengono quindi archiviati nella collection Milvus usando il metodo fromDocuments. Il lastDataHash viene aggiornato per riflettere lo stato corrente dei dati di Strapi, assicurando che le modifiche siano tracciate.
6. Mantenere Milvus aggiornato
Milvus deve rimanere sincronizzato con Strapi per garantire che il database vettoriale contenga i dati più recenti. Implementiamo un processo che rilevi le modifiche nei contenuti di Strapi, identifichi i documenti aggiornati o nuovi e aggiorni di conseguenza la collection Milvus.
// Aggiorna i dati Milvus se necessario
async function updateMilvusData() {
try {
const response = await axios.get(CONFIG.STRAPI.URL, {
timeout: CONFIG.STRAPI.TIMEOUT
});
const currentHash = Buffer.from(JSON.stringify(response.data)).toString('base64');
if (currentHash === lastDataHash) {
console.log('Dati invariati, aggiornamento saltato');
return false;
}
console.log('Contenuto modificato, aggiornamento della collection Milvus');
const processPromises = response.data.data.map(processDocument);
const processedDocs = await Promise.all(processPromises);
const uniqueDocs = new Map();
processedDocs
.filter(Boolean)
.forEach(doc => uniqueDocs.set(doc.metadata.documentId, doc));
const docs = Array.from(uniqueDocs.values());
// Dividi i documenti
const splitPromises = docs.map(doc => splitter.splitDocuments([doc]));
const splitDocs = await Promise.all(splitPromises);
const allSplitDocs = splitDocs.flat();
// Elimina tutti i documenti esistenti prima di aggiungerne di nuovi
await milvusStore.delete({});
// Aggiungi nuovi documenti in batch
for (let i = 0; i < allSplitDocs.length; i += CONFIG.MILVUS.BATCH_SIZE) {
const batch = allSplitDocs.slice(i, i + CONFIG.MILVUS.BATCH_SIZE);
await milvusStore.addDocuments(batch);
console.log(`Aggiunto batch ${Math.floor(i / CONFIG.MILVUS.BATCH_SIZE) + 1} di ${Math.ceil(allSplitDocs.length / CONFIG.MILVUS.BATCH_SIZE)}`);
}
lastDataHash = currentHash;
console.log(`Milvus aggiornato con ${allSplitDocs.length} documenti`);
return true;
} catch (error) {
console.error("Errore durante l'aggiornamento dei dati Milvus:", error);
throw error;
}
}
Questa funzione inizia recuperando i dati più recenti da Strapi e calcolandone l'hash. Se l'hash corrisponde a lastDataHash, i dati non sono cambiati e non è necessario alcun aggiornamento. In caso contrario, la funzione elabora i documenti aggiornati e svuota la collection esistente in Milvus usando il metodo delete. I nuovi chunk dei documenti vengono quindi aggiunti in batch, garantendo un'archiviazione efficiente e mantenendo al contempo il logging per il monitoraggio dell'avanzamento. Infine, aggiorna lastDataHash, per garantire che i controlli futuri rilevino accuratamente le modifiche nei dati.
7. Gestione delle query degli utenti
Dopo aver popolato Milvus con i nostri dati, il passaggio successivo è gestire le query degli utenti. Il sistema recupererà da Milvus i documenti più pertinenti in base alla somiglianza semantica con la query dell'utente. Questi documenti verranno quindi utilizzati per generare risposte consapevoli del contesto usando il large language model GPT-3.5 di OpenAI.
async function handleQuery(chatHistory, input) {
const store = await getMilvusStore();
try {
await updateMilvusData();
} catch (error) {
console.warn("Impossibile verificare la presenza di aggiornamenti:", error);
}
const results = await store.similaritySearchWithScore(
input,
CONFIG.TOP_K
);
console.log('\n=== Documenti recuperati da Milvus ===');
results.forEach(([doc, score], index) => {
console.log(`\nDocumento ${index + 1} (punteggio: ${score}):`);
console.log('Titolo:', doc.metadata.title);
console.log('Contenuto:', doc.pageContent.substring(0, 150) + '...');
});
const chain = await createStuffDocumentsChain({
llm: model,
prompt: ChatPromptTemplate.fromMessages([
[
"system",
`You are an AI assistant specializing in Milvus and Zilliz, vector database technologies. Your goal is to provide accurate and helpful answers based on the provided context.
**Guidelines:**
1. **Milvus/Zilliz-Related Queries:**
- If the query is about Milvus or Zilliz and the context includes relevant information, provide a detailed and structured response.
- Use proper Markdown formatting:
- \`\`\` for code blocks
- **Bold** for emphasis
- Bullet points for lists
- ### Headings for sections
- If no relevant information exists in the context, respond with: *"I don't have that information yet."*
2. **Unrelated Queries:**
- If the query is not about Milvus or Zilliz, respond with:
*"This topic is outside my expertise. I specialize in Milvus and Zilliz. Please ask questions related to these technologies."*
**Context:**
{context}
**User Query:**
{input}`
],
...chatHistory,
["user", "{input}"]
]),
documentPrompt: ChatPromptTemplate.fromTemplate("Content: {page_content}\n\n")
});
const response = await chain.invoke({
input,
context: results.map(([doc]) => doc)
});
return {
answer: response,
context: results.map(([doc]) => ({
content: doc.pageContent,
title: doc.metadata.title,
id: doc.metadata.id,
documentId: doc.metadata.documentId
}))
};
}
Questa funzione inizia assicurandosi che il vector store Milvus sia aggiornato. La funzione updateMilvusData viene chiamata all’interno di un blocco try-catch per gestire potenziali errori senza interrompere il processo di query. La query viene quindi passata al metodo similaritySearchWithScore di Milvus, che recupera dal database i primi k documenti più simili. I documenti recuperati vengono registrati nei log. Ogni documento include metadati come il titolo e l’ID, oltre a un’anteprima troncata del suo contenuto. Questo ci aiuterà a sapere se la nostra pipeline RAG sta recuperando i documenti corretti.
Successivamente, i documenti vengono inseriti in una createStuffDocumentsChain di LangChain usando un prompt che istruisce il modello a rispondere basandosi esclusivamente sul contesto fornito. Questo garantisce che GPT-3.5 di OpenAI generi una risposta fondata sul contenuto recuperato, senza fare affidamento su conoscenze esterne, mantenendo accuratezza e pertinenza. Infine, la funzione restituisce sia la risposta generata sia il contesto dei documenti recuperati.
8. Configurazione degli endpoint API
Ora che il backend può elaborare le query degli utenti e interagire con Milvus, il passaggio successivo è esporre questa funzionalità tramite endpoint API. Questi endpoint fungono da ponte tra il nostro backend e il frontend (o altre applicazioni client).
app.post("/chat", async (req, res) => {
try {
const { chatHistory, input } = req.body;
if (!input?.trim()) {
return res.status(400).json({
error: "Invalid input",
message: "No input provided"
});
}
const formattedHistory = Array.isArray(chatHistory)
? chatHistory.map(msg =>
msg.role === "user"
? new HumanMessage(msg.content)
: new AIMessage(msg.content)
)
: [];
const response = await handleQuery(formattedHistory, input);
res.json(response);
} catch (error) {
console.error("Chat request error:", error);
res.status(500).json({
error: "Internal server error",
message: process.env.NODE_ENV === 'production'
? "An unexpected error occurred"
: error.message
});
}
});
app.get("/health", async (req, res) => {
try {
const store = await getMilvusStore();
res.json({
status: "ok",
timestamp: new Date().toISOString(),
milvusInitialized: !!store,
config: CONFIG
});
} catch (error) {
res.status(500).json({
status: "error",
error: error.message
});
}
});
L’endpoint /chat elabora le richieste POST in arrivo che contengono la query di un utente e la cronologia della chat. La cronologia della chat viene formattata in oggetti HumanMessage e AIMessage per mantenere il contesto. Se la query non è valida (ad esempio, è vuota), l’endpoint risponde con uno stato 400 Bad Request e un messaggio di errore appropriato. Se la query è valida, viene chiamata la funzione handleQuery per elaborare l’input, recuperare i documenti pertinenti da Milvus e generare una risposta. Il risultato viene restituito come oggetto JSON. Se si verifica un errore, viene registrato nei log e il server risponde con un 500 Internal Server Error.
D’altra parte, l’endpoint /health fornisce un modo per monitorare lo stato del server. Verifica se lo store Milvus è inizializzato e risponde con uno stato ok o error, insieme alla configurazione del server e al timestamp corrente.
9. Avvio del server
Il passaggio finale consiste nell’avviare il server e renderlo disponibile per gestire le richieste.
app.listen(CONFIG.PORT, () => {
console.log(`Server running on http://localhost:${CONFIG.PORT}`);
console.log('Configuration:', CONFIG);
});
Il metodo app.listen avvia il server sulla porta specificata nella configurazione, in questo caso la porta 30080. Una volta che il server è in esecuzione, registra un messaggio che indica l’URL a cui è possibile accedervi, oltre alla configurazione corrente. Questo fornisce una conferma che il backend è pronto a gestire le richieste.
Figura- server backend del sistema RAG in esecuzione mentre registra nei log le configurazioni correnti
Figura: server backend del sistema RAG in esecuzione mentre registra nei log le configurazioni correnti
Per eseguire il server, vai al terminale ed esegui questo comando.
node .\server.mjs
Ora abbiamo terminato la creazione del backend per il nostro sistema RAG.
Creazione del frontend del tuo sistema RAG
Con il backend del sistema RAG completamente funzionante, il passo successivo è creare un frontend per consentire agli utenti di interagire con il sistema. Il frontend sarà un’interfaccia chatbot che si connette al backend, consentendo agli utenti di inserire query e ricevere risposte generate dall’IA.
Creazione dell’interfaccia utente
Inizia creando un file denominato ChatbotUI.js nella directory src. Questo file definirà il componente chatbot e gestirà il flusso delle query degli utenti e delle risposte del backend.
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import axios from 'axios';
import './ChatbotUI.css';
const ChatbotUI = () => {
const [chatHistory, setChatHistory] = useState([]);
const [userInput, setUserInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const chatContainerRef = useRef(null);
useEffect(() => {
// Scroll to the latest message when chat history updates
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}, [chatHistory]);
const handleUserInput = (e) => setUserInput(e.target.value);
const handleSendMessage = async () => {
if (userInput.trim() !== '') {
const newMessage = { role: 'user', content: userInput };
const updatedChatHistory = [...chatHistory, newMessage];
setChatHistory(updatedChatHistory);
setUserInput('');
setIsLoading(true);
try {
const response = await axios.post('http://localhost:30080/chat', {
chatHistory: updatedChatHistory,
input: userInput,
});
const botMessage = {
role: 'assistant',
content: response.data.answer || 'Sorry, I didn’t quite get that.',
};
setTimeout(() => {
setChatHistory((prevMessages) => [...prevMessages, botMessage]);
}, 1000); // Simulate typing delay
} catch (err) {
console.error('Error:', err);
setError('Unable to connect to the server.');
} finally {
setIsLoading(false);
}
}
};
return (
<div className="chatbot-ui">
{/* Header */}
<div className="chat-header">
<img src="/milvus_logo.png" alt="Assistant" className="assistant-logo" />
<h2>Chat Assistant</h2>
</div>
{/* Chat Messages */}
<div className="chat-body" ref={chatContainerRef}>
{chatHistory.map((message, index) => (
<div
key={index}
className={`message ${message.role === 'user' ? 'user-message' : 'bot-message'}`}
>
<ReactMarkdown>{message.content}</ReactMarkdown>
</div>
))}
{isLoading && (
<div className="bot-message typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
)}
{error && <div className="error-message">{error}</div>}
</div>
{/* Footer */}
<div className="chat-footer">
<input
type="text"
placeholder="Ask me anything..."
value={userInput}
onChange={handleUserInput}
onKeyPress={(e) => {
if (e.key === 'Enter') handleSendMessage();
}}
disabled={isLoading}
/>
<button onClick={handleSendMessage} disabled={isLoading}>
<span className="send-icon">✈</span>
</button>
</div>
</div>
);
};
export default ChatbotUI;
Questo componente utilizza useState di React per gestire stati come la cronologia della chat e l’input dell’utente. L’hook useEffect assicura che il contenitore della chat scorra automaticamente fino all’ultimo messaggio. Quando l’utente invia una query, questa viene inviata all’endpoint API backend http://localhost:30080/chat tramite una richiesta POST axios. La risposta del backend viene quindi visualizzata nell’interfaccia della chat.
Styling del Chatbot
Successivamente, crea un file chiamato ChatbotUI.css nella directory src. Questo file definirà lo stile dell’interfaccia del chatbot.
/* Full-Screen Gradient Background */
.chatbot-ui {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #8358ff, #00a6ff); /* Milvus color palette */
font-family: 'Roboto', sans-serif;
color: #fff;
}
/* Chat Header */
.chat-header {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #1e1e2d, #28293e);
width: 100%;
padding: 15px;
border-radius: 15px 15px 0 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.assistant-logo {
width: 50px;
height: 50px;
margin-right: 15px;
border-radius: 50%;
background: radial-gradient(circle, #8358ff, #00a6ff); /* Dynamic logo glow */
animation: pulse 2s infinite;
}
.chat-header h2 {
font-size: 1.8rem;
font-weight: bold;
color: #ffffff;
}
.chat-header p {
font-size: 0.9rem;
color: #a0a0b1;
margin-top: 5px;
}
/* Chat Body */
.chat-body {
flex: 1;
width: 100%;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
background: #1e1e2d;
color: #ffffff;
border-radius: 0 0 15px 15px;
}
.message {
max-width: 80%;
padding: 12px 15px;
border-radius: 10px;
font-size: 1rem;
line-height: 1.5;
animation: fadeIn 0.3s ease-in-out;
}
.user-message {
align-self: flex-end;
background: linear-gradient(90deg, #8358ff, #00a6ff);
color: #ffffff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.bot-message {
align-self: flex-start;
background: #29293f;
color: #d4d4e5;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
/* Indicatore di digitazione */
.typing-indicator {
display: flex;
justify-content: flex-start;
gap: 5px;
}
.typing-indicator span {
width: 8px;
height: 8px;
background-color: #00a6ff;
border-radius: 50%;
animation: blink 1.2s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
/* Piè di pagina */
.chat-footer {
display: flex;
align-items: center;
width: 100%;
padding: 10px 15px;
background: #1e1e2d;
border-top: 1px solid #28293e;
border-radius: 0 0 15px 15px;
box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.2);
}
.chat-footer input {
flex: 1;
padding: 12px 15px;
border-radius: 30px;
border: 1px solid #8358ff;
font-size: 1rem;
outline: none;
background: #29293f;
color: #ffffff;
transition: border-color 0.3s ease;
}
.chat-footer input:focus {
border-color: #00a6ff;
}
.chat-footer button {
margin-left: 10px;
padding: 12px 20px;
border: none;
border-radius: 30px;
background: linear-gradient(90deg, #8358ff, #00a6ff);
color: white;
font-size: 1.2rem;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.chat-footer button:hover {
transform: scale(1.1);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4);
}
.chat-footer button:disabled {
background: #29293f;
color: #a0a0b1;
cursor: not-allowed;
}
@keyframes blink {
0%, 80%, 100% {
opacity: 0;
}
40% {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 10px rgba(131, 88, 255, 0.5);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 20px rgba(0, 166, 255, 0.7);
}
}
Gli stili CSS creano un’interfaccia chatbot ispirata alla palette di colori di Milvus. Lo sfondo utilizza un gradiente con tonalità viola intenso e ciano, riflettendo il branding di Milvus. L’intestazione della chat segue uno stile a gradiente scuro, mentre i messaggi degli utenti sono evidenziati con un gradiente luminoso che corrisponde ai colori primari di Milvus. I messaggi del bot sono stilizzati con un tema più scuro e tenue. Questo design sfrutta i colori di Milvus per un’interfaccia utente unificata e funzionale.
Figura- Interfaccia utente completata del sistema RAG
Figura: Interfaccia utente completata del sistema RAG
Ecco come appare l’interfaccia utente finale.
Integrazione del chatbot nell’applicazione
Infine, aggiorna il file App.js nella directory src per renderizzare il componente ChatbotUI.
import React from 'react';
import ChatbotUI from './ChatbotUI';
const App = () => {
return (
<div>
<ChatbotUI />
</div>
);
};
export default App;
Questo garantisce che il chatbot venga renderizzato come componente principale all’avvio dell’applicazione. Il componente App funge da punto di ingresso per l’applicazione React.
Test del tuo sistema RAG
Dopo aver completato la configurazione del backend e del frontend, è il momento di testare l’intera pipeline RAG per assicurarti che funzioni come previsto. Ecco come procedere:
Avvio del backend
Apri un terminale, vai alla directory del progetto e avvia il server backend:
node server.mjs
Assicurati che il backend sia in esecuzione su http://localhost:30080. Controlla i log per verificare che Milvus sia stato inizializzato e sia pronto per le query.
Esecuzione del frontend
Apri un terminale separato, vai alla directory rag-app e avvia il frontend React:
npm start
Questo avvierà l’applicazione su http://localhost:3000 nel browser predefinito.
Test dell’interfaccia del chatbot
Nell'interfaccia del chatbot, digita una domanda relativa a Milvus o Zilliz. Inoltre, prova a digitare domande al di fuori di questi argomenti per vedere se il sistema risponderà con il messaggio appropriato. Infine, prova a digitare domande che potrebbero essere al di fuori della knowledge base di Strapi. Ecco alcuni risultati di esempio:
Figura- Risultato del sistema RAG di una query relativa a Milvus
Figura: Risultato del sistema RAG di una query relativa a Milvus
Figura- Risultati dei test del sistema RAG
Figura: Risultati dei test del sistema RAG
Gli screenshot sopra mostrano che il nostro sistema RAG funziona come previsto e visualizza i risultati corretti. Si basa anche sull'utilizzo della nostra knowledge base, motivo per cui non conosce i fondatori di Milvus, poiché queste informazioni non esistono in Strapi.
Procedi e aggiungi altro contenuto alla knowledge base e testa il sistema.
Conclusione
L'integrazione di LangChain, Milvus e Strapi per creare un sistema di Retrieval-Augmented Generation (RAG) mostra come l'IA possa fornire risposte accurate e specifiche del dominio, basate su conoscenze reali e aggiornate. Questa architettura è ideale per applicazioni come l'assistenza clienti, la gestione della conoscenza e gli strumenti didattici. Con una chiara comprensione dell'architettura e questa guida passo dopo passo, ora puoi creare sistemi RAG su misura per le tue esigenze specifiche.
Risorse correlate
Continua a leggere

Announcing the General Availability of Single Sign-On (SSO) on Zilliz Cloud
SSO is GA on Zilliz Cloud, delivering the enterprise-grade identity management capabilities your teams need to deploy vectorDB with confidence.

Zilliz Cloud Audit Logs Goes GA: Security, Compliance, and Transparency at Scale
Zilliz Cloud Audit Logs are now GA, giving enterprises real-time visibility, compliance-ready trails, and stronger security across AWS, GCP, and Azure.

What is the K-Nearest Neighbors (KNN) Algorithm in Machine Learning?
KNN is a supervised machine learning technique and algorithm for classification and regression. This post is the ultimate guide to KNN.


