RAG


Primeiro, o nome. RAG é a sigla de retrieval-augmented generation (geração aumentada por recuperação): você mesmo busca o texto relevante e entrega ao modelo no momento da pergunta, para que ele responda com base em fatos que você forneceu, em vez de com base numa memória congelada. Este capítulo mostra como funciona esse ciclo de buscar-e-depois-responder e onde ele pode falhar.
O padrão de recuperar e depois gerar
Um modelo não conhece os documentos de ajuda da sua empresa, os detalhes do seu produto, ou qualquer coisa que tenha acontecido depois do seu treinamento. Pergunte sobre isso e ele vai admitir que não pode ajudar ou, pior, inventar algo que parece verdadeiro. O RAG resolve isso entregando ao modelo o texto relevante no momento da pergunta.
A ideia se encaixa a partir do momento em que você entende embeddings, o truque que transforma texto em vetores que você pode comparar por proximidade (a similaridade de cosseno mede o quão próximos dois vetores apontam). A geração aumentada por recuperação consiste em duas etapas acopladas a uma chamada normal ao modelo:
- Recuperar. Pegue a pergunta do usuário, busque nos seus documentos as partes mais relevantes e reúna as melhores correspondências.
- Gerar. Coloque essas partes no prompt como contexto e peça ao modelo para responder à pergunta usando apenas esse contexto.
A capacidade linguística geral do modelo é quem escreve; o texto que você recuperou fornece os fatos. Você obtém respostas fluentes que estão fundamentadas no seu conteúdo e que podem se manter atualizadas, porque você controla o que entra ali.
Ajuda entender por que isso funciona tão bem, em termos do ciclo de previsão. Sem RAG, você está pedindo ao modelo que se lembre de um fato a partir dos padrões congelados em seus parâmetros — fato que ele pode nem ter —, o que leva a um palpite confiante. Com RAG, o fato está ali, bem à mão, no contexto, então a tarefa do modelo muda de "lembre-se disso" para "leia o que está na sua frente e responda com base nisso". A recuperação transfere o trabalho da memória para a leitura. A imagem mais clara é a de uma prova: uma prova sem consulta convida ao palpite, enquanto o RAG a transforma numa prova com consulta, com a página relevante já aberta sobre a mesa.
Chunking
Você não transforma documentos inteiros em embeddings. Primeiro você os divide em pedaços menores, os chunks, cada um com um parágrafo ou algumas frases, e transforma esses pedaços em embeddings. Duas razões. Primeiro, a recuperação fica mais precisa: você pega o único parágrafo relevante em vez de um manual inteiro de 40 páginas. Segundo, os chunks cabem na janela de contexto, onde um documento gigante talvez não caiba.
# um chunker simples: divide por parágrafos
def chunk(text):
return [c.strip() for c in text.split("\n\n") if c.strip()]O tamanho do chunk é um equilíbrio, e o motivo remete aos embeddings. Cada chunk se transforma num único vetor que resume todo o seu significado. Se o chunk for muito grande, ele cobre vários tópicos, então seu único vetor é uma média confusa de todos eles, que não corresponde bem a nenhuma pergunta específica. Se for muito pequeno, ele perde o contexto ao redor que era necessário para fazer sentido, então o vetor aponta para um fragmento. Chunks do tamanho de um parágrafo carregam cada um uma ideia coerente. A partir daí, você ajusta com base no desempenho da sua recuperação.
Onde os vetores vivem
Para um punhado de chunks, você pode manter os vetores em memória e compará-los usando a função de similaridade de cosseno do capítulo de embeddings, que é exatamente o que o exemplo mais adiante faz. Para milhares ou milhões de chunks, isso fica lento, e você recorre a um vector store: um banco de dados feito para encontrar os vetores mais próximos rapidamente, em escala.
Você não precisa de um para aprender RAG, e não precisa escolher um agora. Existem vários, tanto como serviços hospedados quanto como bibliotecas; o que importa é o conceito: um lugar para armazenar embeddings e buscá-los rapidamente. Comece em memória e migre para um vector store quando a coleção crescer demais.
Fundamentação e citação
A etapa de geração é um prompt, e a forma como você o escreve decide se o RAG de fato reduz respostas inventadas. Duas instruções fazem a maior parte do trabalho: diga ao modelo para responder apenas com base no contexto fornecido, e diga a ele para admitir isso quando o contexto não contiver a resposta.
context_text = "\n\n".join(retrieved_chunks)
system_prompt = f'''Responda à pergunta usando APENAS o contexto abaixo.
Se o contexto não contiver a resposta, diga "Não tenho essa informação."
Cite a parte relevante do contexto na sua resposta.
Contexto:
"""
{context_text}
"""'''Isso reúne duas lições anteriores. Os delimitadores e o "responda apenas com base nisto" explícito vêm de prompting. A permissão para dizer "não tenho essa informação" é a defesa contra alucinação: um modelo fundamentado com uma rota de escape inventa muito menos do que um modelo deixado à própria sorte para chutar. Fundamentação mais uma rota de escape é o que reduz respostas inventadas. Pedir para ele citar a fonte também dá aos usuários algo para verificar.
Além dos vizinhos mais próximos: busca híbrida e re-ranking
A busca vetorial pura combina por significado, o que é o que você quer na maior parte do tempo. Mas ela tem um ponto cego: pode deixar passar uma palavra exata que importa. Busque por um código de erro como E_4021 ou um nome específico de produto, e uma busca baseada em significado pode devolver chunks que são sobre o mesmo tópico, mas pular justamente aquele que contém a string exata.
A solução tem nome, busca híbrida: combinar busca por significado com a boa e velha busca por palavra-chave, de modo que termos exatos e significado geral tenham, ambos, voz ativa. Você não precisa construir isso para começar com RAG. Recorra a isso quando termos exatos importam e os vetores puros continuam deixando-os passar.
Na prática
Uma resposta RAG minimalista, reaproveitando os auxiliares search e embed do capítulo de embeddings:
def answer_from_docs(question, index):
# 1. recuperar: os chunks que melhor correspondem à pergunta
query_vector = embed(question)
scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
top = sorted(scored, key=lambda x: x["score"], reverse=True)[:3]
# 2. gerar: responder usando apenas esses chunks
context = "\n\n".join(t["text"] for t in top)
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f'Responda usando APENAS este contexto. Se não estiver aqui, diga que não sabe.\n\n"""{context}"""'},
{"role": "user", "content": question},
],
)
return response.choices[0].message.contentRecupere os três chunks mais próximos, coloque-os no prompt e deixe o modelo responder com base neles. Esse é o pipeline de RAG completo: a busca por embeddings alimentando uma chamada ao modelo, com um prompt fundamentado amarrando tudo. Quase toda a qualidade está em recuperar os chunks certos.
Quando você não precisa de RAG
RAG não é gratuito, e nem sempre é a resposta certa. Deixe de usá-lo quando:
- Os dados são pequenos e cabem no prompt. Se toda a sua base de conhecimento tem alguns parágrafos, cole-a direto. RAG serve para quando há mais conteúdo do que se pode enviar de uma vez.
- Um modelo de contexto longo consegue conter tudo. Alguns modelos aceitam entradas muito grandes, suficientes para receber um documento inteiro de uma vez, o que pode ser menos trabalho do que construir uma recuperação.
E lembre-se do limite real: a qualidade do RAG é, na maior parte, a qualidade da recuperação. Se a etapa de busca devolve os chunks errados, nem um prompt perfeito consegue salvar a resposta. Na maioria dos sistemas RAG reais, o esforço vai para recuperar o texto certo, não para a chamada final ao modelo.
Embeddings e RAG dão ao modelo conhecimento no momento da pergunta, sem nunca mudar o modelo em si. O próximo capítulo, Fine-tuning, aborda a outra opção: mudar o modelo com seus próprios exemplos, e quando isso vale mais a pena do que prompting ou RAG.

