Skip to content
This page has been auto-translated and may contain errors.View in English

RAG

docs.scrimba.com

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:

  1. Recuperar. Pegue a pergunta do usuário, procure seus documentos pelas partes mais relevantes e colete as correspondências principais.
  2. 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.

JunoO padrão recuperar-então-gerar RAG adiciona dois passos a uma chamada de modelo normal: recuperar os documentos mais relevantes para a pergunta e depois gerar uma resposta usando-os como contexto. O modelo fornece a linguagem, seu texto recuperado fornece os fatos. Isso deixa você responder a partir do seu conteúdo em vez de da memória, o que evita que ele invente respostas. Demorou um tempo para confiar nisso, mas um modelo aberto realmente adivinha menos que um fechado.

Um modelo não carrega seus documentos de ajuda, detalhes do produto ou nada após seu corte de treinamento. Pergunte mesmo assim e você obtém uma recusa ou uma resposta inventada que parece fina. Geração aumentada por recuperação fecha essa lacuna buscando texto relevante e o colocando no prompt no momento da solicitação, para que o modelo leia o fato em vez de recordá-lo. O mecanismo se apoia em embeddings, onde cada pedaço de texto se torna um vetor e a similaridade é a distância entre vetores (similaridade de cosseno, o cosseno do ângulo entre eles).

O loop é dois passos:

  1. Recuperar. Incorpore a pergunta no mesmo espaço vetorial que seus documentos e depois puxe os chunks mais próximos pela similaridade.
  2. Gerar. Concatene esses chunks no prompt como contexto e instrua o modelo a responder apenas com base neles.

A etapa de recuperação tem um marcador que você define chamado top-k: quantos dos chunks mais próximos você realmente mantém. k é um equilíbrio. Muito pequeno e você perde o chunk que tinha a resposta, então o modelo não tem nada com que trabalhar. Muito grande e você preenche o contexto com texto vagamente relacionado, o que dilui o sinal, aumenta o custo e piora o problema de perda no meio. Um k de 3 a 5 é um lugar razoável para começar e você o ajusta com base em perguntas reais.

Aqui está a jogada: estruture o índice como uma lista de registros, cada um contendo o texto do chunk e seu vetor pré-computado, e estruture o prompt para que o contexto recuperado fique em um bloco claramente delimitado acima da pergunta. A capacidade de linguagem do modelo escreve; seu texto recuperado decide o que é verdade.

