Voltar ao início

AI Internals - RAG

12 min de leitura
Cover Image for AI Internals - RAG
Lucas LemosLucas Lemos

Introduction

Em AI Internals - Embeddings e Retrieval paramos na busca: embeddar passagens, indexar, buscar vizinhos, opcionalmente fundir e reranquear. O modelo generativo não entrou na história.

RAG (retrieval-augmented generation) é a ligação: pegar esses vizinhos, empacotá-los no contrato de messages de Prompts & Context e fazer a completion depender dessa evidência. Chunking decide quais unidades existem para recuperar. Grounding decide se o modelo pode inventar além delas.

A maioria dos demos trata RAG como "colar top-k no prompt." A dor de produção mora em como você corta documentos, como orça tokens e o que faz quando o retrieval não devolve nada útil.

RAG é um pipeline, não um feature flag

Retrieval sozinho responde "quais passagens parecem perto da query?" Geração sozinha responde "qual string é provável a seguir?" RAG responde uma terceira pergunta: "dadas estas passagens como conhecimento temporário, produza uma resposta em que o app possa confiar."

Três propriedades saem desse loop:

  • Frescor vem do seu índice, não do pré-treino. Atualize o doc store e as respostas mudam sem fine-tune.
  • Atribuição só é possível se você guardar IDs junto com o texto injetado e pedi-los de volta (ou anexá-los você mesmo).
  • Falha é composta. Chunk errado, chunk truncado, regra de citação ausente ou modelo que ignora a evidência — tudo isso parece "RAG está quebrado" por fora.

Fine-tuning ainda tem lugar — estilo, schemas de tools, jargão de domínio que deve viver nos pesos — mas é substituto fraco para uma base de conhecimento que muda toda semana. RAG mantém os fatos fora do modelo congelado.

Chunking: a unidade de verdade

No artigo anterior, uma "passagem" já era uma string com ID. Aqui importa como essa string foi cortada do documento fonte.

Chunking é irreversível para o índice: se a sentença certa nunca convive na mesma unidade com o cabeçalho que lhe dá sentido, a busca densa não inventa esse pareamento depois. Rerankers e prompts melhores não consertam um corte ruim.

Tamanho é trade-off, não constante

Chunks pequenos (cerca de 100–300 tokens) melhoram precisão: o vizinho tem mais chance de ser sobre uma coisa só. Também perdem definições ao redor, cabeçalhos de tabela e ressalvas do tipo "esta seção vale só para planos anuais" que estavam dois parágrafos acima.

Chunks grandes (cerca de 800–1500 tokens) guardam mais contexto local e sobrevivem melhor quando a resposta atravessa algumas sentenças. Gastam budget de contexto quando só uma frase importava, e diluem o sinal do embedding — vizinhos quase-errados sobem no ranking.

Não existe número universal. Calibre no seu corpus e num conjunto de queries rotuladas: meça recall@k do retrieval e fidelidade da resposta, não só se "os chunks parecem ok."

Fronteiras batem contagem de caracteres

Janelas deslizantes de N caracteres com overlap são baseline razoável e fonte comum de lixo. Prefira cortes estruturais quando a fonte tiver:

  • Headings de Markdown / HTML como quebras duras
  • Fronteiras de parágrafo ou sentença dentro de uma seção
  • Código em limites de função ou tipo, não no meio da linha
  • Tabelas intactas ou resumidas como unidade orientada a linha com a legenda das colunas anexada

Overlap (por exemplo 10–20% do tamanho do chunk) ajuda quando uma sentença cai no meio do corte. Também duplica texto no índice e pode fazer o mesmo fato aparecer duas vezes no contexto empacotado — orce isso.

Parent–child e metadata

Um padrão frequente em knowledge bases de produção:

  1. Child chunks — unidades pequenas de retrieval para matching preciso.
  2. Parent documents — blocos maiores (seção ou página) guardados por ID.
  3. Na query, recupere children e expanda para o parent (ou para children vizinhos) antes de empacotar.

Você paga um pouco mais de storage e lógica de join. Evita responder a partir de uma sentença flutuante que só fazia sentido sob o heading ## Refunds.

Metadata em cada unidade importa tanto quanto o texto: doc_id, section_title, url, updated_at, tenant_id, versão do produto. Os filtros do artigo de retrieval valem aqui; skew de versão ("docs da v2, produto na v3") é falha de RAG mesmo com scores de similaridade bonitos.

