RAG

Primeiro, o nome. RAG é a abreviação de geração aumentada por recuperação: você busca o texto relevante por conta própria e o passa ao modelo no momento da pergunta, para que ele responda a partir dos fatos que você forneceu em vez de da memória congelada. Este capítulo aborda como esse loop de busca-e-resposta funciona e onde ele falha.
O padrão recuperar-então-gerar
Um modelo não conhece os documentos de ajuda da sua empresa, os detalhes do seu produto ou nada que tenha acontecido após seu treinamento. Pergunte a ele sobre eles e ele vai admitir que não pode ajudar ou, pior, inventar algo que pareça correto. RAG corrige isso passando o texto relevante ao modelo no momento da pergunta.
A ideia se concretiza uma vez que você tem embeddings, o truque que transforma texto em vetores que você pode comparar pela proximidade (a similaridade de cosseno mede o quão perto dois vetores apontam). Geração aumentada por recuperação é dois passos acoplados a uma chamada de modelo normal:
- Recuperar. Pegue a pergunta do usuário, procure seus documentos pelas partes mais relevantes e colete as correspondências principais.
- Gerar. Coloque essas partes no prompt como contexto e peça ao modelo para responder usando apenas esse contexto.
A capacidade geral de linguagem do modelo faz a escrita; seu texto recuperado fornece os fatos. Você obtém respostas fluidas que são fundamentadas no seu conteúdo e podem estar atualizadas, porque você controla o que entra.
Ajuda ver por que isso funciona tão bem, em termos do loop de predição. Sem RAG, você está pedindo ao modelo para lembrar um fato dos padrões congelados em seus parâmetros, que pode não ter, levando a um palpite confiante. Com RAG, o fato está ali mesmo no contexto, então o trabalho do modelo muda de "lembrar disso" para "ler o que está na sua frente e responder a partir disso". Recuperação move o trabalho da memória para a leitura. A imagem clara é um exame: um exame fechado convida ao palpite, enquanto RAG o transforma em um exame aberto onde a página relevante está aberta na mesa.
Chunking
Você não incorpora documentos inteiros. Você os divide em chunks menores primeiro, um parágrafo ou algumas frases cada, e incorpora esses. Duas razões. Primeiro, a recuperação fica mais precisa: você puxa um 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 pode não caber.
# um chunker pequeno: dividir em 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 a razão rastreia de volta aos embeddings. Cada chunk é transformado em um único vetor que resume seu significado completo. Faça um chunk muito grande e ele cobre vários tópicos, então seu único vetor é uma média turva de todos eles que não combina bem com nenhuma pergunta específica. Faça-o muito pequeno e ele perde o significado circundante que precisava para fazer sentido, então o vetor aponta para um fragmento. Chunks de tamanho de parágrafo cada um contêm uma ideia coerente. Você se ajusta a partir daí com base em como sua recuperação funciona.
Onde os vetores vivem
Para um punhado de chunks, você pode manter os vetores na memória e compará-los com a função similaridade-cosseno do capítulo embeddings, que é o que o exemplo depois faz. Para milhares ou milhões de chunks, isso fica lento e você usa um armazenamento de vetores: um banco de dados construído 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. Vários existem como serviços hospedados e bibliotecas; o conceito é o que importa: um lugar para armazenar embeddings e procurá-los rapidamente. Comece na memória, mude para um armazenamento quando a coleção a ultrapassar.
Fundamentação e citação
A etapa de geração é um prompt e como você o escreve decide se RAG realmente reduz respostas inventadas. Duas instruções fazem o trabalho pesado: diga ao modelo para responder apenas do contexto fornecido e diga a ele para falar quando o contexto não contém a resposta.
context_text = "\n\n".join(retrieved_chunks)
system_prompt = f'''Answer the question using ONLY the context below.
If the context does not contain the answer, say "I don't have that information."
Quote the relevant part of the context in your answer.
Context:
"""
{context_text}
"""'''Isso reúne duas lições anteriores. Os delimitadores e o "responda apenas disso" explícito vêm de prompting. A permissão de dizer "I don't have that information" é a defesa alucinação: um modelo fundamentado com uma escotilha de escape inventa muito menos que um deixado para adivinhar. Fundamentação mais uma escotilha de escape é o que reduz respostas inventadas. Pedir que cite a fonte também dá aos usuários algo para verificar.
Além de vizinho mais próximo: busca híbrida e re-ranking
Busca de vetor puro combina significado, que é o que você quer a maior parte do tempo. Mas tem um ponto cego: pode perder uma palavra exata que importa. Procure por um código de erro como E_4021 ou um nome de produto específico, e uma busca baseada em significado pode devolver chunks que são sobre o mesmo tópico, enquanto pula aquele que contém a sequência exata.
A correção tem um nome, busca híbrida: combine busca baseada em significado com busca de palavra-chave antiga, para que termos exatos e significado geral obtenham um voto. Você não precisa construir isso para começar com RAG. Recorra a ele quando termos exatos importam e vetores puros continuam perdendo-os.
Na prática
Uma resposta RAG mínima, reutilizando os helpers search e embed do capítulo embeddings:
def answer_from_docs(question, index):
# 1. recuperar: os chunks mais relevantes para a 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'Answer using ONLY this context. If it is not here, say you don\'t know.\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 a partir deles. Esse é todo o pipeline RAG: a busca embeddings alimentando uma chamada de modelo, com um prompt fundamentado mantendo-o junto. Quase toda a qualidade vive em recuperar os chunks certos.
Quando você não precisa de RAG
RAG não é grátis e nem sempre é a resposta. Pule quando:
- Os dados são pequenos e cabem no prompt. Se toda sua base de conhecimento é alguns parágrafos, cole-a diretamente. RAG é para quando há muito para enviar tudo de uma vez.
- Um modelo de contexto longo pode sustentá-lo tudo. Alguns modelos aceitam entradas muito grandes, o suficiente para levar um documento inteiro de uma vez, o que pode ser menos trabalho que construir recuperação.
E lembre-se do limite real: A qualidade de RAG é principalmente qualidade de recuperação. Se a etapa de busca retorna os chunks errados, nem mesmo um prompt perfeito pode salvar a resposta. A maioria do esforço em sistemas RAG reais vai para recuperar o texto certo, não para a chamada final do 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, cobre a outra opção: mudar o modelo com seus próprios exemplos e quando vale a pena em relação a prompting ou RAG.