python
def retrieve(question, index, k=4):
    query_vector = embed(question)
    scored = [
        {"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])}
        for item in index
    ]
    return sorted(scored, key=lambda x: x["score"], reverse=True)[:k]
JunoO padrão recuperar-então-gerar RAG é recuperar depois gerar: incorpore a pergunta, puxe os chunks mais próximos, coloque-os no prompt e responda com base neles. O marcador que importa aqui é top-k, o número de chunks que você mantém: muito poucos e você perde a resposta, muitos e você a enterra no ruído e paga pelo privilégio. Comece em torno de 3 a 5 e ajuste com base em perguntas reais, não em vibes.

Um modelo não possui seu corpus privado ou nada após seu corte, e não dirá qual é; ele responde no mesmo tom confiante de qualquer forma. Geração aumentada por recuperação é o padrão de buscar texto relevante no momento da solicitação e fundamentar a resposta nele, para que a correção se baseie em uma fonte que você controla em vez de na recordação paramétrica. Funciona em embeddings: texto mapeado para vetores, classificados por similaridade de cosseno (o cosseno do ângulo entre dois vetores, então proximidade de significado independente do comprimento).

A estrutura de dois passos, recuperar depois gerar, está correta, mas esconde onde o trabalho está. Recuperação é um problema de busca com cara de geração, e a qualidade de todo o sistema é definida lá, não na chamada do modelo. Então trate recuperar-então-gerar como recuperar-classificar-então-gerar desde o início, porque a versão ingênua, incorpore a consulta, pegue o k mais próximo, cole-o, quebra nos modos de falha que os níveis inferiores ainda não encontraram: perdas de correspondência exata, chunks quase duplicados amontoando-se contra a cobertura e texto obsoleto respondendo confiante.

O primeiro hábito vale a pena começar desde cedo é que este é um sistema mensurável, não um vibe. Retenha um conjunto de perguntas reais com os chunks que deveriam respondê-las e acompanhe recall@k: dos chunks que deveriam ter sido recuperados, a fração que desembarcou no seu top k. Recall@k é seu teto. Se o chunk certo não estiver no contexto, nenhuma quantidade de elaboração de prompt o recupera, e a etapa de geração não pode responder o que a recuperação nunca entregou. Tudo depois disso é limitado por este número, então é a primeira coisa a medir e a primeira coisa a mover.

python
def retrieve(question, index, k=8):
    query_vector = embed(question)
    scored = [
        {"text": it["text"], "score": cosine_similarity(query_vector, it["vector"]), "meta": it["meta"]}
        for it in index
    ]
    # over-fetch here, then re-rank down to a smaller final set (see below)
    return sorted(scored, key=lambda x: x["score"], reverse=True)[:k]
JunoO padrão recuperar-então-gerar RAG é um problema de busca com cara de geração: a qualidade é definida na recuperação, não na chamada do modelo, então a versão ingênua de incorporar-pegar-k-colar vai deixá-lo na mão em correspondências exatas e texto obsoleto. O número que limita tudo é recall@k, a fração dos chunks que deveriam ter sido recuperados que realmente chegaram ao seu top k. Se o chunk certo nunca chegar ao contexto, nenhum prompt o salva. Meça isso antes de ajustar qualquer coisa, senão você está polindo a ponta errada.

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.

python
# 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.

JunoChunking Antes de incorporar, divida documentos em chunks menores, aproximadamente de tamanho de parágrafo, e incorpore cada um. Chunks menores tornam a recuperação mais precisa e se ajustam melhor à janela de contexto. O tamanho do chunk é um equilíbrio: muito grande é nebuloso e desperdiçador, muito pequeno perde o significado circundante. Quando minhas respostas voltaram vagas, os chunks quase sempre eram muito grandes.

Você incorpora chunks, não documentos inteiros, porque cada chunk se torna um único vetor que deve resumir seu significado completo. Divida um documento em pedaços de tamanho de parágrafo e incorpore cada um. Um chunk muito grande faz a média de vários tópicos em um vetor que não combina com nada; um chunk muito pequeno perde o contexto que o tornava significativo.

Então trate o tamanho do chunk como um marcador de ajuste, não uma regra fixa. Um chunk de algumas centenas de tokens tende a conter uma ideia de forma clara. O marcador interage com top-k: chunks maiores significam menos deles caberem em seu orçamento de contexto, então você leva menos cobertura por solicitação. Chunks menores aumentam a precisão, mas precisam de um k maior para remontar uma ideia que os atravessa.

O segundo marcador é sobreposição: deixe chunks consecutivos compartilharem uma fatia de texto em seus limites, digamos 10 a 20 por cento. Sem sobreposição, uma frase que responde à pergunta pode desembarcar dividida através de um limite, metade em um chunk e metade no próximo, então nenhum chunk recupera bem. Sobreposição custa alguma duplicação e alguns vetores a mais, e compra de volta as respostas que os limites cortariam em dois.

python
def chunk(text, size=800, overlap=120):
    # size e overlap em caracteres aqui; orçamento em tokens para sistemas reais
    out, start = [], 0
    while start < len(text):
        out.append(text[start:start + size])
        start += size - overlap
    return out
JunoChunking Tamanho de chunk e sobreposição são marcadores de ajuste, não configurações que você acerta uma vez. Aime por chunks que contêm uma ideia, algumas centenas de tokens, e lembre que o marcador se negocia contra top-k: chunks maiores, menos cabem. Adicione 10 a 20 por cento de sobreposição para que uma resposta que atravessa um limite ainda viva inteira dentro de pelo menos um chunk. Os limites são onde a recuperação silenciosamente perde respostas.

Chunking é onde o recall da recuperação é ganho ou perdido antes de uma única consulta ser executada. Cada chunk se torna um único vetor que deve estar em pé de igualdade para seu significado inteiro, então a divisão decide o que é encontrável. Limites de parágrafo ou seção batem janelas de caracteres fixos quando o documento tem estrutura, porque um vetor sobre uma seção coerente é um alvo de consulta mais limpo do que um vetor sobre metade de duas seções.

O tamanho do chunk e a sobreposição estão acoplados ao seu orçamento completo, não parâmetros livres. O tamanho se negocia contra top-k contra a janela de contexto: chunks grandes carregam mais contexto cada, mas menos cabem, então a cobertura cai; chunks pequenos aumentam a precisão, mas fragmentam ideias e precisam de um k maior para remontar, o que custa tokens e reintroduz a perda no meio. Sobreposição de aproximadamente 10 a 20 por cento salva respostas que atravessam um limite, ao custo de duplicação em seu índice. A duplicação tem uma mordida a jusante: chunks sobrepostos quase idênticos podem preencher vários de seus slots top-k com o mesmo conteúdo, matando a cobertura. Essa é uma das razões pelas quais re-ranking e de-duplicação ganham seu lugar depois.

O custo que a maioria das pessoas pula até doer: a estratégia de chunking é assada em seu índice, então mudá-la significa re-incorporar e re-indexar o corpus inteiro. Esse é computação real, dinheiro real e uma janela de migração onde vetores antigos e novos não devem ser misturados. Decida o tamanho do chunk com base em seus documentos e consultas reais desde cedo, meça recall@k em um conjunto retido e trate uma re-chunk como um re-índice deliberado, não uma ajuste de config que você envia numa sexta.

JunoChunking Divida na estrutura quando tiver, porque cada chunk é um único vetor em pé pela sua ideia completa, e o tamanho se negocia contra top-k contra a janela tudo de uma vez. Sobreposição de 10 a 20 por cento salva respostas que atravessam limites, mas gera quase-duplicatas que amontoam seu top-k, que é parte da razão para re-ranking existir. E lembre que a escolha de chunking está assada no índice: mudá-la é uma re-incorporação e re-índice completos, custo real e uma migração, não uma virada de config.

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.

JunoOnde os vetores vivem Para alguns chunks, mantenha vetores na memória e compare-os diretamente. Para coleções grandes, um armazenamento de vetores é um banco de dados que encontra vetores mais próximos rapidamente em escala. Você não precisa de um para aprender RAG: comece na memória e mude para um armazenamento de vetores quando a coleção a ultrapassar. Não precisa fazer compras de banco de dados no primeiro dia.

Para alguns cem chunks, vetores vivem na memória e você pontua cada um contra a consulta. Isso é uma varredura linear: está bem em pequena escala, lento uma vez que a coleção chega a milhares ou milhões, porque você compara contra cada vetor em cada consulta. Nesse ponto você muda para um armazenamento de vetores, um banco de dados que indexa vetores para busca de vizinho mais próximo rápida.

A assimetria que vale a pena manter é incorporar-uma-vez versus incorporar-por-consulta. Você incorpora e indexa cada chunk de documento exatamente uma vez, antecipadamente, e esse trabalho é reutilizado para cada pergunta futura. Você incorpora a consulta uma vez por solicitação, no momento da solicitação, e esse é o único custo de incorporação no caminho ativo. Então o custo de indexação pesado é pago antecipadamente e amortizado; o custo por consulta é uma chamada de incorporação mais uma busca. Isso é o que torna RAG barato de servir uma vez construído.

Você não precisa escolher um armazenamento para aprender o padrão. Comece com uma lista na memória de registros, troque por um armazenamento de vetores quando a escala ou persistência exigir, e mantenha a interface igual: incorpore consulta, procure, retorne top-k.

JunoOnde os vetores vivem Na memória você varre cada vetor por consulta, o que está bem até a coleção crescer, então um armazenamento de vetores oferece busca de vizinho mais próximo indexada. Mantenha a assimetria em sua cabeça: documentos são incorporados e indexados uma vez e reutilizados para sempre, enquanto a consulta é incorporada nova em cada solicitação. O custo antecipado é pago uma vez e amortizado, então servir continua barato. Comece na memória, troque por um armazenamento quando a escala ou persistência forçar.

Na memória você faz uma varredura linear, pontuando a consulta contra cada vetor de chunk, que é exato mas O(n) por consulta e para de dimensionar em algum lugar nas dezenas de milhares. Um armazenamento de vetores substitui a varredura por um índice de vizinho aproximado (ANN): ele troca um pouco de recall por uma grande aceleração ao não verificar cada vetor. Essa troca é um marcador e em escala de produção você aceita que o índice pode ocasionalmente perder um verdadeiro vizinho mais próximo, que se dobra direto de volta em seu recall@k.

A assimetria de incorporar-uma-vez define sua forma de custo. Indexação incorpora o corpus inteiro uma vez, um grande custo de lote antecipado; servir incorpora uma consulta e executa uma busca por solicitação. Então seu orçamento de latência de estado estacionário é aproximadamente: incorpore a consulta, procure o índice, depois a chamada gerar. A busca é geralmente a fatia mais pequena; a chamada gerar domina a latência e a query embedding é um imposto fixo que você paga antes de poder recuperar. Vale a pena medir cada perna, porque as pessoas buscam um armazenamento de vetor mais rápido quando a latência real estava na geração o tempo todo.

Então há obsolescência, o modo de falha que o brinquedo na memória nunca mostra. Seu índice é um instantâneo; os documentos de origem derivam. Uma política muda, um doc é editado, um chunk antigo ainda fica no índice apontando para o passado, e recuperação o serve com confiança completa. Então um índice não é escrita-uma-vez: você precisa de um caminho de re-indexação que re-incorpora documentos alterados, e idealmente metadados em cada chunk (id de origem, versão, timestamp) para que você possa invalidar ou filtrar entradas obsoletas. Decida a cadência de atualização com base em o quão rápido sua verdade se move e trate "o índice está atual" como algo que você verifica, não assume.

JunoOnde os vetores vivem Um armazenamento de vetores troca a varredura linear exata por um índice de vizinho aproximado: mais rápido, mas pode perder um verdadeiro vizinho, que desembarca de volta em seu recall@k. Incorporação é uma vez para o corpus e uma vez por consulta, então a latência de estado estacionário é incorporar-consulta mais busca mais gerar, e a chamada gerar geralmente domina, então meça antes de culpar o armazenamento. A armadilha que o brinquedo nunca mostra é obsolescência: o índice é um instantâneo, as fontes derivam, então construa um caminho de re-indexação e carimbe chunks com versão e timestamp em vez de confiar que o índice está atual.

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.

python
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.

JunoFundamentação e citação A etapa de geração é um prompt e decide se RAG funciona. Diga ao modelo para responder apenas do contexto fornecido e fale quando a resposta não estiver lá. Essa fundamentação, mais permissão de admitir que não sabe, é o que reduz respostas inventadas. Pedir que cite a fonte dá aos usuários algo para verificar e lhe dá uma pista quando a recuperação perdeu.

A recuperação pode entregar contexto perfeito ao modelo e o modelo ainda pode ignorá-lo. O prompt de geração é o que vincula a resposta ao contexto, então escreva deliberadamente: instrua o modelo a responder apenas do contexto fornecido, a admitir quando o contexto carece da resposta e citar o intervalo que usou.

python
context_text = "\n\n".join(f"[{i}] {c}" for i, c in enumerate(retrieved_chunks))

system_prompt = f'''Answer using ONLY the context below.
If the answer is not in the context, reply exactly: "I don't have that information."
Cite the chunk number(s) you used, like [2].

Context:
"""
{context_text}
"""'''

Numerando os chunks transforma "cite a fonte" em uma citação que você pode verificar programaticamente: o modelo aponta para chunk [2], e você pode confirmar que o chunk [2] realmente suporta a alegação. Esse é seu identificador no modo de falha mais perdido em RAG, a resposta certa com os chunks errados recuperados, onde o modelo produz uma resposta que soa correta, mas o contexto fornecido não suporta. Sem citações você não pode dizer uma resposta fundamentada de um palpite sorte.

A escotilha de escape importa tanto quanto a instrução. Um modelo instruído a responder apenas do contexto mas sem permissão para falhar ainda vai juntar algo de uma recuperação pobre. Dê a ele uma saída explícita, a sequência exata "I don't have that information", e uma miss de recuperação emerge como uma lacuna limpa em vez de uma invenção confiante. A forma de mensagem aqui é estilo OpenAI; o arranjo sistema-depois-contexto varia por provedor, mas o princípio se mantém.

JunoFundamentação e citação O prompt de geração vincula a resposta ao contexto: responda apenas a partir dele, admita quando não estiver lá e cite o chunk. Numere os chunks para que a citação seja verificável, o que pega a falha furtiva onde a resposta está certa, mas os chunks recuperados nunca a suportaram. Dê ao modelo a sequência de escape exata, ou uma recuperação fraca fica tecida em uma resposta confiante e errada. A forma varia por provedor, mas a estrutura sistema-depois-contexto se mantém.

Fundamentação é o contrato entre recuperação e o modelo, e é o marcador de confiabilidade mais barato que você tem, então gaste o prompt nele. Instrua o modelo a responder apenas do contexto fornecido, retorne uma sequência de abstinência fixa quando o contexto não suporte uma resposta e cite os ids de chunk que usou.

python
context_text = "\n\n".join(f"[{c['id']}] {c['text']}" for c in retrieved)

system_prompt = f'''Answer using ONLY the context below.
If the context does not support an answer, reply exactly: "I don't have that information."
For each claim, cite the chunk id(s) in brackets, e.g. [doc12].

Context:
"""
{context_text}
"""'''

Citações não são decoração; é como você torna a fundamentação mensurável. Com ids de chunk na saída você pode verificar, automaticamente, se um chunk citado realmente contém a alegação, o que separa uma resposta fundamentada de um palpite fluente. Isso também é como você detecta a falha que derrota RAG ingênuo: a resposta é plausivelmente correta, mas o contexto recuperado nunca a suportou, então a recuperação perdeu e o modelo preencheu a lacuna da memória paramétrica. Sem ids você está fazendo verificações pontuais; com eles você pode pontuar fundamentação em seus evals (um conjunto retido de perguntas com respostas conhecidas boas, pontuadas automaticamente) e observar fidelidade como um número.

Duas notas de produção. O caminho de abstinência precisa ser barato e sancionado, porque um modelo sem permissão para falhar sempre produzirá algo, e uma recuperação pobre então se torna uma resposta confiante errada em vez de uma lacuna limpa. E a estrutura de mensagem aqui é forma OpenAI-SDK; a divisão sistema-versus-usuário e como o contexto se vincula varia por provedor, então trate o arranjo como portável, os nomes de campo exatos como não. A regra duradoura por baixo: nunca deixe o modelo ser a fonte, apenas o parafraseador de uma fonte que você pode citar e verificar.

JunoFundamentação e citação Fundamentação é o contrato: responda apenas do contexto, abstenda com uma sequência fixa, e cite ids de chunk para que fidelidade se torne algo que você pontua em evals, não observa a olho. Citações são como você pega a resposta que está certa, mas não suportada, onde recuperação perdeu e o modelo remendou a lacuna da memória. Mantenha o caminho de abstinência barato e sancionado ou uma recuperação fraca se torna uma resposta confiante errada. A forma varia por provedor; a estrutura se porta, os nomes de campo não.

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.

JunoAlém de vizinho mais próximo: busca híbrida e re-ranking Busca de vetor combina significado, que é ótimo até alguém procurar por um código exato ou nome de produto e o chunk certo ficar pulado. Busca híbrida mistura busca de palavra-chave simples para que termos exatos contem também. Você pode começar RAG sem ela e adicioná-la uma vez que notar misses de correspondência exata se acumulando.

Busca de vizinho mais próximo de vetor combina significado, e isso também é sua fraqueza. Pode classificar um chunk topicamente próximo acima do chunk que contém o token exato que você precisa, um código de erro, um SKU, um nome de função, um substantivo próprio raro. Embeddings espalham esses em "aproximadamente essa área de significado" e sequências exatas se perdem no espalhamento.

Busca híbrida executa dois recuperadores e os mescla: a busca de vetor por significado, mais uma busca de palavra-chave (correspondência de termo clássica, frequentemente BM25, que pontua documentos por quantas das palavras exatas da consulta eles contêm e quão raras essas palavras são). Você pega candidatos de ambos e combina as classificações, então um chunk que vence em termos exatos ou proximidade semântica pode emergir. O retorno prático é recall em exatamente as consultas que vetores puros tropeçam.

A segunda técnica é re-ranking: um segundo passe que re-pontua seus candidatos de primeiro passe com um modelo mais preciso e mais caro. O padrão é sobre-buscar depois estreitar. Recupere um conjunto generoso de forma barata, digamos os 20 principais, depois execute um re-ranker que lê cada candidato contra a consulta e os reordena, e guarde os 4 principais para realmente enviar. Recuperação de primeiro passe é rápida e bruta; o re-ranker é lento e afiado, então você só o executa na lista curta. O resultado é um k final melhor sem pagar o custo do re-ranker em todo o seu índice.

JunoAlém de vizinho mais próximo: busca híbrida e re-ranking Busca de vetor puro perde sequências exatas como códigos de erro e SKUs porque embeddings as embaçam em uma região de significado. Busca híbrida adiciona correspondência de palavra-chave (BM25) para que termos exatos obtenham um voto junto aos semânticos. Re-ranking é um padrão barato-depois-afiado: sobre-busque um conjunto amplo com recuperação rápida, depois re-pontue a lista curta com um modelo mais lento e mais preciso e guarde os melhores. Você obtém resultados finais melhores sem executar o scorer caro em tudo.

Vizinho-mais-próximo aproximado puro é um padrão forte e uma superfície de falha conhecida. Otimiza proximidade semântica, então tem desempenho inferior exatamente onde a consulta depende de um token exato, um código de erro, um identificador, um SKU, um substantivo próprio raro, porque a incorporação comprime esse token em um bairro de significado onde sequências exatas se embaçam. Isso não é um problema de ajuste; é o que vetores densos fazem.

Busca híbrida o aborda executando recuperação densa (vetores, significado) junto com recuperação esparsa (correspondência de termo de palavra-chave, normalmente BM25, que pontua por frequência de termo exata ponderada pela raridade de termo) e fundindo as listas de resultado, comumente com fusão de classificação recíproca, que combina duas classificações por posição de cada item em vez de por pontuações brutas incomparáveis. O ganho é mensurável como recall@k em consultas de correspondência exata, que é a fatia que densa pura silenciosamente deixa cair. O custo é um segundo índice e uma etapa de fusão, então compensa precisamente quando seu tráfego inclui identificadores e termos raros, e menos quando é tudo parafraseado em linguagem natural.

Re-ranking ataca uma miss diferente. Recuperação de primeiro passe (densa, esparsa ou híbrida) é construída para velocidade em um índice grande, então classifica de forma bruta. Um re-ranker é um codificador cruzado (um modelo que lê a consulta e um chunk candidato juntos e pontua sua relevância conjuntamente, em vez de comparar dois vetores pré-computados) que é mais preciso e muito caro demais para executar sobre o corpus inteiro. Então você sobre-busca com o recuperador barato, 20 a 50 principais, depois re-classifica essa lista curta e guarde os últimos.

Isso é o fix de recall com o maior retorno na maioria dos RAG de produção: ataca diretamente a falha de resposta-certa-chunks-errados promovendo o chunk que realmente suporta a resposta acima dos meramente topicais, e você mede o ganho como recall@k antes versus depois da etapa de re-rank. Orçamento: o re-ranker adiciona uma latência real no caminho ativo e custo por consulta, então dimensione a sobre-busca contra seu alvo de latência, não contra quantos candidatos você pode permitir pontuação de forma abstrata.

JunoAlém de vizinho mais próximo: busca híbrida e re-ranking Vetores densos embaçam tokens exatos, então busca híbrida executa recuperação de palavra-chave esparsa (BM25) junto com densa e funde as listas com fusão de classificação recíproca, recomprandocall-recall@k em identificadores e termos raros. Re-ranking é o maior ganho: sobre-busque de forma barata, depois re-pontue a lista curta com um codificador cruzado que lê consulta e chunk juntos, e fix direto resposta-certa-chunks-errados promovendo o chunk que suporta a resposta. Meça recall@k antes e depois e orçamento a latência do re-ranker e custo por consulta contra seu alvo de caminho ativo.

Na prática

Uma resposta RAG mínima, reutilizando os helpers search e embed do capítulo embeddings:

python
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.content

Recupere 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.

JunoNa prática Uma resposta RAG mínima é busca de embeddings alimentando uma chamada de modelo: recupere os chunks mais próximos, coloque-os no prompt e peça ao modelo para responder apenas com base neles. A recuperação fornece os fatos e o prompt fundamentado mantém a resposta ancorada neles. Quase toda a qualidade vive em recuperar os chunks certos, então é ali que você gasta seu tempo.

Uma chamada RAG funcional é recuperar, fundamentar, gerar, com o identificador de citação de antes conectado:

python
def answer_from_docs(question, index, k=4):
    top = retrieve(question, index, k)  # do padrão acima
    context = "\n\n".join(f"[{i}] {t['text']}" for i, t in enumerate(top))
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
                f'Answer using ONLY this context. If it is not here, reply '
                f'"I don\'t have that information." Cite chunks like [1].\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

A coisa toda é a busca embeddings alimentando uma chamada de modelo, mantida junto por um prompt fundamentado. A forma chat.completions é estilo OpenAI; os papéis de mensagem e assinatura de chamada variam por provedor, mas o fluxo recuperar-depois-fundamentar-depois-gerar é o mesmo em todos os lugares.

Quando uma resposta está errada, depure em ordem de etapa. Primeiro verifique os chunks recuperados: o texto relevante desembarcou em top? Se não, a correção está na recuperação, aumente k, corrija chunking, adicione híbrido ou re-ranking, não no prompt. Se o chunk certo foi recuperado e a resposta ainda está errada, então é um problema de prompt. Esta ordem evita que você ajuste a redação do prompt contra um bug de recuperação, que nunca converge.

JunoNa prática Uma chamada RAG funcional é recuperar, fundamentar com citações, gerar, e a forma estilo OpenAI se porta para outros provedores em estrutura, se não em nomes de campo. Depure em ordem fixa: olhe os chunks recuperados primeiro. Se o texto certo nunca desembarcou, corrija recuperação (k, chunking, híbrido, re-rank), não o prompt. Apenas uma vez que o chunk certo está no contexto uma resposta errada é um problema de prompt.

Na prática o pipeline recuperar-classificar-gerar é uma função e as partes que você instrumenta são as junções entre etapas:

python
def answer_from_docs(question, index, k_fetch=20, k_final=4):
    candidates = retrieve(question, index, k=k_fetch)   # densa (ou híbrida)
    top = rerank(question, candidates)[:k_final]        # passe de codificador cruzado
    context = "\n\n".join(f"[{c['id']}] {c['text']}" for c in top)
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
                f'Answer using ONLY this context. If unsupported, reply '
                f'"I don\'t have that information." Cite ids like [doc12].\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content, [c["id"] for c in top]

Retornar os ids de chunk junto com a resposta é a jogada que torna o sistema observável: você registra o que foi recuperado, o que foi citado e se os ids citados realmente suportam a resposta, que é seu sinal de fidelidade em evals. A forma chat.completions é OpenAI-SDK; trate o fluxo como portável e os nomes de campo como específicos do provedor.

O mapa de latência e custo deste pipeline vale a pena manter em sua cabeça, porque otimização vai onde o tempo está:

  • Incorporação de consulta é um imposto fixo pequeno.
  • Recuperação de primeiro passe é barata.
  • Re-ranking adiciona um impacto real por consulta dimensionado por seu tamanho de sobre-busca.
  • A chamada gerar domina tanto latência quanto custo em dólar.

Então antes de perseguir um armazenamento de vetor mais rápido, confirme a busca foi sempre o gargalo; geralmente era geração.

E depure falhas por etapa em ordem: conjunto recuperado, depois conjunto re-classificado, depois citações, depois prompt. Uma resposta errada cujo chunk certo nunca entrou em candidates é um bug de recuperação e nenhuma mudança de prompt o corrige.

JunoNa prática O pipeline é recuperar, re-classificar, fundamentar, gerar, e retornar ids de chunk junto com a resposta é o que torna observável: registre recuperado versus citado e você pode pontuar fidelidade em evals em vez de adivinhar. Observe o mapa de custo, geração domina latência e dólares, então confirme busca foi o gargalo antes de trocar armazenamentos de vetor. Depure por etapa em ordem, recuperado depois re-classificado depois citado depois prompt, porque nenhum prompt corrige um chunk que nunca entrou no conjunto candidato. A forma é OpenAI-SDK; o fluxo se porta, os nomes de campo não.

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.

JunoQuando você não precisa de RAG Pule RAG quando seus dados são pequenos demais para colar no prompt ou quando um modelo de contexto longo pode sustentá-lo de uma vez. E mantenha o limite real em mente: a qualidade de RAG é principalmente qualidade de recuperação, então se a busca retorna os chunks errados, nenhum prompt pode resgatar a resposta. A maioria do trabalho real está em recuperar o texto certo, não na chamada final.

RAG é infraestrutura, um índice, uma etapa de incorporação, uma etapa de recuperação, um caminho de re-índice, e ganha esse overhead apenas quando os dados são muito grandes para enviar e mudam frequentemente o bastante que retreinamento não é uma opção. Pule quando uma forma mais barata faz o trabalho:

  • O corpus cabe no prompt. Alguns parágrafos ou um pequeno documento de política vão direto no contexto. RAG só ganha seu valor quando há mais do que você pode enviar de uma vez.
  • Um modelo de contexto longo sustenta a fonte inteira. Se o documento cabe em uma janela grande, enviá-lo inteiro pode vencer um pipeline de recuperação que você tem que construir e ajustar, embora você pague esses tokens em cada chamada e ainda bata a perda no meio.

Mantenha o fato governante: a qualidade de RAG é limitada por qualidade de recuperação. Um prompt perfeito sobre chunks errados ainda responde errado, então o índice e a etapa de recuperação são onde o trabalho e os ganhos estão, não a chamada final do modelo. Quando você realmente precisa mudar comportamento em vez de fornecer fatos, Fine-tuning é o próximo capítulo e resolve um problema diferente que RAG faz.

JunoQuando você não precisa de RAG RAG é infraestrutura com overhead real, então pule quando o corpus cabe no prompt ou um modelo de contexto longo pode engolir a fonte inteira, lembrando que a rota de contexto longo paga tokens em cada chamada e ainda deriva no meio. O fato governante permanece: a qualidade é limitada por recuperação, então um prompt perfeito sobre chunks errados ainda responde errado. Gaste no índice e recuperação, não na chamada final.

RAG lhe compra moeda e fundamentação pelo preço de um sistema de recuperação que você constrói, ajusta, monitora e re-indexa. Então a decisão é uma de custo-benefício, não um padrão. Pule quando uma forma mais leve limpa a barra:

  • Corpus pequeno e estável. Se cabe no prompt e raramente muda, coloque no contexto e se apoie em cache de prompt para que o bloco fixo seja servido de forma barata em chamadas repetidas. Um pipeline de recuperação aqui é overhead sem retorno.
  • Contexto longo, reuso modesto. Se a fonte cabe em uma janela grande, enviá-la inteira pula a construção, mas você paga esses tokens de entrada em cada solicitação e recall ainda apodrece no meio, então perde para RAG uma vez que volume ou tamanho de documento sobe.

A restrição governante é a mesma que dirige todo o capítulo: a qualidade ponta-a-ponta é limitada por recall@k. Nenhum prompt, nenhuma atualização de modelo, nenhuma re-classificação recupera um fato que recuperação nunca pôs no contexto. Então o investimento de engenharia vai para recuperação, chunking, híbrido, re-ranking, evals, re-indexação e a chamada do modelo é a parte barata para acertar. E mantenha RAG e fine-tuning em suas pistas separadas: recuperação fornece o que é verdade e permanece atual conforme as fontes mudam, enquanto fine-tuning molda como o modelo responde e é uma forma obsoleta e sem fonte de injetar fatos. Use recuperação para conhecimento, fine-tuning para forma, e não peça a nenhum deles fazer o trabalho do outro.

JunoQuando você não precisa de RAG RAG lhe compra moeda e fundamentação pelo preço de um sistema que você constrói e re-indexa, então pule quando o corpus é pequeno e estável (cole-o, apoie-se em cache de prompt) ou cabe em uma janela longa com reuso modesto. A restrição que dirige tudo é recall@k: nada a jusante recupera um fato que recuperação nunca superficializou, então invista em recuperação, não na chamada do modelo. Mantenha as pistas limpas: recuperação para o que é verdade, fine-tuning para como soa, e nunca peça a um fazer o trabalho do outro.