source: docs/billing.md
section: ## Annual plan refunds
chunk_id: billing.md#annual-plan-refunds:2
text: "Annual plans can be refunded within 14 days of purchase..."
meta: { product: "billing", version: "2026-07", url: "..." }

Se você não consegue apontar de um chunk para uma URL que o usuário abre, citações na resposta são teatro.

De hits para um prompt

Retrieval devolve candidatos. O assembler de prompt decide o que o modelo de fato vê.

Reaproveite o contrato de messages de Prompts & Context:

System:
- Papel e restrições duras
- Regra de grounding: responda só a partir de Evidence; diga que falta evidência quando faltar
- Regra de citação: se afirmar um fato, anexe chunk IDs

User:
- Objetivo
- Evidence: snippets numerados com IDs
- Pergunta

Uma ordem de packing que costuma funcionar:

  1. Reserve tokens para as instruções de system e a pergunta do usuário (conte com a mesma família de tokenizer do modelo de chat).
  2. Preencha o budget restante com snippets reranqueados, os melhores primeiro.
  3. Descarte o resto. Não truncar silenciosamente no meio do snippet se puder dropar unidades inteiras — meia tabela é pior do que omitir.
  4. Deduplique chunks de overlap quase idênticos antes de empacotar.

Latência e custo escalam com tokens empacotados (prefill) mais o comprimento da completion. Recuperar 50 e empacotar 8 é normal; empacotar os 50 "por precaução" queima dinheiro e muitas vezes piora a qualidade quando ruído dilui a evidência.

Grounding: tornando a evidência vinculante

Injetar texto não é o mesmo que grounding. Modelos ainda misturam priors do pré-treino com seus snippets a menos que instruções e evals empurrem o contrário.

Alavancas práticas:

  • Caminho explícito de recusa. "Se Evidence não contém a resposta, diga que não sabe e peça um link de doc — não chute." Sem isso, retrieval vazio ou errado ainda produz ficção fluente.
  • Separe Evidence da Question visualmente. Labels, fences ou tags no estilo XML reduzem a chance de o modelo tratar o texto de política como tempero opcional.
  • Citações como requisito de produto. Peça marcadores [chunk_id] e verifique-os no código contra os IDs que você de fato enviou. Strings de citação sem verificação são fáceis de inventar.
  • Temperature baixa em modos de resposta factual. Criatividade de sampling briga com grounding.
  • Saída estruturada quando o app precisa de campos answer, citations[], confidence em vez de prosa livre.

Grounding também é política de produto. Bots de suporte podem parafrasear. Superfícies legais ou médicas podem exigir fidelidade no nível da citação e revisão humana. O pipeline é o mesmo; o contrato no system muda.

Forma mínima de um request RAG

Abaixo está a forma de empacotar rows recuperadas numa chamada de chat. Retrieval é dado como feito (hybrid + rerank do artigo anterior); esta é a metade do lado da geração.

package main

import (
  "bytes"
  "encoding/json"
  "fmt"
  "io"
  "net/http"
  "os"
  "strings"
)

type Chunk struct {
  ID   string
  Text string
}

func packEvidence(chunks []Chunk, maxChars int) string {
  var b strings.Builder
  used := 0
  for _, c := range chunks {
    block := fmt.Sprintf("[%s]\n%s\n\n", c.ID, c.Text)
    if used+len(block) > maxChars {
      break
    }
    b.WriteString(block)
    used += len(block)
  }
  return b.String()
}

func main() {
  chunks := []Chunk{
    {
      ID:   "billing.md#refunds:1",
      Text: "Annual plans can be refunded within 14 days of purchase.",
    },
    {
      ID:   "billing.md#api-keys:1",
      Text: "API keys are rotated from the developer settings page.",
    },
  }

  evidence := packEvidence(chunks, 2000)
  question := "Can I get a refund on a yearly plan bought last week?"

  body, _ := json.Marshal(map[string]any{
    "model": "gpt-4.1-mini",
    "temperature": 0.2,
    "messages": []map[string]string{
      {
        "role": "system",
        "content": "Answer using only Evidence. If Evidence is insufficient, say you lack evidence. Cite chunk IDs in square brackets.",
      },
      {
        "role": "user",
        "content": "Evidence:\n" + evidence + "Question: " + question,
      },
    },
  })

  req, _ := http.NewRequest(
    "POST",
    "https://api.openai.com/v1/chat/completions",
    bytes.NewReader(body),
  )
  req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
  req.Header.Set("Content-Type", "application/json")

  res, err := http.DefaultClient.Do(req)
  if err != nil {
    panic(err)
  }
  defer res.Body.Close()
  raw, _ := io.ReadAll(res.Body)
  fmt.Println(string(raw))
}

