LLM precisa de evidência para responder corretamente: RAG explicado com exemplos
1. Visão geral
Por mais poderoso que seja um LLM, ele não sabe automaticamente o conteúdo dos documentos internos da sua empresa — ele só sabe o que foi colocado na entrada. Este artigo explica o mecanismo de Retrieval-Augmented Generation (RAG): por que você precisa dele, como a retrieval funciona, e por que a qualidade da resposta depende quase inteiramente do passo de busca antes de gerar o texto. Ao terminar de ler, você entenderá claramente o fluxo de dados em um sistema RAG e conseguirá escrever um pipeline de retrieval simples em Java por conta própria.
2. O problema: LLM não tem “memória” dos dados privados
O modelo foi treinado em um conjunto de dados público, congelado em um determinado momento. Ele não sabe sobre a nova política de reembolso que sua empresa lançou na semana passada, e muito menos sobre o conteúdo de algum contrato interno.
Existem duas maneiras de introduzir informações privadas:
- Inserir direto no prompt — só é viável quando o documento é pequeno e cabe na janela de contexto.
- Recuperar (retrieval) — quando o conjunto de documentos é grande, a aplicação seleciona apenas alguns trechos mais relevantes e os coloca no prompt.
// Abordagem 1: inserir todo o documento — funciona apenas com texto curto
String prompt = """
Baseado na seguinte política, responda à pergunta do cliente.
Política: %s
Pergunta: %s
""".formatted(fullPolicyText, userQuestion);
Esta abordagem é simples, mas não escala: a janela de contexto tem um limite de tokens, e quanto mais texto não relacionado você inserir, mais fácil o modelo se “perder” (lost in the middle). Quando o conjunto de documentos tem milhares de páginas, você é forçado a usar retrieval.
3. O que é RAG e como funciona
RAG (Retrieval-Augmented Generation) separa o problema em dois estágios independentes:
- Retrieval: encontrar os trechos de texto corretos relacionados à pergunta.
- Generation: o LLM usa esses trechos para gerar a resposta, com citações de fontes.
Pergunta do usuário
│
▼
[Embedding model] ── converte a pergunta em um vetor
│
▼
[Vector database] ── encontra os trechos de texto semanticamente mais próximos
│
▼
[Trechos originais] ── coloca o texto original no prompt
│
▼
[LLM] ── gera a resposta + citações de fontes
O ponto crucial: o LLM não “consulta” diretamente o banco de dados vetorial. Ele apenas recebe texto simples (plain text) fornecido pela aplicação após a retrieval estar completa. O banco de dados vetorial é apenas uma ferramenta para auxiliar a busca, não é um local onde o modelo lê “conhecimento”.
3.1. Embedding — convertendo pergunta em números
O modelo de embedding converte um trecho de texto em um vetor de números reais (por exemplo, 1536 dimensões). Dois trechos de texto com significado semelhante produzirão vetores próximos um do outro nesse espaço.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
@Service
public class PolicyEmbeddingService {
private final EmbeddingModel embeddingModel;
public PolicyEmbeddingService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
public float[] embed(String text) {
EmbeddingResponse response = embeddingModel.embedForResponse(List.of(text));
return response.getResult().getOutput();
}
}
embed() recebe um trecho de texto (por exemplo, o conteúdo da pergunta “Qual é a política de cancelamento de hotel?”) e retorna um array float[] — um vetor que representa a semântica dessa frase. Este é exatamente o valor que será comparado com os vetores já armazenados no banco de dados vetorial.
3.2. Banco de dados vetorial — encontrando trechos de texto semanticamente próximos
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
@Service
public class PolicyRetrievalService {
private final VectorStore vectorStore;
public PolicyRetrievalService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void indexPolicy(String policyId, String content) {
Document document = new Document(content, Map.of("policyId", policyId));
vectorStore.add(List.of(document));
}
public List<Document> search(String question) {
SearchRequest request = SearchRequest.query(question).withTopK(3);
return vectorStore.similaritySearch(request);
}
}
indexPolicy()salva um trecho de política no banco de dados vetorial, com metadados (policyId) para você saber depois qual documento esse trecho de texto pertence.search()recebe a pergunta do usuário, faz o embedding automaticamente internamente e encontra os 3 trechos semanticamente mais próximos (topK(3)).
Resultado de search("Qual é a política de cancelamento de hotel?"):
1. "Os hóspedes podem cancelar o quarto gratuitamente até 24 horas antes..."
2. "Casos de reserva sem reembolso (non-refundable)..."
3. "Taxa de cancelamento aplicável a reservas em grupo com 5 ou mais pessoas..."
3.3. Geração — LLM respondendo com base em evidências
@Service
public class PolicyAnswerService {
private final ChatClient chatClient;
private final PolicyRetrievalService retrievalService;
public PolicyAnswerService(ChatClient.Builder builder,
PolicyRetrievalService retrievalService) {
this.chatClient = builder.build();
this.retrievalService = retrievalService;
}
public String answer(String question) {
List<Document> passages = retrievalService.search(question);
String context = passages.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n---\n"));
String prompt = """
Responda baseado apenas nos trechos de texto a seguir. Se não houver informação suficiente, deixe claro que não sabe.
Trechos de texto:
%s
Pergunta: %s
""".formatted(context, question);
return chatClient.prompt(prompt).call().content();
}
}
Note o comando “Responda baseado apenas nos trechos de texto a seguir” — esta é uma restrição importante para reduzir alucinações: o modelo é solicitado a se ater à evidência em vez de inventar a partir de seu conhecimento de treinamento.
Nota: Se você não restringir claramente a “apenas com base no contexto”, o modelo ainda pode misturar seu conhecimento de fundo com o conteúdo recuperado, resultando em respostas que parecem lógicas, mas divergem do documento real.
4. Por que retrieval ruim resulta em respostas ruins
Esta é a parte frequentemente negligenciada: o LLM é bom em interpretar o que recebe, mas não tem uma forma de verificar por si próprio se a informação está correta ou completa.
Duas situações comuns:
Situação 1 — Retrieval obtém documentos desatualizados. Se o banco de dados vetorial ainda tem indexada uma versão antiga da política (ainda não foi atualizada/removida), o sistema retornará o trecho de texto antigo. O LLM lê esse trecho e responde com total confiança — mas é uma confiança errada.
Situação 2 — Retrieval perde contexto importante. Suponha que o documento original tenha a seguinte estrutura:
Artigo 5. Política de Cancelamento de Quarto
Os hóspedes podem cancelar o quarto gratuitamente até 24 horas antes do check-in.
Artigo 5.1. Exceções
Para reservas feitas durante o feriado de Ano Novo, o cancelamento gratuito não se aplica;
a taxa de cancelamento fixa é 50% do valor da reserva.
Se o passo de divisão de documentos (chunking) separar o “Artigo 5” e o “Artigo 5.1” em dois chunks independentes, e a retrieval conseguir apenas o chunk “Artigo 5” (porque a pergunta não contém as palavras “Ano Novo” ou “exceção”), o LLM responderá “pode cancelar gratuitamente até 24 horas antes” — parece muito lógico, mas perde a importante condição de exceção.
@Test
void quandoFaltaChunkExcecao_LLMPerde() {
// Simula retrieval retornando apenas 1 chunk, faltando a seção de exceção
List<Document> incompletePassages = List.of(
new Document("Os hóspedes podem cancelar o quarto gratuitamente até 24 horas antes do check-in.")
);
String answer = fakeAnswerService.answerFromPassages(
"Eu reservei no feriado de Ano Novo, cancelar tem taxa?",
incompletePassages
);
// A resposta será incorreta porque falta a evidência sobre a exceção do feriado
assertThat(answer).doesNotContain("50%");
}
Este teste não visa pegar um erro do LLM — ele ilustra que o problema está na retrieval, não na capacidade de raciocínio do modelo. Se você fornecer evidência suficiente (ambos os chunks), a resposta estará correta mesmo com um modelo de médio porte.
5. Chunking: a chave para retrieval no “nível de detalhe certo”
Como o exemplo acima mostra as consequências de um chunking inadequado, este é o lugar onde você deve investir mais ao construir RAG.
@Service
public class PolicyChunkingService {
public List<String> splitByHeading(String document) {
// Divide por padrão "Artigo X" ou "Artigo X.Y" para manter o contexto pai-filho intacto
String[] rawChunks = document.split("(?=Artigo \\d+)");
return Arrays.stream(rawChunks)
.map(String::trim)
.filter(chunk -> !chunk.isBlank())
.toList();
}
}
Em vez de dividir por um número fixo de caracteres (por exemplo, cortar a cada 500 caracteres — fácil de cortar no meio entre um artigo e sua exceção), esta função divide por limites semânticos (“Artigo X”). Essa abordagem ajuda cada chunk a manter uma unidade de informação completa, reduzindo o risco de separar uma regra de suas exceções.
@Test
void splitByHeading_DeveIncluirArteMaioExcecoes() {
String document = """
Artigo 5. Política de Cancelamento
Os hóspedes podem cancelar gratuitamente até 24 horas antes.
Artigo 5.1. Exceções
Durante o feriado de Ano Novo, a taxa de cancelamento é fixa em 50%.
""";
List<String> chunks = chunkingService.splitByHeading(document);
assertThat(chunks).hasSize(2);
assertThat(chunks.get(1)).contains("Exceções", "50%");
}
Nota: Chunking por heading é eficaz apenas quando o documento tem uma estrutura clara (contratos, regulamentos, documentação técnica). Para texto livre (emails, chat logs), você precisa de uma estratégia diferente — por exemplo, dividir por parágrafos com sobreposição, ou usar semantic chunking baseado em similaridade entre frases consecutivas.
6. Quando NÃO usar RAG
RAG não é uma solução universal:
- Conjunto de dados pequeno e fixo: se toda a documentação tem apenas alguns milhares de tokens, inserir diretamente no prompt é mais simples e com menos erros do que construir um pipeline de retrieval inteiro.
- Perguntas que requerem raciocínio complexo em múltiplas etapas onde a resposta não está diretamente em um único trecho — RAG encontra “trechos similares à pergunta”, não “trechos que contêm a resposta inferida”.
- Dados que mudam continuamente a cada segundo (preços de ações, estoque em tempo real): é melhor chamar a API diretamente (function calling) em vez de reindexar continuamente o banco de dados vetorial.
7. Conclusão
O LLM não sabe seus dados privados — ele é bom em interpretar a evidência fornecida. RAG resolve esse problema separando a retrieval (encontrar os trechos de texto corretos) da geração (produzir a resposta), com embedding models e bancos de dados vetoriais como ponte. A qualidade da resposta final depende quase inteiramente de como você indexa e divide seus dados — um LLM poderoso não consegue salvar uma retrieval ruim. Se você está construindo RAG, dedique tempo a verificar seu chunking e a proporção de retrieval correta/incorreta antes de considerar trocar para um modelo maior.