Melhorando as respostas de um LLM: RAG de vídeo do Fábio Akita
Tags: ai, rag, vectordatabase, semanticsearch
Published: 2024-03-21 07:32:01

#### Uma introdução a Retrieval-Augmented Generation com Pinecone

## O que é RAG?

A geração aumentada de recuperação (_Retrieval-Augmented Generation_) é "_uma maneira de otimizar o resultado de um LLM com informações direcionadas sem modificar o próprio modelo subjacente; as informações direcionadas podem ser mais atualizadas do que o LLM, bem como serem específicas para uma determinada organização e setor. Isso significa que o sistema de IA generativa pode fornecer respostas mais contextualmente apropriadas às solicitações, bem como basear essas respostas em dados extremamente atuais._"

Fonte: [O que é geração aumentada de recuperação](https://www.oracle.com/br/artificial-intelligence/generative-ai/retrieval-augmented-generation-rag/)
## Como funciona

De forma simplificada, para implementar RAG, uma base de conhecimento composta por textos, documentos, etc, será segmentada em pedaços (`chunks`) e cada segmento será transformado em um _embedding_. Embeddings são representações numéricas (um vetor) de uma informação qualquer que serão armazenados em um banco de dados vetorial como o [Pinecone](https://www.pinecone.io/).

A vantagem de utilizar vetores é que as pesquisas em um banco de dados vetoriais serão feitas por similaridade semântica, calculando-se quais são os trechos mais similares à query pesquisada. Estes trechos mais relevantes serão, então, passados como contexto para um grande modelo de linguagem (LLM) junto à pergunta feita pelo usuário.

## Iniciando com RAG

A idéia desse artigo é documentar o processo que fiz para experimentar pela primeira vez o uso de RAG. É apenas uma introdução prática à essa técnica e encorajo a todas as pessoas interessadas no assunto que pesquisem mais a respeito para que possam se aprofundar e entender melhor as nuâncias envolvidas em cada etapa do processo.

Este teste de conceito foi criado adaptando o [exemplo de Semantic Search](https://github.com/pinecone-io/semantic-search-example) disponibilizado pela equipe do Pinecone.

Criei um fork ([https://github.com/igorMSoares/semantic-search-example](https://github.com/igorMSoares/semantic-search-example)) adaptando o exemplo do Pinecone. Para rodar os exemplos mostrados aqui, clone o repositório, e acompanhe as instruções descritas no README para configurar e rodar o projeto.

Para compor a base de conhecimentos utilizei o [roteiro do vídeo do Fábio Akita](https://www.akitaonrails.com/2023/12/16/akitando-149-configurando-docker-compose-postgres-com-testes-de-carga-parte-final-da-rinha-de-backend) `"Configurando Docker Compose, Postgres, com Testes de Carga - Parte Final da Rinha de Backend"`

## Tecnologias utilizadas

- [Pinecone DB](https://www.pinecone.io/)
- [all-MiniLM-L6-v2 sentence-transformer](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)
- [Chat-GPT-3.5](https://chat.openai.com/)

## Gerando a base de conhecimentos

### Semantic Chunking

Foi feito o _Semantic Chunking_ do roteiro do vídeo utilizando o Chat-GPT.
Para isso o seguinte prompt foi utilizado:

> Nos próximos prompts irei passar, por partes, o roteiro de um vídeo do Fábio Akita.
>
> Para cada prompt que eu passar a seguir, use técnicas de processamento de linguagem natural para identificar limites semânticos lógicos para segmentar o texto (Semantic Chunking).
>
> Cada segmento deve ser identificado por um título que represente corretamente o conteúdo do segmento.
>
> Além do título, o conteúdo do segmento deve ser apresentado como uma listagem com APENAS UM nível de profundidade.
>
> EM HIPOTÉSE ALGUMA utilize sublistas no conteúdo do segmento. A sua resposta deverá ser no formato markdown, de forma que o título identificador do conteúdo seja precedido por "### " e o conteúdo referente a este título deverá ser uma lista em que cada item será precedido por "- ".
>
> Exemplo do formato de resposta:
>
> ### Título desta seção
>
> - Primeiro ítem do conteúdo da seção
> - Segundo ítem do conteúdo da seção
>
> ### Título de outra seção
>
> - Primeiro ítem do conteúdo da outra seção
> - Segundo ítem do conteúdo da outra seção
>
> Confirme que entendeu as instruções e, a partir da próxima mensagem que eu enviar, já estarei passando o primeiro trecho do roteiro do vídeo.

### Estruturando os dados obtidos

A partir do markdown obtido pelo Semantic Chunking, será gerado um CSV que será utilizado para criar os _embeddings_ que serão armazenados no _Pinecone_.

#### Observação

A abordagem mais utilizada seria salvar os trechos do roteiro do vídeo em um banco de dados de documentos, como MongoDB, e os embeddings gerados na etapa anterior deveriam conter o id do trecho à qual se referem. Desta forma, os chunks vetorizados armazenados no _Pinecone_ poderão ser relacionados ao trecho original do roteiro armazenado no _MongoDB_.

Para simplificar este teste de conceito, não armazenei os trechos originais do roteiro e, portanto, os chunks não possuem referência ao texto original. Além disso, os _chunks_ foram armazenados no próprio Pinecone como metadados dos _embeddings_.

Dessa forma, ao invés de enviar os trechos originais como contexto para a resposta das perguntas, o contexto passado será composto apenas pelos próprios chunks gerados pelo Chat-GPT e armazenados no _Pinecone_.

### Criando os embeddings

O [exemplo](https://github.com/pinecone-io/semantic-search-example) apresentado pela equipe do Pinecone define uma classe `Embedder` que é responsável por:

- Carregar o arquivo estruturado dos chunks da base de conhecimento (`semantic-chunks.csv`)
- Conectar à instância do Pinecone
- Gerar os embeddings dos chunks utilizando o modelo [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)
- Salvar no Pinecone DB os embeddings gerados

_Para mais informações sobre o setup do projeto, conferir o [README](https://github.com/igorMSoares/semantic-search-example/blob/main/README.md) no repositório deste exemplo._

Para criar os embeddings a partir do CSV com os dados pré-processados e salvá-las no Pinecone, utilizar o seguinte comando, no diretório raiz do projeto clonado:

```bash
npm start -- load --csvPath=semantic-chunks.csv --column=CHUNK
```

#### Estrutura do CSV utilizado

Possui uma única coluna (CHUNK) formatada como `Title:"título da seção",Content:"item-1",...,"intem-N"|` e o caracter `|` é o delimitador de coluna.

```csv
CHUNK
Title:"Entendendo o HTTP e a Importância da Troca de Mensagens",Content:"Introdução ao HTTP","Relevância da troca de mensagens em formato texto","Ferramentas como Curl e Wget para navegação de linha de comando"|
Title:"Importância do Conhecimento Básico de HTTP",Content:"Necessidade de entender como enviar e receber mensagens HTTP","Essencial para compreensão da web e desenvolvimento web","Implicações para entender APIs e problemas de segurança"|
Title:"Introdução ao Gatling",Content:"Descrição do Gatling como ferramenta de teste de carga","Patrocínio da ferramenta pela rinha","Linguagens suportadas para scripts: Scala ou Kotlin"|
```

### Pesquisando no Pinecone DB

Quando o usuário faz uma pergunta à IA, iremos fazer o embedding da pergunta e pesquisar no Pinecone por chunks que sejam semanticamente similares à pergunta.
E os chunks retornados serão passados como contexto para o Chat-GPT.

No nosso exemplo, a pesquisa por similaridade é feita utilizando o seguinte comando:

```bash
npm start -- query --query="Como configurar a rede do docker?" --topK=5
```

- `npm start -- query --query="" --topK=n`: executa a função que irá fazer a busca no espaço vetorial, retornando os _n_ resultados mais relevantes.

_Adaptei a função `query` para salvar o resultado do match no arquivo `out.json`_

Em seguida, utilizo o comando `jq` para retornar apenas a chave `text` do json retornado pela query e o resultado desse comando copio para o clipboard utilizando `xclip`:

```bash
jq --raw-output '.[].text' out.json | xclip -selection clipboard
```

E o resultado dessa última operação é o que será passado como contexto para o Chat-GPT:

```txt
Title:"Ajustes no Docker Compose e Nginx: Modificações no Docker Compose",Content:"Atualização dos serviços para utilizar network mode host","Ajuste das portas de comunicação para evitar conflitos"
Title:"Configuração do PostgreSQL no Docker Compose: Bulk Insert e Upserts",Content:"Estratégias importantes para operações eficientes de inserção em massa de dados","Reduzem o tempo e os recursos necessários para inserir grandes volumes de dados de uma só vez","Cada banco de dados tem suas próprias peculiaridades de sintaxe para essas operações"
Title:"Tentativa de Melhoria: Alteração para Network Mode Host",Content:"Modificação no código para utilizar network mode host no Docker Compose","Ajuste da porta de comunicação para evitar conflitos","Atualização da URL de conexão com o banco de dados para localhost"
Title:"Ajustes no Docker Compose e Nginx: Configuração do Nginx",Content:"Alteração dos servidores upstream para utilizar localhost","Configuração das portas para os serviços nas portas 8080 e 8081"
Title:"Configuração do PostgreSQL no Docker Compose: Métodos de Configuração",Content:"Alterar diretamente a linha de comando do PostgreSQL no Docker Compose, adicionando o parâmetro `max_connections`","Utilizar um arquivo `postgres.conf` mapeado como volume para dentro do container PostgreSQL","Configurar o número máximo de conexões na aplicação ao conectar pela primeira vez, enviando comandos SQL"
```

Como nessa demonstração estou usando como contexto os chunks gerados pelo GPT, e não o trecho original do roteiro, percebi que a qualidade das respostas melhorava ao aumentar a quantidade de chunks passados no contexto.

 Nessa etapa é válido experimentar diferentes abordagens para compor o contexto como, por exemplo, retornar apenas os 2 chunks mais relevantes e, além dos chunks, incluir no contexto todo o parágrafo do trecho original do roteiro referenciado pelo chunk e então comparar e analisar a qualidade das respostas.

No meu caso, ao invés de rodar a query com `--topK=2` utilizei `--topK=10` e passo as 10 chunks retornadas como contexto para o GPT.
## Interagindo com o Chat-GPT

Por fim, para obter respostas acerca do vídeo do Fábio Akita, passei o seguinte prompt para o Chat-GPT:

> Nos próximos prompts, irei fazer perguntas sobre um vídeo do Fábio Akita.
> Informações relevantes que você deverá OBRIGATORIAMENTE utilizar para compor as respostas serão informadas no começo de cada prompt, antes da pergunta em questão.
>
> Os prompts terão o seguinte formato:
>
> Title:\<titulo da seção que categoriza o respectivo conteúdo\>,Content:\<uma lista de conteúdos relevantes associados a essa seção\>
> Title:\<titulo de outra seção que categoriza o respectivo conteúdo\>,Content:\<uma lista de conteúdos relevantes referentes a essa seção\>
>
> \<Aqui é a pergunta que deverá ser respondida utilizando o contexto passado acima.\>
>
> Responda de maneira natural, objetiva e sucinta, utilizando como referência informações contidas nos campos "Title" e também nos campos "Content".
>
> EVITE enumerar as respostas como uma listagem de tópicos.
>
> Confirme que entendeu as instruções e, a partir do meu próximo prompt, irei enviar o conjunto de informações relevante para a resposta e a respectiva pergunta.

Este prompt está bem simplificado e com certeza poderia ser melhor elaborado mas, ainda assim, obtive respostas bem relevantes para as perguntas que fiz sobre os assuntos abordados no vídeo. Por exemplo, pode ser do seu interesse instruir o modelo de linguagem a informar que não possui informação suficiente para responder caso entenda que os dados passados no contexto não são relevantes o suficiente para responder a pergunta.

### Exemplos

- Antes de cada pergunta, mas ainda no mesmo prompt, envio os chunks de contexto retornados pelo banco de dados vetorial referentes à pergunta em questão


![Resposta do chat-gpt sobre banco de dados](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x0nhux44re9reh8vcw16.png)

- Aqui fiz uma pergunta sobre algo que não é falado no vídeo e achei que a resposta dada foi bem assertiva, pois deixa claro que essa informação não está presente no vídeo mas complementa com informações que foram sim comentadas na fonte em questão.


![Resposta do chat-gpt sobre a rinha de backend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nv8cmbbi8vjik2x1puot.png)

## Conclusão

Este foi apenas um experimento básico feito com o objetivo de testar **RAG** e **vector embeddings** na prática.

Os passos descritos aqui não tem pretensão alguma de serem utilizados em casos práticos reais, servindo apenas como uma forma de aplicar os conceitos e testar como seriam as respostas do Chat-GPT sobre um assunto que normalmente ele não teria conhecimento (no caso, o [vídeo do Fábio Akita](https://youtu.be/-yGHG3pnHLg?si=2_v0iPwUwiGzwwWu) sobre a rinha de backend)

Com este teste de conceito pude perceber de forma prática o poder e o potencial do uso de RAG (Retrieval-Augmented Generation) para o refinamento das interações com LLMs como o Chat-GPT e agora me sinto melhor preparado para me aprofundar no assunto a fim de implementar soluções reais com o uso dessa abordagem.

Ficou evidente para mim que para o RAG ser eficiente é totalmente fundamental que as etapas de construção da base de conhecimentos sejam muito bem pensadas e executadas. A qualidade e a assertividade das respostas dadas pela IA estão diretamente relacionadas à uma melhor seleção e categorização dos dados utilizados para a criação dos embeddings. Além disso, as respostas podem ser refinadas ainda mais utilizando técnicas mais elaboradas para a obtenção dos dados que serão passados como contexto para a formulação da resposta pela IA.