Budgets em caracteres são um proxy grosseiro de budgets em tokens. Num serviço real, conte tokens para o modelo que você chama e deixe folga para a completion (max_tokens).

Truques do lado da query que pertencem ao RAG

Retrieval puro embedda a string do usuário como veio. Sistemas RAG muitas vezes remodelam a query antes da busca:

  • Rewrite — transformar "aquela coisa de ontem" mais o histórico do chat numa string de busca autônoma.
  • Multi-query — espalhar paráfrases, unir as listas de hits e reranquear uma vez.
  • Estilo HyDE — o modelo rascunha uma passagem de resposta hipotética, você embedda isso e busca (ajuda em alguns corpora; também pode recuperar nonsense confiante se o rascunho estiver errado).

Esses passos custam chamadas extras de LLM ou embedding. Adicione quando métricas mostrarem que a utterance crua do usuário é má chave de busca — curta, anafórica ou cheia de jargão — não porque um post de blog listou o truque.

Modos de falha que só aparecem depois da ligação

A parte 4 cobriu índices vazios e vizinhos errados. Com a geração acoplada, bugs novos aparecem:

  • Retrieval certo, resposta errada. Evidence foi empacotada; o modelo ignorou ou sobrescreveu com fluff do pré-treino. Aperte regras de grounding, baixe temperature, verifique citações no código.
  • Resposta cita IDs que você nunca enviou. Trate como resposta falha. Não mostre notas de rodapé falsas na UI.
  • Fronteira do chunk amputou a regra. Score de retrieval alto; a sentença do limite de 14 dias vivia no child chunk seguinte. Expanda para o parent ou aumente o overlap.
  • Parent velho, child novo (ou o inverso) depois de reindex parcial. Versionar o corpus e rebuildar de forma consistente.
  • Context stuffing. Demasiados chunks medianos; o modelo hesita ou mistura políticas conflitantes. Empacote menos unidades, melhores, depois do rerank.
  • Conflito silencioso de política. Dois snippets recuperados discordam; o modelo escolhe um sem sinalizar. Instrua a expor conflitos, ou resolva no retrieval com metadata (preferir updated_at mais novo).
  • "Não sei" nunca dispara. Caminho de recusa ausente ou contradito em outro trecho do system prompt. Usuários então recebem SLAs inventados.

Hábito de debug: logue query, query reescrita, IDs recuperados com scores, IDs empacotados e a completion final. Se você só guarda a resposta visível ao usuário, não dá para separar bugs de retrieval de bugs de grounding.

Comparação: até onde levar o pipeline

FormaO que você constróiFraquezasUse quando
Naive top-k pasteEmbedda query, empacota hits crus, pede respostaBugs de fronteira; recusa fraca; sem citaçõesSpikes e tools internas
Chunk + budget + groundChunks estruturais, budget de tokens, recusa + citeAinda busca one-shotMaioria do Q&A de produto sobre docs
+ rewrite / multi-queryMelhores chaves de busca a partir de usuários falantesLatência e custo extrasAssistentes multi-turn
+ parent expand / hybridMatch preciso, evidência mais completa, keywordsMais complexidade de índice e joinProsa misturada com IDs / códigos de erro

Você não precisa de um framework de grafos para começar. Precisa de chunking mensurável, um budget de packing e regras de grounding que o app enforce — não só a promessa do modelo.

Conclusão

RAG é retrieval mais construção disciplinada de contexto: cortar documentos em unidades que preservam significado, buscá-las com a stack de search do artigo anterior, empacotar sob um budget real de tokens e vincular a completion a essa evidência com regras de recusa e citação que você verifica no código.

No próximo vamos para Tools & Agents — quando o modelo precisa executar ações (não só ler snippets), como tool calls encaixam no mesmo loop de messages, e quando um agent loop é o formato errado.