Geração aumentada de recuperação com MongoDB e Spring AI: Trazendo AI para seus aplicativos Java
Avalie esse Tutorial
AI isso, AI isso. Bem, o que a AI pode realmente fazer pormim? Neste tutorial, vamos discutir como podemos aproveitar nossos próprios dados para aproveitar ao máximo a AI generativa.
E é aqui que entra ageração aumentada de recuperação (RAG). Ela usa a AI onde deve estar — recuperando as informações certas e gerando respostas inteligentes e sensíveis ao contexto. Neste tutorial, vamos construir um aplicativo RAG usando Spring Boot, MongoDB Atlase OpenAI. O código completo está disponível em Github.
RAG permite que você use dados que não estavam disponíveis para treinar um modelo de AI para preencher seu prompt e, em seguida, usar esses dados para complementar a resposta do modelo de linguagem grande (LLM).
Os LLMs são um tipo de inteligência artificial (AI) que pode gerar e compreender dados. Eles são formados em massivos conjuntos de dados e podem ser usados para responder às suas perguntas de forma informativa.
Embora os LLMs sejam muito poderosos, eles têm algumas limitações. Uma limitação é que nem sempre são precisos ou atualizados. Isso ocorre porque os LLMs são formados em dados que desde então se tornaram desatualizados, incompletos ou não têm conhecimento proprietário sobre um caso de uso ou domínio específico.
Se você tiver dados que precisam permanecer internos por motivos de segurança de dados, ou mesmo apenas perguntas sobre dados mais atualizados, o RAG pode ajudá-lo.
O RAG consiste em três componentes principais:
- Seu LLM pré-treinado: é isso que gerará a resposta - OpenAI, em nosso caso.
- Pesquisa vetorial (pesquisa semântica): é assim que recuperamos documentos relevantes de nosso banco de banco de dados MongoDB .
- Incorporações vetoriais: uma representação numérica de nossos dados captura o significado semântica de nossos dados.
Antes de iniciar este tutorial, verifique se você tem o seguinte instalado e configurado:
- Java 21 ou superior.
- Maven ou Gradle (para gerenciar dependências): Usamos o Maven para este tutorial.
- É necessário um cluster mínimo de10+ para usar o armazenamento de vetores do Spring AI MongoDB , pois ele cria o índice de pesquisa em nosso banco de dados de dados programaticamente.
- Chave de API OpenAI: inscreva-se no OpenAI e obtenha uma chave de API.
- Outros modelos estão disponíveis, mas este tutorial usa OpenAI.
Para inicializar o projeto:
- Configure os metadados do projeto :
- Grupo:
com.mongodb
- Artefato:
RagApp
- Dependencies:
- Spring Web
- Banco de dados vetorial do MongoDB Atlas
- Open AI
- Baixe o projeto e abra-o no IDE de sua preferência.
Antes de fazer qualquer coisa, Go ao nosso arquivo
pom.xml
e verificar se a versão da Spring AI é <spring-ai.version>1.0.0-SNAPSHOT</spring-ai.version>
. Talvez precisemos alterá-lo para isso, dependendo da versão do Spring que estamos usando.A configuração deste projeto envolve a configuração de dois componentes principais:
- O EmbeddingModel usando OpenAI para gerar incorporações para documentos.
- Um MongoDBAtlasVectorStore para armazenar e gerenciar vetores de documento para pesquisas de similaridade.
Precisamos configurar nosso projeto para se conectar ao OpenAI e ao MongoDB Atlas adicionando várias propriedades ao arquivo
application.properties
, junto com as credenciais necessárias.1 spring.application.name=RagApp 2 3 spring.ai.openai.api-key=<Your-API-Key> 4 spring.ai.openai.chat.options.model=gpt-4o 5 6 spring.ai.vectorstore.mongodb.initialize-schema=true 7 8 spring.data.mongodb.uri=<Your-Connection-URI> 9 spring.data.mongodb.database=rag
Você verá que aqui temos
initialize.schema
definido como True
. Isso cria automaticamente o índice em nossa coleção, usando o Spring AI. Se você estiver executando um cluster gratuito, isto não estará disponível. Uma solução alternativa para isso é criá-lo manualmente, o que você pode aprender a fazer na documentação do MongoDB.Crie um pacote de configuração e adicione um
Config.java
para trabalhar. Veja como a configuração é feita na classeConfig
:1 import org.springframework.ai.embedding.EmbeddingModel; 2 import org.springframework.ai.openai.OpenAiEmbeddingModel; 3 import org.springframework.ai.openai.api.OpenAiApi; 4 import org.springframework.ai.vectorstore.MongoDBAtlasVectorStore; 5 import org.springframework.ai.vectorstore.VectorStore; 6 import org.springframework.beans.factory.annotation.Value; 7 import org.springframework.context.annotation.Bean; 8 import org.springframework.context.annotation.Configuration; 9 import org.springframework.data.mongodb.core.MongoTemplate; 10 11 12 public class Config { 13 14 15 private String openAiKey; 16 17 18 public EmbeddingModel embeddingModel() { 19 return new OpenAiEmbeddingModel(new OpenAiApi(openAiKey)); 20 } 21 22 23 public VectorStore mongodbVectorStore(MongoTemplate mongoTemplate, EmbeddingModel embeddingModel) { 24 return new MongoDBAtlasVectorStore(mongoTemplate, embeddingModel, 25 MongoDBAtlasVectorStore.MongoDBVectorStoreConfig.builder().build(), true); 26 } 27 28 }
Essa classe inicializa a conexão com a API OpenAI e configura o armazenamento de vetores baseado no MongoDB para armazenar incorporações de documento .
Para este tutorial, estamos usando o conjunto de dadosMongoDB/devcenter-argumentos, disponível em Abraçar a Face. Este conjunto de dados consiste em artigos do MongoDB Developer Center. Em nossos recursos, crie um diretório chamado Docs e adicione nosso arquivo para ler.
Para incorporar e armazenar dados no armazenamento de vetores, usaremos um serviço que lê documentos de um arquivo JSON, os converte em incorporações e os armazena no armazenamento de vetores do MongoDB Atlas . Isso é feito usando o
DocsLoaderService.java
que criaremos em um pacoteservice
:1 package com.mongodb.RagApp.service; 2 3 import com.fasterxml.jackson.databind.ObjectMapper; 4 import org.springframework.ai.document.Document; 5 import org.springframework.ai.vectorstore.VectorStore; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.core.io.ClassPathResource; 8 import org.springframework.stereotype.Service; 9 10 import java.io.BufferedReader; 11 import java.io.InputStream; 12 import java.io.InputStreamReader; 13 import java.util.ArrayList; 14 import java.util.List; 15 import java.util.Map; 16 17 18 public class DocsLoaderService { 19 20 private static final int MAX_TOKENS_PER_CHUNK = 2000; 21 private final VectorStore vectorStore; 22 private final ObjectMapper objectMapper; 23 24 25 public DocsLoaderService(VectorStore vectorStore, ObjectMapper objectMapper) { 26 this.vectorStore = vectorStore; 27 this.objectMapper = objectMapper; 28 } 29 30 public String loadDocs() { 31 try (InputStream inputStream = new ClassPathResource("docs/devcenter-content-snapshot.2024-05-20.json").getInputStream(); 32 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { 33 34 List<Document> documents = new ArrayList<>(); 35 String line; 36 37 while ((line = reader.readLine()) != null) { 38 Map<String, Object> jsonDoc = objectMapper.readValue(line, Map.class); 39 String content = (String) jsonDoc.get("body"); 40 41 // Split the content into smaller chunks if it exceeds the token limit 42 List<String> chunks = splitIntoChunks(content, MAX_TOKENS_PER_CHUNK); 43 44 // Create a Document for each chunk and add it to the list 45 for (String chunk : chunks) { 46 Document document = createDocument(jsonDoc, chunk); 47 documents.add(document); 48 } 49 // Add documents in batches to avoid memory overload 50 if (documents.size() >= 100) { 51 vectorStore.add(documents); 52 documents.clear(); 53 } 54 } 55 if (!documents.isEmpty()) { 56 vectorStore.add(documents); 57 } 58 59 return "All documents added successfully!"; 60 } catch (Exception e) { 61 return "An error occurred while adding documents: " + e.getMessage(); 62 } 63 } 64 65 private Document createDocument(Map<String, Object> jsonMap, String content) { 66 Map<String, Object> metadata = (Map<String, Object>) jsonMap.get("metadata"); 67 68 metadata.putIfAbsent("sourceName", jsonMap.get("sourceName")); 69 metadata.putIfAbsent("url", jsonMap.get("url")); 70 metadata.putIfAbsent("action", jsonMap.get("action")); 71 metadata.putIfAbsent("format", jsonMap.get("format")); 72 metadata.putIfAbsent("updated", jsonMap.get("updated")); 73 74 return new Document(content, metadata); 75 } 76 77 private List<String> splitIntoChunks(String content, int maxTokens) { 78 List<String> chunks = new ArrayList<>(); 79 String[] words = content.split("\\s+"); 80 StringBuilder chunk = new StringBuilder(); 81 int tokenCount = 0; 82 83 for (String word : words) { 84 // Estimate token count for the word (approximated by character length for simplicity) 85 int wordTokens = word.length() / 4; // Rough estimate: 1 token = ~4 characters 86 if (tokenCount + wordTokens > maxTokens) { 87 chunks.add(chunk.toString()); 88 chunk.setLength(0); // Clear the buffer 89 tokenCount = 0; 90 } 91 chunk.append(word).append(" "); 92 tokenCount += wordTokens; 93 } 94 if (chunk.length() > 0) { 95 chunks.add(chunk.toString()); 96 } 97 return chunks; 98 } 99 }
Esse serviço lê um arquivo JSON, processa cada documento e o armazena no MongoDB, junto com um vetor incorporado de nosso conteúdo.
Agora, essa é uma abordagem muito simplista de chunking (divisão de documentos grandes em partes menores que permanecem dentro do limite de token e os processam separadamente) implementada. Isso ocorre porque o Go tem um limite de token, então alguns de nossos documentos são grandes demais para serem incorporados de uma só vez. Isso é bom para testes, mas se você estiver mudando para a produção, faça sua pesquisa e decida sua melhor maneira de lidar com esses documentos grandes.
Chame esse método como quiser, mas criei um
DocsLoaderController
simples no meu pacotecontroller
para teste.1 import com.mongodb.RagApp.service.DocsLoaderService; 2 import org.springframework.web.bind.annotation.GetMapping; 3 import org.springframework.web.bind.annotation.RequestMapping; 4 import org.springframework.web.bind.annotation.RestController; 5 6 7 8 public class DocsLoaderController { 9 10 private DocsLoaderService docsLoaderService; 11 12 public DocsLoaderController(DocsLoaderService docsLoaderService) { 13 this.docsLoaderService = docsLoaderService; 14 } 15 16 17 public String loadDocuments() { 18 return docsLoaderService.loadDocs(); 19 } 20 21 }
Depois que os dados forem incorporados e armazenados, podemos recuperá-los por meio de uma API que usa uma pesquisa vetorial para retornar resultados relevantes. A classe
RagController
é responsável por isso:1 import org.springframework.ai.chat.client.ChatClient; 2 import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; 3 import org.springframework.ai.vectorstore.SearchRequest; 4 import org.springframework.ai.vectorstore.VectorStore; 5 import org.springframework.web.bind.annotation.CrossOrigin; 6 import org.springframework.web.bind.annotation.GetMapping; 7 import org.springframework.web.bind.annotation.RequestParam; 8 import org.springframework.web.bind.annotation.RestController; 9 10 11 public class RagController { 12 13 private final ChatClient chatClient; 14 15 public RagController(ChatClient.Builder builder, VectorStore vectorStore) { 16 this.chatClient = builder 17 .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults())) 18 .build(); 19 } 20 21 22 public String question( String message) { 23 return chatClient.prompt() 24 .user(message) 25 .call() 26 .content(); 27 } 28 }
Está passando um pouco aqui. Vejamos o
ChatClient
. Ele oferece uma API para comunicação com nosso modelo de AI .O modelo de AI processa dois tipos de mensagens: 1. Mensagens do usuário, que são entradas diretas do usuário. 2. Mensagens do sistema, que são geradas pelo sistema para orientar a conversa.
Para a mensagem do sistema, estamos usando o padrão do
QuestionsAnswerAdvisor
:1 private static final String DEFAULT_USER_TEXT_ADVISE = """ 2 Context information is below. 3 --------------------- 4 {question_answer_context} 5 --------------------- 6 Given the context and provided history information and not prior knowledge, 7 reply to the user comment. If the answer is not in the context, inform 8 the user that you can't answer the question. 9 """;
Mas podemos editar esta mensagem e adaptá-la às nossas necessidades. Também há opções de prompt que podem ser especificadas, como a configuração de temperatura que controla a aleatoriedade ou a Criatividade da saída gerada. Você pode descobrir mais na documentação da Spring.
O endpoint
/question
permite que os usuários façam perguntas e recupera respostas do armazenamento de vetores pesquisando nos documentos incorporados semanticamente e enviando-os para o LLM com nosso contexto.Para testar nossa implementação:
- Inicie o aplicação Spring Boot .
- Navegue até
http://localhost:8080/api/docs/load
para carregar documentos no armazenamento de vetores. - Use
http://localhost:8080/question?message=Your question here
para testar a funcionalidade de pergunta-resposta.
Por exemplo, tente perguntar:
http://localhost:8080/question?message=How to analyze time-series data with Python and MongoDB?Explain the steps
Devemos receber uma resposta relevante do aplicativo RAG, formada a partir dos dados do documento incorporado e do LLM.
Neste projeto, integramos um geração aumentada de recuperação (RAG) usando MongoDB, incorporações OpenAI e Spring Boot. O sistema pode incorporar grandes quantidades de dados de documento e responder a perguntas, aproveitando pesquisas de similaridade vetorial de um armazenamento vetorial do MongoDB Atlas .
Em seguida, saiba mais sobre o que você pode fazer com Java e MongoDB. Você pode aproveitar o Armazenamento de mídia contínua: Integrando o Armazenamento de Blobs do Azure e o MongoDB com o Spring Boot. Ou acesse os fóruns da comunidade e veja o que outras pessoas estão fazendo com o MongoDB.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.