Crie RAG com LangChainJS, Milvus e Strapi
Fornecer respostas precisas e relevantes às consultas dos usuários é crucial ao criar aplicações de IA. Embora capazes de conversas naturais, chatbots tradicionais e modelos de IA muitas vezes não têm acesso a informações específicas e atualizadas. Suas respostas vêm de dados de treinamento que podem estar desatualizados ou ser genéricos demais para necessidades especializadas.
Retrieval Augmented Generation (RAG) aborda essa limitação combinando as capacidades generativas dos modelos de IA com uma base de conhecimento personalizada alimentada por um banco de dados vetorial como o Milvus. Em vez de depender exclusivamente de conhecimento pré-treinado, o RAG pesquisa ativamente seu conteúdo para encontrar informações relevantes antes de gerar respostas. Essa abordagem garante que as respostas sejam precisas e contextualmente adequadas.
Entendendo RAG e Busca Vetorial
O RAG preenche a lacuna entre respostas genéricas de IA e conhecimento especializado ao integrar um mecanismo de recuperação ao processo de geração. Diferentemente dos modelos de linguagem tradicionais, que dependem exclusivamente de conhecimento pré-treinado, o RAG primeiro recupera informações relevantes de uma base de conhecimento personalizada para complementar as respostas da IA.
Por exemplo, quando um assistente de IA é questionado sobre uma política ou procedimento específico, ele não depende apenas da memória; em vez disso, pesquisa documentos relevantes antes de fornecer uma resposta. Esse processo de recuperação é alimentado por modelos de embedding e bancos de dados vetoriais.
Agora, vamos ver como um RAG padrão funciona.
Embeddings de Texto: O texto é convertido em representações numéricas, ou vetores, que capturam seu significado semântico por meio de um modelo de embedding. Por exemplo, as frases Como faço para devolver um item? e Qual é o processo de reembolso? podem usar palavras diferentes, mas compartilham um significado semelhante, refletido em sua similaridade vetorial.
Busca Vetorial: Quando um usuário faz uma pergunta, a consulta também é transformada em um vetor. Esse vetor é então comparado com outros armazenados em um banco de dados vetorial, como o Milvus, para encontrar as entradas semanticamente mais semelhantes.
Geração de Resposta: As informações recuperadas, juntamente com a consulta do usuário, são passadas para um grande modelo de linguagem. O modelo gera uma resposta fundamentada no conteúdo recuperado, garantindo que a resposta seja relevante e precisa.
Essa combinação de recuperação e geração garante que as respostas da IA sejam contextualmente adequadas e fundamentadas nos dados mais relevantes da base de conhecimento.
Nas seções a seguir, criaremos um sistema de FAQ alimentado por RAG capaz de responder a perguntas sobre o Milvus usando uma base de conhecimento gerenciada no Strapi. Esse sistema integrará o Milvus para busca vetorial, o LangChain.js para coordenação de fluxos de trabalho e os modelos de linguagem da OpenAI para fornecer respostas precisas e contextuais. O conteúdo será organizado no Strapi, um CMS headless Node.js de código aberto, e consultado em tempo real para fundamentar as respostas da IA em seus dados personalizados.
Se quiser dar uma olhada rápida, aqui está o código completo e o backend Strapi.
Configurando Seu Ambiente de Desenvolvimento
Antes de escrever código, precisamos configurar os três componentes principais que discutimos acima: Milvus para armazenamento vetorial, Strapi para gerenciamento de conteúdo e uma chave de API da OpenAI para gerar embeddings e respostas. Vamos configurar cada um deles.
Instalando o Milvus
Vamos começar configurando o Milvus, nosso banco de dados vetorial. Você pode baixar o arquivo Docker Compose pré-configurado manualmente ou usando wget. No seu terminal, execute:
wget https://github.com/milvus-io/milvus/releases/download/v2.0.2/milvus-standalone-docker-compose.yml -O docker-compose.yml
Com o arquivo de configuração no lugar, inicie o Milvus usando Docker:
sudo docker-compose up -d
Se você estiver usando Docker Compose V2 em vez da V1, use docker compose em vez de docker-compose. Você pode verificar sua versão executando docker compose version.
Instalando o Strapi
Agora vamos configurar o Strapi para gerenciar o conteúdo da nossa base de conhecimento. Crie um novo projeto Strapi usando o seguinte comando:
npx create-strapi-app@latest my-project
Escolha SQLite como seu banco de dados quando solicitado. Após a instalação, o Strapi será aberto no seu navegador, onde você criará sua conta de administrador.
No painel de administração do Strapi, crie um novo tipo de coleção chamado MILVUS-KNOWLEDGEBASE. Adicione dois campos essenciais: um campo Title usando o tipo Text e um campo Content usando o tipo Rich text.
Figura- Adicionando campos a uma coleção do Strapi
Figura: Adicionando campos a uma coleção do Strapi
Essa estrutura nos ajudará a organizar nossas entradas da base de conhecimento de forma eficaz.
Depois que sua coleção estiver configurada, vá para o Content Manager para adicionar algum conteúdo de exemplo.
Figura- Coleção do Strapi preenchida com dados
Figura: Coleção do Strapi preenchida com dados
Certifique-se de publicar suas entradas depois de criá-las; entradas não publicadas não serão acessíveis para nossa aplicação.
Depois de publicar as entradas, precisamos disponibilizar o conteúdo para o sistema RAG configurando permissões. Vá para Settings, depois Roles, e selecione a função Public. Encontre sua coleção MILVUS-KNOWLEDGEBASE e habilite as permissões find e findOne.
Obtendo sua chave de API da OpenAI
A última peça de que precisamos é uma chave de API da OpenAI para gerar embeddings e respostas. Visite a plataforma da OpenAI e cadastre-se ou faça login. Navegue até a seção de chaves de API e crie uma nova chave secreta. Copie e salve a chave em um local seguro, pois precisaremos dela mais tarde e você só poderá visualizá-la uma vez.
Criando a aplicação React
Com nossos serviços principais prontos, vamos configurar a aplicação React que hospedará nosso sistema RAG. Usaremos a ferramenta create-react-app para criar um novo projeto React e instalar as dependências necessárias.
Primeiro, crie uma nova aplicação React:
npx create-react-app rag-app
cd rag-app
Este comando configura um projeto React completo com todas as ferramentas de build e configurações necessárias. Em seguida, instale os pacotes necessários para o nosso sistema RAG:
npm install @langchain/community @langchain/openai @zilliz/milvus2-sdk-node axios cors express langchain react-markdown
Aqui está o que cada pacote fará em nossa aplicação:
@langchain/community: Fornece acesso a integrações com várias ferramentas e serviços. Vamos usá-lo para a integração com o repositório vetorial Milvus.@langchain/openai: Lida com interações com os modelos da OpenAI. Vamos usá-lo para criar embeddings e gerar respostas usando GPT-3.5.@zilliz/milvus2-sdk-node: O SDK oficial do Milvus para Node.js. Isso nos permite nos comunicar diretamente com nossa instância do Milvus para operações vetoriais.axios: Este é um cliente HTTP. Vamos usá-lo para buscar conteúdo da nossa API Strapi e fazer requisições entre nosso frontend e backend.cors: Middleware para Express que habilita o Cross-Origin Resource Sharing. Isso permite que nosso frontend se comunique com nosso servidor backend com segurança.express: Um framework web para Node.js. Vamos usá-lo para criar nossa API de backend que coordena entre nosso frontend, Milvus e OpenAI.langchain: A biblioteca principal LangChain que ajuda a orquestrar nosso fluxo de trabalho RAG, gerenciando o fluxo entre recuperar conteúdo e gerar respostas.react-markdown: Renderiza conteúdo Markdown em nossos componentes React. Vamos usá-lo para exibir respostas formatadas do nosso assistente de IA.
Construindo o Servidor Backend
Agora que concluímos a configuração do nosso ambiente de desenvolvimento, vamos começar a construir o servidor que alimentará nosso sistema RAG. O servidor lidará com várias tarefas importantes: buscar conteúdo no Strapi, converter texto em embeddings vetoriais, armazená-los no Milvus e coordenar o fluxo de trabalho RAG para gerar respostas.
Crie um arquivo chamado server.mjs e outro chamado .env na raiz do seu projeto. No arquivo .env, cole sua chave da OpenAI da seguinte forma:
OPENAI_API_KEY= your-api-key
Este é o arquivo que usaremos para armazenar nossas informações sensíveis. Agora abra o arquivo do servidor e vamos começar a programar.
1. Configurando o Servidor e o Ambiente
Comece importando as bibliotecas necessárias e configurando variáveis de ambiente para garantir que o sistema seja executado com segurança.
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();
Importar as bibliotecas que instalamos em nosso ambiente para o nosso código garante que possamos chamar e usar suas funções e métodos em nossos códigos. A função dotenv.config() carrega nossa chave de API, garantindo que ela esteja acessível em toda a aplicação sem expô-la diretamente no código.
2. Configurando a Aplicação
Em seguida, centralize as configurações da sua aplicação usando um objeto de configuração. Esta etapa ajuda a manter todos os parâmetros-chave em um só lugar, tornando-os mais fáceis de gerenciar e ajustar conforme necessário.
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
};
Este objeto de configuração define parâmetros críticos para nossa aplicação. A configuração PORT determina em qual porta o servidor escuta. A seção STRAPI contém a URL do sistema de gerenciamento de conteúdo e um valor de timeout para evitar longas esperas ao buscar dados.
A seção MILVUS configura o banco de dados vetorial, especificando detalhes como o nome da coleção, limites de comprimento do texto e parâmetros de busca. nprobe controla a precisão das buscas vetoriais ajustando quantos clusters o Milvus examina. Por fim, a seção CHUNKING descreve como os documentos são divididos em partes menores, incluindo o tamanho do chunk e a sobreposição, garantindo que nenhum contexto seja perdido durante o processamento.
3. Inicializando o Servidor e os Serviços de IA
Com a configuração em vigor, o próximo passo é inicializar o servidor Express e configurar os serviços de IA. Isso inclui o modelo de linguagem grande para gerar respostas e o serviço de embeddings para converter texto em vetores.
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", " ", ""]
});
O objeto app é criado usando Express, com middleware adicionado para analisar solicitações JSON recebidas e lidar com solicitações de origem cruzada usando CORS. Isso garante que o servidor possa aceitar payloads de diferentes domínios, o que é essencial para a comunicação com o frontend.
Em seguida, criamos uma instância de ChatOpenAI, que usaremos para conectar o sistema ao modelo GPT-3.5 da OpenAI para gerar respostas. O parâmetro temperature controla a criatividade das respostas, com valores mais baixos produzindo saídas mais previsíveis e valores mais altos gerando respostas mais variadas e criativas.. A instância de OpenAIEmbeddings converterá texto em vetores, permitindo buscas semânticas. Por fim, a instância de RecursiveCharacterTextSplitter dividirá o texto em partes com base no tamanho e na sobreposição definidos na configuração.
4. Preparando Documentos para Processamento
Antes de podermos armazenar ou pesquisar documentos usando o Milvus, precisamos limpar e estruturar o conteúdo bruto em um formato consistente. Isso garante que os dados estejam prontos para embedding e recuperação eficiente posteriormente no fluxo de trabalho.
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;
};
No código acima, as variáveis let milvusStore e let lastDataHash atuam como referências globais. milvusStore mantém a conexão inicializada com a coleção do Milvus, e lastDataHash rastreia se os dados dos documentos subjacentes mudaram desde a última atualização.
A função processDocument nos ajudará a pegar dados brutos do Strapi e formatá-los em um objeto Document estruturado. Primeiro, ela verifica se os campos de conteúdo e título estão presentes. Se forem válidos, ela percorre as seções do conteúdo, extraindo texto dos parágrafos e limpando-o ao remover espaços desnecessários. Essas seções são unidas em um bloco de texto coeso. A função então retorna o texto processado envolvido em um objeto Document, incluindo metadados como o título do documento, ID e origem. Esse formato garante compatibilidade com Milvus e LangChain para processamento posterior.
5. Conectando-se ao Milvus ou Inicializando-o
Agora que os documentos estão prontos, o próximo passo é integrá-los ao Milvus. Estabeleceremos uma conexão com uma coleção existente do Milvus ou, se ela não existir, criaremos uma e a preencheremos com os documentos preparados.
async function getMilvusStore() {
if (milvusStore) {
return milvusStore;
}
try {
// Primeiro tenta conectar à coleção existente
try {
console.log('Tentando conectar à coleção existente');
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('A coleção não existe, uma nova será criada');
// Se a coleção não existir, busca os dados e a cria
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());
// Divide os documentos
const splitPromises = docs.map(doc => splitter.splitDocuments([doc]));
const splitDocs = await Promise.all(splitPromises);
const allSplitDocs = splitDocs.flat();
console.log(`Criando nova coleção com ${allSplitDocs.length} documentos`);
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('Falha ao inicializar o Milvus:', error);
throw error;
}
}`
Esta função gerencia a conexão com a coleção do Milvus. Se a variável milvusStore já contiver uma conexão ativa, a função a retorna imediatamente para evitar operações desnecessárias. Caso contrário, ela tenta conectar-se a uma coleção existente no Milvus usando o método fromExistingCollection. Se a coleção não existir, a função prossegue para criar uma nova. Primeiro, ela busca dados do Strapi, processa os documentos usando a função processDocument e garante que cada documento seja único usando o documentId como chave em um Map. Em seguida, os documentos são divididos em partes menores usando o splitter, o que garante que os documentos não sejam grandes demais para incorporação ou indexação no Milvus. As partes processadas são então armazenadas na coleção do Milvus usando o método fromDocuments. O lastDataHash é atualizado para refletir o estado atual dos dados do Strapi, garantindo que as alterações sejam rastreadas.
6. Mantendo o Milvus atualizado
O Milvus deve permanecer sincronizado com o Strapi para garantir que o banco de dados vetorial contenha os dados mais recentes. Vamos implementar um processo que detecte alterações no conteúdo do Strapi, identifique documentos atualizados ou novos e atualize a coleção do Milvus de acordo.
// 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;
}
}
Esta função começa buscando os dados mais recentes do Strapi e calculando seu hash. Se o hash corresponder ao lastDataHash, os dados não foram alterados, e nenhuma atualização é necessária. Caso contrário, a função processa os documentos atualizados e limpa a coleção existente no Milvus usando o método delete. Os novos fragmentos de documentos são então adicionados em lotes, garantindo armazenamento eficiente enquanto mantém registros para acompanhar o progresso. Por fim, ela atualiza o lastDataHash, para garantir que verificações futuras detectem com precisão alterações nos dados.
7. Tratamento de consultas de usuários
Depois de preencher o Milvus com nossos dados, o próximo passo é lidar com consultas de usuários. O sistema recuperará os documentos mais relevantes do Milvus com base na similaridade semântica com a consulta do usuário. Esses documentos serão então usados para gerar respostas sensíveis ao contexto usando o grande modelo de linguagem GPT-3.5 da OpenAI.
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
}))
};
}
Esta função começa garantindo que o armazenamento vetorial Milvus esteja atualizado. A função updateMilvusData é chamada dentro de um bloco try-catch para lidar com possíveis erros sem interromper o processo de consulta. A consulta é então passada para o método similaritySearchWithScore do Milvus, que recupera os k documentos mais semelhantes do banco de dados. Os documentos recuperados são registrados. Cada documento inclui metadados como o título e o ID, bem como uma prévia truncada de seu conteúdo. Isso nos ajudará a saber se nosso pipeline RAG está recuperando os documentos corretos.
Em seguida, os documentos são inseridos em uma createStuffDocumentsChain do LangChain usando um prompt que instrui o modelo a responder com base exclusivamente no contexto fornecido. Isso garante que o GPT-3.5 da OpenAI gere uma resposta fundamentada no conteúdo recuperado, sem depender de conhecimento externo, mantendo a precisão e a relevância. Por fim, a função retorna tanto a resposta gerada quanto o contexto dos documentos recuperados.
8. Configurando endpoints de API
Agora que o backend pode processar consultas de usuários e interagir com o Milvus, o próximo passo é expor essa funcionalidade por meio de endpoints de API. Esses endpoints servem como a ponte entre nosso backend e frontend (ou outros aplicativos cliente).
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
});
}
});
O endpoint /chat processa solicitações POST recebidas que contêm a consulta e o histórico de chat de um usuário. O histórico de chat é formatado em objetos HumanMessage e AIMessage para manter o contexto. Se a consulta for inválida (por exemplo, vazia), o endpoint responde com um status 400 Bad Request e uma mensagem de erro apropriada. Se a consulta for válida, a função handleQuery é chamada para processar a entrada, recuperar documentos relevantes do Milvus e gerar uma resposta. O resultado é retornado como um objeto JSON. Se ocorrer um erro, ele é registrado, e o servidor responde com um 500 Internal Server Error.
Por outro lado, o endpoint /health fornece uma maneira de monitorar o status do servidor. Ele verifica se o armazenamento Milvus está inicializado e responde com um status de ok ou error, junto com a configuração do servidor e o timestamp atual.
9. Iniciando o Servidor
A etapa final é iniciar o servidor e disponibilizá-lo para lidar com solicitações.
app.listen(CONFIG.PORT, () => {
console.log(`Server running on http://localhost:${CONFIG.PORT}`);
console.log('Configuration:', CONFIG);
});
O método app.listen inicia o servidor na porta especificada na configuração, neste caso, a porta 30080. Assim que o servidor estiver em execução, ele registra uma mensagem indicando a URL onde pode ser acessado, bem como a configuração atual. Isso fornece uma confirmação de que o backend está pronto para lidar com solicitações.
Figura- servidor backend do sistema RAG em execução enquanto registra as configurações atuais
Figura: servidor backend do sistema RAG em execução enquanto registra as configurações atuais
Para executar o servidor, vá ao terminal e execute este comando.
node .\server.mjs
Agora terminamos de criar o backend para nosso sistema RAG.
Criando o Frontend do Seu Sistema RAG
Com o backend do sistema RAG totalmente funcional, a próxima etapa é criar um frontend para permitir que os usuários interajam com o sistema. O frontend será uma interface de chatbot que se conecta ao backend, permitindo que os usuários insiram consultas e recebam respostas geradas por IA.
Criando a Interface do Usuário
Comece criando um arquivo chamado ChatbotUI.js no diretório src. Este arquivo definirá o componente do chatbot e gerenciará o fluxo das consultas dos usuários e das respostas do 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;
Este componente usa o useState do React para gerenciar estados como o histórico do chat e a entrada do usuário. O hook useEffect garante que o contêiner do chat role automaticamente para a mensagem mais recente. Quando o usuário envia uma consulta, ela é enviada para o endpoint da API de backend http://localhost:30080/chat por meio de uma solicitação POST com axios. A resposta do backend é então exibida na interface do chat.
Estilizando o Chatbot
Em seguida, crie um arquivo chamado ChatbotUI.css no diretório src. Este arquivo estilizará a interface do 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);
}
/* Typing Indicator */
.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;
}
/* Footer */
.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);
}
}
Os estilos CSS criam uma interface de chatbot inspirada na paleta de cores do Milvus. O plano de fundo usa um gradiente com tons de roxo profundo e ciano, refletindo a identidade visual do Milvus. O cabeçalho do chat segue um estilo de gradiente escuro, enquanto as mensagens do usuário são destacadas com um gradiente brilhante que combina com as cores primárias do Milvus. As mensagens do bot são estilizadas com um tema mais escuro e discreto. Este design aproveita as cores do Milvus para criar uma interface de usuário unificada e funcional.
Figura- interface de usuário concluída do sistema RAG
Figura: interface de usuário concluída do sistema RAG
É assim que a UI final se parece.
Integrando o Chatbot à Aplicação
Por fim, atualize o arquivo App.js no diretório src para renderizar o componente ChatbotUI.
import React from 'react';
import ChatbotUI from './ChatbotUI';
const App = () => {
return (
<div>
<ChatbotUI />
</div>
);
};
export default App;
Isso garante que o chatbot seja renderizado como o componente principal quando a aplicação iniciar. O componente App serve como o ponto de entrada para a aplicação React.
Testando Seu Sistema RAG
Depois de concluir a configuração do backend e do frontend, é hora de testar todo o pipeline RAG para garantir que ele funcione conforme esperado. Veja como prosseguir:
Iniciando o Backend
Abra um terminal, navegue até o diretório do projeto e inicie o servidor backend:
node server.mjs
Certifique-se de que o backend esteja em execução em http://localhost:30080. Verifique os logs para confirmar que o Milvus foi inicializado e está pronto para consultas.
Executando o Frontend
Abra um terminal separado, navegue até o diretório rag-app e inicie o frontend React:
npm start
Isso iniciará a aplicação em http://localhost:3000 no seu navegador padrão.
Testando a Interface do Chatbot
Na interface do chatbot, digite uma pergunta relacionada ao Milvus ou à Zilliz. Além disso, tente digitar perguntas fora desses tópicos para ver se o sistema responderá com a mensagem apropriada. Por fim, tente digitar perguntas que possam estar fora da base de conhecimento do Strapi. Aqui estão alguns resultados de exemplo:
Figura- resultado do sistema RAG de uma consulta relacionada ao Milvus
Figura: resultado do sistema RAG de uma consulta relacionada ao Milvus
Figura- resultados de teste do sistema RAG
Figura: resultados de teste do sistema RAG
As capturas de tela acima mostram que nosso sistema RAG está funcionando conforme esperado e exibindo os resultados corretos. Ele também está fundamentado no uso da nossa base de conhecimento, por isso não sabe quem são os fundadores do Milvus, já que essa informação não existe no Strapi.
Vá em frente e adicione mais conteúdo à base de conhecimento e teste o sistema.
Conclusão
Integrar LangChain, Milvus e Strapi para criar um sistema de Geração Aumentada por Recuperação (RAG) mostra como a IA pode entregar respostas precisas e específicas de domínio, fundamentadas em conhecimento real e atualizado. Essa arquitetura é ideal para aplicações como suporte ao cliente, gestão do conhecimento e ferramentas educacionais. Com uma compreensão clara da arquitetura e este guia passo a passo, agora você pode criar sistemas RAG adaptados às suas necessidades específicas.
Recursos Relacionados
Continue lendo

Zilliz Cloud On-Demand Compute: Pay Only for What You Use
The customer case behind Zilliz Cloud On-Demand: how a $10K vector search bill came down to under $500, and the engineering changes that made it possible.

Context Engineering Strategies for AI Agents: A Developer’s Guide
Learn practical context engineering strategies for AI agents. Explore frameworks, tools, and techniques to improve reliability, efficiency, and cost.

Vector Databases vs. Graph Databases
Use a vector database for AI-powered similarity search; use a graph database for complex relationship-based queries and network analysis.


