RAG mit LangChainJS, Milvus und Strapi erstellen
Die Bereitstellung präziser und relevanter Antworten auf Benutzeranfragen ist beim Erstellen von KI-Anwendungen entscheidend. Obwohl traditionelle Chatbots und KI-Modelle zu natürlicher Konversation fähig sind, fehlt ihnen oft der Zugriff auf spezifische, aktuelle Informationen. Ihre Antworten stammen aus Trainingsdaten, die veraltet oder für spezialisierte Anforderungen zu allgemein sein können.
Retrieval Augmented Generation (RAG) behebt diese Einschränkung, indem es die generativen Fähigkeiten von KI-Modellen mit einer benutzerdefinierten Wissensdatenbank kombiniert, die von einer Vektordatenbank wie Milvus unterstützt wird. Anstatt sich ausschließlich auf vortrainiertes Wissen zu verlassen, durchsucht RAG aktiv Ihre Inhalte, um relevante Informationen zu finden, bevor Antworten generiert werden. Dieser Ansatz stellt sicher, dass Antworten sowohl präzise als auch kontextuell angemessen sind.
RAG und Vektorsuche verstehen
RAG schließt die Lücke zwischen generischen KI-Antworten und spezialisiertem Wissen, indem es einen Abrufmechanismus in den Generierungsprozess integriert. Im Gegensatz zu traditionellen Sprachmodellen, die sich ausschließlich auf vortrainiertes Wissen verlassen, ruft RAG zunächst relevante Informationen aus einer benutzerdefinierten Wissensdatenbank ab, um die Antworten der KI zu ergänzen.
Wenn ein KI-Assistent beispielsweise nach einer bestimmten Richtlinie oder einem bestimmten Verfahren gefragt wird, verlässt er sich nicht allein auf sein Gedächtnis; stattdessen durchsucht er relevante Dokumente, bevor er eine Antwort gibt. Dieser Abrufprozess wird von Embedding-Modellen und Vektordatenbanken unterstützt.
Sehen wir uns nun an, wie ein Standard-RAG funktioniert.
Text-Embeddings: Text wird durch ein Embedding-Modell in numerische Darstellungen oder Vektoren umgewandelt, die seine semantische Bedeutung erfassen. Zum Beispiel könnten die Ausdrücke Wie gebe ich einen Artikel zurück? und Wie läuft der Rückerstattungsprozess ab? unterschiedliche Wörter verwenden, aber eine ähnliche Bedeutung haben, was sich in ihrer Vektorähnlichkeit widerspiegelt.
Vektorsuche: Wenn ein Benutzer eine Frage stellt, wird auch die Anfrage in einen Vektor umgewandelt. Dieser Vektor wird dann mit anderen in einer Vektordatenbank wie Milvus gespeicherten Vektoren verglichen, um die semantisch ähnlichsten Einträge zu finden.
Antwortgenerierung: Die abgerufenen Informationen werden zusammen mit der Benutzeranfrage an ein großes Sprachmodell übergeben. Das Modell generiert eine Antwort, die auf den abgerufenen Inhalten basiert, wodurch sichergestellt wird, dass die Antwort relevant und präzise ist.
Diese Kombination aus Abruf und Generierung stellt sicher, dass die Antworten der KI kontextuell angemessen sind und auf den relevantesten Daten aus der Wissensdatenbank basieren.
In den folgenden Abschnitten erstellen wir ein RAG-gestütztes FAQ-System, das Fragen zu Milvus mithilfe einer in Strapi verwalteten Wissensdatenbank beantworten kann. Dieses System integriert Milvus für die Vektorsuche, LangChain.js für die Workflow-Koordination und die Sprachmodelle von OpenAI, um präzise und kontextbezogene Antworten bereitzustellen. Inhalte werden in Strapi organisiert, einem quelloffenen Headless-CMS auf Node.js-Basis, und in Echtzeit abgefragt, um die Antworten der KI in Ihren benutzerdefinierten Daten zu verankern.
Wenn Sie einen schnellen Blick darauf werfen möchten, finden Sie hier den vollständigen Code und das Strapi-Backend.
Einrichten Ihrer Entwicklungsumgebung
Bevor wir Code schreiben, müssen wir die drei oben besprochenen Schlüsselkomponenten einrichten: Milvus für die Vektorspeicherung, Strapi für das Content-Management und einen OpenAI-API-Schlüssel zum Generieren von Embeddings und Antworten. Richten wir sie nacheinander ein.
Milvus installieren
Beginnen wir mit der Einrichtung von Milvus, unserer Vektordatenbank. Sie können die vorkonfigurierte Docker-Compose-Datei entweder manuell herunterladen oder wget verwenden. Führen Sie in Ihrem Terminal aus:
wget https://github.com/milvus-io/milvus/releases/download/v2.0.2/milvus-standalone-docker-compose.yml -O docker-compose.yml
Starten Sie Milvus mit Docker, sobald die Konfigurationsdatei vorhanden ist:
sudo docker-compose up -d
Wenn Sie Docker Compose V2 statt V1 verwenden, nutzen Sie docker compose anstelle von docker-compose. Sie können Ihre Version überprüfen, indem Sie docker compose version ausführen.
Strapi installieren
Richten wir nun Strapi ein, um die Inhalte unserer Wissensdatenbank zu verwalten. Erstellen Sie mit dem folgenden Befehl ein neues Strapi-Projekt:
npx create-strapi-app@latest my-project
Wählen Sie SQLite als Datenbank, wenn Sie dazu aufgefordert werden. Nach der Installation öffnet sich Strapi in Ihrem Browser, wo Sie Ihr Admin-Konto erstellen.
Erstellen Sie im Strapi-Admin-Panel einen neuen Sammlungstyp namens MILVUS-KNOWLEDGEBASE. Fügen Sie zwei wesentliche Felder hinzu: ein Title-Feld vom Typ Text und ein Content-Feld vom Typ Rich text.
Abbildung – Felder zu einer Strapi-Sammlung hinzufügen
Abbildung: Felder zu einer Strapi-Sammlung hinzufügen
Diese Struktur hilft uns, unsere Einträge in der Wissensdatenbank effektiv zu organisieren.
Sobald Ihre Sammlung eingerichtet ist, gehen Sie zum Content Manager, um einige Beispielinhalte hinzuzufügen.
Abbildung – Strapi-Sammlung mit Daten befüllt
Abbildung: Strapi-Sammlung mit Daten befüllt
Stellen Sie sicher, dass Sie Ihre Einträge nach dem Erstellen veröffentlichen, unveröffentlichte Einträge sind für unsere Anwendung nicht zugänglich.
Nach dem Veröffentlichen der Einträge müssen wir den Inhalt für das RAG-System verfügbar machen, indem wir Berechtigungen konfigurieren. Gehen Sie zu Settings, dann zu Roles, und wählen Sie die Rolle Public aus. Suchen Sie Ihre Sammlung MILVUS-KNOWLEDGEBASE und aktivieren Sie die Berechtigungen find und findOne.
Ihren OpenAI-API-Schlüssel erhalten
Das letzte Element, das wir benötigen, ist ein OpenAI-API-Schlüssel, um Embeddings und Antworten zu generieren. Besuchen Sie die OpenAI-Plattform und registrieren Sie sich oder melden Sie sich an. Navigieren Sie zum Abschnitt für API-Schlüssel und erstellen Sie einen neuen geheimen Schlüssel. Kopieren und speichern Sie den Schlüssel an einem sicheren Ort, da wir ihn später benötigen und Sie ihn nur einmal anzeigen können.
Die React-Anwendung erstellen
Da unsere Kerndienste bereit sind, richten wir nun die React-Anwendung ein, die unser RAG-System hosten wird. Wir verwenden das Tool create-react-app, um ein neues React-Projekt zu erstellen und die notwendigen Abhängigkeiten zu installieren.
Erstellen Sie zunächst eine neue React-Anwendung:
npx create-react-app rag-app
cd rag-app
Dieser Befehl richtet ein vollständiges React-Projekt mit allen notwendigen Build-Tools und der Konfiguration ein. Installieren Sie als Nächstes die für unser RAG-System benötigten Pakete:
npm install @langchain/community @langchain/openai @zilliz/milvus2-sdk-node axios cors express langchain react-markdown
Hier ist, was jedes Paket in unserer Anwendung tun wird:
@langchain/community: Bietet Zugriff auf Integrationen mit verschiedenen Tools und Diensten. Wir verwenden es für die Milvus-Vektorspeicher-Integration.@langchain/openai: Verarbeitet Interaktionen mit OpenAIs Modellen. Wir verwenden es, um Embeddings zu erstellen und Antworten mit GPT-3.5 zu generieren.@zilliz/milvus2-sdk-node: Das offizielle Milvus-SDK für Node.js. Damit können wir direkt mit unserer Milvus-Instanz für Vektoroperationen kommunizieren.axios: Dies ist ein HTTP-Client. Wir verwenden ihn, um Inhalte von unserer Strapi-API abzurufen und Anfragen zwischen unserem Frontend und Backend zu stellen.cors: Middleware für Express, die Cross-Origin Resource Sharing ermöglicht. Dadurch kann unser Frontend sicher mit unserem Backend-Server kommunizieren.express: Ein Web-Framework für Node.js. Wir verwenden es, um unsere Backend-API zu erstellen, die zwischen unserem Frontend, Milvus und OpenAI koordiniert.langchain: Die zentrale LangChain-Bibliothek, die dabei hilft, unseren RAG-Workflow zu orchestrieren und den Ablauf zwischen dem Abrufen von Inhalten und dem Generieren von Antworten zu verwalten.react-markdown: Rendert Markdown-Inhalte in unseren React-Komponenten. Wir verwenden es, um formatierte Antworten unseres KI-Assistenten anzuzeigen.
Aufbau des Backend-Servers
Nachdem wir die Einrichtung unserer Entwicklungsumgebung abgeschlossen haben, beginnen wir mit dem Aufbau des Servers, der unser RAG-System antreiben wird. Der Server übernimmt mehrere zentrale Aufgaben: das Abrufen von Inhalten aus Strapi, das Umwandeln von Text in Vektoreinbettungen, das Speichern dieser in Milvus und das Koordinieren des RAG-Workflows zur Generierung von Antworten.
Erstellen Sie eine Datei mit dem Namen server.mjs und eine weitere mit dem Namen .env im Stammverzeichnis Ihres Projekts. Fügen Sie in der .env-Datei Ihren OpenAI-Schlüssel wie folgt ein:
OPENAI_API_KEY= your-api-key
Dies ist die Datei, die wir zum Speichern unserer sensiblen Informationen verwenden werden. Öffnen Sie nun die Serverdatei, und lassen Sie uns mit dem Programmieren beginnen.
1. Einrichten des Servers und der Umgebung
Beginnen Sie damit, die notwendigen Bibliotheken zu importieren und Umgebungsvariablen zu konfigurieren, um sicherzustellen, dass das System sicher läuft.
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();
Das Importieren der Bibliotheken, die wir in unserer Umgebung installiert haben, in unseren Code stellt sicher, dass wir ihre Funktionen und Methoden in unserem Code aufrufen und verwenden können. Die Funktion dotenv.config() lädt unseren API-Schlüssel und stellt sicher, dass er in der gesamten Anwendung zugänglich ist, ohne ihn direkt im Code offenzulegen.
2. Konfigurieren der Anwendung
Als Nächstes zentralisieren Sie die Einstellungen Ihrer Anwendung mithilfe eines Konfigurationsobjekts. Dieser Schritt hilft dabei, alle wichtigen Parameter an einem Ort zu halten, wodurch sie bei Bedarf leichter zu verwalten und anzupassen sind.
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
};
Dieses Konfigurationsobjekt definiert kritische Parameter für unsere Anwendung. Die Einstellung PORT bestimmt, auf welchem Port der Server lauscht. Der Abschnitt STRAPI enthält die URL für das Content-Management-System und einen Timeout-Wert, um lange Wartezeiten beim Abrufen von Daten zu vermeiden.
Der Abschnitt MILVUS konfiguriert die Vektordatenbank und gibt Details wie den Namen der Collection, Begrenzungen der Textlänge und Suchparameter an. nprobe steuert die Präzision von Vektorsuchen, indem angepasst wird, wie viele Cluster Milvus untersucht. Schließlich beschreibt der Abschnitt CHUNKING, wie Dokumente in kleinere Teile aufgeteilt werden, einschließlich Chunk-Größe und Überlappung, um sicherzustellen, dass während der Verarbeitung kein Kontext verloren geht.
3. Initialisieren des Servers und der KI-Dienste
Nachdem die Konfiguration eingerichtet ist, besteht der nächste Schritt darin, den Express-Server zu initialisieren und die KI-Dienste einzurichten. Dazu gehören das Large Language Model zum Generieren von Antworten und der Embedding-Dienst zum Umwandeln von Text in Vektoren.
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", " ", ""]
});
Das app-Objekt wird mit Express erstellt, wobei Middleware hinzugefügt wird, um eingehende JSON-Anfragen zu parsen und domänenübergreifende Anfragen mithilfe von CORS zu verarbeiten. Dadurch wird sichergestellt, dass der Server Payloads von verschiedenen Domains akzeptieren kann, was für die Kommunikation mit dem Frontend unerlässlich ist.
Als Nächstes erstellen wir eine ChatOpenAI-Instanz, die wir verwenden, um das System mit dem GPT-3.5-Modell von OpenAI zur Generierung von Antworten zu verbinden. Der Parameter temperature steuert die Kreativität der Antworten, wobei niedrigere Werte vorhersehbarere Ausgaben erzeugen und höhere Werte vielfältigere und kreativere Ergebnisse liefern.. Die OpenAIEmbeddings-Instanz wandelt Text in Vektoren um und ermöglicht so semantische Suchen. Schließlich teilt die RecursiveCharacterTextSplitter-Instanz den Text basierend auf der in der Konfiguration definierten Größe und Überlappung in Chunks auf.
4. Dokumente für die Verarbeitung vorbereiten
Bevor wir Dokumente mit Milvus speichern oder durchsuchen können, müssen wir den Rohinhalt bereinigen und in ein konsistentes Format strukturieren. Dadurch wird sichergestellt, dass die Daten für das Embedding und den effizienten Abruf später im Workflow bereit sind.
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;
};
Im obigen Code dienen die Variablen let milvusStore und let lastDataHash als globale Referenzen. milvusStore enthält die initialisierte Verbindung zur Milvus-Collection, und lastDataHash verfolgt, ob sich die zugrunde liegenden Dokumentdaten seit der letzten Aktualisierung geändert haben.
Die Funktion processDocument hilft uns dabei, Rohdaten aus Strapi zu nehmen und sie in ein strukturiertes Document-Objekt zu formatieren. Zunächst prüft sie, ob die Inhalts- und Titelfelder vorhanden sind. Wenn sie gültig sind, durchläuft sie die Abschnitte des Inhalts, extrahiert Text aus Absätzen und bereinigt ihn, indem unnötige Leerzeichen entfernt werden. Diese Abschnitte werden zu einem zusammenhängenden Textblock verbunden. Anschließend gibt die Funktion den verarbeiteten Text in einem Document-Objekt zurück, einschließlich Metadaten wie Titel, ID und Quelle des Dokuments. Dieses Format stellt die Kompatibilität mit Milvus und LangChain für die weitere Verarbeitung sicher.
5. Verbindung zu Milvus herstellen oder Milvus initialisieren
Da die Dokumente nun bereit sind, besteht der nächste Schritt darin, sie in Milvus zu integrieren. Wir stellen eine Verbindung zu einer vorhandenen Milvus-Collection her, oder falls sie nicht existiert, erstellen wir eine und füllen sie mit den vorbereiteten Dokumenten.
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;
}
}`
Diese Funktion verwaltet die Verbindung zur Milvus-Collection. Wenn die Variable milvusStore bereits eine aktive Verbindung enthält, gibt die Funktion sie sofort zurück, um unnötige Vorgänge zu vermeiden. Andernfalls versucht sie, mithilfe der Methode fromExistingCollection eine Verbindung zu einer bestehenden Collection in Milvus herzustellen. Wenn die Collection nicht existiert, fährt die Funktion damit fort, eine neue zu erstellen. Zunächst ruft sie Daten von Strapi ab, verarbeitet die Dokumente mit der Funktion processDocument und stellt sicher, dass jedes Dokument eindeutig ist, indem die documentId als Schlüssel in einer Map verwendet wird. Anschließend werden die Dokumente mithilfe des splitter in kleinere Abschnitte aufgeteilt, wodurch sichergestellt wird, dass die Dokumente nicht zu groß für das Embedding oder die Indexierung in Milvus sind. Die verarbeiteten Abschnitte werden dann mithilfe der Methode fromDocuments in der Milvus-Collection gespeichert. lastDataHash wird aktualisiert, um den aktuellen Zustand der Strapi-Daten widerzuspiegeln und sicherzustellen, dass Änderungen nachverfolgt werden.
6. Milvus auf dem neuesten Stand halten
Milvus muss mit Strapi synchronisiert bleiben, um sicherzustellen, dass die Vektordatenbank die aktuellsten Daten enthält. Implementieren wir einen Prozess, der Änderungen an den Inhalten von Strapi erkennt, aktualisierte oder neue Dokumente identifiziert und die Milvus-Collection entsprechend aktualisiert.
// Update Milvus data if needed
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('Data unchanged, skipping update');
return false;
}
console.log('Content changed, updating Milvus collection');
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();
// Delete all existing documents before adding new ones
await milvusStore.delete({});
// Add new documents in batches
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(`Added batch ${Math.floor(i / CONFIG.MILVUS.BATCH_SIZE) + 1} of ${Math.ceil(allSplitDocs.length / CONFIG.MILVUS.BATCH_SIZE)}`);
}
lastDataHash = currentHash;
console.log(`Updated Milvus with ${allSplitDocs.length} documents`);
return true;
} catch (error) {
console.error("Error updating Milvus data:", error);
throw error;
}
}
Diese Funktion beginnt damit, die neuesten Daten von Strapi abzurufen und ihren Hash zu berechnen. Wenn der Hash mit dem lastDataHash übereinstimmt, haben sich die Daten nicht geändert, und es ist kein Update erforderlich. Andernfalls verarbeitet die Funktion die aktualisierten Dokumente und leert die bestehende Collection in Milvus mithilfe der Methode delete. Die neuen Dokument-Chunks werden anschließend in Batches hinzugefügt, wodurch eine effiziente Speicherung gewährleistet wird, während gleichzeitig Logging zur Fortschrittsverfolgung beibehalten wird. Abschließend aktualisiert sie den lastDataHash, um sicherzustellen, dass zukünftige Prüfungen Änderungen in den Daten zuverlässig erkennen.
7. Verarbeitung von Benutzeranfragen
Nachdem Milvus mit unseren Daten befüllt wurde, besteht der nächste Schritt darin, Benutzeranfragen zu verarbeiten. Das System ruft die relevantesten Dokumente aus Milvus ab, basierend auf der semantischen Ähnlichkeit zur Anfrage des Benutzers. Diese Dokumente werden anschließend verwendet, um kontextbewusste Antworten mithilfe des großen Sprachmodells GPT-3.5 von OpenAI zu generieren.
async function handleQuery(chatHistory, input) {
const store = await getMilvusStore();
try {
await updateMilvusData();
} catch (error) {
console.warn("Failed to check for updates:", error);
}
const results = await store.similaritySearchWithScore(
input,
CONFIG.TOP_K
);
console.log('\n=== Retrieved Documents from Milvus ===');
results.forEach(([doc, score], index) => {
console.log(`\nDocument ${index + 1} (score: ${score}):`);
console.log('Title:', doc.metadata.title);
console.log('Content:', 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
}))
};
}
Diese Funktion beginnt damit, sicherzustellen, dass der Milvus-Vektorspeicher auf dem neuesten Stand ist. Die Funktion updateMilvusData wird innerhalb eines try-catch-Blocks aufgerufen, um potenzielle Fehler zu behandeln, ohne den Abfrageprozess zu unterbrechen. Die Abfrage wird anschließend an Milvus’ Methode similaritySearchWithScore übergeben, die die obersten k ähnlichsten Dokumente aus der Datenbank abruft. Die abgerufenen Dokumente werden protokolliert. Jedes Dokument enthält Metadaten wie den Titel und die ID sowie eine gekürzte Vorschau seines Inhalts. Dies hilft uns zu erkennen, ob unsere RAG-Pipeline die richtigen Dokumente abruft.
Als Nächstes werden die Dokumente in eine LangChain-createStuffDocumentsChain eingespeist, wobei ein Prompt verwendet wird, der das Modell anweist, ausschließlich auf Grundlage des bereitgestellten Kontexts zu antworten. Dadurch wird sichergestellt, dass OpenAIs GPT-3.5 eine Antwort generiert, die auf den abgerufenen Inhalten basiert, ohne auf externes Wissen zurückzugreifen, wodurch Genauigkeit und Relevanz gewahrt bleiben. Abschließend gibt die Funktion sowohl die generierte Antwort als auch den Kontext der abgerufenen Dokumente zurück.
8. Einrichten von API-Endpunkten
Da das Backend nun Benutzerabfragen verarbeiten und mit Milvus interagieren kann, besteht der nächste Schritt darin, diese Funktionalität über API-Endpunkte bereitzustellen. Diese Endpunkte dienen als Brücke zwischen unserem Backend und Frontend (oder anderen Client-Anwendungen).
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
});
}
});
Der /chat-Endpunkt verarbeitet eingehende POST-Anfragen, die die Anfrage eines Benutzers und den Chatverlauf enthalten. Der Chatverlauf wird in HumanMessage- und AIMessage-Objekte formatiert, um den Kontext beizubehalten. Wenn die Anfrage ungültig ist (z. B. leer), antwortet der Endpunkt mit einem Status 400 Bad Request und einer entsprechenden Fehlermeldung. Wenn die Anfrage gültig ist, wird die Funktion handleQuery aufgerufen, um die Eingabe zu verarbeiten, relevante Dokumente aus Milvus abzurufen und eine Antwort zu generieren. Das Ergebnis wird als JSON-Objekt zurückgegeben. Wenn ein Fehler auftritt, wird er protokolliert, und der Server antwortet mit einem 500 Internal Server Error.
Andererseits bietet der /health-Endpunkt eine Möglichkeit, den Status des Servers zu überwachen. Er prüft, ob der Milvus Store initialisiert ist, und antwortet mit einem Status von ok oder error, zusammen mit der Serverkonfiguration und dem aktuellen Zeitstempel.
9. Starten des Servers
Der letzte Schritt besteht darin, den Server zu starten und ihn für die Verarbeitung von Anfragen verfügbar zu machen.
app.listen(CONFIG.PORT, () => {
console.log(`Server running on http://localhost:${CONFIG.PORT}`);
console.log('Configuration:', CONFIG);
});
Die Methode app.listen startet den Server auf dem in der Konfiguration angegebenen Port, in diesem Fall Port 30080. Sobald der Server läuft, protokolliert er eine Meldung, die die URL angibt, unter der er erreichbar ist, sowie die aktuelle Konfiguration. Dies bestätigt, dass das Backend bereit ist, Anfragen zu verarbeiten.
Abbildung – Backend-Server des RAG-Systems läuft und protokolliert die aktuellen Konfigurationen
Abbildung: Backend-Server des RAG-Systems läuft und protokolliert die aktuellen Konfigurationen
Um den Server auszuführen, wechseln Sie zum Terminal und führen Sie diesen Befehl aus.
node .\server.mjs
Wir sind nun mit der Erstellung des Backends für unser RAG-System fertig.
Erstellen des Frontends Ihres RAG-Systems
Da das Backend des RAG-Systems vollständig funktionsfähig ist, besteht der nächste Schritt darin, ein Frontend zu erstellen, damit Benutzer mit dem System interagieren können. Das Frontend wird eine Chatbot-Oberfläche sein, die eine Verbindung zum Backend herstellt und es Benutzern ermöglicht, Anfragen einzugeben und KI-generierte Antworten zu erhalten.
Erstellen der Benutzeroberfläche
Beginnen Sie damit, eine Datei namens ChatbotUI.js im Verzeichnis src zu erstellen. Diese Datei definiert die Chatbot-Komponente und verwaltet den Ablauf von Benutzeranfragen und Backend-Antworten.
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;
Diese Komponente verwendet Reacts useState, um Zustände wie den Chatverlauf und die Benutzereingabe zu verwalten. Der useEffect-Hook stellt sicher, dass der Chat-Container automatisch zur neuesten Nachricht scrollt. Wenn der Benutzer eine Anfrage absendet, wird sie über eine axios-POST-Anfrage an den Backend-API-Endpunkt http://localhost:30080/chat gesendet. Die Backend-Antwort wird anschließend in der Chat-Oberfläche angezeigt.
Styling des Chatbots
Erstellen Sie als Nächstes im Verzeichnis src eine Datei mit dem Namen ChatbotUI.css. Diese Datei gestaltet die Chatbot-Oberfläche.
/* 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);
}
/* Tippindikator */
.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;
}
/* Fußzeile */
.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);
}
}
Die CSS-Stile erstellen eine Chatbot-Oberfläche, die von der Milvus-Farbpalette inspiriert ist. Der Hintergrund verwendet einen Farbverlauf mit tiefen Lila- und Cyan-Tönen, der das Branding von Milvus widerspiegelt. Der Chat-Header folgt einem dunklen Farbverlaufsstil, während Benutzernachrichten mit einem hellen Farbverlauf hervorgehoben werden, der den Primärfarben von Milvus entspricht. Bot-Nachrichten werden mit einem dunkleren, gedämpften Theme gestaltet. Dieses Design nutzt die Farben von Milvus für eine einheitliche und funktionale Benutzeroberfläche.
Abbildung – Fertige Benutzeroberfläche des RAG-Systems
Abbildung: Fertige Benutzeroberfläche des RAG-Systems
So sieht die endgültige UI aus.
Integration des Chatbots in die Anwendung
Aktualisieren Sie abschließend die Datei App.js im Verzeichnis src, um die Komponente ChatbotUI zu rendern.
import React from 'react';
import ChatbotUI from './ChatbotUI';
const App = () => {
return (
<div>
<ChatbotUI />
</div>
);
};
export default App;
Dadurch wird sichergestellt, dass der Chatbot beim Start der Anwendung als Hauptkomponente gerendert wird. Die Komponente App dient als Einstiegspunkt für die React-Anwendung.
Testen Ihres RAG-Systems
Nachdem Sie die Backend- und Frontend-Einrichtung abgeschlossen haben, ist es an der Zeit, die gesamte RAG-Pipeline zu testen, um sicherzustellen, dass sie wie erwartet funktioniert. So gehen Sie vor:
Starten des Backends
Öffnen Sie ein Terminal, navigieren Sie zum Projektverzeichnis und starten Sie den Backend-Server:
node server.mjs
Stellen Sie sicher, dass das Backend unter http://localhost:30080 läuft. Überprüfen Sie die Logs, um sicherzustellen, dass Milvus initialisiert wurde und für Abfragen bereit ist.
Ausführen des Frontends
Öffnen Sie ein separates Terminal, navigieren Sie zum Verzeichnis rag-app und starten Sie das React-Frontend:
npm start
Dadurch wird die Anwendung unter http://localhost:3000 in Ihrem Standardbrowser gestartet.
Testen der Chatbot-Oberfläche
Geben Sie in der Chatbot-Oberfläche eine Frage ein, die sich auf Milvus oder Zilliz bezieht. Versuchen Sie außerdem, Fragen außerhalb dieser Themen einzugeben, um zu sehen, ob das System mit der entsprechenden Nachricht antwortet. Versuchen Sie schließlich, Fragen einzugeben, die möglicherweise außerhalb der Strapi-Wissensdatenbank liegen. Hier sind Beispielergebnisse:
Abbildung- Ergebnis des RAG-Systems für eine Abfrage zu Milvus
Abbildung: Ergebnis des RAG-Systems für eine Abfrage zu Milvus
Abbildung- Testergebnisse des RAG-Systems
Abbildung: Testergebnisse des RAG-Systems
Die obigen Screenshots zeigen, dass unser RAG-System wie erwartet läuft und die korrekten Ergebnisse anzeigt. Es basiert außerdem auf der Nutzung unserer Wissensdatenbank, weshalb es die Gründer von Milvus nicht kennt, da diese Information in Strapi nicht vorhanden ist.
Fügen Sie der Wissensdatenbank weitere Inhalte hinzu und testen Sie das System.
Fazit
Die Integration von LangChain, Milvus und Strapi zum Aufbau eines Retrieval-Augmented-Generation-(RAG)-Systems zeigt, wie KI genaue, domänenspezifische Antworten liefern kann, die auf realem, aktuellem Wissen basieren. Diese Architektur eignet sich ideal für Anwendungen wie Kundensupport, Wissensmanagement und Bildungstools. Mit einem klaren Verständnis der Architektur und dieser Schritt-für-Schritt-Anleitung können Sie nun RAG-Systeme erstellen, die auf Ihre spezifischen Anforderungen zugeschnitten sind.
Verwandte Ressourcen
Weiterlesen

Will Amazon S3 Vectors Kill Vector Databases—or Save Them?
AWS S3 Vectors aims for 90% cost savings for vector storage. But will it kill vectordbs like Milvus? A deep dive into costs, limits, and the future of tiered storage.

Why Context Engineering Is Becoming the Full Stack of AI Agents
Discover how context engineering unifies prompts, RAG, and tools to build smarter, production-ready AI agents powered by Milvus.

Announcing the General Availability of Zilliz Cloud BYOC on Google Cloud Platform
Zilliz Cloud BYOC on GCP offers enterprise vector search with full data sovereignty and seamless integration.


