image

Acesse bootcamps ilimitados e +650 cursos

50
%OFF
Article image
Vinicius Fernandes
Vinicius Fernandes26/02/2025 21:31
Compartilhe

Construíndo um assistente de serviço ao consumidor com Java, Spring Boot e Spring AI

  • #Spring
  • #Java
  • #IA Generativa

Introdução

Olá pessoal,

Esse artigo tem como objetivo implementar um assistente de serviço ao consumidor para uma oficina mecânica utilizando Java, Spring Boot e Spring AI.

Para isso, iremos utilizar o processo de geração aumentada de recuperação (RAG). De forma resumida nosso objetivo é otimizar a saída de um large language model (LLM) com base em referências que iremos fornecer de acordo com o input do usuário.

Meu objetivo com esse artigo não é entrar nos detalhes de cada processo, termo ou tecnologia. Mas instigar a curiosidade de vocês demonstrando o que é possível fazer com poucas linhas de código.

O processo será algo com o seguinte fluxo:

Documento é adicionado na vector store -> Usuário envia uma mensagem -> Buscamos na vector store o documento mais relevante baseado no input do usuário -> Criamos o prompt adicionando o documento mais relevante ao contexto.

Criando o projeto

Para criar um projeto que utiliza Spring, geralmente eu utilizo o https://start.spring.io/.

No nosso caso vamos configurar da seguinte forma:

image

Na seção "Project Metadata" fique a vontade para escolher os nomes que vier na sua mente.

Com a configuração feita clique em generate e abra o projeto na sua IDE de preferência. Particularmente gosto de utilizar o IntelliJ.

Configurações iniciais

No arquivo application.properties iremos adicionar as seguintes configurações:

spring.application.name=mechanical-csm

spring.ai.openai.base-url=http://localhost:11434
spring.ai.openai.chat.options.model=mistral
spring.ai.openai.embedding.options.model=mistral
spring.ai.openai.api-key=""

Observe que aqui estou rodando um modelo localmente, para isso estou utilizando o https://ollama.com/ e como modelo https://ollama.com/library/mistral.

Nesse caso conseguimos utilizar o client da OpenAI porém precisamos indicar o modelo de embedding com a propriedade spring.ai.openai.embedding.options.model.

No nosso caso também poderíamos utilizar: https://docs.spring.io/spring-ai/reference/api/chat/ollama-chat.html.

Uma opção aqui poderia ser utilizar alguma API como as da OpenAI porém há um custo para isso: https://platform.openai.com/docs/pricing.

Para outros modelos você pode consultar a documentação : https://docs.spring.io/spring-ai/reference/api/chatmodel.html e configurar conforme desejado.

Vector Store

De forma muito simplificada, um banco de dados vetorial é um sistema especializado em armazenar e gerenciar vetores. Com esses vetores capturamos significados e características de diversos tipos de dados, por exemplo no nosso caso um texto. Com isso conseguimos fazer comparações e buscas com base na semântica.

Alguns exemplos de banco de dados vetoriais são:

  • Neo4J
  • OpenSearch
  • PGvector

Você pode consultar a documentação : https://docs.spring.io/spring-ai/reference/api/vectordbs.html para conferir mais tecnologias e como utilizar elas com o Spring.

No nosso caso, iremos simplificar as coisas e utilizar a SimpleVectorStore, você pode conferir a implementação dela aqui: https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java. Basicamente é um banco de dados vetorial em memória, uma ideia similar ao H2.

Vamos começar criando uma classe para incluir a configuração do nosso SimpleVectorStore. Você pode organizar seu projeto como quiser, no meu caso a classe foi criada em src->configuration

import org.springframework.ai.embedding.EmbeddingModel;

import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class VectorStoreConfiguration {

  @Bean
  public SimpleVectorStore simpleVectorStore(EmbeddingModel embeddingModel) {
      return SimpleVectorStore.builder(embeddingModel).build();
  }
}

Essa é toda a configuração que precisamos.

Vector store service

Esse serviço terá dois objetivos principais:

  • Adicionar documentos à vector store
  • Recuperar documentos relevantes com base no input do usuário

Segue a implementação:

import com.vinfern.mechanical_csm.models.ServiceInfo;
import jakarta.annotation.PostConstruct;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class VectorStoreService {
  @Autowired
  SimpleVectorStore vectorStore;
  @PostConstruct
  private void initiateServiceInfo(){
      var basicStoreInfo= List.of(
              new ServiceInfo("In the heart of a quaint, picturesque small town, nestled between charming cafes and antique stores, lies a\n" +
                      "bustling mechanical shop known as \"Town Mechanics.\" The shop is easily recognizable by its distinctive red and\n" +
                      "white sign adorned with an image of a gear and wrench."),
              new ServiceInfo("Upon entering the shop, customers are greeted by the pleasant smell of oil and freshly-cut metal, a testament to\n" +
                      "the skilled craftsmanship that takes place within. The walls are lined with shelves filled with an array of tools\n" +
                      "and spare parts, each carefully organized and ready for use. A large, open garage area occupies most of the space,\n" +
                      "where mechanics work tirelessly on various vehicles, from classic cars to modern SUVs."),
              new ServiceInfo("The team at Town Mechanics takes pride in their attention to detail and commitment to quality. They understand\n" +
                      "that a vehicle is not just a means of transportation but a cherished possession for many, and they treat each one\n" +
                      "with the care and respect it deserves. Customers can expect friendly service, fair prices, and expert advice on\n" +
                      "everything from routine maintenance to extensive repairs."),
              new ServiceInfo("In addition to their services, Town Mechanics also hosts workshops and classes for those interested in learning\n" +
                      "more about auto repair. Whether a seasoned mechanic or a curious beginner, there's always something new to learn\n" +
                      "at this welcoming shop. With its knowledgeable staff, state-of-the-art equipment, and warm, community-oriented\n" +
                      "atmosphere, Town Mechanics has earned its place as the go-to mechanical shop in the small town it calls home.")
      );
      this.addServiceInfo(basicStoreInfo);
  }

  public void addServiceInfo(List<ServiceInfo> serviceInfo) {
      serviceInfo.parallelStream().forEach(s->{
          Document document = new Document(s.content());
          vectorStore.add(List.of(document));
      });
  }

  public String retrieveRelevantInfo(String query) {
      List<Document> results = vectorStore.similaritySearch(query);
      return results.isEmpty() ? "No relevant info found." : results.get(0).getFormattedContent();
  }
}

O ServiceInfo é apenas um record bem simples:

public record ServiceInfo(String content) {
}

Método initiateServiceInfo

Esse método tem como objetivo inserir algumas informações no nosso banco de dados vetorial assim que a aplicação é iniciada.

Método addServiceInfo

Adiciona informações ao banco de dados vetorial, será utilizado por um endpoint especifico onde o usuário pode enviar novas informações para serem salvas.

Método retrieveRelevantInfo

Obtém o primeiro resultado mais relevante e retorna o seu valor, será utilizado posteriormente para construir o nosso prompt.

Csm Service

Esse será o serviço responsável por obter a query do usuário, construir o prompt, enviar o prompt para o modelo e retornar a resposta do modelo.

import com.vinfern.mechanical_csm.models.Query;
import com.vinfern.mechanical_csm.models.QueryAnswer;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class CsmService {
  @Autowired
  VectorStoreService vectorStoreService;
  @Autowired
  ChatModel chatModel;


  public QueryAnswer query(Query userQuery) {
      String context = vectorStoreService.retrieveRelevantInfo(userQuery.query());
      if (context.equals("No relevant info found.")) {
          context = "No relevant context available.";
      }

      String promptMessage = String.format("""
              You are a customer service assistant for a mechanical shop.
              Your responses must be based **only** on the provided context.
              If the context does not contain enough information to answer, politely state that you don't have the required details and ask for clarification if needed. Remember that you are a customer service assistant for a mechanical shop and should restrict your answer to this.
              Keep your response **concise and to the point**, with a maximum of three lines.

              **Context:** %s

              **Question:** %s
                              """, context, userQuery.query());

      Prompt prompt = new Prompt(new UserMessage(promptMessage));

      return new QueryAnswer(chatModel.call(prompt).getResult().getOutput().getText());
  }
}


O QueryAnswer é o seguinte Record:

public record QueryAnswer(String answer) {
}

E o Query é o seguinte Record:

public record Query(String query) {
}

Controller

Aqui iremos criar um Controller simples com dois endpoints, sendo um para a inserção de novas informações e outro para o envio da query:

import com.vinfern.mechanical_csm.models.Query;
import com.vinfern.mechanical_csm.models.QueryAnswer;
import com.vinfern.mechanical_csm.models.ServiceInfo;
import com.vinfern.mechanical_csm.services.CsmService;
import com.vinfern.mechanical_csm.services.VectorStoreService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/customer-service")
public class MechanicalShopCustomerServiceController {

  @Autowired
  VectorStoreService vectorStoreService;
  @Autowired
  CsmService csmService;

  @PostMapping("/services")
  @ResponseStatus(HttpStatus.CREATED)
  public void addServiceInfo(@RequestBody List<ServiceInfo> serviceInfo) {
      vectorStoreService.addServiceInfo(serviceInfo);
  }

  @PostMapping("/query")
  @ResponseStatus(HttpStatus.OK)
  public QueryAnswer query(@RequestBody Query userQuery) {
      return csmService.query(userQuery);
  }
}

Testando nosso assistente

Agora que temos todo o código pronto, basta executar o projeto.

Podemos começar adicionando informações:

curl --request POST \
--url http://localhost:8080/customer-service/services \
--header 'content-type: application/json' \
--data '[
{
"content":"Company e-mail: contact@mechanical.com"
},
  {
"content":"Company phone: +55 11 99100-0000"
},
{
  "content":"Company address:  123 Maple Avenue, Anytown, USA 12345"
}
]'

E na seguinte enviar nossa pergunta:

curl --request POST \
--url http://localhost:8080/customer-service/query \
--header 'content-type: application/json' \
--data '{
"query":"What are the main services that you offer? Where are you located?"
}
'

Conclusão

Bom pessoal, como comentei no inicio meu objetivo era apresentar uma abordagem mais prática, sem aprofundar muito nos detalhes.

Você pode encontrar o projeto completo nesse repositório: https://github.com/vinicius-fernandes/mechanical-csm.

Como sugestão para desafios baseados nesse projeto:

  • Utilizar outro modelo
  • Utilizar um vector store diferente
  • Otimizar o tempo de execução
  • Entender como o sistema se comporta com diferentes línguas, por exemplo adicionar e fazer queries em português.
  • Como controlar perguntas fora de contexto? Por exemplo o usuário envia uma query pedindo uma receita de bolo de cenoura, o que acontece nesse caso?
Compartilhe
Recomendados para você
Deal - Spring Boot e Angular (17+)
Cognizant - Arquitetura com Spring Boot e Cloud
Claro - Java com Spring Boot
Comentários (0)